Skip to content

Commit 19808d0

Browse files
authored
fix: load storage state at launch when --state / AGENT_BROWSER_STATE is set (#1241)
* fix: load storage state at launch when --state / AGENT_BROWSER_STATE is set The `--state` flag and `AGENT_BROWSER_STATE` env var were documented as restoring saved browser state (cookies + localStorage) at launch, but `load_state()` was never called after the browser started. The feature has been broken since it was introduced. Adds `try_load_storage_state()` and calls it from every early-return path in `auto_launch()` (lazy launch triggered by commands like `navigate`) and from `handle_launch()` (explicit `launch` command). Also adds 4 e2e tests covering all state-persistence paths: - Explicit launch with `storageState` field - Auto-launch via `AGENT_BROWSER_STATE` env var - Session-name auto-restore via `try_auto_restore_state` - Explicit `state_load` command (baseline sanity check) Fixes #1164. * style: apply cargo fmt to e2e_tests.rs Reformats a single long format\! call to satisfy CI's rustfmt check. No behavior change. * fix: call try_load_storage_state in all handle_launch branches The CDP URL, CDP port, auto-connect, and provider early-return branches were skipping storage state loading because try_load_storage_state was only called in the normal BrowserManager::launch() path at the bottom of handle_launch(). Also compute storage_state_owned once and reuse it across all branches rather than borrowing storage_state (a &str tied to cmd) in a helper that needs an owned Option<String>. * Fix storage state reload on reused launches * Fix storage-state launch errors * Fix storage state replay ordering * Align storage-state errors across launch paths * Fix storageState launch cleanup
1 parent a884960 commit 19808d0

2 files changed

Lines changed: 702 additions & 2 deletions

File tree

cli/src/native/actions.rs

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,12 @@ struct DrainedEvents {
169169
/// Compute a hash of the [`LaunchOptions`] fields that require a browser
170170
/// relaunch when changed (baked into the Chrome process at startup).
171171
///
172-
/// Fields NOT hashed (adjustable at runtime via CDP without relaunch):
173-
/// ignore_https_errors, color_scheme, download_path, storage_state
172+
/// Fields NOT hashed:
173+
/// ignore_https_errors, color_scheme, download_path
174+
///
175+
/// `storage_state` is handled separately in `handle_launch()`: explicit
176+
/// `storageState` launches always require a clean local browser so the loaded
177+
/// state replaces the prior session instead of merging into it.
174178
fn launch_hash(opts: &LaunchOptions) -> u64 {
175179
use std::collections::hash_map::DefaultHasher;
176180
use std::hash::{Hash, Hasher};
@@ -1504,6 +1508,9 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> {
15041508
}
15051509
let engine = env::var("AGENT_BROWSER_ENGINE").ok();
15061510

1511+
// Extract storage_state before options is moved into BrowserManager::launch.
1512+
let storage_state_path = options.storage_state.clone();
1513+
15071514
// Store proxy credentials for Fetch.authRequired handling
15081515
let has_proxy_auth = options.proxy_username.is_some();
15091516
if has_proxy_auth {
@@ -1527,6 +1534,7 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> {
15271534
state.start_dialog_handler();
15281535
state.update_stream_client().await;
15291536
try_auto_restore_state(state).await;
1537+
try_load_storage_state(state, &storage_state_path).await;
15301538
return Ok(());
15311539
}
15321540

@@ -1538,6 +1546,7 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> {
15381546
state.start_dialog_handler();
15391547
state.update_stream_client().await;
15401548
try_auto_restore_state(state).await;
1549+
try_load_storage_state(state, &storage_state_path).await;
15411550
return Ok(());
15421551
}
15431552

@@ -1572,6 +1581,7 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> {
15721581
state.update_stream_client().await;
15731582
write_provider_file(&state.session_id, &p);
15741583
try_auto_restore_state(state).await;
1584+
try_load_storage_state(state, &storage_state_path).await;
15751585
return Ok(());
15761586
}
15771587
Err(e) => {
@@ -1604,6 +1614,7 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> {
16041614
}
16051615

16061616
try_auto_restore_state(state).await;
1617+
try_load_storage_state(state, &storage_state_path).await;
16071618
Ok(())
16081619
}
16091620

@@ -1665,6 +1676,64 @@ async fn try_auto_restore_state(state: &mut DaemonState) {
16651676
}
16661677
}
16671678

1679+
/// Load storage state if a path is configured.
1680+
///
1681+
/// Explicit launch should surface this error. Best-effort callers can ignore
1682+
/// the returned `Result` and keep their previous behavior.
1683+
async fn load_storage_state(state: &DaemonState, path: &Option<String>) -> Result<(), String> {
1684+
if let Some(ref path) = path {
1685+
if let Some(ref mgr) = state.browser {
1686+
if let Ok(session_id) = mgr.active_session_id() {
1687+
state::load_state(&mgr.client, session_id, path).await?;
1688+
}
1689+
}
1690+
}
1691+
1692+
Ok(())
1693+
}
1694+
1695+
async fn rollback_failed_launch(state: &mut DaemonState) -> Result<(), String> {
1696+
let close_error = if let Some(mut mgr) = state.browser.take() {
1697+
mgr.close().await.err()
1698+
} else {
1699+
None
1700+
};
1701+
1702+
state.launch_hash = None;
1703+
state.screencasting = false;
1704+
state.reset_input_state();
1705+
state.ref_map.clear();
1706+
state.update_stream_client().await;
1707+
1708+
if let Some(err) = close_error {
1709+
return Err(err);
1710+
}
1711+
1712+
Ok(())
1713+
}
1714+
1715+
async fn load_storage_state_or_rollback(
1716+
state: &mut DaemonState,
1717+
path: &Option<String>,
1718+
) -> Result<(), String> {
1719+
if let Err(err) = load_storage_state(state, path).await {
1720+
if let Err(close_err) = rollback_failed_launch(state).await {
1721+
return Err(format!(
1722+
"{} (also failed to roll back browser after launch: {})",
1723+
err, close_err
1724+
));
1725+
}
1726+
return Err(err);
1727+
}
1728+
1729+
Ok(())
1730+
}
1731+
1732+
/// Load storage state from AGENT_BROWSER_STATE if set.
1733+
async fn try_load_storage_state(state: &DaemonState, path: &Option<String>) {
1734+
let _ = load_storage_state(state, path).await;
1735+
}
1736+
16681737
// ---------------------------------------------------------------------------
16691738
// Phase 1 handlers
16701739
// ---------------------------------------------------------------------------
@@ -1688,6 +1757,7 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result<Value, St
16881757
.collect()
16891758
});
16901759
let storage_state = cmd.get("storageState").and_then(|v| v.as_str());
1760+
let storage_state_owned = storage_state.map(|s| s.to_string());
16911761

16921762
let launch_options = LaunchOptions {
16931763
headless,
@@ -1768,8 +1838,10 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result<Value, St
17681838
let is_external = cdp_url.is_some() || cdp_port.is_some() || auto_connect;
17691839
let was_external = mgr.is_cdp_connection();
17701840
let hash_changed = !is_external && state.launch_hash != Some(new_hash);
1841+
let storage_state_requires_clean_launch = storage_state_owned.is_some() && !is_external;
17711842
is_external != was_external
17721843
|| hash_changed
1844+
|| storage_state_requires_clean_launch
17731845
|| mgr.has_process_exited()
17741846
|| !mgr.is_connection_alive().await
17751847
} else {
@@ -1786,6 +1858,7 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result<Value, St
17861858
state.update_stream_client().await;
17871859
}
17881860
} else {
1861+
load_storage_state(state, &storage_state_owned).await?;
17891862
return Ok(json!({ "launched": true, "reused": true }));
17901863
}
17911864
state.ref_map.clear();
@@ -1807,6 +1880,7 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result<Value, St
18071880
state.start_fetch_handler();
18081881
state.start_dialog_handler();
18091882
state.update_stream_client().await;
1883+
load_storage_state_or_rollback(state, &storage_state_owned).await?;
18101884
return Ok(json!({ "launched": true }));
18111885
}
18121886

@@ -1817,6 +1891,7 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result<Value, St
18171891
state.start_fetch_handler();
18181892
state.start_dialog_handler();
18191893
state.update_stream_client().await;
1894+
load_storage_state_or_rollback(state, &storage_state_owned).await?;
18201895
return Ok(json!({ "launched": true }));
18211896
}
18221897

@@ -1827,6 +1902,7 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result<Value, St
18271902
state.start_fetch_handler();
18281903
state.start_dialog_handler();
18291904
state.update_stream_client().await;
1905+
load_storage_state_or_rollback(state, &storage_state_owned).await?;
18301906
return Ok(json!({ "launched": true }));
18311907
}
18321908

@@ -1863,6 +1939,7 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result<Value, St
18631939
state.start_dialog_handler();
18641940
state.update_stream_client().await;
18651941
write_provider_file(&state.session_id, provider);
1942+
load_storage_state_or_rollback(state, &storage_state_owned).await?;
18661943

18671944
if let Some(info) = providers::get_agentcore_info() {
18681945
return Ok(json!({
@@ -1955,6 +2032,11 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result<Value, St
19552032
}
19562033
}
19572034

2035+
// Load storage state only after Fetch interception is active so replayed
2036+
// origin navigations go through the same domain and proxy handling as
2037+
// normal browser traffic.
2038+
load_storage_state_or_rollback(state, &storage_state_owned).await?;
2039+
19582040
Ok(json!({ "launched": true }))
19592041
}
19602042

0 commit comments

Comments
 (0)