Skip to main content

agentmux_srv/muxbus/
mod.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4pub mod agent_credentials;
5pub mod cloud_subscriber;
6pub mod delivery_status;
7pub mod discovery;
8pub mod wan_lease;
9pub mod pkce;
10pub mod relay;
11pub mod wan_presence;
12pub mod wan_publish;
13pub mod wan_verify;
14
15use crate::backend::storage::muxbus::MuxBusCredentials;
16use crate::broker::CredentialState;
17
18/// Identifier MuxBus's single global credential set is registered under
19/// with both the broker scheduler (`crate::broker`) and the OS keychain
20/// (`backend::storage::muxbus`'s `secret_store` key) — deliberately the same
21/// string in both places so the two are trivially correlated in logs.
22pub const CREDENTIAL_ID: &str = "muxbus:global";
23
24/// Why the stored AgentMux Cloud sign-in can't work anymore, or `None` if it
25/// still can (docs/specs/SPEC_CLOUD_SETTINGS_DISCOVERY_2026_09_27.md §3.3).
26/// Stale means only a new sign-in helps:
27/// - the cloud publishes a different desktop client id than the one the
28///   credential was issued to (it belongs to a user pool the cloud has left);
29/// - or the broker's refresh was refused outright (`NeedsReauth` with
30///   `rejected`), and the stored token isn't fresh. A fresh one means someone
31///   signed in since, possibly from another channel sharing the store; the
32///   broker catches up on its next check.
33///
34/// Transient failures that merely piled up into `NeedsReauth` are not stale:
35/// the network coming back can still cure them.
36pub(crate) fn stale_sign_in_reason(
37    broker: Option<&CredentialState>,
38    creds: &MuxBusCredentials,
39    discovered: Option<&discovery::CloudSettings>,
40) -> Option<String> {
41    if discovery::client_id_superseded(discovered, &creds.client_id) {
42        return Some("the sign-in belongs to a user pool AgentMux Cloud no longer uses".to_string());
43    }
44    match broker {
45        Some(CredentialState::NeedsReauth { rejected: true, reason, .. })
46            if !(creds.is_valid() && !creds.nearly_expired()) =>
47        {
48            Some(format!("the sign-in was refused: {reason}"))
49        }
50        _ => None,
51    }
52}
53
54/// [`stale_sign_in_reason`] against the global broker and the cloud's
55/// published settings (cached, see `discovery::cloud_settings`).
56pub(crate) async fn stale_sign_in(creds: &MuxBusCredentials, http: &reqwest::Client) -> Option<String> {
57    let broker = crate::broker::get_global().and_then(|s| s.state(CREDENTIAL_ID));
58    let discovered = discovery::cloud_settings(http).await;
59    stale_sign_in_reason(broker.as_ref(), creds, discovered.as_ref())
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    fn creds(client_id: &str, expires_in: i64) -> MuxBusCredentials {
67        MuxBusCredentials {
68            client_id: client_id.to_string(),
69            access_token: "at".to_string(),
70            refresh_token: "rt".to_string(),
71            expires_at: agentmux_common::time::now_secs() + expires_in,
72            ..Default::default()
73        }
74    }
75
76    fn needs_reauth(rejected: bool) -> CredentialState {
77        CredentialState::NeedsReauth { since_unix: 0, reason: "invalid_grant".to_string(), rejected }
78    }
79
80    fn published(client_id: &str) -> discovery::CloudSettings {
81        let doc = format!(
82            r#"{{"version":1,"api":"https://a.test","ws":"wss://b.test","cognito":{{"domain":"https://c.test","clientId":"{client_id}"}}}}"#
83        );
84        discovery::parse(&doc, "https://a.test").unwrap()
85    }
86
87    #[test]
88    fn a_refused_refresh_is_stale_until_a_fresh_token_is_stored() {
89        let expired = creds("client", -10);
90        assert!(stale_sign_in_reason(Some(&needs_reauth(true)), &expired, None).is_some());
91        // Nearly expired counts too: that is when the refresh was attempted.
92        assert!(stale_sign_in_reason(Some(&needs_reauth(true)), &creds("client", 60), None).is_some());
93        // Signed in again (here or from another channel): not stale.
94        assert!(stale_sign_in_reason(Some(&needs_reauth(true)), &creds("client", 3600), None).is_none());
95    }
96
97    #[test]
98    fn piled_up_transient_failures_and_healthy_states_are_not_stale() {
99        let expired = creds("client", -10);
100        assert!(stale_sign_in_reason(Some(&needs_reauth(false)), &expired, None).is_none());
101        assert!(stale_sign_in_reason(Some(&CredentialState::Fresh), &expired, None).is_none());
102        let failed = CredentialState::Failed { consecutive_failures: 2, last_error: "net".to_string() };
103        assert!(stale_sign_in_reason(Some(&failed), &expired, None).is_none());
104        assert!(stale_sign_in_reason(None, &expired, None).is_none());
105    }
106
107    #[test]
108    fn a_sign_in_from_a_client_the_cloud_no_longer_publishes_is_stale_even_while_its_token_lasts() {
109        let valid = creds("old-client", 3600);
110        let reason = stale_sign_in_reason(Some(&CredentialState::Fresh), &valid, Some(&published("new-client")));
111        assert!(reason.unwrap().contains("user pool"));
112        assert!(stale_sign_in_reason(Some(&CredentialState::Fresh), &valid, Some(&published("old-client"))).is_none());
113    }
114}