From 96732c8932ddad39bd23d624075b95f9a2f95106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 9 May 2026 00:44:15 -0400 Subject: [PATCH] Gate plugins in private profiles --- .../shell/internal_pages/plugin_catalog.rs | 23 ++++---- .../shell/internal_pages/plugin_details.rs | 52 ++++++++++++++--- .../src/shell/internal_pages/plugins.rs | 46 +++++++++++++-- .../src/shell/internal_pages/task_manager.rs | 19 ++++--- crates/ely_app/src/shell/plugins.rs | 17 ++++++ crates/ely_browser_core/src/state.rs | 2 + crates/ely_browser_core/src/state/plugins.rs | 56 ++++++++++++++++++- crates/ely_browser_core/tests/plugins.rs | 52 +++++++++++++++++ 8 files changed, 236 insertions(+), 31 deletions(-) diff --git a/crates/ely_app/src/shell/internal_pages/plugin_catalog.rs b/crates/ely_app/src/shell/internal_pages/plugin_catalog.rs index 49c857b..d9d65f1 100644 --- a/crates/ely_app/src/shell/internal_pages/plugin_catalog.rs +++ b/crates/ely_app/src/shell/internal_pages/plugin_catalog.rs @@ -166,25 +166,24 @@ fn render_plugin_catalog_list( .overflow_y_scrollbar() .border_t_1() .border_color(rgb(colors::HAIRLINE)) - .children( - snapshot - .installed_plugins - .iter() - .enumerate() - .map(|(index, plugin)| render_plugin_catalog_row(index, plugin, cx)), - ) + .children(snapshot.installed_plugins.iter().enumerate().map(|(index, plugin)| { + render_plugin_catalog_row(index, plugin, &snapshot.active_profile_kind, cx) + })) .into_any_element() } fn render_plugin_catalog_row( index: usize, plugin: &InstalledPlugin, + profile_kind: &ely_domain::ProfileKind, cx: &mut Context, ) -> AnyElement { let detail_route = format!("ely://plugin/{}", plugin.id().as_str()); let high_risk_count = plugin.manifest().high_risk_permissions().count(); - let status_color = if plugin.enabled() { colors::SUCCESS } else { colors::MUTED }; - let status_label = if plugin.enabled() { "Enabled" } else { "Disabled" }; + let status_color = + if plugin.enabled_for_profile(profile_kind) { colors::SUCCESS } else { colors::MUTED }; + let status_label = + if plugin.enabled_for_profile(profile_kind) { "Enabled" } else { "Disabled" }; div() .py_3() @@ -251,7 +250,11 @@ fn render_plugin_catalog_row( } fn enabled_plugin_count(snapshot: &BrowserSnapshot) -> usize { - snapshot.installed_plugins.iter().filter(|plugin| plugin.enabled()).count() + snapshot + .installed_plugins + .iter() + .filter(|plugin| plugin.enabled_for_profile(&snapshot.active_profile_kind)) + .count() } fn high_risk_plugin_count(snapshot: &BrowserSnapshot) -> usize { diff --git a/crates/ely_app/src/shell/internal_pages/plugin_details.rs b/crates/ely_app/src/shell/internal_pages/plugin_details.rs index 72faabf..66148e3 100644 --- a/crates/ely_app/src/shell/internal_pages/plugin_details.rs +++ b/crates/ely_app/src/shell/internal_pages/plugin_details.rs @@ -1,6 +1,6 @@ use ely_browser_core::{BrowserSnapshot, InstalledPlugin}; use ely_design_system::colors; -use ely_domain::{PluginId, PluginManifest, PluginPermission, PluginPermissionRisk}; +use ely_domain::{PluginId, PluginManifest, PluginPermission, PluginPermissionRisk, ProfileKind}; use gpui::prelude::FluentBuilder; use gpui::{AnyElement, Context, IntoElement, ParentElement, Styled, div, px, rgb}; use gpui_component::{ @@ -30,10 +30,14 @@ impl ElyShell { .flex_col() .gap_5() .when_some(plugin, |this, plugin| { - this.child(self.render_plugin_detail_header(plugin, cx)) - .child(render_plugin_security_summary(plugin)) - .child(render_plugin_manifest_rows(plugin.manifest())) - .child(render_plugin_permission_list(plugin.manifest())) + this.child(self.render_plugin_detail_header( + plugin, + &snapshot.active_profile_kind, + cx, + )) + .child(render_plugin_security_summary(plugin)) + .child(render_plugin_manifest_rows(plugin.manifest())) + .child(render_plugin_permission_list(plugin.manifest())) }) .when(plugin.is_none(), |this| { this.child(render_missing_plugin_detail(plugin_id.as_ref(), cx)) @@ -44,14 +48,19 @@ impl ElyShell { fn render_plugin_detail_header( &mut self, plugin: &InstalledPlugin, + profile_kind: &ProfileKind, cx: &mut Context, ) -> AnyElement { let plugin_id = plugin.id().clone(); let target_enabled = !plugin.enabled(); - let status_label = if plugin.enabled() { "Enabled" } else { "Disabled" }; - let status_color = if plugin.enabled() { colors::SUCCESS } else { colors::MUTED }; + let private_allowed = plugin.private_window_allowed(); + let status_label = plugin_status_label(plugin, profile_kind); + let status_color = + if plugin.enabled_for_profile(profile_kind) { colors::SUCCESS } else { colors::MUTED }; let action_label = if plugin.enabled() { "Disable" } else { "Enable" }; let action_icon = if plugin.enabled() { IconName::CircleX } else { IconName::Check }; + let private_action_label = if private_allowed { "Block Private" } else { "Allow Private" }; + let private_action_icon = if private_allowed { IconName::CircleX } else { IconName::Check }; div() .flex() @@ -82,6 +91,24 @@ impl ElyShell { .gap_3() .text_xs() .child(div().font_semibold().text_color(rgb(status_color)).child(status_label)) + .child( + Button::new("toggle-plugin-detail-private") + .ghost() + .xsmall() + .icon(private_action_icon) + .label(private_action_label) + .tooltip("Set Private Window Permission") + .on_click(cx.listener({ + let plugin_id = plugin_id.clone(); + move |shell, _, _, cx| { + shell.set_plugin_private_window_allowed( + plugin_id.clone(), + !private_allowed, + cx, + ); + } + })), + ) .child( Button::new("toggle-plugin-detail-enabled") .xsmall() @@ -108,6 +135,17 @@ impl ElyShell { } } +fn plugin_status_label(plugin: &InstalledPlugin, profile_kind: &ProfileKind) -> &'static str { + if !plugin.enabled() { + return "Disabled"; + } + match profile_kind { + ProfileKind::Standard => "Enabled", + ProfileKind::Private if plugin.private_window_allowed() => "Private Enabled", + ProfileKind::Private => "Private Off", + } +} + fn render_plugin_security_summary(plugin: &InstalledPlugin) -> AnyElement { let manifest = plugin.manifest(); div() diff --git a/crates/ely_app/src/shell/internal_pages/plugins.rs b/crates/ely_app/src/shell/internal_pages/plugins.rs index 0f0151e..c900bc6 100644 --- a/crates/ely_app/src/shell/internal_pages/plugins.rs +++ b/crates/ely_app/src/shell/internal_pages/plugins.rs @@ -1,6 +1,6 @@ use ely_browser_core::{BrowserSnapshot, InstalledPlugin, PluginAuditAction, PluginAuditEvent}; use ely_design_system::colors; -use ely_domain::PluginId; +use ely_domain::{PluginId, ProfileKind}; use gpui::prelude::FluentBuilder; use gpui::{AnyElement, Context, IntoElement, ParentElement, Styled, div, px, rgb}; use gpui_component::{ @@ -252,7 +252,7 @@ fn render_plugin_list(snapshot: &BrowserSnapshot, cx: &mut Context) -> .installed_plugins .iter() .enumerate() - .map(|(index, plugin)| render_plugin_row(index, plugin, cx)) + .map(|(index, plugin)| render_plugin_row(index, plugin, &snapshot.active_profile_kind, cx)) .collect::>(); div() @@ -267,11 +267,13 @@ fn render_plugin_list(snapshot: &BrowserSnapshot, cx: &mut Context) -> fn render_plugin_row( index: usize, plugin: &InstalledPlugin, + profile_kind: &ProfileKind, cx: &mut Context, ) -> AnyElement { let high_risk_count = plugin.manifest().high_risk_permissions().count(); - let status_color = if plugin.enabled() { colors::SUCCESS } else { colors::MUTED }; - let status_label = if plugin.enabled() { "Enabled" } else { "Disabled" }; + let status_color = + if plugin.enabled_for_profile(profile_kind) { colors::SUCCESS } else { colors::MUTED }; + let status_label = plugin_status_label(plugin, profile_kind); let plugin_id = plugin.id().clone(); let plugin_name = plugin.manifest().name().to_string(); let target_enabled = !plugin.enabled(); @@ -323,11 +325,23 @@ fn render_plugin_row( target_enabled, cx, )) + .child(render_plugin_private_button(index, plugin, plugin_id.clone(), cx)) .child(render_plugin_uninstall_button(index, plugin_id, plugin_name, cx)), ) .into_any_element() } +fn plugin_status_label(plugin: &InstalledPlugin, profile_kind: &ProfileKind) -> &'static str { + if !plugin.enabled() { + return "Disabled"; + } + match profile_kind { + ProfileKind::Standard => "Enabled", + ProfileKind::Private if plugin.private_window_allowed() => "Private Enabled", + ProfileKind::Private => "Private Off", + } +} + fn render_plugin_state_button( index: usize, plugin: &InstalledPlugin, @@ -354,6 +368,28 @@ fn render_plugin_state_button( } } +fn render_plugin_private_button( + index: usize, + plugin: &InstalledPlugin, + plugin_id: PluginId, + cx: &mut Context, +) -> AnyElement { + let allowed = plugin.private_window_allowed(); + let label = if allowed { "Block Private" } else { "Allow Private" }; + let icon = if allowed { IconName::CircleX } else { IconName::Check }; + + Button::new(("set-plugin-private", index)) + .ghost() + .xsmall() + .icon(icon) + .label(label) + .tooltip("Set Private Window Permission") + .on_click(cx.listener(move |shell, _, _, cx| { + shell.set_plugin_private_window_allowed(plugin_id.clone(), !allowed, cx); + })) + .into_any_element() +} + fn render_plugin_uninstall_button( index: usize, plugin_id: PluginId, @@ -423,6 +459,8 @@ fn plugin_audit_action_label(action: &PluginAuditAction) -> &'static str { PluginAuditAction::Installed => "Installed", PluginAuditAction::Enabled => "Enabled", PluginAuditAction::Disabled => "Disabled", + PluginAuditAction::PrivateWindowAllowed => "Private Allowed", + PluginAuditAction::PrivateWindowBlocked => "Private Blocked", PluginAuditAction::Uninstalled => "Uninstalled", } } diff --git a/crates/ely_app/src/shell/internal_pages/task_manager.rs b/crates/ely_app/src/shell/internal_pages/task_manager.rs index fdf29bd..586e984 100644 --- a/crates/ely_app/src/shell/internal_pages/task_manager.rs +++ b/crates/ely_app/src/shell/internal_pages/task_manager.rs @@ -119,11 +119,9 @@ fn render_task_table(snapshot: &BrowserSnapshot) -> AnyElement { }) .when(!snapshot.installed_plugins.is_empty(), |this| { this.child(render_section_header("Plugins")).children( - snapshot - .installed_plugins - .iter() - .enumerate() - .map(|(index, plugin)| render_plugin_task_row(index, plugin)), + snapshot.installed_plugins.iter().enumerate().map(|(index, plugin)| { + render_plugin_task_row(index, plugin, &snapshot.active_profile_kind) + }), ) }) .into_any_element() @@ -169,9 +167,14 @@ fn render_download_task_row(index: usize, entry: &DownloadEntry) -> AnyElement { ) } -fn render_plugin_task_row(index: usize, plugin: &InstalledPlugin) -> AnyElement { - let status = if plugin.enabled() { "Enabled" } else { "Disabled" }; - let status_color = if plugin.enabled() { colors::SUCCESS } else { colors::MUTED }; +fn render_plugin_task_row( + index: usize, + plugin: &InstalledPlugin, + profile_kind: &ely_domain::ProfileKind, +) -> AnyElement { + let status = if plugin.enabled_for_profile(profile_kind) { "Enabled" } else { "Disabled" }; + let status_color = + if plugin.enabled_for_profile(profile_kind) { colors::SUCCESS } else { colors::MUTED }; let detail = format!( "{} permissions - {} contributions", plugin.manifest().permissions().len(), diff --git a/crates/ely_app/src/shell/plugins.rs b/crates/ely_app/src/shell/plugins.rs index 105145b..d02d76e 100644 --- a/crates/ely_app/src/shell/plugins.rs +++ b/crates/ely_app/src/shell/plugins.rs @@ -170,6 +170,23 @@ impl ElyShell { cx.notify(); } + pub(super) fn set_plugin_private_window_allowed( + &mut self, + plugin_id: PluginId, + allowed: bool, + cx: &mut Context, + ) { + let result = match &mut self.state { + ShellState::Ready(core) => core + .set_plugin_private_window_allowed(&plugin_id, allowed) + .map_err(|error| error.to_string()), + ShellState::StartupError(message) => Err(message.clone()), + }; + + self.plugin_install_error = result.err(); + cx.notify(); + } + fn uninstall_plugin(&mut self, plugin_id: PluginId, cx: &mut Context) { let result = match &mut self.state { ShellState::Ready(core) => PluginPackageStore::application() diff --git a/crates/ely_browser_core/src/state.rs b/crates/ely_browser_core/src/state.rs index 9f1fd47..06f2b06 100644 --- a/crates/ely_browser_core/src/state.rs +++ b/crates/ely_browser_core/src/state.rs @@ -103,6 +103,7 @@ pub struct BrowserSnapshot { pub active_profile_id: ProfileId, pub active_space_name: String, pub active_profile_name: String, + pub active_profile_kind: ProfileKind, pub active_download_policy: DownloadPolicy, pub search_engine: SearchEngine, pub new_tab_destination: NewTabDestination, @@ -362,6 +363,7 @@ impl BrowserCore { active_profile_id: self.active_profile_id.clone(), active_space_name: active_space.name().to_string(), active_profile_name: active_profile.name().to_string(), + active_profile_kind: active_profile.kind().clone(), active_download_policy: active_profile.download_policy().clone(), search_engine: self.search_engine, new_tab_destination: self.new_tab_destination, diff --git a/crates/ely_browser_core/src/state/plugins.rs b/crates/ely_browser_core/src/state/plugins.rs index 8f04f62..49c7562 100644 --- a/crates/ely_browser_core/src/state/plugins.rs +++ b/crates/ely_browser_core/src/state/plugins.rs @@ -1,6 +1,6 @@ use std::time::SystemTime; -use ely_domain::{PluginId, PluginManifest}; +use ely_domain::{PluginId, PluginManifest, ProfileKind}; use crate::CoreError; @@ -10,6 +10,7 @@ use super::BrowserCore; pub struct InstalledPlugin { manifest: PluginManifest, enabled: bool, + private_window_allowed: bool, high_risk_confirmed: bool, installed_at: SystemTime, } @@ -19,6 +20,8 @@ pub enum PluginAuditAction { Installed, Enabled, Disabled, + PrivateWindowAllowed, + PrivateWindowBlocked, Uninstalled, } @@ -31,13 +34,23 @@ pub struct PluginAuditEvent { impl InstalledPlugin { fn new(manifest: PluginManifest, high_risk_confirmed: bool, installed_at: SystemTime) -> Self { - Self { manifest, enabled: true, high_risk_confirmed, installed_at } + Self { + manifest, + enabled: true, + private_window_allowed: false, + high_risk_confirmed, + installed_at, + } } fn set_enabled(&mut self, enabled: bool) { self.enabled = enabled; } + fn set_private_window_allowed(&mut self, allowed: bool) { + self.private_window_allowed = allowed; + } + #[must_use] pub fn manifest(&self) -> &PluginManifest { &self.manifest @@ -53,6 +66,20 @@ impl InstalledPlugin { self.enabled } + #[must_use] + pub fn private_window_allowed(&self) -> bool { + self.private_window_allowed + } + + #[must_use] + pub fn enabled_for_profile(&self, profile_kind: &ProfileKind) -> bool { + self.enabled + && match profile_kind { + ProfileKind::Standard => true, + ProfileKind::Private => self.private_window_allowed, + } + } + #[must_use] pub fn high_risk_confirmed(&self) -> bool { self.high_risk_confirmed @@ -120,6 +147,31 @@ impl BrowserCore { Ok(()) } + pub fn set_plugin_private_window_allowed( + &mut self, + plugin_id: &PluginId, + allowed: bool, + ) -> Result<(), CoreError> { + self.plugin_mut(plugin_id)?.set_private_window_allowed(allowed); + let action = if allowed { + PluginAuditAction::PrivateWindowAllowed + } else { + PluginAuditAction::PrivateWindowBlocked + }; + self.record_plugin_audit_event(plugin_id.clone(), action); + Ok(()) + } + + pub fn enabled_plugins_for_active_profile(&self) -> Result, CoreError> { + let profile_kind = self.active_profile()?.kind(); + Ok(self + .installed_plugins + .iter() + .filter(|plugin| plugin.enabled_for_profile(profile_kind)) + .cloned() + .collect()) + } + pub fn uninstall_plugin(&mut self, plugin_id: &PluginId) -> Result<(), CoreError> { let plugin_index = self .installed_plugins diff --git a/crates/ely_browser_core/tests/plugins.rs b/crates/ely_browser_core/tests/plugins.rs index 2bfba83..fc59346 100644 --- a/crates/ely_browser_core/tests/plugins.rs +++ b/crates/ely_browser_core/tests/plugins.rs @@ -30,6 +30,7 @@ fn installs_standard_plugin_and_records_audit_event() -> Result<(), Box Result<(), Box> { Ok(()) } +#[test] +fn private_profile_disables_plugins_until_allowed() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::private_window()?)?; + let plugin_id = + core.install_plugin(plugin_manifest("com.elydora.reader", &["page:metadata"])?, false)?; + + assert!(core.enabled_plugins_for_active_profile()?.is_empty()); + assert!(!core.snapshot()?.installed_plugins[0].private_window_allowed()); + + core.set_plugin_private_window_allowed(&plugin_id, true)?; + let allowed_plugins = core.enabled_plugins_for_active_profile()?; + let snapshot = core.snapshot()?; + + assert_eq!(allowed_plugins.len(), 1); + assert_eq!(allowed_plugins[0].id(), &plugin_id); + assert!(snapshot.installed_plugins[0].private_window_allowed()); + assert_eq!(snapshot.plugin_audit_events[1].action(), &PluginAuditAction::PrivateWindowAllowed); + Ok(()) +} + +#[test] +fn disabled_plugin_overrides_private_permission() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::private_window()?)?; + let plugin_id = + core.install_plugin(plugin_manifest("com.elydora.reader", &["page:metadata"])?, false)?; + + core.set_plugin_private_window_allowed(&plugin_id, true)?; + core.disable_plugin(&plugin_id)?; + + assert!(core.enabled_plugins_for_active_profile()?.is_empty()); + assert_eq!(core.snapshot()?.plugin_audit_events[2].action(), &PluginAuditAction::Disabled); + Ok(()) +} + +#[test] +fn blocks_unknown_plugin_private_window_permission() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + let plugin_id = PluginId::parse("com.elydora.missing")?; + + let error = core + .set_plugin_private_window_allowed(&plugin_id, true) + .err() + .ok_or_else(|| io::Error::other("missing plugin permission update succeeded"))?; + + assert!(matches!( + error, + CoreError::PluginNotFound { id } if id.as_str() == "com.elydora.missing" + )); + Ok(()) +} + #[test] fn rejects_unknown_plugin_state_change() -> Result<(), Box> { let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;