feat(connect): install + recognize BOTH native-messaging host names (staged extension migration)

Stage 1 of the agent-browser → chrome-use extension migration: the CLI now
writes a host manifest under both com.agent_browser.connect (extension ≤0.4.2)
AND com.leeguoo.chrome_use (the rebrand 0.5.0+), both pointing at the same
launcher, and host_installed()/uninstall recognize both. So the relay works no
matter which extension version a user has, with no forced re-install — which
lets the store roll 0.4.3 (cosmetic name only, host unchanged) and later 0.5.0
(new host) without ever breaking the relay or re-popping the consent dialog.
This commit is contained in:
leeguooooo
2026-06-12 14:50:42 +09:00
parent eb60053183
commit 4106a151a1
+41 -23
View File
@@ -16,9 +16,19 @@ use std::io::Write;
use std::path::PathBuf; use std::path::PathBuf;
/// Native-messaging host name; must match `HOST_NAME` in the extension and the /// Native-messaging host name; must match `HOST_NAME` in the extension and the
/// manifest filename. /// manifest filename. `com.agent_browser.connect` is the original name, used by
/// every shipped extension up to ab-connect 0.4.2.
pub const HOST_NAME: &str = "com.agent_browser.connect"; pub const HOST_NAME: &str = "com.agent_browser.connect";
/// Alternate host name for the chrome-use rebrand era (ab-connect 0.5.0+). We
/// install AND recognize both names so the relay works regardless of which
/// extension version a user has — old (0.4.2) or new — with no forced
/// re-install. See [`install_native_host`] / [`host_installed`].
pub const HOST_NAME_ALT: &str = "com.leeguoo.chrome_use";
/// Every native-messaging host name this CLI installs and accepts.
pub const HOST_NAMES: &[&str] = &[HOST_NAME, HOST_NAME_ALT];
/// Stable id of the `ab-connect` extension, pinned by the `key` in its /// Stable id of the `ab-connect` extension, pinned by the `key` in its
/// manifest.json (and the signing key of the published `.crx`). Chrome only lets /// manifest.json (and the signing key of the published `.crx`). Chrome only lets
/// that extension talk to this host, and the force-install policy references it. /// that extension talk to this host, and the force-install policy references it.
@@ -182,18 +192,9 @@ fn install_native_host() -> Result<Vec<String>, String> {
let _ = std::fs::set_permissions(&launcher, std::fs::Permissions::from_mode(0o755)); let _ = std::fs::set_permissions(&launcher, std::fs::Permissions::from_mode(0o755));
} }
let manifest = serde_json::json!({ // Write a manifest under EVERY accepted host name (both point to the same
"name": HOST_NAME, // launcher + allowed extensions), so any extension version's
"description": "chrome-use connect — native messaging host", // `connectNative(<its host name>)` finds a matching host json.
"path": launcher.display().to_string(),
"type": "stdio",
"allowed_origins": [
format!("chrome-extension://{EXTENSION_ID}/"),
format!("chrome-extension://{STORE_EXTENSION_ID}/"),
],
});
let body = serde_json::to_string_pretty(&manifest).map_err(|e| e.to_string())?;
let mut written = Vec::new(); let mut written = Vec::new();
for dir in native_messaging_dirs() { for dir in native_messaging_dirs() {
if let Some(parent) = dir.parent() { if let Some(parent) = dir.parent() {
@@ -202,9 +203,22 @@ fn install_native_host() -> Result<Vec<String>, String> {
} }
} }
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?; std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
let path = dir.join(format!("{HOST_NAME}.json")); for host in HOST_NAMES {
std::fs::write(&path, &body).map_err(|e| e.to_string())?; let manifest = serde_json::json!({
written.push(path.display().to_string()); "name": host,
"description": "chrome-use connect — native messaging host",
"path": launcher.display().to_string(),
"type": "stdio",
"allowed_origins": [
format!("chrome-extension://{EXTENSION_ID}/"),
format!("chrome-extension://{STORE_EXTENSION_ID}/"),
],
});
let body = serde_json::to_string_pretty(&manifest).map_err(|e| e.to_string())?;
let path = dir.join(format!("{host}.json"));
std::fs::write(&path, &body).map_err(|e| e.to_string())?;
written.push(path.display().to_string());
}
} }
if written.is_empty() { if written.is_empty() {
return Err("no Chrome/Chromium NativeMessagingHosts directory found".into()); return Err("no Chrome/Chromium NativeMessagingHosts directory found".into());
@@ -289,9 +303,11 @@ fn remove_force_install_profile() -> bool {
fn remove_host_manifests() -> usize { fn remove_host_manifests() -> usize {
let mut n = 0; let mut n = 0;
for dir in native_messaging_dirs() { for dir in native_messaging_dirs() {
let path = dir.join(format!("{HOST_NAME}.json")); for host in HOST_NAMES {
if path.exists() && std::fs::remove_file(&path).is_ok() { let path = dir.join(format!("{host}.json"));
n += 1; if path.exists() && std::fs::remove_file(&path).is_ok() {
n += 1;
}
} }
} }
n n
@@ -334,7 +350,7 @@ fn native_messaging_dirs() -> Vec<PathBuf> {
fn host_manifest_path_for_chrome() -> Option<PathBuf> { fn host_manifest_path_for_chrome() -> Option<PathBuf> {
native_messaging_dirs() native_messaging_dirs()
.into_iter() .into_iter()
.map(|d| d.join(format!("{HOST_NAME}.json"))) .flat_map(|d| HOST_NAMES.iter().map(move |h| d.join(format!("{h}.json"))))
.find(|p| p.exists()) .find(|p| p.exists())
.or_else(|| { .or_else(|| {
native_messaging_dirs() native_messaging_dirs()
@@ -352,9 +368,11 @@ fn host_manifest_path_for_chrome() -> Option<PathBuf> {
/// service worker; this manifest is the durable signal that the extension is /// service worker; this manifest is the durable signal that the extension is
/// the chosen path. /// the chosen path.
pub fn host_installed() -> bool { pub fn host_installed() -> bool {
native_messaging_dirs() native_messaging_dirs().into_iter().any(|d| {
.into_iter() HOST_NAMES
.any(|d| d.join(format!("{HOST_NAME}.json")).exists()) .iter()
.any(|h| d.join(format!("{h}.json")).exists())
})
} }
fn report(json: bool, ok: bool, msg: &str) { fn report(json: bool, ok: bool, msg: &str) {