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
129 changes: 127 additions & 2 deletions computer-use-linux/src/bin/codex-chrome-extension-host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ struct Client {
struct PendingChromeRequest {
client_id: usize,
client_request_id: Value,
fallback_extension_info: bool,
}

#[derive(Clone)]
Expand Down Expand Up @@ -65,6 +66,7 @@ impl ChromeClientRouteError {
struct HostState {
stdout: Arc<Mutex<io::Stdout>>,
rollout_tracker: RolloutTracker,
extension_id: Option<String>,
clients: HashMap<usize, Client>,
pending_chrome_requests: HashMap<String, PendingChromeRequest>,
pending_client_requests: HashMap<String, PendingClientRequest>,
Expand All @@ -74,10 +76,15 @@ struct HostState {
}

impl HostState {
fn new(stdout: Arc<Mutex<io::Stdout>>, rollout_tracker: RolloutTracker) -> Self {
fn new(
stdout: Arc<Mutex<io::Stdout>>,
rollout_tracker: RolloutTracker,
extension_id: Option<String>,
) -> Self {
Self {
stdout,
rollout_tracker,
extension_id,
clients: HashMap::new(),
pending_chrome_requests: HashMap::new(),
pending_client_requests: HashMap::new(),
Expand Down Expand Up @@ -305,7 +312,12 @@ fn main() -> Result<()> {

let stdout = Arc::new(Mutex::new(io::stdout()));
let rollout_tracker = RolloutTracker::new(Arc::clone(&stdout));
let state = Arc::new(Mutex::new(HostState::new(stdout, rollout_tracker)));
let extension_id = extension_id_from_args();
let state = Arc::new(Mutex::new(HostState::new(
stdout,
rollout_tracker,
extension_id,
)));

log(&format!("listening on {}", socket_path.display()));

Expand Down Expand Up @@ -339,6 +351,19 @@ fn sessions_root() -> Option<PathBuf> {
.map(|home| home.join(".codex").join("sessions"))
}

fn extension_id_from_args() -> Option<String> {
env::args().skip(1).find_map(|arg| {
arg.strip_prefix("chrome-extension://")
.and_then(|value| value.split('/').next())
.filter(|value| is_extension_id(value))
.map(ToString::to_string)
})
}

fn is_extension_id(value: &str) -> bool {
value.len() == 32 && value.bytes().all(|byte| matches!(byte, b'a'..=b'p'))
}

fn socket_path(socket_dir: &Path) -> PathBuf {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
Expand Down Expand Up @@ -561,6 +586,7 @@ fn handle_client_message(state: &SharedState, client_id: usize, message: Value)
let Some(client_request_id) = message.get("id").cloned() else {
return;
};
let fallback_extension_info = message.get("method").and_then(Value::as_str) == Some("getInfo");

let mut state = state.lock().expect("host state mutex poisoned");
if !state.clients.contains_key(&client_id) {
Expand All @@ -573,6 +599,7 @@ fn handle_client_message(state: &SharedState, client_id: usize, message: Value)
PendingChromeRequest {
client_id,
client_request_id,
fallback_extension_info,
},
);
state.send_chrome(&with_id(message, Value::String(chrome_id)));
Expand All @@ -589,6 +616,19 @@ fn handle_chrome_message(state: &SharedState, message: Value) {
return;
};

// chrome.runtime.getVersion() is available in Chrome/Chromium 143+.
// Keep forwarding getInfo for browsers that support it, and only
// synthesize discovery metadata for this older-runtime compatibility
// failure.
if pending.fallback_extension_info && is_missing_chrome_runtime_get_version_error(&message)
{
state.send_client(
pending.client_id,
&extension_info_response(pending.client_request_id, state.extension_id.as_deref()),
);
return;
}

state.send_client(
pending.client_id,
&with_id(message, pending.client_request_id),
Expand Down Expand Up @@ -672,6 +712,43 @@ fn with_id(mut message: Value, id: Value) -> Value {
message
}

fn is_missing_chrome_runtime_get_version_error(message: &Value) -> bool {
message
.get("error")
.and_then(|error| error.get("message"))
.and_then(Value::as_str)
.is_some_and(|message| message.contains("chrome.runtime.getVersion is not a function"))
}

fn extension_info_response(id: Value, extension_id: Option<&str>) -> Value {
let mut metadata = serde_json::Map::new();
if let Some(extension_id) = extension_id {
metadata.insert(
"extensionId".to_string(),
Value::String(extension_id.to_string()),
);
}

json!({
"jsonrpc": "2.0",
"id": id,
"result": {
"name": "Chrome",
"version": "unknown",
"type": "extension",
"capabilities": {
"tab": [
{
"id": "pageAssets",
"description": "List assets already observed in the current page state and bundle selected assets into a temporary local artifact."
}
]
},
"metadata": Value::Object(metadata)
}
})
}

fn session_turn_from_message(message: &Value) -> Option<(String, String)> {
let params = message.get("params")?;
let session_id = non_empty_string(params.get("session_id")?)?;
Expand Down Expand Up @@ -957,6 +1034,7 @@ mod tests {
PendingChromeRequest {
client_id: first_client_id,
client_request_id: json!("client-request-1"),
fallback_extension_info: false,
},
);
state.pending_client_requests.insert(
Expand Down Expand Up @@ -994,6 +1072,50 @@ mod tests {
assert_eq!(state.next_chrome_id, 1);
}

#[test]
fn get_info_falls_back_when_runtime_get_version_is_missing() {
let (client_writer, mut client_reader) = UnixStream::pair().unwrap();
let mut state = test_host_state();
state.clients.insert(
1,
Client {
writer: Arc::new(Mutex::new(client_writer)),
},
);
state.pending_chrome_requests.insert(
"linux-1-1".to_string(),
PendingChromeRequest {
client_id: 1,
client_request_id: json!("info-1"),
fallback_extension_info: true,
},
);
state.extension_id = Some("abcdefghijklmnopabcdefghijklmnop".to_string());
let state = Arc::new(Mutex::new(state));

handle_chrome_message(
&state,
json!({
"jsonrpc": "2.0",
"id": "linux-1-1",
"error": {
"code": 1,
"message": "chrome.runtime.getVersion is not a function"
}
}),
);

let message = read_frame(&mut client_reader).unwrap().unwrap();
assert_eq!(message["id"], "info-1");
assert_eq!(message["result"]["type"], "extension");
assert_eq!(message["result"]["version"], "unknown");
assert_eq!(
message["result"]["metadata"]["extensionId"],
"abcdefghijklmnopabcdefghijklmnop"
);
assert!(state.lock().unwrap().pending_chrome_requests.is_empty());
}

#[test]
fn disconnect_cleanup_removes_pending_state_for_client() {
let mut pending_chrome = HashMap::from([
Expand All @@ -1002,13 +1124,15 @@ mod tests {
PendingChromeRequest {
client_id: 1,
client_request_id: json!("chrome-request-1"),
fallback_extension_info: false,
},
),
(
"drop".to_string(),
PendingChromeRequest {
client_id: 2,
client_request_id: json!("chrome-request-2"),
fallback_extension_info: false,
},
),
]);
Expand Down Expand Up @@ -1055,6 +1179,7 @@ mod tests {
stdout,
sessions_root: None,
},
Some("abcdefghijklmnopabcdefghijklmnop".to_string()),
)
}

Expand Down
25 changes: 22 additions & 3 deletions launcher/start.sh.template
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,7 @@ write_chrome_native_host_manifests() {
local host_path="$1"
local plugin_dir="$2"

python3 - "$host_path" "$HOME" "$plugin_dir" <<'PY'
python3 - "$host_path" "$HOME" "$plugin_dir" "$SCRIPT_DIR" <<'PY'
import json
import pathlib
import re
Expand All @@ -637,6 +637,7 @@ import sys
host_path = sys.argv[1]
home = pathlib.Path(sys.argv[2])
plugin_dir = pathlib.Path(sys.argv[3])
app_dir = pathlib.Path(sys.argv[4])
scripts_dir = plugin_dir / "scripts"

extension_id = None
Expand Down Expand Up @@ -674,11 +675,29 @@ manifest = {
}
text = json.dumps(manifest, separators=(",", ":"))

for relative in (
manifest_locations = [
".config/google-chrome/NativeMessagingHosts",
".config/BraveSoftware/Brave-Browser/NativeMessagingHosts",
".config/chromium/NativeMessagingHosts",
):
]

extra_locations = app_dir / ".codex-linux" / "chrome-native-host-manifest-paths"
try:
for line in extra_locations.read_text(encoding="utf-8").splitlines():
relative = line.strip()
if (
not relative
or relative.startswith("#")
or pathlib.PurePosixPath(relative).is_absolute()
or ".." in pathlib.PurePosixPath(relative).parts
):
continue
if relative not in manifest_locations:
manifest_locations.append(relative)
except OSError:
pass

for relative in manifest_locations:
directory = home / relative
directory.mkdir(parents=True, exist_ok=True)
path = directory / manifest_name
Expand Down
29 changes: 29 additions & 0 deletions linux-features/thorium-chrome-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Thorium Chrome Plugin Support

This optional Linux feature extends the bundled Chrome plugin to recognize
Thorium as a Chromium-family browser.

It is disabled by default because Thorium is a narrower browser variant that the
core Linux port does not regularly test. Enable it by adding the feature id to
`linux-features/features.json` before building or installing:

```json
{
"enabled": [
"thorium-chrome-plugin"
]
}
```

When enabled, the feature:

- adds Thorium native-messaging manifest locations for the generated launcher
- patches the staged Chrome plugin scripts to detect Thorium installs, profiles,
running processes, default-browser desktop IDs, and launch commands
- adds Thorium to the Electron-side Chrome extension settings/status helper

Run the focused tests with:

```bash
node --test linux-features/thorium-chrome-plugin/test.js
```
10 changes: 10 additions & 0 deletions linux-features/thorium-chrome-plugin/feature.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"id": "thorium-chrome-plugin",
"title": "Thorium Chrome Plugin Support",
"description": "Adds disabled-by-default Thorium support to the bundled Chrome plugin on Linux.",
"defaultEnabled": false,
"entrypoints": {
"patches": "./patch.js",
"stageHook": "./stage.sh"
}
}
Loading
Loading