diff --git a/CHANGELOG.md b/CHANGELOG.md index cb0aefdc..0effcd6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0. ### Fixed +- The menu bar icon opens its panel after Plug was closed by a quit-all and + came back by itself. It came back hidden, and a hidden app shows nothing. +- Codex CLI keeps its sign-in when one of its processes sat idle for more + than a week before renewing with a copy another had already used. The + allowance is now thirty days, as long as Plug remembers a used copy. +- A client allowed single tools of a server gets those tools only, not the + server's resources and prompts. - Server icons no longer turn into a letter in a colored square after a disk cleaner runs. Found icons are kept with Plug's own files, not in the system cache a cleaner empties, and a search cut short while Plug starts diff --git a/PlugApp/PlugApp/App/PlugApplication.swift b/PlugApp/PlugApp/App/PlugApplication.swift index 30659921..a2922e1b 100644 --- a/PlugApp/PlugApp/App/PlugApplication.swift +++ b/PlugApp/PlugApp/App/PlugApplication.swift @@ -6,6 +6,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ notification: Notification) { NSApp.setActivationPolicy(.accessory) + // The daemon opens Plug hidden so no window comes up. A hidden app + // cannot show its menu bar panel either, so it is hidden only until + // it has finished opening. + if NSApp.isHidden { NSApp.unhide(nil) } NotificationService.shared.install() MenuBarPresence.standard.appDidOpen() installSnapshotHook() diff --git a/plug-core/src/downstream_oauth/mod.rs b/plug-core/src/downstream_oauth/mod.rs index 31c3f527..ef55a745 100644 --- a/plug-core/src/downstream_oauth/mod.rs +++ b/plug-core/src/downstream_oauth/mod.rs @@ -40,8 +40,9 @@ const REFRESH_TOKEN_LIFETIME_SECS: u64 = 30 * 24 * 3600; /// read when it started: one renews, and another presents the spent copy /// when its own access token runs out, an hour later or after sitting idle /// for days. Codex CLI does this. Without this the later one is a replay and -/// the whole sign-in is revoked for every process. -const REFRESH_REUSE_LEEWAY_SECS: u64 = 7 * 24 * 3600; +/// the whole sign-in is revoked for every process. As long as a spent token +/// is remembered at all, so a process idle for weeks does not end it either. +const REFRESH_REUSE_LEEWAY_SECS: u64 = REFRESH_TOKEN_LIFETIME_SECS; const REGISTRATION_LIFETIME_SECS: u64 = 90 * 24 * 3600; const UNACTIVATED_REGISTRATION_LIFETIME_SECS: u64 = 3600; const MAX_REGISTRATIONS: usize = 100; diff --git a/plug-core/src/proxy/catalog.rs b/plug-core/src/proxy/catalog.rs index 5b4b0ca8..588b008e 100644 --- a/plug-core/src/proxy/catalog.rs +++ b/plug-core/src/proxy/catalog.rs @@ -857,7 +857,9 @@ impl super::ToolRouter { snapshot .resource_routes .get(&resource.uri) - .is_none_or(|server_id| self.client_may_use_server(client_key, server_id)) + .is_none_or(|server_id| { + self.client_may_use_all_of_server(client_key, server_id) + }) }) .cloned() .collect(), @@ -902,7 +904,9 @@ impl super::ToolRouter { snapshot .resource_routes .get(&template.uri_template) - .is_none_or(|server_id| self.client_may_use_server(client_key, server_id)) + .is_none_or(|server_id| { + self.client_may_use_all_of_server(client_key, server_id) + }) }) .cloned() .collect(), @@ -947,7 +951,7 @@ impl super::ToolRouter { .prompt_routes .get(prompt.name.as_str()) .is_none_or(|(server_id, _)| { - self.client_may_use_server(client_key, server_id) + self.client_may_use_all_of_server(client_key, server_id) }) }) .cloned() diff --git a/plug-core/src/proxy/completion.rs b/plug-core/src/proxy/completion.rs index 3c70557b..af5ad014 100644 --- a/plug-core/src/proxy/completion.rs +++ b/plug-core/src/proxy/completion.rs @@ -38,7 +38,7 @@ impl super::ToolRouter { .map(str::to_string) }) .or_else(|| skills::only_server(&snapshot.resource_routes, uri)) - .filter(|server_id| self.client_may_use_server(client_key, server_id)) + .filter(|server_id| self.client_may_use_all_of_server(client_key, server_id)) .ok_or_else(|| { McpError::from(ProtocolError::InvalidRequest { detail: format!("resource not found: {uri}"), @@ -84,7 +84,7 @@ impl super::ToolRouter { let (server_id, prompt_name) = snapshot .prompt_routes .get(name) - .filter(|(server_id, _)| self.client_may_use_server(client_key, server_id)) + .filter(|(server_id, _)| self.client_may_use_all_of_server(client_key, server_id)) .cloned() .ok_or_else(|| { McpError::from(ProtocolError::InvalidRequest { @@ -136,7 +136,9 @@ impl super::ToolRouter { let (sid, original_name) = snapshot .prompt_routes .get(&prompt_ref.name) - .filter(|(server_id, _)| self.client_may_use_server(client_key, server_id)) + .filter(|(server_id, _)| { + self.client_may_use_all_of_server(client_key, server_id) + }) .cloned() .ok_or_else(|| { McpError::from(ProtocolError::InvalidRequest { @@ -155,7 +157,7 @@ impl super::ToolRouter { .or_else(|| { resolve_resource_route(&snapshot.resource_routes, &resource_ref.uri) }) - .filter(|server_id| self.client_may_use_server(client_key, server_id)) + .filter(|server_id| self.client_may_use_all_of_server(client_key, server_id)) .ok_or_else(|| { McpError::from(ProtocolError::InvalidRequest { detail: format!("resource not found: {}", resource_ref.uri), diff --git a/plug-core/src/proxy/mod.rs b/plug-core/src/proxy/mod.rs index 4311dfea..038a834d 100644 --- a/plug-core/src/proxy/mod.rs +++ b/plug-core/src/proxy/mod.rs @@ -1305,6 +1305,22 @@ impl ToolRouter { .is_none_or(|access| !access.keeps_from_server(server_id)) } + /// Whether the client behind `client_key` gets what `server_id` has + /// besides tools: its resources and prompts. A list that names single + /// tools of a server gives those tools and nothing else of it. + pub fn client_may_use_all_of_server(&self, client_key: Option<&str>, server_id: &str) -> bool { + let Some(client_key) = client_key else { + return true; + }; + self.client_access + .load() + .get(client_key) + .is_none_or(|access| { + !access.keeps_from_server(server_id) + && (!access.has_allow_list() || access.allowed_servers.contains(server_id)) + }) + } + /// Whether the client behind `client_key` may be sent `notification`, by /// the rules as they stand now. Asked at every delivery on every path, /// so a block added while a client is connected applies to the next diff --git a/plug-core/src/proxy/subscriptions.rs b/plug-core/src/proxy/subscriptions.rs index e62eb29a..d3150d85 100644 --- a/plug-core/src/proxy/subscriptions.rs +++ b/plug-core/src/proxy/subscriptions.rs @@ -1407,7 +1407,7 @@ impl super::ToolRouter { let server_id = snapshot .resource_routes .get(uri) - .filter(|server_id| self.client_may_use_server(client_key, server_id)) + .filter(|server_id| self.client_may_use_all_of_server(client_key, server_id)) .cloned() .ok_or_else(|| { McpError::from(ProtocolError::InvalidRequest { @@ -1592,7 +1592,7 @@ impl super::ToolRouter { for target in subscribers { if let Some(server_id) = &server_id && let Some(client_key) = self.subscriber_client_keys.get(&target) - && !self.client_may_use_server(Some(client_key.as_str()), server_id) + && !self.client_may_use_all_of_server(Some(client_key.as_str()), server_id) { continue; } diff --git a/plug-core/src/proxy/tests.rs b/plug-core/src/proxy/tests.rs index 3b363ea3..7bc1c21a 100644 --- a/plug-core/src/proxy/tests.rs +++ b/plug-core/src/proxy/tests.rs @@ -4916,6 +4916,13 @@ fn a_client_with_an_allow_list_gets_only_what_is_on_it() { assert!(!router.client_may_use_server(Some("radar"), "notion")); assert!(router.client_is_kept_from_a_server(Some("radar"))); + // Single tools of a server are those tools, not its resources or + // prompts. + assert!(router.client_may_use_server(Some("bot"), "git")); + assert!(!router.client_may_use_all_of_server(Some("bot"), "git")); + assert!(router.client_may_use_all_of_server(Some("radar"), "slack")); + assert!(router.client_may_use_all_of_server(Some("other"), "git")); + // A result is read by the same rule, and one whose tool was not // recorded is not on a list of single tools. assert!(router.client_may_read_result_of(Some("bot"), Some("git"), Some("commit"), "x"));