Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
69 changes: 69 additions & 0 deletions scripts/lib/bundled-plugins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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<helper>[A-Za-z_$][\w$]*)\((?P<url>[A-Za-z_$][\w$]*)\)\{'
r'if\((?P<allowlist>[A-Za-z_$][\w$]*)\.has\((?P=url)\)\)return\s*(?:true|!0);'
r'let\s+(?P<parsed>[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<semicolon>;?)\}'
),
re.compile(
r'function\s+(?P<helper>[A-Za-z_$][\w$]*)\((?P<url>[A-Za-z_$][\w$]*)\)\{'
r'if\((?P<allowlist>[A-Za-z_$][\w$]*)\.has\((?P=url)\)\)return\s*(?:true|!0);'
r'(?:const|let|var)\s+(?P<parsed>[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<semicolon>;?)\}'
),
]

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"

Expand Down Expand Up @@ -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
Expand Down
77 changes: 76 additions & 1 deletion tests/scripts_smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
Loading