Skip to content

Commit ed88e64

Browse files
committed
fix(browser): scope socket directory per Linux user
1 parent b24fa4f commit ed88e64

5 files changed

Lines changed: 145 additions & 11 deletions

File tree

computer-use-linux/src/bin/codex-chrome-extension-host.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use chrome_runtime::RuntimeManager;
2626
const HOST_NAME: &str = "com.openai.codexextension";
2727
const SOCKET_DIR_ENV: &str = "CODEX_BROWSER_USE_SOCKET_DIR";
2828
const SESSIONS_DIR_ENV: &str = "CODEX_BROWSER_USE_SESSIONS_DIR";
29-
const DEFAULT_SOCKET_DIR: &str = "/tmp/codex-browser-use";
29+
const SOCKET_DIR_NAME: &str = "codex-browser-use";
3030
const ROLLOUT_POLL_INTERVAL: Duration = Duration::from_millis(500);
3131
const OBSERVED_TURN_TTL: Duration = Duration::from_secs(6 * 60 * 60);
3232
const ROLLOUT_SEARCH_MAX_DEPTH: usize = 5;
@@ -365,9 +365,20 @@ fn main() -> Result<()> {
365365
}
366366

367367
fn socket_dir() -> PathBuf {
368-
env::var_os(SOCKET_DIR_ENV)
368+
if let Some(path) = env::var_os(SOCKET_DIR_ENV).filter(|value| !value.is_empty()) {
369+
return PathBuf::from(path);
370+
}
371+
372+
let runtime_dir = env::var_os("XDG_RUNTIME_DIR");
373+
default_socket_dir(runtime_dir.as_deref(), unsafe { libc::geteuid() })
374+
}
375+
376+
fn default_socket_dir(runtime_dir: Option<&std::ffi::OsStr>, effective_uid: u32) -> PathBuf {
377+
runtime_dir
378+
.filter(|value| !value.is_empty())
369379
.map(PathBuf::from)
370-
.unwrap_or_else(|| PathBuf::from(DEFAULT_SOCKET_DIR))
380+
.map(|path| path.join(SOCKET_DIR_NAME))
381+
.unwrap_or_else(|| PathBuf::from(format!("/tmp/{SOCKET_DIR_NAME}-{effective_uid}")))
371382
}
372383

373384
fn sessions_root() -> Option<PathBuf> {
@@ -1027,6 +1038,22 @@ fn log(message: &str) {
10271038
mod tests {
10281039
use super::*;
10291040

1041+
#[test]
1042+
fn socket_directory_prefers_the_per_user_runtime_directory() {
1043+
assert_eq!(
1044+
default_socket_dir(Some(std::ffi::OsStr::new("/run/user/1000")), 1000),
1045+
PathBuf::from("/run/user/1000/codex-browser-use")
1046+
);
1047+
}
1048+
1049+
#[test]
1050+
fn socket_directory_fallback_is_scoped_by_uid() {
1051+
assert_eq!(
1052+
default_socket_dir(None, 1000),
1053+
PathBuf::from("/tmp/codex-browser-use-1000")
1054+
);
1055+
}
1056+
10301057
#[test]
10311058
fn frame_round_trip_uses_native_length_prefix() {
10321059
let message = json!({ "jsonrpc": "2.0", "id": "1", "method": "ping" });

scripts/lib/bundled-plugins.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,20 @@ patch_browser_client_iab_socket_scope() {
10211021
fi
10221022
}
10231023

1024+
patch_browser_client_linux_socket_dir() {
1025+
local client="$1"
1026+
local patcher="$SCRIPT_DIR/scripts/lib/patch-browser-client-iab-socket-scope.js"
1027+
1028+
if [ ! -f "$patcher" ]; then
1029+
warn "Browser socket-directory patch helper not found at $patcher; leaving browser-client.mjs unchanged"
1030+
return 0
1031+
fi
1032+
1033+
if ! node "$patcher" "$client" --socket-dir-only >&2; then
1034+
warn "Browser socket-directory patch helper failed; leaving browser-client.mjs unchanged"
1035+
fi
1036+
}
1037+
10241038
normalize_plugin_script_executable_modes() {
10251039
local target_plugin="$1"
10261040
local scripts_dir="$target_plugin/scripts"
@@ -1066,6 +1080,7 @@ stage_chrome_plugin_from_upstream() {
10661080
patch_browser_use_node_repl_config_shim "$target_plugin/scripts/browser-client.mjs"
10671081
patch_browser_use_native_pipe_import_meta_bridge "$target_plugin/scripts/browser-client.mjs"
10681082
patch_browser_use_site_status_allowlist_fallback "$target_plugin/scripts/browser-client.mjs"
1083+
patch_browser_client_linux_socket_dir "$target_plugin/scripts/browser-client.mjs"
10691084
normalize_plugin_script_executable_modes "$target_plugin"
10701085
if ! install_chrome_extension_host_resource "$target_plugin"; then
10711086
rm -rf "$target_plugin"

scripts/lib/patch-browser-client-iab-socket-scope.js

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,41 @@ const fs = require("node:fs");
55

66
const clientPath = process.argv[2];
77
if (!clientPath) {
8-
throw new Error("Usage: patch-browser-client-iab-socket-scope.js /path/to/browser-client.mjs");
8+
throw new Error(
9+
"Usage: patch-browser-client-iab-socket-scope.js /path/to/browser-client.mjs [--socket-dir-only]",
10+
);
911
}
12+
const socketDirOnly = process.argv.includes("--socket-dir-only");
1013

11-
const marker = "/*codexLinuxIabSocketScope*/";
12-
const source = fs.readFileSync(clientPath, "utf8");
13-
if (source.includes(marker)) {
14+
const socketDirMarker = "/*codexLinuxPerUserBrowserSocketDir*/";
15+
const iabMarker = "/*codexLinuxIabSocketScope*/";
16+
let source = fs.readFileSync(clientPath, "utf8");
17+
18+
if (!source.includes(socketDirMarker)) {
19+
const socketDirectoryPattern =
20+
/([A-Za-z_$][\w$]*)=([A-Za-z_$][\w$]*)=>\2==="win32"\?("(?:\\.|[^"\\])*codex-browser-use"):"\/tmp\/codex-browser-use"/g;
21+
const socketDirectoryMatches = [...source.matchAll(socketDirectoryPattern)];
22+
if (socketDirectoryMatches.length === 1) {
23+
const [target, resolver, platform, windowsSocket] = socketDirectoryMatches[0];
24+
const helper =
25+
`function codexLinuxBrowserUseSocketDir(){let e=process.env.CODEX_BROWSER_USE_SOCKET_DIR;` +
26+
`if(typeof e==="string"&&e.length>0)return e;let r=process.env.XDG_RUNTIME_DIR;` +
27+
`if(typeof r==="string"&&r.length>0)return r.replace(/\\\/+$/,"")+"/codex-browser-use";` +
28+
`let t=typeof process.getuid==="function"?process.getuid():null;` +
29+
`if(Number.isInteger(t)&&t>=0)return \`/tmp/codex-browser-use-\${t}\`;` +
30+
`throw Error("Browser Use cannot resolve a per-user Linux socket directory")}${socketDirMarker}`;
31+
const replacement =
32+
`${resolver}=${platform}=>${platform}==="win32"?${windowsSocket}:codexLinuxBrowserUseSocketDir()`;
33+
source = helper + source.replace(target, replacement);
34+
} else if (source.includes("/tmp/codex-browser-use")) {
35+
process.stderr.write(
36+
`WARN: Expected one Browser Use socket-directory resolver, found ${socketDirectoryMatches.length}; leaving its path unchanged\n`,
37+
);
38+
}
39+
}
40+
41+
if (socketDirOnly || source.includes(iabMarker)) {
42+
fs.writeFileSync(clientPath, source, "utf8");
1443
process.exit(0);
1544
}
1645

@@ -20,9 +49,10 @@ const matches = [...source.matchAll(socketListingPattern)];
2049
if (matches.length !== 1) {
2150
if (source.includes("codex-browser-use")) {
2251
process.stderr.write(
23-
`WARN: Expected one IAB Browser socket listing target, found ${matches.length}; leaving browser-client.mjs unchanged\n`,
52+
`WARN: Expected one IAB Browser socket listing target, found ${matches.length}; leaving IAB discovery unchanged\n`,
2453
);
2554
}
55+
fs.writeFileSync(clientPath, source, "utf8");
2656
process.exit(0);
2757
}
2858

@@ -40,7 +70,7 @@ const [
4070
const replacement =
4171
`${dispatcher}=()=>${platform}()==="win32"?${windowsListing}():${unixListing}(),` +
4272
`${unixListing}=async()=>(await ${readDirectory}(${socketDirectory}))` +
43-
`.filter(${entry}=>!${entry}.startsWith("extension-")${marker})` +
73+
`.filter(${entry}=>!${entry}.startsWith("extension-")${iabMarker})` +
4474
`.map(${entry}=>${pathModule}.resolve(${socketDirectory},${entry})),` +
4575
`${windowsListing}=async()=>`;
4676
fs.writeFileSync(clientPath, source.replace(target, replacement), "utf8");

scripts/lib/patch-browser-client-iab-socket-scope.test.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,68 @@ const fs = require("node:fs");
77
const os = require("node:os");
88
const path = require("node:path");
99
const test = require("node:test");
10+
const vm = require("node:vm");
1011
const { pathToFileURL } = require("node:url");
1112

1213
const patcher = path.join(__dirname, "patch-browser-client-iab-socket-scope.js");
1314

15+
test("Linux socket discovery uses the override, runtime directory, then UID fallback", () => {
16+
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "codex-user-socket-dir-"));
17+
const clientPath = path.join(workspace, "browser-client.mjs");
18+
const fixture =
19+
'var kE=t=>t==="win32"?"\\\\\\\\.\\\\pipe\\\\codex-browser-use":"/tmp/codex-browser-use";globalThis.result=kE("linux");';
20+
21+
try {
22+
fs.writeFileSync(clientPath, fixture, "utf8");
23+
const firstPatch = spawnSync(process.execPath, [patcher, clientPath, "--socket-dir-only"], {
24+
encoding: "utf8",
25+
});
26+
assert.equal(firstPatch.status, 0, firstPatch.stderr);
27+
const patched = fs.readFileSync(clientPath, "utf8");
28+
assert.match(patched, /codexLinuxPerUserBrowserSocketDir/);
29+
30+
const resolve = (env, uid = 1000) => {
31+
const context = { process: { env, getuid: () => uid } };
32+
context.globalThis = context;
33+
vm.runInNewContext(patched, context);
34+
return context.result;
35+
};
36+
assert.equal(
37+
resolve({ CODEX_BROWSER_USE_SOCKET_DIR: "/custom/browser-sockets" }),
38+
"/custom/browser-sockets",
39+
);
40+
assert.equal(
41+
resolve({ XDG_RUNTIME_DIR: "/run/user/1000/" }),
42+
"/run/user/1000/codex-browser-use",
43+
);
44+
assert.equal(resolve({}, 1001), "/tmp/codex-browser-use-1001");
45+
46+
const secondPatch = spawnSync(process.execPath, [patcher, clientPath, "--socket-dir-only"], {
47+
encoding: "utf8",
48+
});
49+
assert.equal(secondPatch.status, 0, secondPatch.stderr);
50+
assert.equal(fs.readFileSync(clientPath, "utf8"), patched);
51+
} finally {
52+
fs.rmSync(workspace, { recursive: true, force: true });
53+
}
54+
});
55+
56+
test("keeps the per-user socket patch when IAB discovery cannot be identified", () => {
57+
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "codex-user-socket-only-"));
58+
const clientPath = path.join(workspace, "browser-client.mjs");
59+
const fixture =
60+
'var kE=t=>t==="win32"?"\\\\\\\\.\\\\pipe\\\\codex-browser-use":"/tmp/codex-browser-use";';
61+
62+
try {
63+
fs.writeFileSync(clientPath, fixture, "utf8");
64+
const result = spawnSync(process.execPath, [patcher, clientPath], { encoding: "utf8" });
65+
assert.equal(result.status, 0, result.stderr);
66+
assert.match(fs.readFileSync(clientPath, "utf8"), /codexLinuxPerUserBrowserSocketDir/);
67+
} finally {
68+
fs.rmSync(workspace, { recursive: true, force: true });
69+
}
70+
});
71+
1472
test("IAB discovery excludes extension sockets before connecting", async () => {
1573
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "codex-iab-socket-scope-"));
1674
const clientPath = path.join(workspace, "browser-client.mjs");

tests/scripts_smoke.sh

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ JSON
163163
{"name":"browser","version":"0.1.0-alpha2","interface":{"category":"Engineering"}}
164164
JSON
165165
cat > "$resources_dir/plugins/openai-bundled/plugins/browser/scripts/browser-client.mjs" <<'JS'
166-
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,t){let r=await bS(e.endpoint,{method:"GET"});if(!r.ok)throw new Error(ae(`${t} cannot determine if ${e.displayUrl} is allowed. Please try again later or use another source.`));let n=await r.json();return TF(n)}}var Cb=kE(hV.platform()),EV=()=>_P()==="win32"?TV():CV(),CV=async()=>(await yP(Cb)).map(e=>wP.resolve(Cb,e)),TV=async()=>[];export function setupAtlasRuntime() {}
166+
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,t){let r=await bS(e.endpoint,{method:"GET"});if(!r.ok)throw new Error(ae(`${t} cannot determine if ${e.displayUrl} is allowed. Please try again later or use another source.`));let n=await r.json();return TF(n)}}var kE=t=>t==="win32"?"\\\\.\\pipe\\codex-browser-use":"/tmp/codex-browser-use";var Cb=kE(hV.platform()),EV=()=>_P()==="win32"?TV():CV(),CV=async()=>(await yP(Cb)).map(e=>wP.resolve(Cb,e)),TV=async()=>[];export function setupAtlasRuntime() {}
167167
JS
168168
}
169169

@@ -7296,6 +7296,8 @@ test_browser_plugin_renamed_upstream_staging() {
72967296
assert_contains "$browser_dir/scripts/browser-client.mjs" "codexLinuxSiteStatusAllowlistFallback"
72977297
assert_contains "$browser_dir/scripts/browser-client.mjs" "codexLinuxFileUrlPolicy"
72987298
assert_contains "$browser_dir/scripts/browser-client.mjs" "codexLinuxIabSocketScope"
7299+
assert_contains "$browser_dir/scripts/browser-client.mjs" "codexLinuxPerUserBrowserSocketDir"
7300+
assert_not_contains "$browser_dir/scripts/browser-client.mjs" '"/tmp/codex-browser-use"'
72997301
assert_contains "$browser_dir/scripts/browser-client.mjs" 'protocol==="file:"'
73007302
assert_not_contains "$browser_dir/scripts/browser-client.mjs" 'protocol==="data:"'
73017303
assert_contains "$marketplace" '"name": "browser"'
@@ -7944,7 +7946,7 @@ MD
79447946
JSON
79457947
cat > "$chrome_dir/scripts/browser-client.mjs" <<'JS'
79467948
const browserPreference={};function preferredWindowIdFor(){}function getForUrl(){}const extensionInstanceId=null;
7947-
var Cb=kE(hV.platform()),EV=()=>_P()==="win32"?TV():CV(),CV=async()=>(await yP(Cb)).map(e=>wP.resolve(Cb,e)),TV=async()=>[];
7949+
var kE=t=>t==="win32"?"\\\\.\\pipe\\codex-browser-use":"/tmp/codex-browser-use";var Cb=kE(hV.platform()),EV=()=>_P()==="win32"?TV():CV(),CV=async()=>(await yP(Cb)).map(e=>wP.resolve(Cb,e)),TV=async()=>[];
79487950
function lu(e){let t=globalThis.nodeRepl?.env[e];return typeof t=="string"?t:void 0}
79497951
function Me(){let e=globalThis.nodeRepl;return e?.config==null?void 0:e}
79507952
import{platform as yT}from"node:os";function eh(){return"privileged native pipe bridge is not available; browser-client is not trusted"}function th(){let e=globalThis.nodeRepl?.nativePipe;return e==null||typeof e.createConnection!="function"?null:e}var ml=class e{constructor(t){this.socket=t}static async create(t){let r=th();if(r!=null){let n=await r.createConnection(t);return new e(n)}throw new Error(eh())}};
@@ -8139,6 +8141,8 @@ test_chrome_plugin_staging() {
81398141
assert_not_contains "$chrome_dir/scripts/browser-client.mjs" "codexLinuxNativePipeFallback"
81408142
assert_not_contains "$chrome_dir/scripts/browser-client.mjs" 'await import("node:net")'
81418143
assert_contains "$chrome_dir/scripts/browser-client.mjs" "codexLinuxSiteStatusAllowlistFallback"
8144+
assert_contains "$chrome_dir/scripts/browser-client.mjs" "codexLinuxPerUserBrowserSocketDir"
8145+
assert_not_contains "$chrome_dir/scripts/browser-client.mjs" '"/tmp/codex-browser-use"'
81428146
assert_not_contains "$chrome_dir/scripts/browser-client.mjs" "codexLinuxIabSocketScope"
81438147
assert_contains "$chrome_dir/skills/control-chrome/SKILL.md" "agent.browsers.list()"
81448148
assert_contains "$chrome_dir/skills/control-chrome/SKILL.md" "browser.tabs.new()"

0 commit comments

Comments
 (0)