From 2abcfacf8181ba51d905026e7fe085cb4a0bd149 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 20:08:52 +0900 Subject: [PATCH 01/14] test(browser): require shared forced-close teardown deadline --- ...close_shared_teardown_deadline_contract.py | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 tests/test_agent_task_forced_close_shared_teardown_deadline_contract.py diff --git a/tests/test_agent_task_forced_close_shared_teardown_deadline_contract.py b/tests/test_agent_task_forced_close_shared_teardown_deadline_contract.py new file mode 100644 index 000000000..329194b3e --- /dev/null +++ b/tests/test_agent_task_forced_close_shared_teardown_deadline_contract.py @@ -0,0 +1,90 @@ +"""Contract for one total post-shutdown teardown deadline in the forced-close lane.""" + +from __future__ import annotations + +import pathlib +import runpy +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +RUNNER = ROOT / "scripts" / "ci" / "run_mv3_compatibility.py" + + +class AgentTaskForcedCloseSharedTeardownDeadlineContractTests(unittest.TestCase): + """Prevent root and process-set teardown polling from multiplying the budget.""" + + def test_runner_exposes_one_combined_teardown_waiter(self) -> None: + """Root and sampled-set evidence must be observed under one timeout authority.""" + + namespace = runpy.run_path( + str(RUNNER), run_name="forced_close_shared_teardown_deadline" + ) + self.assertIn("_wait_for_linux_process_teardown", namespace) + + def test_combined_waiter_preserves_partial_evidence_at_one_deadline(self) -> None: + """A root may exit while a descendant remains live when the one deadline expires.""" + + namespace = runpy.run_path( + str(RUNNER), run_name="forced_close_shared_teardown_behavior" + ) + waiter = namespace["_wait_for_linux_process_teardown"] + + class FakeTime: + def __init__(self) -> None: + self.now = 0.0 + + def monotonic(self) -> float: + return self.now + + def sleep(self, seconds: float) -> None: + self.now += seconds + + fake_time = FakeTime() + root_identity = (101, 1_001) + child_identity = (202, 2_002) + + def fake_read(process_id: int) -> tuple[int, int] | None: + if process_id == root_identity[0]: + return None if fake_time.now >= 0.05 else root_identity + if process_id == child_identity[0]: + return child_identity + raise AssertionError(f"unexpected process id: {process_id}") + + waiter.__globals__["time"] = fake_time + waiter.__globals__["_read_linux_proc_stat_process_identity"] = fake_read + + root_terminated, process_set_terminated = waiter( + root_identity[0], + root_identity[1], + (root_identity, child_identity), + timeout_seconds=0.10, + ) + self.assertIs(root_terminated, True) + self.assertIs(process_set_terminated, False) + self.assertLessEqual(fake_time.now, 0.1000001) + + def test_combined_waiter_requires_root_identity_in_the_sampled_set(self) -> None: + """A separate root identity may not be paired with an unrelated process set.""" + + namespace = runpy.run_path( + str(RUNNER), run_name="forced_close_shared_teardown_identity" + ) + waiter = namespace["_wait_for_linux_process_teardown"] + with self.assertRaises(ValueError): + waiter(101, 1_001, ((202, 2_002),), timeout_seconds=0) + + def test_forced_close_browser_pass_uses_only_the_combined_waiter(self) -> None: + """The forced-close pass must not run independent root and set timeout windows.""" + + runner = RUNNER.read_text(encoding="utf-8") + start = runner.index("def _run_agent_task_forced_close_browser_pass(") + end = runner.index("\ndef _run_agent_task_forced_close_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) + + +if __name__ == "__main__": + unittest.main() From 9b52c532706904fa3cf8f767ff112e1835d912a5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 21:43:13 +0900 Subject: [PATCH 02/14] fix(browser): share forced-close teardown deadline --- scripts/ci/run_mv3_compatibility.py | 88 ++++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 7 deletions(-) diff --git a/scripts/ci/run_mv3_compatibility.py b/scripts/ci/run_mv3_compatibility.py index c38500283..0de08d43c 100755 --- a/scripts/ci/run_mv3_compatibility.py +++ b/scripts/ci/run_mv3_compatibility.py @@ -573,6 +573,73 @@ def _wait_for_linux_process_identity_set_exit( time.sleep(min(0.05, remaining_seconds)) +def _wait_for_linux_process_teardown( + root_process_id: int, + root_start_time_ticks: int, + process_identities: tuple[tuple[int, int], ...], + *, + timeout_seconds: float = PROCESS_EXIT_TIMEOUT_SECONDS, +) -> tuple[bool, bool]: + """Observe root and sampled-set termination under one monotonic deadline.""" + + if isinstance(root_process_id, bool) or not isinstance(root_process_id, int) or root_process_id <= 0: + raise ValueError("invalid Linux root process identifier") + if ( + isinstance(root_start_time_ticks, bool) + or not isinstance(root_start_time_ticks, int) + or root_start_time_ticks <= 0 + ): + raise ValueError("invalid Linux root process start time") + if not process_identities or len(process_identities) > MAX_BROWSER_PROCESS_TREE_SIZE: + raise ValueError("invalid Linux process identity-set size") + + expected: dict[int, tuple[int, int]] = {} + for identity in process_identities: + if not isinstance(identity, tuple) or len(identity) != 2: + raise ValueError("invalid Linux process identity") + process_id, start_time_ticks = identity + if isinstance(process_id, bool) or not isinstance(process_id, int) or process_id <= 0: + raise ValueError("invalid Linux process identifier") + if ( + isinstance(start_time_ticks, bool) + or not isinstance(start_time_ticks, int) + or start_time_ticks <= 0 + ): + raise ValueError("invalid Linux process start time") + if process_id in expected: + raise ValueError("Linux process identity-set PIDs must be unique") + expected[process_id] = identity + + root_identity = (root_process_id, root_start_time_ticks) + if expected.get(root_process_id) != root_identity: + raise ValueError("Linux root process identity must belong to the sampled process set") + if ( + isinstance(timeout_seconds, bool) + or not isinstance(timeout_seconds, (int, float)) + or timeout_seconds < 0 + or not math.isfinite(timeout_seconds) + ): + raise ValueError("invalid Linux process-teardown timeout") + + deadline = time.monotonic() + float(timeout_seconds) + while True: + live_process_ids: set[int] = set() + for process_id, expected_identity in expected.items(): + current_identity = _read_linux_proc_stat_process_identity(process_id) + if current_identity == expected_identity: + live_process_ids.add(process_id) + + root_terminated = root_process_id not in live_process_ids + process_set_terminated = not live_process_ids + if process_set_terminated: + return root_terminated, True + + remaining_seconds = deadline - time.monotonic() + if remaining_seconds <= 0: + return root_terminated, False + time.sleep(min(0.05, remaining_seconds)) + + def _sample_linux_process_rss_bytes(process_id: int) -> int: """Read one attributed Linux process RSS through a bounded ``/proc`` status file.""" @@ -1583,15 +1650,22 @@ def _run_agent_task_forced_close_browser_pass( if browser_process_id is None or browser_process_start_time_ticks is None: raise RuntimeError("Agent Task forced-close 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: failure_evidence: dict[str, Any] = { "failure_type": browser_failure_type, From edc5ba56ae19fea19a4458a7c96105d3a7441796 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 21:46:34 +0900 Subject: [PATCH 03/14] test(browser): align forced-close teardown contract --- ..._task_forced_close_process_termination_contract.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/test_agent_task_forced_close_process_termination_contract.py b/tests/test_agent_task_forced_close_process_termination_contract.py index c2b2d93b4..2ea30608f 100644 --- a/tests/test_agent_task_forced_close_process_termination_contract.py +++ b/tests/test_agent_task_forced_close_process_termination_contract.py @@ -25,8 +25,7 @@ def test_forced_close_browser_pass_binds_and_waits_for_process_identities(self) "_read_linux_proc_stat_process_identity", "_snapshot_linux_process_evidence", "_read_linux_process_identity_set", - "_wait_for_linux_process_identity_exit", - "_wait_for_linux_process_identity_set_exit", + "_wait_for_linux_process_teardown", '"browser_process_terminated"', '"chromium_process_set_terminated"', ): @@ -85,12 +84,10 @@ def test_forced_close_browser_failure_is_returned_after_teardown_waits(self) -> self.assertIn(expected, browser_pass) shutdown = browser_pass.index("driver.wait(timeout=5)") - root_wait = browser_pass.index("_wait_for_linux_process_identity_exit(") - set_wait = browser_pass.index("_wait_for_linux_process_identity_set_exit(") + teardown_wait = browser_pass.index("_wait_for_linux_process_teardown(") failure_return = browser_pass.index("if browser_failure_type is not None:") - self.assertLess(shutdown, root_wait) - self.assertLess(root_wait, failure_return) - self.assertLess(set_wait, failure_return) + self.assertLess(shutdown, teardown_wait) + self.assertLess(teardown_wait, failure_return) def test_forced_close_trial_preserves_failure_process_set_teardown_evidence(self) -> None: """False root/set teardown evidence must survive the trial failure envelope.""" From c876a44d7a3e6f18dae075cece312ba3eb9c4be9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 21:51:17 +0900 Subject: [PATCH 04/14] docs(browser): record shared teardown deadline --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c2048da0..728a1aaad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ 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 and every PID in the already sampled bounded Chromium root-plus-descendant process set to exact Linux `/proc//stat` start-time identities before shutdown and fails closed unless those exact identities terminate after session/driver shutdown; 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 those exact sampled identities terminated; this remains bounded compatibility evidence and does not attest cgroup ownership, post-snapshot processes, cross-platform supervision, 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. - 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, 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; 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. From 308dbe81e466c75f7f3c572db0dd4eadb821a1d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 19:12:22 -0700 Subject: [PATCH 05/14] fix(browser): share forced-close teardown deadline --- scripts/ci/run_mv3_compatibility.py | 93 +++++++++++++++++-- ...rced_close_process_termination_contract.py | 13 +-- 2 files changed, 90 insertions(+), 16 deletions(-) mode change 100644 => 100755 scripts/ci/run_mv3_compatibility.py diff --git a/scripts/ci/run_mv3_compatibility.py b/scripts/ci/run_mv3_compatibility.py old mode 100644 new mode 100755 index 82c526471..ae18ceb9f --- a/scripts/ci/run_mv3_compatibility.py +++ b/scripts/ci/run_mv3_compatibility.py @@ -598,6 +598,76 @@ def _wait_for_linux_process_identity_set_exit( time.sleep(min(0.05, remaining_seconds)) +def _wait_for_linux_process_teardown( + root_process_id: int, + root_start_time_ticks: int, + process_identities: tuple[tuple[int, int], ...], + *, + timeout_seconds: float = PROCESS_EXIT_TIMEOUT_SECONDS, +) -> tuple[bool, bool]: + """Observe root and sampled-set termination under one bounded shared deadline.""" + + if ( + isinstance(root_process_id, bool) + or not isinstance(root_process_id, int) + or root_process_id <= 0 + ): + raise ValueError("invalid Linux root process identifier") + if ( + isinstance(root_start_time_ticks, bool) + or not isinstance(root_start_time_ticks, int) + or root_start_time_ticks <= 0 + ): + raise ValueError("invalid Linux root process start time") + if not process_identities or len(process_identities) > MAX_BROWSER_PROCESS_TREE_SIZE: + raise ValueError("invalid Linux process identity-set size") + + expected: dict[int, tuple[int, int]] = {} + for identity in process_identities: + if not isinstance(identity, tuple) or len(identity) != 2: + raise ValueError("invalid Linux process identity") + process_id, start_time_ticks = identity + if isinstance(process_id, bool) or not isinstance(process_id, int) or process_id <= 0: + raise ValueError("invalid Linux process identifier") + if ( + isinstance(start_time_ticks, bool) + or not isinstance(start_time_ticks, int) + or start_time_ticks <= 0 + ): + raise ValueError("invalid Linux process start time") + if process_id in expected: + raise ValueError("Linux process identity-set PIDs must be unique") + expected[process_id] = identity + + root_identity = (root_process_id, root_start_time_ticks) + if expected.get(root_process_id) != root_identity: + raise ValueError("Linux root process identity must belong to the sampled process set") + if ( + isinstance(timeout_seconds, bool) + or not isinstance(timeout_seconds, (int, float)) + or timeout_seconds < 0 + or not math.isfinite(timeout_seconds) + ): + raise ValueError("invalid Linux process teardown timeout") + + deadline = time.monotonic() + float(timeout_seconds) + while True: + live_process_ids: set[int] = set() + for process_id, expected_identity in expected.items(): + current_identity = _read_linux_proc_stat_process_identity(process_id) + if current_identity == expected_identity: + live_process_ids.add(process_id) + + root_terminated = root_process_id not in live_process_ids + process_set_terminated = not live_process_ids + if process_set_terminated: + return root_terminated, True + remaining_seconds = deadline - time.monotonic() + if remaining_seconds <= 0: + return root_terminated, False + time.sleep(min(0.05, remaining_seconds)) + + def _sample_linux_process_rss_bytes(process_id: int) -> int: """Read one attributed Linux process RSS through a bounded ``/proc`` status file.""" @@ -1692,15 +1762,22 @@ def _run_agent_task_forced_close_browser_pass( if browser_process_id is None or browser_process_start_time_ticks is None: raise RuntimeError("Agent Task forced-close 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 = ( + 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 @@ -2137,4 +2214,4 @@ def main() -> int: if __name__ == "__main__": - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) diff --git a/tests/test_agent_task_forced_close_process_termination_contract.py b/tests/test_agent_task_forced_close_process_termination_contract.py index d7a0e8871..9b172f429 100644 --- a/tests/test_agent_task_forced_close_process_termination_contract.py +++ b/tests/test_agent_task_forced_close_process_termination_contract.py @@ -27,8 +27,7 @@ def test_forced_close_browser_pass_binds_and_waits_for_process_identities(self) "_snapshot_linux_process_evidence", "_read_linux_process_identity_set", "_terminate_owned_process_bounded", - "_wait_for_linux_process_identity_exit", - "_wait_for_linux_process_identity_set_exit", + "_wait_for_linux_process_teardown", '"driver_process_terminated"', '"driver_kill_fallback_used"', '"browser_process_terminated"', @@ -97,12 +96,10 @@ def test_forced_close_browser_failure_is_returned_after_teardown_waits(self) -> self.assertIn(expected, browser_pass) shutdown = browser_pass.index("_terminate_owned_process_bounded(driver)") - root_wait = browser_pass.index("_wait_for_linux_process_identity_exit(") - set_wait = browser_pass.index("_wait_for_linux_process_identity_set_exit(") - failure_return = browser_pass.index("browser_failure_type is not None", set_wait) - self.assertLess(shutdown, root_wait) - self.assertLess(root_wait, failure_return) - self.assertLess(set_wait, failure_return) + teardown_wait = browser_pass.index("_wait_for_linux_process_teardown(") + failure_return = browser_pass.index("browser_failure_type is not None", teardown_wait) + self.assertLess(shutdown, teardown_wait) + self.assertLess(teardown_wait, failure_return) def test_forced_close_driver_shutdown_timeout_is_bounded_and_typed(self) -> None: """A wedged ChromeDriver after SIGKILL must become failure evidence, not escape.""" From 962e179b53ed0342fde5b50c3deac0e7454b0d4f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 13:25:17 -0700 Subject: [PATCH 06/14] fix(browser): reconcile shared teardown with live base hardening --- scripts/ci/run_mv3_compatibility.py | 91 ++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 7 deletions(-) diff --git a/scripts/ci/run_mv3_compatibility.py b/scripts/ci/run_mv3_compatibility.py index dd9dbbde1..cdae859e5 100644 --- a/scripts/ci/run_mv3_compatibility.py +++ b/scripts/ci/run_mv3_compatibility.py @@ -621,6 +621,76 @@ def _wait_for_linux_process_identity_set_exit( time.sleep(min(0.05, remaining_seconds)) +def _wait_for_linux_process_teardown( + root_process_id: int, + root_start_time_ticks: int, + process_identities: tuple[tuple[int, int], ...], + *, + timeout_seconds: float = PROCESS_EXIT_TIMEOUT_SECONDS, +) -> tuple[bool, bool]: + """Observe root and sampled-set termination under one bounded shared deadline.""" + + if ( + isinstance(root_process_id, bool) + or not isinstance(root_process_id, int) + or root_process_id <= 0 + ): + raise ValueError("invalid Linux root process identifier") + if ( + isinstance(root_start_time_ticks, bool) + or not isinstance(root_start_time_ticks, int) + or root_start_time_ticks <= 0 + ): + raise ValueError("invalid Linux root process start time") + if not process_identities or len(process_identities) > MAX_BROWSER_PROCESS_TREE_SIZE: + raise ValueError("invalid Linux process identity-set size") + + expected: dict[int, tuple[int, int]] = {} + for identity in process_identities: + if not isinstance(identity, tuple) or len(identity) != 2: + raise ValueError("invalid Linux process identity") + process_id, start_time_ticks = identity + if isinstance(process_id, bool) or not isinstance(process_id, int) or process_id <= 0: + raise ValueError("invalid Linux process identifier") + if ( + isinstance(start_time_ticks, bool) + or not isinstance(start_time_ticks, int) + or start_time_ticks <= 0 + ): + raise ValueError("invalid Linux process start time") + if process_id in expected: + raise ValueError("Linux process identity-set PIDs must be unique") + expected[process_id] = identity + + root_identity = (root_process_id, root_start_time_ticks) + if expected.get(root_process_id) != root_identity: + raise ValueError("Linux root process identity must belong to the sampled process set") + if ( + isinstance(timeout_seconds, bool) + or not isinstance(timeout_seconds, (int, float)) + or timeout_seconds < 0 + or not math.isfinite(timeout_seconds) + ): + raise ValueError("invalid Linux process teardown timeout") + + deadline = time.monotonic() + float(timeout_seconds) + while True: + live_process_ids: set[int] = set() + for process_id, expected_identity in expected.items(): + current_identity = _read_linux_proc_stat_process_identity(process_id) + if current_identity == expected_identity: + live_process_ids.add(process_id) + + root_terminated = root_process_id not in live_process_ids + process_set_terminated = not live_process_ids + if process_set_terminated: + return root_terminated, True + remaining_seconds = deadline - time.monotonic() + if remaining_seconds <= 0: + return root_terminated, False + time.sleep(min(0.05, remaining_seconds)) + + def _sample_linux_process_rss_bytes(process_id: int) -> int: """Read one attributed Linux process RSS through a bounded ``/proc`` status file.""" @@ -1770,15 +1840,22 @@ def _run_agent_task_forced_close_browser_pass( if browser_process_id is None or browser_process_start_time_ticks is None: raise RuntimeError("Agent Task forced-close 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 = ( + 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 From 52575632de07bcb90791e491b9a64b6875532ebe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 18:23:48 +0900 Subject: [PATCH 07/14] chore(stack): delegate shared-deadline assertions to dedicated contract For merge synthesis only, use the current parent forced-close contract unchanged. The child-specific waiter assertions remain fully represented by the dedicated shared-teardown-deadline contract, avoiding duplicate overlap while preserving the valid acceptance semantics. Signed-off-by: Seongho Bae --- ...k_forced_close_process_termination_contract.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/test_agent_task_forced_close_process_termination_contract.py b/tests/test_agent_task_forced_close_process_termination_contract.py index 9b172f429..0ad1b9fa4 100644 --- a/tests/test_agent_task_forced_close_process_termination_contract.py +++ b/tests/test_agent_task_forced_close_process_termination_contract.py @@ -27,7 +27,8 @@ def test_forced_close_browser_pass_binds_and_waits_for_process_identities(self) "_snapshot_linux_process_evidence", "_read_linux_process_identity_set", "_terminate_owned_process_bounded", - "_wait_for_linux_process_teardown", + "_wait_for_linux_process_identity_exit", + "_wait_for_linux_process_identity_set_exit", '"driver_process_terminated"', '"driver_kill_fallback_used"', '"browser_process_terminated"', @@ -84,7 +85,7 @@ def test_forced_close_browser_failure_is_returned_after_teardown_waits(self) -> "browser_failure_type", "driver_cleanup_failure_type", "driver_kill_fallback_used", - "except (OSError, ValueError, RuntimeError, json.JSONDecodeError) as exc:", + "except (OSError, ValueError, RuntimeError, json.JSONDecodeError, http.client.HTTPException) as exc:", 'browser_failure_type = type(exc).__name__', "failure_evidence", '"driver_process_terminated": driver_process_terminated', @@ -96,10 +97,12 @@ def test_forced_close_browser_failure_is_returned_after_teardown_waits(self) -> self.assertIn(expected, browser_pass) shutdown = browser_pass.index("_terminate_owned_process_bounded(driver)") - teardown_wait = browser_pass.index("_wait_for_linux_process_teardown(") - failure_return = browser_pass.index("browser_failure_type is not None", teardown_wait) - self.assertLess(shutdown, teardown_wait) - self.assertLess(teardown_wait, failure_return) + root_wait = browser_pass.index("_wait_for_linux_process_identity_exit(") + set_wait = browser_pass.index("_wait_for_linux_process_identity_set_exit(") + failure_return = browser_pass.index("browser_failure_type is not None", set_wait) + self.assertLess(shutdown, root_wait) + self.assertLess(root_wait, failure_return) + self.assertLess(set_wait, failure_return) def test_forced_close_driver_shutdown_timeout_is_bounded_and_typed(self) -> None: """A wedged ChromeDriver after SIGKILL must become failure evidence, not escape.""" From af1b98ba377c73b88baa9633e2232e7f76e76f36 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 18:30:52 +0900 Subject: [PATCH 08/14] test(browser): preserve shared teardown deadline after parent synthesis --- ...ask_forced_close_process_termination_contract.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/test_agent_task_forced_close_process_termination_contract.py b/tests/test_agent_task_forced_close_process_termination_contract.py index 0ad1b9fa4..cea7d58a4 100644 --- a/tests/test_agent_task_forced_close_process_termination_contract.py +++ b/tests/test_agent_task_forced_close_process_termination_contract.py @@ -27,8 +27,7 @@ def test_forced_close_browser_pass_binds_and_waits_for_process_identities(self) "_snapshot_linux_process_evidence", "_read_linux_process_identity_set", "_terminate_owned_process_bounded", - "_wait_for_linux_process_identity_exit", - "_wait_for_linux_process_identity_set_exit", + "_wait_for_linux_process_teardown", '"driver_process_terminated"', '"driver_kill_fallback_used"', '"browser_process_terminated"', @@ -97,12 +96,10 @@ def test_forced_close_browser_failure_is_returned_after_teardown_waits(self) -> self.assertIn(expected, browser_pass) shutdown = browser_pass.index("_terminate_owned_process_bounded(driver)") - root_wait = browser_pass.index("_wait_for_linux_process_identity_exit(") - set_wait = browser_pass.index("_wait_for_linux_process_identity_set_exit(") - failure_return = browser_pass.index("browser_failure_type is not None", set_wait) - self.assertLess(shutdown, root_wait) - self.assertLess(root_wait, failure_return) - self.assertLess(set_wait, failure_return) + teardown_wait = browser_pass.index("_wait_for_linux_process_teardown(") + failure_return = browser_pass.index("browser_failure_type is not None", teardown_wait) + self.assertLess(shutdown, teardown_wait) + self.assertLess(teardown_wait, failure_return) def test_forced_close_driver_shutdown_timeout_is_bounded_and_typed(self) -> None: """A wedged ChromeDriver after SIGKILL must become failure evidence, not escape.""" From 3dff28d9bf2dd27b72507e39979d51b8bf140fb4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 19:11:24 +0900 Subject: [PATCH 09/14] test: align inherited cleanup observer with each trial lane Keep every parent review-evidence assertion and all six methods while injecting the actual individual or combined teardown observer for each lane. Preserve exact root-only identity arguments after an early forced-close protocol error. Production code, deadline, retry policy and failed denominators are unchanged. Fresh exact-parent RED: six methods, two forced-close failures in 10.417 seconds. GREEN: six focused and all 228 Python tests, compileall, full Rust 1.97.1 fmt/check/tests/Clippy/rustdoc and enforced 100% coverage (415 functions, 3555 lines, 4444 regions, 476 branches). Branch instrumentation warning remains explicit. The ordinary-pass two-deadline issue stays separate. Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- CHANGELOG.md | 1 + docs/doctoring.md | 6 ++++++ tests/test_agent_task_review_evidence_contract.py | 14 ++++++++++---- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6b2b4039..61b8208f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added +- Kept the inherited protocol-failure cleanup checks executable after shared-deadline integration by observing the correct cleanup path for ordinary and forced-close trials; all failure, cleanup and diagnostic-redaction assertions remain intact. - Controlled browser trials reject malformed pre-shutdown exit counts, retain validated driver and cleanup outcomes after ordinary task failure, and report mid-request protocol failures without remote diagnostic text; failed trials remain failures, startup retries keep their existing narrow scope, and obsolete HTTP-body close signals are no longer accepted. - Failed controlled Agent Task runs now report whether their original browser process ended after shutdown, alongside temporary-profile cleanup; a failed task never becomes a pass merely because cleanup succeeded. If process observation itself fails, termination remains unproven. This covers the original browser process only, not all descendants or arbitrary browser recovery. - 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. diff --git a/docs/doctoring.md b/docs/doctoring.md index 986d6060a..0ffbe0317 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -118,6 +118,12 @@ The repair restores the existing non-boolean integer range check, forwards only The regressions execute the production success predicate, actual ordinary/forced-close browser-pass and outer-trial paths, and aggregate evidence generation with controlled faults. They check invalid counts, observed root survival, driver cleanup, early failure without invented identity evidence, private-message exclusion, and final rejection with the original trial denominator intact. Unknown fields are not forwarded and malformed known cleanup values are rejected. A sixth regression covers all three final aggregate exception boundaries and server teardown. Controlled tests do not establish live Linux/Chromium termination, cgroup ownership, OS-wide orphan absence or release acceptance; exact-head hosted compatibility remains mandatory. +### Shared-deadline inherited-test reconciliation + +At #147 `af1b98ba377c73b88baa9633e2232e7f76e76f36`, all six parent review-evidence methods were restored, but the forced-close `BadStatusLine` and `IncompleteRead` subcases still injected the old individual root waiter. Native discovery executed all 228 tests and failed those two subcases; a fresh focused run reproduced both failures in 10.417 seconds because the real combined observer consumed its deadline. This was test injection drift after parent integration, not evidence of a production teardown regression. + +The existing test now specifies each lane's actual observer, return value and exact arguments. Ordinary trials keep the individual `False` result. Forced-close trials inject `(False, False)` from the shared observer and require `(321, 654, ((321, 654),))`, preserving root-only capture rather than inventing a complete descendant set. Every existing failed-trial, profile-cleanup, live-root, driver-termination, error-type and redaction assertion remains. No production code, deadline, retry, failed denominator or coverage gate changes. This repair does not address the separate ordinary-pass two-deadline finding or prove real Linux/pinned-Chromium acceptance. + ## References Amazon Web Services. (n.d.). *Set up the Amazon EKS Pod Identity Agent*. Retrieved August 6, 2026, from https://docs.aws.amazon.com/eks/latest/userguide/pod-id-agent-setup.html diff --git a/tests/test_agent_task_review_evidence_contract.py b/tests/test_agent_task_review_evidence_contract.py index c627a3526..035e2fae8 100644 --- a/tests/test_agent_task_review_evidence_contract.py +++ b/tests/test_agent_task_review_evidence_contract.py @@ -74,13 +74,19 @@ def test_ordinary_failure_retains_only_valid_known_cleanup_fields(self) -> None: def test_protocol_faults_keep_real_browser_cleanup_evidence(self) -> None: """Mid-pass protocol faults stay terminal and preserve observed root cleanup.""" - for lane in ("agent_task", "agent_task_forced_close"): + for lane, waiter_name, observed_exit, expected_args in ( + ("agent_task", "_wait_for_linux_process_identity_exit", False, (321, 654)), + ( + "agent_task_forced_close", "_wait_for_linux_process_teardown", + (False, False), (321, 654, ((321, 654),)), + ), + ): for error in (http.client.BadStatusLine("private marker"), http.client.IncompleteRead(b"private marker")): with self.subTest(lane=lane, error=type(error).__name__): namespace = runpy.run_path(str(RUNNER)) trial = namespace[f"_run_{lane}_trial"] driver = mock.Mock() - exit_wait = mock.Mock(return_value=False) + exit_wait = mock.Mock(return_value=observed_exit) def request(_port, method, target, *_args): if target == "/session": @@ -95,7 +101,7 @@ def request(_port, method, target, *_args): "_free_loopback_port": lambda: 12345, "_wait_for_driver": lambda *_: None, "_json_request": request, "_read_linux_proc_stat_process_identity": lambda *_: (321, 654), - "_wait_for_linux_process_identity_exit": exit_wait, + waiter_name: exit_wait, } with mock.patch.dict(trial.__globals__, replacements), mock.patch.object(namespace["subprocess"], "Popen", return_value=driver): result = trial(pathlib.Path("unused-chrome"), pathlib.Path("unused-driver"), "http://127.0.0.1/fixture", 3) @@ -105,7 +111,7 @@ def request(_port, method, target, *_args): self.assertIs(result["driver_process_terminated"], True) self.assertEqual(result["failure_type"], type(error).__name__) self.assertNotIn("private marker", repr(result)) - exit_wait.assert_called_once_with(321, 654) + exit_wait.assert_called_once_with(*expected_args) driver.terminate.assert_called_once_with() def test_all_trial_boundaries_redact_protocol_failures_before_identity_capture(self) -> None: From 3f483abb27eb8ae4abc6a2a7fe71d7632bc21e68 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 23:10:01 +0900 Subject: [PATCH 10/14] test(browser): require one ordinary teardown deadline --- ...est_agent_task_shared_teardown_deadline.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tests/test_agent_task_shared_teardown_deadline.py 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() From 9d07ca7b59553ad21c85b6efcb72c8154cb55c2f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 00:39:51 +0900 Subject: [PATCH 11/14] fix(browser): share ordinary teardown deadline --- scripts/ci/run_mv3_compatibility.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/scripts/ci/run_mv3_compatibility.py b/scripts/ci/run_mv3_compatibility.py index 5fd3b29c2..b202c8997 100644 --- a/scripts/ci/run_mv3_compatibility.py +++ b/scripts/ci/run_mv3_compatibility.py @@ -1456,15 +1456,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 = ( + 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 From a2541c570d5c1743ab9dc14fa25b9ebcc0ac83a4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 01:34:53 +0900 Subject: [PATCH 12/14] test(browser): align process-set teardown contract --- ...t_agent_task_failure_process_set_termination_contract.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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..e59819c50 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,10 @@ 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", + "teardown_identities = (", + "_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): From c2e0a74261168d9e59c7bc8e44c7f944c9af4150 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 01:35:34 +0900 Subject: [PATCH 13/14] test(browser): exercise combined teardown observer --- ...sk_failure_process_termination_contract.py | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/tests/test_agent_task_failure_process_termination_contract.py b/tests/test_agent_task_failure_process_termination_contract.py index 910bf0248..3fd4eab45 100644 --- a/tests/test_agent_task_failure_process_termination_contract.py +++ b/tests/test_agent_task_failure_process_termination_contract.py @@ -36,10 +36,14 @@ def test_failure_cleanup_release_record_preserves_evidence_limits(self) -> None: ) def test_real_failure_path_distinguishes_observed_exit_from_observation_error(self) -> None: - """Exercise both owning helpers without launching a browser or trusting fake success.""" + """Exercise the shared teardown observer without launching a browser or trusting fake success.""" - for exit_observation in (True, False, PermissionError("controlled read failure")): - with self.subTest(exit_observation=type(exit_observation).__name__): + for teardown_observation in ( + (True, True), + (False, False), + PermissionError("controlled read failure"), + ): + with self.subTest(teardown_observation=repr(teardown_observation)): namespace = self._namespace("agent_task_failure_observation_boundary") browser_pass = namespace["_run_agent_task_browser_pass"] run_trial = namespace["_run_agent_task_trial"] @@ -60,15 +64,15 @@ def request(_port, method, target, *_args): raise RuntimeError("private controlled browser failure") return {} - exit_wait = mock.Mock(return_value=exit_observation) - if isinstance(exit_observation, Exception): - exit_wait.side_effect = exit_observation + teardown_wait = mock.Mock(return_value=teardown_observation) + if isinstance(teardown_observation, Exception): + teardown_wait.side_effect = teardown_observation replacements = { "_free_loopback_port": lambda: 12345, "_wait_for_driver": lambda _port: None, "_json_request": request, "_read_linux_proc_stat_process_identity": lambda _pid: (321, 654), - "_wait_for_linux_process_identity_exit": exit_wait, + "_wait_for_linux_process_teardown": teardown_wait, } with mock.patch.dict(browser_pass.__globals__, replacements), mock.patch.object( namespace["subprocess"], "Popen", return_value=driver @@ -82,16 +86,19 @@ def request(_port, method, target, *_args): driver.terminate.assert_called_once_with() driver.wait.assert_called_once_with(timeout=5) - exit_wait.assert_called_once_with(321, 654) + teardown_wait.assert_called_once_with(321, 654, ((321, 654),)) self.assertIs(result["passed"], False) self.assertIs(result["profile_cleaned"], True) self.assertNotIn("private controlled browser failure", repr(result)) - if isinstance(exit_observation, Exception): + if isinstance(teardown_observation, Exception): self.assertEqual(result["failure_type"], "PermissionError") self.assertNotIn("browser_process_terminated", result) else: self.assertEqual(result["failure_type"], "RuntimeError") - self.assertIs(result["browser_process_terminated"], exit_observation) + self.assertIs( + result["browser_process_terminated"], teardown_observation[0] + ) + self.assertNotIn("chromium_process_set_terminated", result) def test_browser_pass_retains_failure_process_termination_evidence(self) -> None: """A browser-pass failure after identity capture must survive teardown as evidence.""" From 364912b7ff23b8ac88832d0f078e00d975707407 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 01:36:50 +0900 Subject: [PATCH 14/14] test(browser): align protocol-fault teardown observer --- tests/test_agent_task_review_evidence_contract.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/tests/test_agent_task_review_evidence_contract.py b/tests/test_agent_task_review_evidence_contract.py index 035e2fae8..3544b3931 100644 --- a/tests/test_agent_task_review_evidence_contract.py +++ b/tests/test_agent_task_review_evidence_contract.py @@ -74,19 +74,13 @@ def test_ordinary_failure_retains_only_valid_known_cleanup_fields(self) -> None: def test_protocol_faults_keep_real_browser_cleanup_evidence(self) -> None: """Mid-pass protocol faults stay terminal and preserve observed root cleanup.""" - for lane, waiter_name, observed_exit, expected_args in ( - ("agent_task", "_wait_for_linux_process_identity_exit", False, (321, 654)), - ( - "agent_task_forced_close", "_wait_for_linux_process_teardown", - (False, False), (321, 654, ((321, 654),)), - ), - ): + for lane in ("agent_task", "agent_task_forced_close"): for error in (http.client.BadStatusLine("private marker"), http.client.IncompleteRead(b"private marker")): with self.subTest(lane=lane, error=type(error).__name__): namespace = runpy.run_path(str(RUNNER)) trial = namespace[f"_run_{lane}_trial"] driver = mock.Mock() - exit_wait = mock.Mock(return_value=observed_exit) + teardown_wait = mock.Mock(return_value=(False, False)) def request(_port, method, target, *_args): if target == "/session": @@ -101,7 +95,7 @@ def request(_port, method, target, *_args): "_free_loopback_port": lambda: 12345, "_wait_for_driver": lambda *_: None, "_json_request": request, "_read_linux_proc_stat_process_identity": lambda *_: (321, 654), - waiter_name: exit_wait, + "_wait_for_linux_process_teardown": teardown_wait, } with mock.patch.dict(trial.__globals__, replacements), mock.patch.object(namespace["subprocess"], "Popen", return_value=driver): result = trial(pathlib.Path("unused-chrome"), pathlib.Path("unused-driver"), "http://127.0.0.1/fixture", 3) @@ -111,7 +105,7 @@ def request(_port, method, target, *_args): self.assertIs(result["driver_process_terminated"], True) self.assertEqual(result["failure_type"], type(error).__name__) self.assertNotIn("private marker", repr(result)) - exit_wait.assert_called_once_with(*expected_args) + teardown_wait.assert_called_once_with(321, 654, ((321, 654),)) driver.terminate.assert_called_once_with() def test_all_trial_boundaries_redact_protocol_failures_before_identity_capture(self) -> None: