Skip to main content

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}