agentmux_srv/server/browser_host_sync.rs
1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! The host's copy of srv's browser-pane records: which panes an agent owns
5//! (native-popups spec §3) and the sites each limited pane may go to
6//! (SPEC_BROWSER_PANE_ALLOWED_ORIGINS_2026_10_09.md §6). The host reads both
7//! inside CEF callbacks that can't wait for srv.
8
9use crate::server::AppState;
10
11/// Keep the host's copy of the agent-owned panes current: push the whole set
12/// whenever it changes or the host registers (`browser_owner::changed`), and
13/// every minute regardless, so a push the host missed is made up for.
14pub(crate) fn spawn(state: AppState) {
15 tokio::spawn(async move {
16 loop {
17 let _ = tokio::time::timeout(
18 std::time::Duration::from_secs(60),
19 crate::server::browser_owner::changed().notified(),
20 )
21 .await;
22 push(&state).await;
23 }
24 });
25}
26
27/// Send the host the agent-owned panes, each limited pane's site list, and
28/// the panes asking the person about a navigation (allowed-origins spec §6).
29/// The sync loop does this on every change; a caller that needs the host to
30/// have it before the next navigation awaits it directly. The host replaces
31/// its copy with each push, so pushes go one at a time, each taking its
32/// snapshot inside the lock: an older one can't arrive after a newer one.
33pub(crate) async fn push(state: &AppState) {
34 static ONE_AT_A_TIME: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
35 let _turn = ONE_AT_A_TIME.lock().await;
36 let Some(host) = state.host_ipc.lock().await.clone() else {
37 return;
38 };
39 let mut body = serde_json::json!({
40 "panes": crate::server::browser_owner::owned_panes(),
41 "allowed": crate::server::browser_allowlist::snapshot(),
42 "asking": crate::server::browser_attention::asking(crate::server::browser_attention::Kind::Navigation),
43 });
44 // Left out when the store can't be read: the host then keeps its jars.
45 if let Some(jars) = crate::server::browser_identity::live_jars(state) {
46 body["jars"] = serde_json::json!(jars);
47 }
48 // The named profiles there are: the host drops the jar and folder of one
49 // deleted. Left out when they can't be read, so it keeps them all.
50 if let Some(ids) = crate::server::browser_identity::profile_ids() {
51 body["profiles"] = serde_json::json!(ids);
52 }
53 if let Err(e) = crate::server::ui_handlers::proxy_to_host_timeout(state, &host, "owned_panes", body, Some(std::time::Duration::from_secs(5))).await {
54 tracing::debug!(error = %e, "[browser-popup] couldn't send the owned panes to the host");
55 }
56}