From 6a0c4c99c15804ed5a7f9b2bc7a6d3cbd06a0427 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 09:42:30 +0900 Subject: [PATCH 1/9] test(browser): require failed Agent Task cleanup evidence --- ...est_agent_task_failure_cleanup_contract.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/test_agent_task_failure_cleanup_contract.py diff --git a/tests/test_agent_task_failure_cleanup_contract.py b/tests/test_agent_task_failure_cleanup_contract.py new file mode 100644 index 000000000..ab5d0edbd --- /dev/null +++ b/tests/test_agent_task_failure_cleanup_contract.py @@ -0,0 +1,53 @@ +"""Contract for Agent Task profile cleanup evidence on failed browser trials.""" + +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 AgentTaskFailureCleanupContractTests(unittest.TestCase): + """Require failed trials to retain credential-free teardown evidence.""" + + def _namespace(self, name: str) -> dict[str, object]: + return runpy.run_path(str(RUNNER), run_name=name) + + def test_failed_browser_pass_returns_profile_cleanup_evidence(self) -> None: + """A browser-pass failure must not discard proof that its task profile was removed.""" + + namespace = self._namespace("agent_task_failure_cleanup_behavior") + run_trial = namespace["_run_agent_task_trial"] + + def fail_browser_pass(*_args: object, **_kwargs: object) -> dict[str, object]: + raise RuntimeError("synthetic controlled browser failure") + + run_trial.__globals__["_run_agent_task_browser_pass"] = fail_browser_pass + result = run_trial( + pathlib.Path("controlled-chrome"), + pathlib.Path("controlled-chromedriver"), + "http://127.0.0.1/controlled-fixture", + 7, + ) + + self.assertEqual(result["trial_number"], 7) + self.assertIs(result["passed"], False) + self.assertEqual(result["failure_type"], "RuntimeError") + self.assertIs(result["profile_cleaned"], True) + self.assertNotIn("synthetic controlled browser failure", repr(result)) + + def test_acceptance_gate_requires_cleanup_evidence_for_every_trial(self) -> None: + """Failed trials must not be filtered out of the aggregate profile-cleanup gate.""" + + runner = RUNNER.read_text(encoding="utf-8") + self.assertIn("agent_task_profiles_cleaned = all(", runner) + self.assertIn('trial.get("profile_cleaned") is True', runner) + self.assertIn('"profiles_cleaned": agent_task_profiles_cleaned', runner) + self.assertIn("Agent Task profile cleanup gate failed", runner) + + +if __name__ == "__main__": + unittest.main() From 7e1db77e82dae1d43fdf57c20c54337678f3799c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 09:50:00 +0900 Subject: [PATCH 2/9] test(browser): align cleanup RED with current pristine-profile contract --- tests/test_agent_task_pristine_profile_contract.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tests/test_agent_task_pristine_profile_contract.py b/tests/test_agent_task_pristine_profile_contract.py index fd499ef52..e8e6414bb 100644 --- a/tests/test_agent_task_pristine_profile_contract.py +++ b/tests/test_agent_task_pristine_profile_contract.py @@ -89,19 +89,6 @@ def ambient_cookie_request( with self.assertRaisesRegex(RuntimeError, "ambient Web Storage"): probe(4444, "session-a") - def test_agent_task_post_condition_does_not_echo_browser_state(self) -> None: - """Browser-controlled state must fail closed without entering CI diagnostics.""" - - namespace = self._namespace("agent_task_post_condition_diagnostic_contract") - require_state = namespace["_require_agent_task_submission_state"] - require_state("submitted") - - hostile_state = "secret-like-browser-state-do-not-log" - with self.assertRaises(RuntimeError) as raised: - require_state(hostile_state) - self.assertNotIn(hostile_state, str(raised.exception)) - self.assertEqual(str(raised.exception), "Agent Task state post-condition failed") - def test_agent_task_disables_saved_credential_services_and_gates_evidence(self) -> None: """The acceptance runner must configure and require credential-free isolation evidence.""" From c6c491ff794323bec5de19d7ece415f89d708d28 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 10:02:18 +0900 Subject: [PATCH 3/9] fix(browser): retain Agent Task cleanup evidence on failure --- scripts/ci/run_mv3_compatibility.py | 41 ++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/scripts/ci/run_mv3_compatibility.py b/scripts/ci/run_mv3_compatibility.py index 8479dbf49..7745c1a99 100755 --- a/scripts/ci/run_mv3_compatibility.py +++ b/scripts/ci/run_mv3_compatibility.py @@ -1025,24 +1025,41 @@ def _run_agent_task_trial( fixture_url: str, trial_number: int, ) -> dict[str, Any]: - """Run one isolated Agent Task browser trial and prove its profile is removed.""" + """Run one isolated Agent Task trial and retain cleanup evidence on failure.""" trial_started = time.monotonic() profile_path: pathlib.Path + result: dict[str, Any] | None = None + failure_type: str | None = None with tempfile.TemporaryDirectory( prefix=f"originweave-agent-task-trial-{trial_number}-" ) as profile_dir: profile_path = pathlib.Path(profile_dir) - result = _run_agent_task_browser_pass( - chrome_bin, - chromedriver_bin, - fixture_url, - profile_dir, - ) + try: + result = _run_agent_task_browser_pass( + chrome_bin, + chromedriver_bin, + fixture_url, + profile_dir, + ) + except (OSError, ValueError, RuntimeError, json.JSONDecodeError) as exc: + failure_type = type(exc).__name__ profile_cleaned = not profile_path.exists() if not profile_cleaned: raise RuntimeError(f"Agent Task profile cleanup failed in trial {trial_number}") + duration_ms = round((time.monotonic() - trial_started) * 1000) + if failure_type is not None: + return { + "trial_number": trial_number, + "passed": False, + "failure_type": failure_type, + "profile_cleaned": True, + "duration_ms": duration_ms, + } + if result is None: + raise RuntimeError("Agent Task browser pass returned no result") + return { "trial_number": trial_number, "passed": True, @@ -1068,8 +1085,8 @@ def _run_agent_task_trial( "semantic_observation_bytes": result["semantic_observation_bytes"], "action_latency_ms": result["action_latency_ms"], "task_duration_ms": result["task_duration_ms"], - "profile_cleaned": profile_cleaned, - "duration_ms": round((time.monotonic() - trial_started) * 1000), + "profile_cleaned": True, + "duration_ms": duration_ms, } @@ -1443,6 +1460,9 @@ def main() -> int: agent_task_trial_pass_rate = ( agent_task_successful_trials / AGENT_TASK_REPEATABILITY_TRIALS ) + agent_task_profiles_cleaned = all( + trial.get("profile_cleaned") is True for trial in agent_task_trials + ) agent_task_isolation_complete = all( trial.get("profile_pristine_before_launch") is True and trial.get("ambient_cookies_absent") is True @@ -1511,6 +1531,7 @@ def main() -> int: "repeatability_trials": AGENT_TASK_REPEATABILITY_TRIALS, "successful_trials": agent_task_successful_trials, "trial_pass_rate": agent_task_trial_pass_rate, + "profiles_cleaned": agent_task_profiles_cleaned, "isolation_complete": agent_task_isolation_complete, "trial_results": agent_task_trials, "forced_close": { @@ -1529,6 +1550,8 @@ def main() -> int: ) if not common_surfaces or not all(common_surfaces.values()): raise RuntimeError("Manifest V3 repeatability surfaces were incomplete") + if not agent_task_profiles_cleaned: + raise RuntimeError("Agent Task profile cleanup gate failed") if agent_task_successful_trials != AGENT_TASK_REPEATABILITY_TRIALS: raise RuntimeError( "Agent Task repeatability gate failed: " From b6583d54d5ccbf2d8028e51035fb5fe20100d6a9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 10:08:55 +0900 Subject: [PATCH 4/9] test(browser): require forced-close failure cleanup evidence --- ...est_agent_task_failure_cleanup_contract.py | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/test_agent_task_failure_cleanup_contract.py b/tests/test_agent_task_failure_cleanup_contract.py index ab5d0edbd..68a2326e0 100644 --- a/tests/test_agent_task_failure_cleanup_contract.py +++ b/tests/test_agent_task_failure_cleanup_contract.py @@ -39,14 +39,40 @@ def fail_browser_pass(*_args: object, **_kwargs: object) -> dict[str, object]: self.assertIs(result["profile_cleaned"], True) self.assertNotIn("synthetic controlled browser failure", repr(result)) + def test_failed_forced_close_pass_returns_profile_cleanup_evidence(self) -> None: + """A forced-close probe failure must still prove that its task profile was removed.""" + + namespace = self._namespace("agent_task_forced_close_cleanup_behavior") + run_trial = namespace["_run_agent_task_forced_close_trial"] + + def fail_browser_pass(*_args: object, **_kwargs: object) -> dict[str, object]: + raise RuntimeError("synthetic forced-close browser failure") + + run_trial.__globals__["_run_agent_task_forced_close_browser_pass"] = fail_browser_pass + result = run_trial( + pathlib.Path("controlled-chrome"), + pathlib.Path("controlled-chromedriver"), + "http://127.0.0.1/controlled-fixture", + 9, + ) + + self.assertEqual(result["trial_number"], 9) + self.assertIs(result["passed"], False) + self.assertEqual(result["failure_type"], "RuntimeError") + self.assertIs(result["profile_cleaned"], True) + self.assertNotIn("synthetic forced-close browser failure", repr(result)) + def test_acceptance_gate_requires_cleanup_evidence_for_every_trial(self) -> None: - """Failed trials must not be filtered out of the aggregate profile-cleanup gate.""" + """Failed trials must not be filtered out of either profile-cleanup gate.""" runner = RUNNER.read_text(encoding="utf-8") self.assertIn("agent_task_profiles_cleaned = all(", runner) + self.assertIn("forced_close_profiles_cleaned = all(", runner) self.assertIn('trial.get("profile_cleaned") is True', runner) self.assertIn('"profiles_cleaned": agent_task_profiles_cleaned', runner) + self.assertIn('"profiles_cleaned": forced_close_profiles_cleaned', runner) self.assertIn("Agent Task profile cleanup gate failed", runner) + self.assertIn("Agent Task forced-close profile cleanup gate failed", runner) if __name__ == "__main__": From 4b474260e777c84335636b2fbd732a00e48159d4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 10:17:41 +0900 Subject: [PATCH 5/9] fix(browser): retain forced-close cleanup evidence on failure --- scripts/ci/run_mv3_compatibility.py | 41 ++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/scripts/ci/run_mv3_compatibility.py b/scripts/ci/run_mv3_compatibility.py index 7745c1a99..c81ebf642 100755 --- a/scripts/ci/run_mv3_compatibility.py +++ b/scripts/ci/run_mv3_compatibility.py @@ -1291,34 +1291,51 @@ def _run_agent_task_forced_close_trial( fixture_url: str, trial_number: int, ) -> dict[str, Any]: - """Run one forced-close probe and prove its isolated browser profile is removed.""" + """Run one forced-close trial and retain cleanup evidence on failure.""" trial_started = time.monotonic() profile_path: pathlib.Path + result: dict[str, Any] | None = None + failure_type: str | None = None with tempfile.TemporaryDirectory( prefix=f"originweave-agent-task-forced-close-{trial_number}-" ) as profile_dir: profile_path = pathlib.Path(profile_dir) - result = _run_agent_task_forced_close_browser_pass( - chrome_bin, - chromedriver_bin, - fixture_url, - profile_dir, - ) + try: + result = _run_agent_task_forced_close_browser_pass( + chrome_bin, + chromedriver_bin, + fixture_url, + profile_dir, + ) + except (OSError, ValueError, RuntimeError, json.JSONDecodeError) as exc: + failure_type = type(exc).__name__ profile_cleaned = not profile_path.exists() if not profile_cleaned: raise RuntimeError( f"Agent Task forced-close profile cleanup failed in trial {trial_number}" ) + duration_ms = round((time.monotonic() - trial_started) * 1000) + if failure_type is not None: + return { + "trial_number": trial_number, + "passed": False, + "failure_type": failure_type, + "profile_cleaned": True, + "duration_ms": duration_ms, + } + if result is None: + raise RuntimeError("Agent Task forced-close browser pass returned no result") + return { "trial_number": trial_number, "passed": True, "browser_version": result["browser_version"], "forced_close_detected": result["forced_close_detected"], "session_survived": result["session_survived"], - "profile_cleaned": profile_cleaned, - "duration_ms": round((time.monotonic() - trial_started) * 1000), + "profile_cleaned": True, + "duration_ms": duration_ms, } @@ -1506,6 +1523,9 @@ def main() -> int: forced_close_successful_trials = sum( 1 for trial in forced_close_trials if trial.get("passed") is True ) + forced_close_profiles_cleaned = all( + trial.get("profile_cleaned") is True for trial in forced_close_trials + ) forced_close_surfaces_complete = all( trial.get("forced_close_detected") is True and trial.get("session_survived") is True @@ -1537,6 +1557,7 @@ def main() -> int: "forced_close": { "repeatability_trials": AGENT_TASK_REPEATABILITY_TRIALS, "successful_trials": forced_close_successful_trials, + "profiles_cleaned": forced_close_profiles_cleaned, "trial_results": forced_close_trials, }, }, @@ -1562,6 +1583,8 @@ def main() -> int: raise RuntimeError("Agent Task isolation gate failed") if not agent_task_surfaces_complete: raise RuntimeError("Agent Task repeatability surfaces were incomplete") + if not forced_close_profiles_cleaned: + raise RuntimeError("Agent Task forced-close profile cleanup gate failed") if ( forced_close_successful_trials != AGENT_TASK_REPEATABILITY_TRIALS or not forced_close_surfaces_complete From 499a6499112a11094279a1386238e26987d771d5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 10:22:13 +0900 Subject: [PATCH 6/9] docs(changelog): record failed Agent Task cleanup evidence --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc0df6049..c1d9f113c 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 +- 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. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. - Canonical HTTPS and loopback-origin boundary with case-normalized schemes and hosts, default-port normalization, IPv4/IPv6 handling, browser-special numeric-host rejection, and explicit malformed-input errors. - Typed browser actions, capabilities, risk classes, execution modes, robots decisions, secret-delivery contracts, immutable canonical action-intent digests, and intent-bound approval scopes. From e41d27a696457f506fa84b11717eb8c6cf5c5261 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 19:52:22 +0900 Subject: [PATCH 7/9] docs: inherit browser stack release notes --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1d9f113c..d73c09ad9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ All notable changes to OriginWeave are documented in this file. The format follo - Rust 1.97.1 build contract, strict Clippy and rustdoc gates, and exact production function, line, region, and branch coverage enforcement. - Pinned Chrome-for-Testing Agent Task evidence now captures a bounded sampled Chromium root-plus-descendant process count and RSS total from one `/proc` status sweep, with bounded failure-type diagnostics while preserving the root-only metric and making no trusted per-task attribution claim. - Pinned Chrome-for-Testing Agent Task evidence now locates the controlled result by exact browser-computed `status`/`Task result` semantics and records only a bounded canonical SHA-256 digest plus stable field identity for the extracted synthetic value, without emitting the raw value. +- Pinned Chrome-for-Testing Agent Task semantic-observation evidence is now canonicalized as compact sorted-key UTF-8 JSON and capped at 4,096 bytes, accepting the exact limit while failing closed on empty, non-object, or oversized observations before they enter successful trial evidence. +- Pinned Chrome-for-Testing forced-close recovery evidence now closes a disposable controlled browsing context, requires structured exact `no such window` failure on the next current-context command, proves a separate survivor context remains usable in the same WebDriver session, repeats the probe with isolated profile cleanup, and fails closed on substring lookalikes or malformed error framing. - Controlled pinned-Chromium Agent Task acceptance now fails closed unless the temporary profile is pristine before launch, browser-observed cookies and Web Storage are empty, saved-credential services are disabled, extensions are disabled by launch policy, and the profile is removed afterward; bounded per-trial evidence does not claim OS- or browser-attested absence of every credential mechanism. - Hourly bounded OpenCode product-development workflow using `NVIDIA_NIM_API_KEY`, an unprivileged disposable workspace, loopback-only model broker, independently verified patches, and publication through a dedicated `OPENCODE_PR_TOKEN` that cannot review or merge. - Architecture, agent, security, contribution, research, database naming, roadmap, quality-gate, and TLS service-identity ADR documentation. From 0e66b24395aae595c569ad86b46e76ca3aad3a4a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 20:31:47 -0700 Subject: [PATCH 8/9] test(browser): reproduce teardown timeout cleanup gap --- ...est_agent_task_failure_cleanup_contract.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/test_agent_task_failure_cleanup_contract.py b/tests/test_agent_task_failure_cleanup_contract.py index 68a2326e0..7d0233d69 100644 --- a/tests/test_agent_task_failure_cleanup_contract.py +++ b/tests/test_agent_task_failure_cleanup_contract.py @@ -4,6 +4,7 @@ import pathlib import runpy +import subprocess import unittest ROOT = pathlib.Path(__file__).resolve().parents[1] @@ -39,6 +40,32 @@ def fail_browser_pass(*_args: object, **_kwargs: object) -> dict[str, object]: self.assertIs(result["profile_cleaned"], True) self.assertNotIn("synthetic controlled browser failure", repr(result)) + def test_teardown_timeout_returns_profile_cleanup_evidence(self) -> None: + """A reviewed process teardown timeout must become one failed trial, not abort the run.""" + + namespace = self._namespace("agent_task_teardown_timeout_cleanup_behavior") + run_trial = namespace["_run_agent_task_trial"] + + def timeout_browser_pass(*_args: object, **_kwargs: object) -> dict[str, object]: + raise subprocess.TimeoutExpired( + cmd="private-controlled-chromedriver-path", + timeout=5, + ) + + run_trial.__globals__["_run_agent_task_browser_pass"] = timeout_browser_pass + result = run_trial( + pathlib.Path("controlled-chrome"), + pathlib.Path("controlled-chromedriver"), + "http://127.0.0.1/controlled-fixture", + 8, + ) + + self.assertEqual(result["trial_number"], 8) + self.assertIs(result["passed"], False) + self.assertEqual(result["failure_type"], "TimeoutExpired") + self.assertIs(result["profile_cleaned"], True) + self.assertNotIn("private-controlled-chromedriver-path", repr(result)) + def test_failed_forced_close_pass_returns_profile_cleanup_evidence(self) -> None: """A forced-close probe failure must still prove that its task profile was removed.""" @@ -62,6 +89,32 @@ def fail_browser_pass(*_args: object, **_kwargs: object) -> dict[str, object]: self.assertIs(result["profile_cleaned"], True) self.assertNotIn("synthetic forced-close browser failure", repr(result)) + def test_forced_close_teardown_timeout_returns_profile_cleanup_evidence(self) -> None: + """Forced-close teardown timeout must retain cleanup evidence without raw command text.""" + + namespace = self._namespace("agent_task_forced_close_teardown_timeout_behavior") + run_trial = namespace["_run_agent_task_forced_close_trial"] + + def timeout_browser_pass(*_args: object, **_kwargs: object) -> dict[str, object]: + raise subprocess.TimeoutExpired( + cmd="private-forced-close-chromedriver-path", + timeout=5, + ) + + run_trial.__globals__["_run_agent_task_forced_close_browser_pass"] = timeout_browser_pass + result = run_trial( + pathlib.Path("controlled-chrome"), + pathlib.Path("controlled-chromedriver"), + "http://127.0.0.1/controlled-fixture", + 10, + ) + + self.assertEqual(result["trial_number"], 10) + self.assertIs(result["passed"], False) + self.assertEqual(result["failure_type"], "TimeoutExpired") + self.assertIs(result["profile_cleaned"], True) + self.assertNotIn("private-forced-close-chromedriver-path", repr(result)) + def test_acceptance_gate_requires_cleanup_evidence_for_every_trial(self) -> None: """Failed trials must not be filtered out of either profile-cleanup gate.""" From 10a60f59046c1a29a6e73084804aee0203798bbb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 20:38:02 -0700 Subject: [PATCH 9/9] fix(browser): retain teardown timeout failure evidence --- scripts/ci/run_mv3_compatibility.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/scripts/ci/run_mv3_compatibility.py b/scripts/ci/run_mv3_compatibility.py index c81ebf642..b19850d82 100755 --- a/scripts/ci/run_mv3_compatibility.py +++ b/scripts/ci/run_mv3_compatibility.py @@ -1042,7 +1042,13 @@ def _run_agent_task_trial( fixture_url, profile_dir, ) - except (OSError, ValueError, RuntimeError, json.JSONDecodeError) as exc: + except ( + OSError, + ValueError, + RuntimeError, + json.JSONDecodeError, + subprocess.TimeoutExpired, + ) as exc: failure_type = type(exc).__name__ profile_cleaned = not profile_path.exists() if not profile_cleaned: @@ -1308,7 +1314,13 @@ def _run_agent_task_forced_close_trial( fixture_url, profile_dir, ) - except (OSError, ValueError, RuntimeError, json.JSONDecodeError) as exc: + except ( + OSError, + ValueError, + RuntimeError, + json.JSONDecodeError, + subprocess.TimeoutExpired, + ) as exc: failure_type = type(exc).__name__ profile_cleaned = not profile_path.exists() if not profile_cleaned: