From 607b7c1bb4c61d4aec2841d18d0badb028cf864b Mon Sep 17 00:00:00 2001 From: Gary Lysenko Date: Sun, 24 May 2026 22:04:34 +0300 Subject: [PATCH] Fix Browser plugin file URL policy --- CHANGELOG.md | 3 ++ scripts/lib/bundled-plugins.sh | 69 ++++++++++++++++++++++++++++++ tests/scripts_smoke.sh | 77 +++++++++++++++++++++++++++++++++- 3 files changed, 148 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e94e5447..52fa3471 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). ### Fixed +- Bundled Browser plugin staging now preserves local `file://` target support + advertised by the Browser plugin while keeping remote file hosts and `data:` + URLs blocked by the URL policy. - `codex-update-manager` now prunes unreferenced updater workspaces under `~/.cache/codex-update-manager/workspaces`, removing heavy build artifacts (`builder/`, `codex-app/`, `dist/`) while preserving lightweight diagnostics such as `logs/` and rebuild reports. - The Chrome native-messaging host now evicts stale browser clients when a newer Codex browser client connects, preventing old Node REPL sessions from repeatedly reattaching CDP and driving extension service-worker CPU. - The bundled Chrome plugin is now auto-installed during app startup, matching Browser Use, so the plugin page no longer falls back to an install button after restart when the Linux native host is already staged. diff --git a/scripts/lib/bundled-plugins.sh b/scripts/lib/bundled-plugins.sh index 41b15dfc..7f636fad 100644 --- a/scripts/lib/bundled-plugins.sh +++ b/scripts/lib/bundled-plugins.sh @@ -643,6 +643,74 @@ path.write_text(source[:match.start()] + replacement + source[match.end():], enc PY } +patch_browser_use_file_url_policy() { + local client="$1" + + if grep -q "codexLinuxFileUrlPolicy" "$client"; then + return 0 + fi + + python3 - "$client" <<'PY' +from pathlib import Path +import re +import sys + +path = Path(sys.argv[1]) +source = path.read_text(encoding="utf-8") +patterns = [ + re.compile( + r'function\s+(?P[A-Za-z_$][\w$]*)\((?P[A-Za-z_$][\w$]*)\)\{' + r'if\((?P[A-Za-z_$][\w$]*)\.has\((?P=url)\)\)return\s*(?:true|!0);' + r'let\s+(?P[A-Za-z_$][\w$]*);' + r'try\{\s*(?P=parsed)\s*=\s*new URL\((?P=url)\);?\s*\}' + r'catch\{\s*return\s*(?:false|!1);?\s*\}' + r'return\s+(?P=parsed)\.protocol\s*===\s*"http:"\s*\|\|\s*' + r'(?P=parsed)\.protocol\s*===\s*"https:"(?P;?)\}' + ), + re.compile( + r'function\s+(?P[A-Za-z_$][\w$]*)\((?P[A-Za-z_$][\w$]*)\)\{' + r'if\((?P[A-Za-z_$][\w$]*)\.has\((?P=url)\)\)return\s*(?:true|!0);' + r'(?:const|let|var)\s+(?P[A-Za-z_$][\w$]*)\s*=\s*new URL\((?P=url)\);' + r'return\s+(?P=parsed)\.protocol\s*===\s*"http:"\s*\|\|\s*' + r'(?P=parsed)\.protocol\s*===\s*"https:"(?P;?)\}' + ), +] + +for pattern in patterns: + match = pattern.search(source) + if match is None: + continue + + parsed = match.group("parsed") + semicolon = match.group("semicolon") + old_body = match.group(0) + old_return = re.compile( + rf'return\s+{re.escape(parsed)}\.protocol\s*===\s*"http:"\s*\|\|\s*' + rf'{re.escape(parsed)}\.protocol\s*===\s*"https:"{re.escape(semicolon)}' + ) + file_policy = ( + f'{parsed}.protocol==="file:"&&' + f'({parsed}.hostname===""||{parsed}.hostname==="localhost")' + f'/*codexLinuxFileUrlPolicy*/' + ) + new_return = ( + f'return {parsed}.protocol==="http:"||{parsed}.protocol==="https:"||' + f'{file_policy}{semicolon}' + ) + new_body, count = old_return.subn(new_return, old_body, count=1) + if count != 1: + continue + + path.write_text(source[:match.start()] + new_body + source[match.end():], encoding="utf-8") + raise SystemExit(0) + +print( + "WARN: Could not find Browser Use URL policy insertion point — leaving browser-client.mjs unchanged", + file=sys.stderr, +) +PY +} + patch_browser_use_node_repl_env_guard() { local client="$1" @@ -789,6 +857,7 @@ stage_browser_plugin_from_upstream() { patch_browser_use_node_repl_env_guard "$target_client" patch_browser_use_native_pipe_import_meta_bridge "$target_client" patch_browser_use_site_status_allowlist_fallback "$target_client" + patch_browser_use_file_url_policy "$target_client" info "Browser plugin staged from upstream DMG" return 0 diff --git a/tests/scripts_smoke.sh b/tests/scripts_smoke.sh index 2a39c063..09fbd77a 100755 --- a/tests/scripts_smoke.sh +++ b/tests/scripts_smoke.sh @@ -123,7 +123,7 @@ JSON {"name":"browser","version":"0.1.0-alpha2","interface":{"category":"Engineering"}} JSON cat > "$resources_dir/plugins/openai-bundled/plugins/browser/scripts/browser-client.mjs" <<'JS' -function lu(e){let t=globalThis.nodeRepl?.env[e];return typeof t=="string"?t:void 0}function th(){let e=import.meta.__codexNativePipe;return e==null||typeof e.createConnection!="function"?null:e}class Uf{async fetchBlocked(e){let r=await bS(e.endpoint,{method:"GET"});if(!r.ok)throw new Error(ae(`Browser Use cannot determine if ${e.displayUrl} is allowed. Please try again later or use another source.`));let n=await r.json();return TF(n)}}export function setupAtlasRuntime() {} +function lu(e){let t=globalThis.nodeRepl?.env[e];return typeof t=="string"?t:void 0}function th(){let e=import.meta.__codexNativePipe;return e==null||typeof e.createConnection!="function"?null:e}var I2=new Set(["about:blank"]);function Gb(e){if(I2.has(e))return!0;let t;try{t=new URL(e)}catch{return!1}return t.protocol==="http:"||t.protocol==="https:"}class Uf{async fetchBlocked(e){let r=await bS(e.endpoint,{method:"GET"});if(!r.ok)throw new Error(ae(`Browser Use cannot determine if ${e.displayUrl} is allowed. Please try again later or use another source.`));let n=await r.json();return TF(n)}}export function setupAtlasRuntime() {} JS } @@ -2396,11 +2396,82 @@ test_browser_use_node_repl_fallback_runtime() { assert_contains "$install_dir/resources/plugins/openai-bundled/plugins/browser/scripts/browser-client.mjs" 'globalThis.nodeRepl?.env?.\[e\]' assert_not_contains "$install_dir/resources/plugins/openai-bundled/plugins/browser/scripts/browser-client.mjs" 'globalThis.nodeRepl?.env\[e\]' assert_contains "$install_dir/resources/plugins/openai-bundled/plugins/browser/scripts/browser-client.mjs" "codexLinuxSiteStatusAllowlistFallback" + assert_contains "$install_dir/resources/plugins/openai-bundled/plugins/browser/scripts/browser-client.mjs" "codexLinuxFileUrlPolicy" assert_contains "$output_log" "Browser Use node_repl runtime is not a Linux executable for x86_64; skipping" assert_not_contains "$output_log" "WARN.*Browser Use node_repl runtime is not a Linux executable" assert_contains "$output_log" "Downloading Browser Use node_repl fallback runtime" } +test_browser_use_file_url_policy_patch_behavior() { + info "Checking Browser Use file URL policy patch behavior" + local workspace="$TMP_DIR/browser-file-url-policy" + local client="$workspace/browser-client.mjs" + local output_log="$workspace/output.log" + + mkdir -p "$workspace" + cat > "$client" <<'JS' +var I2=new Set(["about:blank"]);function Gb(e){if(I2.has(e))return!0;let t;try{t=new URL(e)}catch{return!1}return t.protocol==="http:"||t.protocol==="https:"} +JS + + ( + warn() { echo "[WARN] $*" >&2; } + info() { echo "[INFO] $*" >&2; } + # shellcheck disable=SC1091 + source "$REPO_DIR/scripts/lib/bundled-plugins.sh" + patch_browser_use_file_url_policy "$client" + ) >"$output_log" 2>&1 + + assert_contains "$client" "codexLinuxFileUrlPolicy" + assert_contains "$client" 'protocol==="file:"' + assert_not_contains "$client" 'protocol==="data:"' + assert_not_contains "$output_log" "Could not find Browser Use URL policy insertion point" + + node - "$client" <<'NODE' +const fs = require("fs"); +const vm = require("vm"); + +const client = process.argv[2]; +const source = fs.readFileSync(client, "utf8"); +const context = { URL }; +vm.createContext(context); +vm.runInContext( + `${source} +this.results = { + aboutBlank: Gb("about:blank"), + http: Gb("http://example.com/"), + https: Gb("https://example.com/"), + localFile: Gb("file:///tmp/codex-browser-file-policy.html"), + localhostFile: Gb("file://localhost/tmp/codex-browser-file-policy.html"), + remoteFile: Gb("file://example.com/tmp/codex-browser-file-policy.html"), + data: Gb("data:text/html,hello"), + javascript: Gb("javascript:alert(1)"), + ftp: Gb("ftp://example.com/"), + invalid: Gb("not a url"), +};`, + context, +); + +const expected = { + aboutBlank: true, + http: true, + https: true, + localFile: true, + localhostFile: true, + remoteFile: false, + data: false, + javascript: false, + ftp: false, + invalid: false, +}; + +for (const [key, value] of Object.entries(expected)) { + if (context.results[key] !== value) { + throw new Error(`${key}: expected ${value}, got ${context.results[key]}`); + } +} +NODE +} + test_browser_plugin_renamed_upstream_staging() { info "Checking Browser plugin staging from renamed upstream resources" local workspace="$TMP_DIR/browser-plugin-renamed" @@ -2437,6 +2508,9 @@ test_browser_plugin_renamed_upstream_staging() { assert_contains "$browser_dir/scripts/browser-client.mjs" "nativePipe??import.meta.__codexNativePipe" assert_not_contains "$browser_dir/scripts/browser-client.mjs" "let e=import.meta.__codexNativePipe;return" assert_contains "$browser_dir/scripts/browser-client.mjs" "codexLinuxSiteStatusAllowlistFallback" + assert_contains "$browser_dir/scripts/browser-client.mjs" "codexLinuxFileUrlPolicy" + assert_contains "$browser_dir/scripts/browser-client.mjs" 'protocol==="file:"' + assert_not_contains "$browser_dir/scripts/browser-client.mjs" 'protocol==="data:"' assert_contains "$marketplace" '"name": "browser"' assert_contains "$marketplace" '"path": "./plugins/browser"' assert_contains "$output_log" "Browser plugin staged from upstream DMG" @@ -4627,6 +4701,7 @@ main() { test_native_module_rebuild_accepts_prebuilt_source test_bundled_plugin_builders_accept_prebuilt_binaries test_browser_use_node_repl_fallback_runtime + test_browser_use_file_url_policy_patch_behavior test_browser_plugin_renamed_upstream_staging test_browser_use_node_repl_glibc_pidfd_patch_static test_browser_use_node_repl_ldd_output_compatibility