browser: click-path allowlist, safe tenant script upgrade, allowlist activation plan

- click/click_ref: pre-click allowlist gate on resolvable anchor href
  (same check as navigate; miss refuses before the click executes);
  click/click_ref/click_at: post-click landing URL check.
  click_at has no pre-click gate by design (coordinates name no
  element); JS-driven navigation, evaluate, meta refresh and popups
  are documented as unintercepable with the Tinyproxy 403 backstop.
- Tenant script upgrade is now safe: only proven-unmodified scripts
  (manifest match, 2a569ac known-shipped SHA-256, missing, or
  current) auto-merge; tenant-modified scripts are never overwritten,
  are reported as skipped, and get one deduped audit record. The
  manifest never learns a tenant hash (else the next upgrade would
  mistake a modified file for untouched). Idempotent reruns.
- Activation candidate = deployed Tinyproxy programming set encoded
  with leading-dot entries (plain entries would regress
  docs.python.org); npmjs.org per the deployed filter, not npmjs.com.
  Host activation itself is NOT performed this round: only the plan,
  validation tests and rollback are delivered.
Tests: 28 new (click 13, upgrade 8, activation 7), all fake/no-network;
98 browser-related tests pass including prior suites.

(cherry picked from commit e4a93f5748ec815f163d0bc096ff4dc4b8c651f5)
This commit is contained in:
host committed 2026-11-01 09:06:28 +00:00
1 parent 1141d0d0f8
commit 125b436ab3
8 files changed
+1150 -33

No files matched your search

+209
View File
@@ -0,0 +1,209 @@
"""BROWSER_ALLOWED_HOSTS activation-candidate validation.
The candidate value is the ALREADY-DEPLOYED Tinyproxy
programming-domain set (deploy/egress/tinyproxy-filter) plus the
already-present github.com / pypi.org / npmjs.org -- no invented
domains. NOTE: the task text said "npmjs.com", but the deployed
filter (tinyproxy-filter line 23, new-host-checklist.md, and
docs/platform-browser.md) all say npmjs.org, so the candidate
follows the DEPLOYED set (npmjs.org); npmjs.com is deliberately
NOT in the candidate -- adding it would widen the allowlist
beyond the proxy, breaking allowlist==proxy parity. If the npm
website itself must be browsable, that is a separate filter
change for the parent to decide, not something this round
invents. Because the Tinyproxy lines are subdomain-inclusive
(``^\\(.*\\.\\)*domain$``), the env value uses leading-dot
entries, which are the allowlist encoding of the same
subdomain-inclusive set; plain entries would NOT match
subdomains (proven by test_plain_entries_would_break_subdomains)
and would therefore regress docs.python.org etc.
All checks run against fake browsers / the filter file itself:
no production tenant, no network, no real credentials.
"""
import os
import re
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from playwright_browser import Browser, BrowserError # noqa: E402
from playwright_browser import _check_allowed_url # noqa: E402
# The activation candidate: the deployed Tinyproxy programming
# set, encoded subdomain-inclusively. Domain SET is fixed by the
# task (no new domains); only the encoding (dot prefix) follows
# from the filter's own subdomain rule.
CANDIDATE_DOMAINS = [
"python.org", "nodejs.org", "go.dev", "pkg.go.dev",
"rust-lang.org", "developer.mozilla.org", "crates.io",
"stackoverflow.com", "cve.mitre.org", "nvd.nist.gov",
"github.com", "pypi.org", "npmjs.org",
]
CANDIDATE = ",".join("." + d for d in CANDIDATE_DOMAINS)
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__),
"..", ".."))
FILTER_PATH = os.path.join(REPO_ROOT, "deploy", "egress",
"tinyproxy-filter")
class _FakePage:
def __init__(self):
self.url = ""
self.calls = []
self.mouse = self
def title(self):
return "T"
def goto(self, url, timeout=None, wait_until=None):
self.calls.append(("goto", url))
self.url = url
def locator(self, selector):
raise AssertionError("no locator use in activation tests")
def click(self, selector, timeout=None):
self.calls.append(("click", selector))
def close(self):
pass
class _FakeHandle:
def __init__(self, page):
self._page = page
def new_page(self, viewport=None):
return self._page
def close(self):
pass
class _FakePW:
def __init__(self, page):
self._page = page
self.launch_count = 0
def start(self):
return self
@property
def chromium(self):
outer = self
class _Chromium:
def launch(self, **kwargs):
outer.launch_count += 1
return _FakeHandle(outer._page)
return _Chromium()
def stop(self):
pass
class _EnvGuard:
def setUp(self):
self._old = os.environ.get("BROWSER_ALLOWED_HOSTS")
def tearDown(self):
if self._old is None:
os.environ.pop("BROWSER_ALLOWED_HOSTS", None)
else:
os.environ["BROWSER_ALLOWED_HOSTS"] = self._old
def _filter_domains():
"""Domains covered by subdomain-inclusive filter lines."""
with open(FILTER_PATH, encoding="utf-8") as fh:
lines = fh.read().splitlines()
out = set()
for line in lines:
m = re.match(r"^\^\\\(\.\*\\.\\\)\*(.+)\$$", line)
if m:
out.add(m.group(1).replace("\\.", "."))
return out
class ActivationCandidateTest(_EnvGuard, unittest.TestCase):
def setUp(self):
_EnvGuard.setUp(self)
os.environ["BROWSER_ALLOWED_HOSTS"] = CANDIDATE
self._tmp = tempfile.TemporaryDirectory()
self.page = _FakePage()
self.factory = _FakePW(self.page)
self.browser = Browser(cwd=self._tmp.name,
proxy="http://proxy:8888",
browser_factory=lambda: self.factory)
def tearDown(self):
self._tmp.cleanup()
_EnvGuard.tearDown(self)
def test_candidate_matches_deployed_filter_set(self):
# Every candidate domain has a subdomain-inclusive line
# in the deployed filter; the candidate invents nothing.
filt = _filter_domains()
for domain in CANDIDATE_DOMAINS:
self.assertIn(domain, filt, domain)
def test_candidate_allows_bare_and_subdomains(self):
for domain in CANDIDATE_DOMAINS:
_check_allowed_url("https://%s/" % domain)
_check_allowed_url("https://www.%s/docs" % domain)
# The cases the filter explicitly exists for.
_check_allowed_url("https://docs.python.org/3/")
_check_allowed_url("https://docs.rust-lang.org/")
_check_allowed_url("https://api.github.com/")
_check_allowed_url("https://pypi.org/project/x/")
def test_candidate_blocks_suffix_tricks_and_outsiders(self):
for bad in ("https://notpython.org/",
"https://python.org.evil.com/",
"https://evilgithub.com/",
"https://api.diante.me/",
"https://www.amazon.com/",
"https://mail.google.com/"):
with self.assertRaises(BrowserError, msg=bad):
_check_allowed_url(bad)
def test_plain_entries_would_break_subdomains(self):
# Why the candidate uses dot forms: plain "python.org"
# does not match docs.python.org under the established
# navigate semantics, so a plain-entry activation would
# regress exactly the docs the filter was added for.
os.environ["BROWSER_ALLOWED_HOSTS"] = (
"python.org,nodejs.org,go.dev,pkg.go.dev,rust-lang.org,"
"developer.mozilla.org,crates.io,stackoverflow.com,"
"cve.mitre.org,nvd.nist.gov,github.com,pypi.org,npmjs.org")
with self.assertRaises(BrowserError):
_check_allowed_url("https://docs.python.org/3/")
def test_file_allowed_localhost_blocked_under_candidate(self):
out = self.browser.navigate("file:///tmp/page.html")
self.assertTrue(out["ok"])
with self.assertRaises(BrowserError):
self.browser.navigate("http://localhost:8000/")
# localhost blocking is a behaviour CHANGE vs unset:
# local dev servers need an explicit localhost entry.
self.assertEqual(self.factory.launch_count, 1)
def test_task_level_navigate_allow_and_block(self):
out = self.browser.navigate("https://docs.python.org/3/")
self.assertTrue(out["ok"])
launches = self.factory.launch_count
with self.assertRaises(BrowserError):
self.browser.navigate("https://evil.com/")
self.assertEqual(self.factory.launch_count, launches)
def test_unset_means_no_change(self):
os.environ.pop("BROWSER_ALLOWED_HOSTS", None)
out = self.browser.navigate("https://anything.example/")
self.assertTrue(out["ok"])
_check_allowed_url("https://anything.example/")
@@ -0,0 +1,309 @@
"""Click-path allowlist tests for the Playwright browser (fake only).
Covers the continuation of the navigate-only allowlist: click /
click_ref get the SAME check before the click when the target
anchor href is resolvable, and all three click paths get a
post-click URL check. click_at has no pre-click gate by design
(coordinates identify no element). No real Chromium, no network,
no real credentials -- values here are test placeholders.
"""
import os
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from playwright_browser import Browser, BrowserError # noqa: E402
from playwright_browser import _check_navigated_url # noqa: E402
class _FakeLocator:
def __init__(self, page, href):
self._page = page
self._href = href
@property
def first(self):
return self
def click(self, timeout=None):
self._page.calls.append(("locator_click",))
if self._page.click_landing is not None:
self._page.url = self._page.click_landing
def get_attribute(self, name):
if name == "href":
return self._href
return None
def inner_text(self, timeout=None):
return "text"
class _FakeMouse:
def __init__(self, page):
self._page = page
def click(self, x, y):
self._page.calls.append(("mouse_click", x, y))
if self._page.click_landing is not None:
self._page.url = self._page.click_landing
def move(self, x, y):
self._page.calls.append(("mouse_move", x, y))
class _FakePage:
def __init__(self):
self.url = "https://example.com/"
self.calls = []
self.href_map = {}
self.click_landing = None
self.mouse = _FakeMouse(self)
def title(self):
return "Example"
def goto(self, url, timeout=None, wait_until=None):
self.calls.append(("goto", url))
self.url = url
def locator(self, selector):
self.calls.append(("locator", selector))
return _FakeLocator(self, self.href_map.get(selector))
def click(self, selector, timeout=None):
self.calls.append(("click", selector))
if self.click_landing is not None:
self.url = self.click_landing
def fill(self, selector, text, timeout=None):
self.calls.append(("fill", selector, text))
def evaluate(self, expression):
self.calls.append(("evaluate", expression))
return None
def screenshot(self, path=None):
with open(path, "wb") as fh:
fh.write(b"\x89PNG\r\n\x1a\n" + b"\x00" * 10)
def close(self):
pass
class _FakeHandle:
def __init__(self, page):
self._page = page
def new_page(self, viewport=None):
return self._page
def close(self):
pass
class _FakePW:
def __init__(self, page):
self._page = page
self.launch_count = 0
def start(self):
return self
@property
def chromium(self):
outer = self
class _Chromium:
def launch(self, **kwargs):
outer.launch_count += 1
return _FakeHandle(outer._page)
return _Chromium()
def stop(self):
pass
class _EnvGuard:
def setUp(self):
self._old = os.environ.get("BROWSER_ALLOWED_HOSTS")
def tearDown(self):
if self._old is None:
os.environ.pop("BROWSER_ALLOWED_HOSTS", None)
else:
os.environ["BROWSER_ALLOWED_HOSTS"] = self._old
class ClickAllowlistTest(_EnvGuard, unittest.TestCase):
def setUp(self):
_EnvGuard.setUp(self)
self._tmp = tempfile.TemporaryDirectory()
self.page = _FakePage()
self.factory = _FakePW(self.page)
self.browser = Browser(cwd=self._tmp.name,
proxy="http://proxy:8888",
browser_factory=lambda: self.factory)
def tearDown(self):
self._tmp.cleanup()
_EnvGuard.tearDown(self)
def _start(self, url="https://example.com/"):
return self.browser.navigate(url)
def test_click_allowed_href_executes(self):
os.environ["BROWSER_ALLOWED_HOSTS"] = "example.com, .python.org"
self._start()
self.page.href_map["a.docs"] = "https://docs.python.org/3/"
self.page.click_landing = "https://docs.python.org/3/"
out = self.browser.click("a.docs")
self.assertTrue(out["ok"])
self.assertIn(("click", "a.docs"), self.page.calls)
self.assertEqual(out["url"], "https://docs.python.org/3/")
def test_click_blocked_href_refused_before_click(self):
os.environ["BROWSER_ALLOWED_HOSTS"] = "example.com"
self._start()
self.page.href_map["a.evil"] = "https://evil.com/x"
self.page.click_landing = "https://evil.com/x"
with self.assertRaises(BrowserError) as ctx:
self.browser.click("a.evil")
self.assertIn("evil.com", str(ctx.exception))
self.assertNotIn(("click", "a.evil"), self.page.calls)
self.assertEqual(self.browser._current_url,
"https://example.com/")
def test_click_suffix_trick_blocked(self):
os.environ["BROWSER_ALLOWED_HOSTS"] = ".python.org"
self._start("https://python.org/")
for bad in ("https://notpython.org/",
"https://python.org.evil.com/"):
self.page.href_map["a.bad"] = bad
with self.assertRaises(BrowserError):
self.browser.click("a.bad")
self.assertNotIn(("click", "a.bad"), self.page.calls)
self.page.calls.clear()
def test_click_plain_entry_is_exact_only(self):
# Same semantics as navigate: a plain entry does NOT
# match subdomains (that is what leading-dot entries
# are for). Documented, not a bug: the activation
# candidate therefore uses dot forms.
os.environ["BROWSER_ALLOWED_HOSTS"] = "python.org"
self._start("https://python.org/")
self.page.href_map["a.docs"] = "https://docs.python.org/"
with self.assertRaises(BrowserError):
self.browser.click("a.docs")
self.assertNotIn(("click", "a.docs"), self.page.calls)
def test_click_port_and_case_insensitive(self):
os.environ["BROWSER_ALLOWED_HOSTS"] = "Example.COM"
self._start()
self.page.href_map["a.p"] = "https://EXAMPLE.com:8443/p"
self.page.click_landing = "https://EXAMPLE.com:8443/p"
out = self.browser.click("a.p")
self.assertTrue(out["ok"])
def test_click_file_href_allowed(self):
os.environ["BROWSER_ALLOWED_HOSTS"] = "example.com"
self._start()
self.page.href_map["a.file"] = "file:///tmp/page.html"
self.page.click_landing = "file:///tmp/page.html"
out = self.browser.click("a.file")
self.assertTrue(out["ok"])
def test_click_localhost_needs_explicit_listing(self):
os.environ["BROWSER_ALLOWED_HOSTS"] = "example.com"
self._start()
self.page.href_map["a.local"] = "http://localhost:8000/"
with self.assertRaises(BrowserError):
self.browser.click("a.local")
self.assertNotIn(("click", "a.local"), self.page.calls)
os.environ["BROWSER_ALLOWED_HOSTS"] = "localhost"
self.page.click_landing = "http://localhost:8000/"
out = self.browser.click("a.local")
self.assertTrue(out["ok"])
def test_click_relative_href_resolves_against_current(self):
os.environ["BROWSER_ALLOWED_HOSTS"] = "example.com"
self._start("https://example.com/dir/")
self.page.href_map["a.rel"] = "/other"
self.page.click_landing = "https://example.com/other"
out = self.browser.click("a.rel")
self.assertTrue(out["ok"])
# Fragment-only hrefs never navigate to another host.
self.page.href_map["a.frag"] = "#section"
self.page.click_landing = None
out = self.browser.click("a.frag")
self.assertTrue(out["ok"])
def test_click_unknown_target_post_click_detection(self):
# Button / JS handler: no href, so no pre-click gate.
# The click DOES execute; a blocked landing is then
# reported (detection, not prevention -- the proxy 403
# is the layer that stops the load in production).
os.environ["BROWSER_ALLOWED_HOSTS"] = "example.com"
self._start()
self.page.href_map["button.go"] = None
self.page.click_landing = "https://evil.com/"
with self.assertRaises(BrowserError) as ctx:
self.browser.click("button.go")
self.assertIn("evil.com", str(ctx.exception))
self.assertIn(("click", "button.go"), self.page.calls)
# Same target landing on an allowed host is fine.
self.page.click_landing = "https://example.com/ok"
out = self.browser.click("button.go")
self.assertTrue(out["ok"])
def test_click_unset_allowlist_unchanged(self):
os.environ.pop("BROWSER_ALLOWED_HOSTS", None)
self._start("https://anything.example/")
self.page.href_map["a.evil"] = "https://evil.com/"
self.page.click_landing = "https://evil.com/"
out = self.browser.click("a.evil")
self.assertTrue(out["ok"])
self.assertIn(("click", "a.evil"), self.page.calls)
def test_click_ref_blocked_href_refused_before_click(self):
os.environ["BROWSER_ALLOWED_HOSTS"] = "example.com"
self._start()
self.browser._ref_paths = {"e0": "a.evil", "e1": "a.docs"}
self.page.href_map["a.evil"] = "https://evil.com/"
self.page.href_map["a.docs"] = "https://example.com/docs"
self.page.click_landing = "https://evil.com/"
with self.assertRaises(BrowserError):
self.browser.click_ref("e0")
self.assertNotIn(("locator_click",), self.page.calls)
self.page.click_landing = "https://example.com/docs"
out = self.browser.click_ref("e1")
self.assertTrue(out["ok"])
self.assertIn(("locator_click",), self.page.calls)
def test_click_at_post_click_only(self):
# click_at has no pre-click gate by design; a blocked
# landing raises after the click, an allowed one passes.
os.environ["BROWSER_ALLOWED_HOSTS"] = "example.com"
self._start()
self.page.click_landing = "https://evil.com/"
with self.assertRaises(BrowserError):
self.browser.click_at(100, 200)
self.assertIn(("mouse_click", 100.0, 200.0), self.page.calls)
self.page.click_landing = "https://example.com/"
out = self.browser.click_at(100, 200)
self.assertTrue(out["ok"])
def test_check_navigated_url_skips_non_page_schemes(self):
os.environ["BROWSER_ALLOWED_HOSTS"] = "example.com"
# about:blank / mailto: / javascript: never navigate
# the page to a host, so they must not raise here.
_check_navigated_url("about:blank")
_check_navigated_url("mailto:someone@example.com")
_check_navigated_url("javascript:void(0)")
_check_navigated_url("file:///tmp/page.html")
with self.assertRaises(BrowserError):
_check_navigated_url("https://evil.com/")
+216
View File
@@ -0,0 +1,216 @@
"""Safe tenant-script upgrade tests (no Docker, no network).
The rule under test: only a PROVEN-unmodified tenant script
(manifest match, known-shipped-hash match, missing, or already
current) is auto-merged. A tenant-modified script is NEVER
overwritten: it stays byte-identical, is reported as skipped,
and gets exactly one deduped audit record. Reruns are idempotent.
No real credentials appear here.
"""
import hashlib
import json
import os
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
import playwright_browser as pb # noqa: E402
from playwright_browser import ensure_codex_home_playwright # noqa: E402
NAMES = pb.PLAYWRIGHT_SCRIPT_NAMES
def _write_scripts(d, contents):
for name in NAMES:
with open(os.path.join(d, name), "w", encoding="utf-8") as fh:
fh.write(contents[name])
def _read_scripts(d):
out = {}
for name in NAMES:
with open(os.path.join(d, name), encoding="utf-8") as fh:
out[name] = fh.read()
return out
def _v1():
return {name: "# v1 %s\nprint('v1')\n" % name for name in NAMES}
def _v2():
return {name: "# v2 %s\nprint('v2')\n" % name for name in NAMES}
class UpgradeTest(unittest.TestCase):
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self.src_v1 = os.path.join(self._tmp.name, "src-v1")
self.src_v2 = os.path.join(self._tmp.name, "src-v2")
self.home = os.path.join(self._tmp.name, "home")
os.makedirs(self.src_v1)
os.makedirs(self.src_v2)
os.makedirs(self.home)
_write_scripts(self.src_v1, _v1())
_write_scripts(self.src_v2, _v2())
def tearDown(self):
self._tmp.cleanup()
def _audit_records(self):
path = os.path.join(self.home, "playwright_scripts_audit.jsonl")
if not os.path.exists(path):
return []
with open(path, encoding="utf-8") as fh:
return [json.loads(line) for line in fh if line.strip()]
def test_fresh_seed_and_idempotent_rerun(self):
out = ensure_codex_home_playwright(self.home,
script_dir=self.src_v2)
self.assertEqual(_read_scripts(self.home), _v2())
self.assertFalse(out["script_skipped_modified"])
self.assertEqual(self._audit_records(), [])
# Config section appended exactly once.
with open(os.path.join(self.home, "config.toml"),
encoding="utf-8") as fh:
config = fh.read()
self.assertEqual(config.count("[mcp_servers.playwright]"), 1)
out2 = ensure_codex_home_playwright(self.home,
script_dir=self.src_v2)
self.assertFalse(out2["script_changed"])
self.assertFalse(out2["config_changed"])
self.assertFalse(out2["script_refreshed"])
self.assertEqual(_read_scripts(self.home), _v2())
with open(os.path.join(self.home, "config.toml"),
encoding="utf-8") as fh:
self.assertEqual(
fh.read().count("[mcp_servers.playwright]"), 1)
def test_unmodified_script_auto_merges(self):
ensure_codex_home_playwright(self.home, script_dir=self.src_v1)
out = ensure_codex_home_playwright(self.home,
script_dir=self.src_v2)
self.assertEqual(set(out["script_refreshed"]), set(NAMES))
self.assertFalse(out["script_skipped_modified"])
self.assertEqual(_read_scripts(self.home), _v2())
manifest_path = os.path.join(
self.home, "playwright_scripts_manifest.json")
with open(manifest_path, encoding="utf-8") as fh:
manifest = json.load(fh)
for name in NAMES:
self.assertEqual(
manifest[name],
hashlib.sha256(_v2()[name].encode()).hexdigest())
def test_modified_script_untouched_with_audit(self):
ensure_codex_home_playwright(self.home, script_dir=self.src_v1)
tenant_path = os.path.join(self.home, NAMES[0])
with open(tenant_path, "a", encoding="utf-8") as fh:
fh.write("# tenant tweak: custom timeout\n")
with open(tenant_path, encoding="utf-8") as fh:
tenant_bytes = fh.read()
out = ensure_codex_home_playwright(self.home,
script_dir=self.src_v2)
self.assertEqual(out["script_skipped_modified"], [NAMES[0]])
self.assertEqual(out["script_differs"], [NAMES[0]])
# Modified file byte-identical; the other two auto-merged.
with open(tenant_path, encoding="utf-8") as fh:
self.assertEqual(fh.read(), tenant_bytes)
rest = _read_scripts(self.home)
for name in NAMES[1:]:
self.assertEqual(rest[name], _v2()[name])
records = self._audit_records()
self.assertEqual(len(records), 1)
self.assertEqual(records[0]["action"], "skipped_modified")
self.assertEqual(records[0]["script"], NAMES[0])
self.assertEqual(
records[0]["dst_sha256"],
hashlib.sha256(tenant_bytes.encode()).hexdigest())
def test_modified_rerun_does_not_rewrite_or_duplicate_audit(self):
ensure_codex_home_playwright(self.home, script_dir=self.src_v1)
tenant_path = os.path.join(self.home, NAMES[0])
with open(tenant_path, "a", encoding="utf-8") as fh:
fh.write("# tenant tweak\n")
with open(tenant_path, encoding="utf-8") as fh:
tenant_bytes = fh.read()
ensure_codex_home_playwright(self.home, script_dir=self.src_v2)
out = ensure_codex_home_playwright(self.home,
script_dir=self.src_v2)
self.assertFalse(out["script_changed"])
self.assertEqual(out["script_skipped_modified"], [NAMES[0]])
self.assertEqual(len(self._audit_records()), 1)
with open(tenant_path, encoding="utf-8") as fh:
self.assertEqual(fh.read(), tenant_bytes)
# The manifest must NOT have learned the tenant hash:
# otherwise the next upgrade would mistake the modified
# file for untouched and overwrite it.
with open(os.path.join(
self.home, "playwright_scripts_manifest.json"),
encoding="utf-8") as fh:
manifest = json.load(fh)
self.assertNotEqual(
manifest.get(NAMES[0]),
hashlib.sha256(tenant_bytes.encode()).hexdigest())
def test_legacy_no_manifest_known_hash_merges(self):
# Legacy tenant: files seeded before manifests existed,
# content exactly a known shipped version -> proven
# unmodified, safe to merge even with no manifest.
old = {name: "# legacy %s\n" % name for name in NAMES}
_write_scripts(self.home, old)
patched = {name: hashlib.sha256(old[name].encode()).hexdigest()
for name in NAMES}
saved = pb.KNOWN_SHIPPED_SCRIPT_HASHES
pb.KNOWN_SHIPPED_SCRIPT_HASHES = patched
try:
out = ensure_codex_home_playwright(self.home,
script_dir=self.src_v2)
finally:
pb.KNOWN_SHIPPED_SCRIPT_HASHES = saved
self.assertEqual(set(out["script_refreshed"]), set(NAMES))
self.assertEqual(_read_scripts(self.home), _v2())
def test_legacy_no_manifest_unknown_hash_skips(self):
old = {name: "# tenant-custom %s\n" % name for name in NAMES}
_write_scripts(self.home, old)
saved = pb.KNOWN_SHIPPED_SCRIPT_HASHES
pb.KNOWN_SHIPPED_SCRIPT_HASHES = {}
try:
out = ensure_codex_home_playwright(self.home,
script_dir=self.src_v2)
finally:
pb.KNOWN_SHIPPED_SCRIPT_HASHES = saved
self.assertEqual(set(out["script_skipped_modified"]), set(NAMES))
self.assertEqual(_read_scripts(self.home), old)
self.assertEqual(len(self._audit_records()), 3)
def test_upgrade_never_touches_tenant_data_or_config(self):
notes = os.path.join(self.home, "notes.txt")
with open(notes, "w", encoding="utf-8") as fh:
fh.write("tenant notes, must survive\n")
config = os.path.join(self.home, "config.toml")
with open(config, "w", encoding="utf-8") as fh:
fh.write('[user]\nname = "tenant"\n\n'
'[mcp_servers.playwright.env]\n'
'FOO = "bar"\n')
ensure_codex_home_playwright(self.home, script_dir=self.src_v2)
ensure_codex_home_playwright(self.home, script_dir=self.src_v2)
with open(notes, encoding="utf-8") as fh:
self.assertEqual(fh.read(), "tenant notes, must survive\n")
with open(config, encoding="utf-8") as fh:
text = fh.read()
# Existing sections intact, playwright section not
# duplicated (it was absent, so appended exactly once).
self.assertIn('[user]\nname = "tenant"', text)
self.assertIn('FOO = "bar"', text)
self.assertEqual(text.count("[mcp_servers.playwright]"), 1)
def test_known_shipped_hashes_are_well_formed(self):
self.assertEqual(set(pb.KNOWN_SHIPPED_SCRIPT_HASHES), set(NAMES))
for value in pb.KNOWN_SHIPPED_SCRIPT_HASHES.values():
self.assertEqual(len(value), 64)
int(value, 16) # must be hex