feat(profile): --profile auto + stop steering users into temp-profile launches
Addresses the footgun raised in issue #1 follow-up: plain `--launch` silently uses a temporary EMPTY profile (no cookies/login), and the connect-failure error even recommended it — trapping agents into thinking they reused the logged-in browser when they didn't. - `--profile auto`: resolves to the Chrome profile last used (from Local State `profile.last_used`), falling back to "Default", then the first profile. So `--launch --profile auto open <url>` reuses real login state without naming the profile. (--profile <name>/Default already worked.) - connect-failure error now recommends `--launch --profile auto` and states plainly that bare `--launch` is a temporary EMPTY profile — no cookies/login. - bare `--launch` (no --profile, not CI) now prints a warning to that effect. - README: fix Setup (relaunch with --remote-debugging-port, not chrome://inspect) and split Standalone mode into throwaway vs. keep-your-login (`--profile auto`). Tests: resolve_chrome_profile("auto") prefers last_used, falls back to Default.
This commit is contained in:
@@ -93,14 +93,24 @@ agent-browser screenshot ./page.png
|
|||||||
|
|
||||||
The agent operates in your Chrome — you'll see tabs opening, pages loading, clicks happening in real time. You can take over at any point (e.g. solve a CAPTCHA), then let the agent continue.
|
The agent operates in your Chrome — you'll see tabs opening, pages loading, clicks happening in real time. You can take over at any point (e.g. solve a CAPTCHA), then let the agent continue.
|
||||||
|
|
||||||
### Standalone mode
|
### Standalone mode (`--launch`)
|
||||||
|
|
||||||
If you need a separate browser (CI, testing, etc.):
|
Spawn a separate browser instead of attaching to your running Chrome:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Throwaway: fresh, EMPTY profile — no cookies, no login (good for CI/testing)
|
||||||
agent-browser --launch open https://example.com
|
agent-browser --launch open https://example.com
|
||||||
|
|
||||||
|
# Keep your login: launch with your real Chrome profile (cookies/sessions intact)
|
||||||
|
agent-browser --launch --profile auto open https://x.com/home
|
||||||
|
# or name it explicitly: --profile Default / --profile "Profile 1"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> ⚠️ Plain `--launch` (no `--profile`) uses a **temporary empty profile** — you will
|
||||||
|
> NOT be logged into anything. For logged-in sites use `--profile auto` (picks the
|
||||||
|
> Chrome profile you used most recently) or `--profile <name>`. agent-browser prints
|
||||||
|
> a warning when you `--launch` without a profile.
|
||||||
|
|
||||||
In CI environments, standalone mode is used automatically.
|
In CI environments, standalone mode is used automatically.
|
||||||
|
|
||||||
## Anti-detection
|
## Anti-detection
|
||||||
|
|||||||
@@ -531,6 +531,18 @@ fn main() {
|
|||||||
let mut flags = parse_flags(&args);
|
let mut flags = parse_flags(&args);
|
||||||
let clean = clean_args(&args);
|
let clean = clean_args(&args);
|
||||||
|
|
||||||
|
// Loudly warn when launching a fresh browser with no profile: it gets a
|
||||||
|
// temporary EMPTY profile (no cookies / no login). For logged-in sites the
|
||||||
|
// user almost always wants --profile auto (their real Chrome profile).
|
||||||
|
// Skipped under CI (force_launch is implicit there and login isn't expected).
|
||||||
|
if flags.force_launch && flags.profile.is_none() && env::var("CI").is_err() {
|
||||||
|
eprintln!(
|
||||||
|
"⚠ --launch uses a temporary EMPTY browser profile (no cookies, no login). \
|
||||||
|
For logged-in sites, add `--profile auto` (or `--profile Default`) to reuse \
|
||||||
|
your real Chrome session."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let has_help = args.iter().any(|a| a == "--help" || a == "-h");
|
let has_help = args.iter().any(|a| a == "--help" || a == "-h");
|
||||||
let has_version = args.iter().any(|a| a == "--version" || a == "-V");
|
let has_version = args.iter().any(|a| a == "--version" || a == "-V");
|
||||||
|
|
||||||
|
|||||||
@@ -1609,9 +1609,12 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> {
|
|||||||
"Could not connect to your Chrome browser.\n\n\
|
"Could not connect to your Chrome browser.\n\n\
|
||||||
If Chrome showed an \"Allow remote debugging?\" dialog, click \
|
If Chrome showed an \"Allow remote debugging?\" dialog, click \
|
||||||
Allow and re-run — that consent is what lets agent-browser attach.\n\n\
|
Allow and re-run — that consent is what lets agent-browser attach.\n\n\
|
||||||
Otherwise, to let agent-browser work with your existing Chrome (recommended):\n\
|
Otherwise, to let agent-browser reuse your logged-in Chrome (recommended):\n\
|
||||||
{}\n\n\
|
{}\n\n\
|
||||||
Or start a standalone browser with: agent-browser --launch open <url>\n\n\
|
Or launch a separate browser that KEEPS your login state:\n \
|
||||||
|
agent-browser --launch --profile auto open <url>\n\
|
||||||
|
(plain `--launch` alone uses a temporary EMPTY profile — no cookies, \
|
||||||
|
no logged-in sessions.)\n\n\
|
||||||
Note: remote debugging is a startup flag, not a Chrome setting — \
|
Note: remote debugging is a startup flag, not a Chrome setting — \
|
||||||
chrome://inspect/#remote-debugging only enables target discovery and \
|
chrome://inspect/#remote-debugging only enables target discovery and \
|
||||||
does NOT expose the CDP HTTP API on /json/version. \
|
does NOT expose the CDP HTTP API on /json/version. \
|
||||||
@@ -2172,9 +2175,12 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result<Value, St
|
|||||||
"Could not connect to your Chrome browser.\n\n\
|
"Could not connect to your Chrome browser.\n\n\
|
||||||
If Chrome showed an \"Allow remote debugging?\" dialog, click \
|
If Chrome showed an \"Allow remote debugging?\" dialog, click \
|
||||||
Allow and re-run — that consent is what lets agent-browser attach.\n\n\
|
Allow and re-run — that consent is what lets agent-browser attach.\n\n\
|
||||||
Otherwise, to let agent-browser work with your existing Chrome (recommended):\n\
|
Otherwise, to let agent-browser reuse your logged-in Chrome (recommended):\n\
|
||||||
{}\n\n\
|
{}\n\n\
|
||||||
Or start a standalone browser with: agent-browser --launch open <url>\n\n\
|
Or launch a separate browser that KEEPS your login state:\n \
|
||||||
|
agent-browser --launch --profile auto open <url>\n\
|
||||||
|
(plain `--launch` alone uses a temporary EMPTY profile — no cookies, \
|
||||||
|
no logged-in sessions.)\n\n\
|
||||||
Note: remote debugging is a startup flag, not a Chrome setting — \
|
Note: remote debugging is a startup flag, not a Chrome setting — \
|
||||||
chrome://inspect/#remote-debugging only enables target discovery and \
|
chrome://inspect/#remote-debugging only enables target discovery and \
|
||||||
does NOT expose the CDP HTTP API on /json/version. \
|
does NOT expose the CDP HTTP API on /json/version. \
|
||||||
|
|||||||
@@ -917,6 +917,18 @@ pub fn list_chrome_profiles(user_data_dir: &Path) -> Vec<ChromeProfile> {
|
|||||||
/// 3. Case-insensitive directory name match
|
/// 3. Case-insensitive directory name match
|
||||||
///
|
///
|
||||||
/// Returns the resolved directory name, or an error with available profiles.
|
/// Returns the resolved directory name, or an error with available profiles.
|
||||||
|
/// Read `profile.last_used` (the directory name of the profile Chrome opened
|
||||||
|
/// most recently) from a user-data dir's `Local State`. Used to resolve
|
||||||
|
/// `--profile auto`.
|
||||||
|
fn read_last_used_profile(user_data_dir: &Path) -> Option<String> {
|
||||||
|
let content = std::fs::read_to_string(user_data_dir.join("Local State")).ok()?;
|
||||||
|
let json: serde_json::Value = serde_json::from_str(&content).ok()?;
|
||||||
|
json.get("profile")?
|
||||||
|
.get("last_used")?
|
||||||
|
.as_str()
|
||||||
|
.map(String::from)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn resolve_chrome_profile(user_data_dir: &Path, input: &str) -> Result<String, String> {
|
pub fn resolve_chrome_profile(user_data_dir: &Path, input: &str) -> Result<String, String> {
|
||||||
let profiles = list_chrome_profiles(user_data_dir);
|
let profiles = list_chrome_profiles(user_data_dir);
|
||||||
|
|
||||||
@@ -928,6 +940,21 @@ pub fn resolve_chrome_profile(user_data_dir: &Path, input: &str) -> Result<Strin
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "auto": pick the profile Chrome last used (else "Default", else the first
|
||||||
|
// one), so `--profile auto` reuses the real logged-in profile without the
|
||||||
|
// user having to name it explicitly.
|
||||||
|
if input.eq_ignore_ascii_case("auto") {
|
||||||
|
if let Some(lu) = read_last_used_profile(user_data_dir) {
|
||||||
|
if let Some(p) = profiles.iter().find(|p| p.directory == lu) {
|
||||||
|
return Ok(p.directory.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(p) = profiles.iter().find(|p| p.directory == "Default") {
|
||||||
|
return Ok(p.directory.clone());
|
||||||
|
}
|
||||||
|
return Ok(profiles[0].directory.clone());
|
||||||
|
}
|
||||||
|
|
||||||
// Tier 1: exact directory name match
|
// Tier 1: exact directory name match
|
||||||
if let Some(p) = profiles.iter().find(|p| p.directory == input) {
|
if let Some(p) = profiles.iter().find(|p| p.directory == input) {
|
||||||
return Ok(p.directory.clone());
|
return Ok(p.directory.clone());
|
||||||
@@ -1673,6 +1700,44 @@ mod tests {
|
|||||||
assert!(!is_chrome_profile_name("relative/path"));
|
assert!(!is_chrome_profile_name("relative/path"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_chrome_profile_auto_prefers_last_used() {
|
||||||
|
let tmp = std::env::temp_dir().join("ab-auto-lastused-test");
|
||||||
|
let _ = std::fs::remove_dir_all(&tmp);
|
||||||
|
std::fs::create_dir_all(&tmp).unwrap();
|
||||||
|
let local_state = serde_json::json!({
|
||||||
|
"profile": {
|
||||||
|
"last_used": "Profile 2",
|
||||||
|
"info_cache": { "Default": {"name": "Person 1"}, "Profile 2": {"name": "Work"} }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
std::fs::write(
|
||||||
|
tmp.join("Local State"),
|
||||||
|
serde_json::to_string(&local_state).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(resolve_chrome_profile(&tmp, "auto").unwrap(), "Profile 2");
|
||||||
|
assert_eq!(resolve_chrome_profile(&tmp, "AUTO").unwrap(), "Profile 2");
|
||||||
|
let _ = std::fs::remove_dir_all(&tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_chrome_profile_auto_falls_back_to_default() {
|
||||||
|
let tmp = std::env::temp_dir().join("ab-auto-default-test");
|
||||||
|
let _ = std::fs::remove_dir_all(&tmp);
|
||||||
|
std::fs::create_dir_all(&tmp).unwrap();
|
||||||
|
let local_state = serde_json::json!({
|
||||||
|
"profile": { "info_cache": { "Default": {"name": "Person 1"}, "Profile 2": {"name": "Work"} } }
|
||||||
|
});
|
||||||
|
std::fs::write(
|
||||||
|
tmp.join("Local State"),
|
||||||
|
serde_json::to_string(&local_state).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(resolve_chrome_profile(&tmp, "auto").unwrap(), "Default");
|
||||||
|
let _ = std::fs::remove_dir_all(&tmp);
|
||||||
|
}
|
||||||
|
|
||||||
/// Helper to create a fake Chrome user-data dir with a `Local State` file.
|
/// Helper to create a fake Chrome user-data dir with a `Local State` file.
|
||||||
fn create_fake_local_state(base: &Path, profiles: &[(&str, &str)]) {
|
fn create_fake_local_state(base: &Path, profiles: &[(&str, &str)]) {
|
||||||
let mut info_cache = serde_json::Map::new();
|
let mut info_cache = serde_json::Map::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user