Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,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.
- 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.
- 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.
Expand Down
77 changes: 60 additions & 17 deletions scripts/ci/run_mv3_compatibility.py
Comment thread
seonghobae marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -723,26 +723,55 @@ def _run_restart_trial(
fixture_url: str,
trial_number: int,
) -> dict[str, Any]:
"""Run one independent initial/restart pair and return credential-free evidence."""
"""Run one independent initial/restart pair and retain cleanup evidence on failure."""

trial_started = time.monotonic()
profile_path: pathlib.Path
initial: dict[str, Any] | None = None
restarted: dict[str, Any] | None = None
failure_type: str | None = None
with tempfile.TemporaryDirectory(
prefix=f"originweave-mv3-trial-{trial_number}-"
) as profile_dir:
initial = _run_browser_pass(
chrome_bin,
chromedriver_bin,
fixture_url,
profile_dir,
"initialized",
)
restarted = _run_browser_pass(
chrome_bin,
chromedriver_bin,
fixture_url,
profile_dir,
"persisted",
)
profile_path = pathlib.Path(profile_dir)
try:
initial = _run_browser_pass(
chrome_bin,
chromedriver_bin,
fixture_url,
profile_dir,
"initialized",
)
restarted = _run_browser_pass(
chrome_bin,
chromedriver_bin,
fixture_url,
profile_dir,
"persisted",
)
except (
OSError,
ValueError,
RuntimeError,
json.JSONDecodeError,
subprocess.TimeoutExpired,
) as exc:
failure_type = type(exc).__name__
Comment on lines +752 to +759

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Unexpected exceptions skip cleanup verification

The inner except catches only OSError, ValueError, RuntimeError, json.JSONDecodeError, and subprocess.TimeoutExpired. Other classes propagate past the profile_cleaned check (run_mv3_compatibility.py:760) and are not caught by the main loop (run_mv3_compatibility.py:1414), aborting the script with no cleanup evidence. The author states unexpected classes stay unnormalized, so this is consistent with intent.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

profile_cleaned = not profile_path.exists()
if not profile_cleaned:
raise RuntimeError(f"Manifest V3 profile cleanup failed in trial {trial_number}")
Comment on lines +760 to +762

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Cleanup check largely tautological

profile_cleaned = not profile_path.exists() runs after the TemporaryDirectory context has already deleted the directory, so it is True in nearly all paths. It only catches the rare case where deletion is blocked (which itself raises). This mirrors the existing forced-close trial.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


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 initial is None or restarted is None:
raise RuntimeError("Manifest V3 restart trial returned incomplete browser evidence")

initial_count = int(initial["worker_start_count"])
restarted_count = int(restarted["worker_start_count"])
Expand All @@ -758,7 +787,14 @@ def _run_restart_trial(
}
)
if not all(surfaces.values()):
raise RuntimeError(f"compatibility surface failed in trial {trial_number}")
return {
"trial_number": trial_number,
"passed": False,
"failure_type": "CompatibilitySurfaceFailure",
"surfaces": surfaces,
"profile_cleaned": True,
"duration_ms": duration_ms,
}

return {
"trial_number": trial_number,
Expand All @@ -777,7 +813,8 @@ def _run_restart_trial(
"storage_persistence": restarted["storage_persistence"],
},
],
"duration_ms": round((time.monotonic() - trial_started) * 1000),
"profile_cleaned": True,
"duration_ms": duration_ms,
}


Expand Down Expand Up @@ -1424,6 +1461,9 @@ def main() -> int:
1 for trial in trial_results if trial.get("passed") is True
)
trial_pass_rate = successful_trials / REPEATABILITY_TRIALS
mv3_profiles_cleaned = all(
trial.get("profile_cleaned") is True for trial in trial_results
)
Comment thread
seonghobae marked this conversation as resolved.
successful_results = [
trial for trial in trial_results if trial.get("passed") is True
]
Expand Down Expand Up @@ -1552,6 +1592,7 @@ def main() -> int:
"repeatability_trials": REPEATABILITY_TRIALS,
"successful_trials": successful_trials,
"trial_pass_rate": trial_pass_rate,
"profiles_cleaned": mv3_profiles_cleaned,
"surfaces": common_surfaces,
"trial_results": trial_results,
"browser_passes": (
Expand All @@ -1576,6 +1617,8 @@ def main() -> int:
"duration_ms": round((time.monotonic() - started) * 1000),
}
print(json.dumps(evidence, sort_keys=True))
if not mv3_profiles_cleaned:
raise RuntimeError("Manifest V3 profile cleanup gate failed")
if successful_trials != REPEATABILITY_TRIALS:
raise RuntimeError(
"Manifest V3 repeatability gate failed: "
Expand Down
88 changes: 88 additions & 0 deletions tests/test_mv3_failure_profile_cleanup_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Contract for MV3 restart-trial profile cleanup evidence on browser failure."""

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 Mv3FailureProfileCleanupContractTests(unittest.TestCase):
"""Require failed extension-compatibility trials to retain teardown evidence."""

def _namespace(self, name: str) -> dict[str, object]:
return runpy.run_path(str(RUNNER), run_name=name)

def test_failed_restart_pass_returns_profile_cleanup_evidence(self) -> None:
"""A failed initial or restarted browser pass must retain profile cleanup proof."""

namespace = self._namespace("mv3_failure_cleanup_behavior")
run_trial = namespace["_run_restart_trial"]

def fail_browser_pass(*_args: object, **_kwargs: object) -> dict[str, object]:
raise RuntimeError("synthetic MV3 browser failure")

run_trial.__globals__["_run_browser_pass"] = fail_browser_pass
result = run_trial(
pathlib.Path("controlled-chrome"),
pathlib.Path("controlled-chromedriver"),
"http://127.0.0.1/controlled-fixture",
11,
)

self.assertEqual(result["trial_number"], 11)
self.assertIs(result["passed"], False)
self.assertEqual(result["failure_type"], "RuntimeError")
self.assertIs(result["profile_cleaned"], True)
self.assertNotIn("synthetic MV3 browser failure", repr(result))

def test_surface_failure_preserves_cleanup_and_surface_evidence(self) -> None:
"""A compatibility regression must not be mislabeled as profile cleanup failure."""

namespace = self._namespace("mv3_surface_failure_cleanup_behavior")
run_trial = namespace["_run_restart_trial"]
browser_pass_number = 0

def browser_pass(*_args: object, **_kwargs: object) -> dict[str, object]:
nonlocal browser_pass_number
browser_pass_number += 1
persisted = browser_pass_number == 2
return {
"browser_version": "controlled-browser",
"worker_start_count": browser_pass_number,
"storage_persistence": "persisted" if persisted else "initialized",
"surfaces": {
"service-worker": True,
"real-browser-click": not persisted,
},
}

run_trial.__globals__["_run_browser_pass"] = browser_pass
result = run_trial(
pathlib.Path("controlled-chrome"),
pathlib.Path("controlled-chromedriver"),
"http://127.0.0.1/controlled-fixture",
12,
)

self.assertEqual(result["trial_number"], 12)
self.assertIs(result["passed"], False)
self.assertEqual(result["failure_type"], "CompatibilitySurfaceFailure")
self.assertIs(result["profile_cleaned"], True)
self.assertIs(result["surfaces"]["real-browser-click"], False)

def test_acceptance_gate_requires_cleanup_evidence_for_every_mv3_trial(self) -> None:
"""Failed MV3 trials must not be filtered out of the profile-cleanup gate."""

runner = RUNNER.read_text(encoding="utf-8")
self.assertIn("mv3_profiles_cleaned = all(", runner)
self.assertIn('trial.get("profile_cleaned") is True', runner)
self.assertIn('"profiles_cleaned": mv3_profiles_cleaned', runner)
self.assertIn("Manifest V3 profile cleanup gate failed", runner)


if __name__ == "__main__":
unittest.main()
45 changes: 45 additions & 0 deletions tests/test_mv3_restart_timeout_cleanup_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Regression contract for MV3 restart teardown timeout cleanup evidence."""

from __future__ import annotations

import pathlib
import runpy
import subprocess
import unittest

ROOT = pathlib.Path(__file__).resolve().parents[1]
RUNNER = ROOT / "scripts" / "ci" / "run_mv3_compatibility.py"


class Mv3RestartTimeoutCleanupContractTests(unittest.TestCase):
"""Require reviewed ChromeDriver teardown timeouts to retain MV3 cleanup proof."""

def test_teardown_timeout_returns_profile_cleanup_evidence(self) -> None:
"""A bounded process teardown timeout must become failed-trial evidence."""

namespace = runpy.run_path(str(RUNNER), run_name="mv3_restart_timeout_cleanup")
run_trial = namespace["_run_restart_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_browser_pass"] = timeout_browser_pass
result = run_trial(
pathlib.Path("controlled-chrome"),
pathlib.Path("controlled-chromedriver"),
"http://127.0.0.1/controlled-fixture",
12,
)

self.assertEqual(result["trial_number"], 12)
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))


if __name__ == "__main__":
unittest.main()
Loading