Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions PlugApp/PlugApp/App/PlugApplication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
5 changes: 3 additions & 2 deletions plug-core/src/downstream_oauth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 7 additions & 3 deletions plug-core/src/proxy/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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()
Expand Down
10 changes: 6 additions & 4 deletions plug-core/src/proxy/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}"),
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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),
Expand Down
16 changes: 16 additions & 0 deletions plug-core/src/proxy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions plug-core/src/proxy/subscriptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
Expand Down
7 changes: 7 additions & 0 deletions plug-core/src/proxy/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down
Loading