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
6 changes: 5 additions & 1 deletion docs/doctoring/mv3-compatibility.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Manifest V3 compatibility evidence baseline

- **Status:** Active implementation evidence for issue #27
- **Reviewed:** 2026-08-21
- **Reviewed:** 2026-09-04
- **Pinned browser:** Chrome for Testing `150.0.7871.129`, Chromium revision `r1639810`

OriginWeave uses Chromium as its compatibility kernel, so browser-extension compatibility must be demonstrated with executable Chromium evidence rather than inferred from architecture alone. The protected-main lane exercises a controlled unpacked Manifest V3 extension against one exact Chrome for Testing build and proves service-worker, content-script, storage, declarative-network-request, tabs, windows, scripting, commands, side-panel, bookmarks/history read compatibility, restart persistence, repeatability, and one real WebDriver click/post-condition. Active stacked compatibility work adds downloads, bounded bookmark/history mutation, profile isolation, explicit extension update/version-migration evidence, and an exact content-script isolated-world check. OriginWeave does **not claim 100% Chrome extension compatibility**.
Expand Down Expand Up @@ -48,6 +48,8 @@ For downloads compatibility specifically, the current official Chrome Extensions

RFC 9112 requires a well-formed HTTP/1.1 status-line and a message body that matches the announced framing. W3C WebDriver sends commands over that HTTP transport. When ChromeDriver returns a malformed status-line or an incomplete body, the compatibility runner raises only `WebDriver transport protocol failure`; when a WebDriver response supplies a recognized protocol error, it retains only an allow-listed error code. Raw status-line text, partial body bytes, paths, URLs, browser messages, or tokens must not enter exception text or trial evidence. This classification lets `main` record the failure in `trial_results` instead of aborting the compatibility run with an unclassified parser exception.

The 2 July 2026 WebDriver Working Draft defines remote error responses with a standardized error code plus implementation-defined `message` and `stacktrace` strings and optional `data`; its user-prompt example shows page-originated prompt text in `data.text`. OriginWeave therefore treats remote-end diagnostic text and returned capability values as untrusted evidence input. The compatibility runner retains only locally owned or explicit allow-list classifications: HTTP status, a reviewed WebDriver error code, `failure_kind`, sanitized fixture-surface tokens, and the expected pinned browser version. Raw response bodies, `message`, `stacktrace`, `data`, capability values, startup exception text, DOM values, and returned page text are not copied into CI/audit exceptions. This is an evidence-provenance boundary, not browser-policy authority and not an authentication claim about WebDriver, Chromium, or page content.

## ChromeDriver startup-record robustness boundary

ChromeDriver startup stdout is diagnostic input, not authority. The compatibility runner retains at most `MAX_CHROMEDRIVER_STARTUP_LINE_BYTES + 1` bytes from one record and drains the remainder through bounded reads. A prefixed record that is oversized, lacks the required terminal period, carries a non-decimal port, or names a port outside `1..65535` is treated as non-authoritative and ignored while the existing bounded startup wait continues. A later well-formed candidate can therefore recover from malformed-but-expected startup diagnostics without turning the malformed record into success.
Expand Down Expand Up @@ -95,3 +97,5 @@ Google Chrome Labs. (2026, July 21). *Chrome for Testing availability*. https://
Fielding, R., Nottingham, M., & Reschke, J. (Eds.). (2022). *HTTP/1.1* (RFC 9112). Internet Engineering Task Force. https://doi.org/10.17487/RFC9112

World Wide Web Consortium. (2018, June 5). *WebDriver* (W3C Recommendation). https://www.w3.org/TR/2018/REC-webdriver1-20180605/

World Wide Web Consortium. (2026, July 2). *WebDriver* (Working Draft). https://www.w3.org/TR/2026/WD-webdriver2-20260702/
175 changes: 175 additions & 0 deletions tests/test_mv3_fixture_evidence_redaction_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
"""Regression contracts for MV3 evidence diagnostic redaction."""

from __future__ import annotations

import pathlib
import runpy
import tempfile
import unittest
import unittest.mock

ROOT = pathlib.Path(__file__).resolve().parents[1]
RUNNER = ROOT / "scripts" / "ci" / "run_mv3_compatibility.py"
SENSITIVE_MARKERS = (
"secret-token",
"/home/runner/private",
"example.invalid",
)


class ManifestV3FixtureEvidenceRedactionTests(unittest.TestCase):
"""Prevent page-controlled fixture values from entering CI diagnostics."""

def assert_redacted(self, rendered: str) -> None:
"""Require representative token, path, and URL material to be absent."""

for marker in SENSITIVE_MARKERS:
with self.subTest(marker=marker):
self.assertNotIn(marker, rendered)

def test_fixture_timeout_does_not_echo_page_controlled_values(self) -> None:
"""A non-converging fixture must report local classifications, not DOM values."""

namespace = runpy.run_path(str(RUNNER), run_name="mv3_fixture_redaction_contract")
wait_for_extension_evidence = namespace["_wait_for_extension_evidence"]
sensitive = "secret-token /home/runner/private https://example.invalid"
hostile = {
"content": sensitive,
"storage": sensitive,
"storagePersistence": sensitive,
"workerReply": sensitive,
"workerState": sensitive,
"workerStartCount": "0",
"dnr": sensitive,
"tabs": sensitive,
"windows": sensitive,
"scripting": sensitive,
"scriptingExecuted": sensitive,
"commands": sensitive,
"sidePanel": sensitive,
"bookmarks": sensitive,
"history": sensitive,
}

with unittest.mock.patch.dict(
wait_for_extension_evidence.__globals__,
{
"_execute": unittest.mock.Mock(return_value=hostile),
"FIXTURE_TIMEOUT_SECONDS": 1.0,
},
), unittest.mock.patch.object(
namespace["time"],
"monotonic",
side_effect=(0.0, 0.0, 2.0),
), unittest.mock.patch.object(namespace["time"], "sleep", return_value=None):
with self.assertRaises(RuntimeError) as raised:
wait_for_extension_evidence(9515, "session", "initialized")

self.assert_redacted(str(raised.exception))

def test_click_mismatch_does_not_echo_page_controlled_text(self) -> None:
"""A click post-condition mismatch must not retain returned DOM text."""

namespace = runpy.run_path(str(RUNNER), run_name="mv3_click_redaction_contract")
exercise_real_click = namespace["_exercise_real_click"]
element_key = namespace["W3C_ELEMENT_KEY"]
sensitive = "secret-token /home/runner/private https://example.invalid"
responses = (
{"value": {element_key: "element.one"}},
{},
{"value": {element_key: "element.two"}},
{"value": sensitive},
)

with unittest.mock.patch.dict(
exercise_real_click.__globals__,
{"_json_request": unittest.mock.Mock(side_effect=responses)},
):
with self.assertRaises(RuntimeError) as raised:
exercise_real_click(9515, "session")

self.assert_redacted(str(raised.exception))

def test_driver_startup_timeout_does_not_echo_remote_error_details(self) -> None:
"""Startup timeout evidence must classify failure without retaining exception text."""

namespace = runpy.run_path(str(RUNNER), run_name="mv3_driver_startup_redaction_contract")
wait_for_driver = namespace["_wait_for_driver"]
sensitive = "secret-token /home/runner/private https://example.invalid"

with unittest.mock.patch.dict(
wait_for_driver.__globals__,
{
"_json_request": unittest.mock.Mock(side_effect=RuntimeError(sensitive)),
"STARTUP_TIMEOUT_SECONDS": 1.0,
},
), unittest.mock.patch.object(
namespace["time"],
"monotonic",
side_effect=(0.0, 0.0, 2.0),
), unittest.mock.patch.object(namespace["time"], "sleep", return_value=None):
with self.assertRaises(RuntimeError) as raised:
wait_for_driver(9515)

self.assert_redacted(str(raised.exception))

def test_browser_version_mismatch_does_not_echo_remote_capability_value(self) -> None:
"""A pin mismatch must not copy the WebDriver capability value into evidence."""

namespace = runpy.run_path(str(RUNNER), run_name="mv3_browser_version_redaction_contract")
run_browser_pass = namespace["_run_browser_pass"]
sensitive = "secret-token /home/runner/private https://example.invalid"

class FakeDriver:
def terminate(self) -> None:
return None

def wait(self, timeout: float) -> int:
_ = timeout
return 0

def kill(self) -> None:
return None

def fake_json_request(
_driver_port: int,
method: str,
path: str,
_payload: object = None,
**_kwargs: object,
) -> dict[str, object]:
if method == "POST" and path == "/session":
return {
"value": {
"sessionId": "session.one",
"capabilities": {"browserVersion": sensitive},
}
}
if method == "DELETE":
return {"value": None}
raise AssertionError(f"unexpected request: {method} {path}")

with unittest.mock.patch.dict(
run_browser_pass.__globals__,
{
"_start_chromedriver": unittest.mock.Mock(
return_value=(FakeDriver(), 9515)
),
"_wait_for_driver": unittest.mock.Mock(return_value=None),
"_json_request": fake_json_request,
},
), tempfile.TemporaryDirectory() as profile_dir:
with self.assertRaises(RuntimeError) as raised:
run_browser_pass(
pathlib.Path("/controlled/chrome"),
pathlib.Path("/controlled/chromedriver"),
"http://127.0.0.1:9516/page.html",
profile_dir,
"initialized",
)

self.assert_redacted(str(raised.exception))


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