Check protected-file access before macOS startup guidance

Run a conservative, read-only preflight off the UI thread before offering Full Disk Access setup. Successful or inconclusive checks stay silent; only a protected-access denial offers guidance. Honor the native reminder opt-out locally and expose manual settings access in the macOS app menu.
This commit is contained in:
Ryan Ward
2026-10-21 13:42:28 +00:00
parent 7948555bb9
commit d5fa711e5c
3 changed files with 149 additions and 0 deletions
+6
View File
@@ -158,6 +158,12 @@ fn make_new_app_menu(ctx: &AppContext) -> Menu {
let preferences_menu_items = vec![ let preferences_menu_items = vec![
updateable_custom_item_without_checkmark(CustomAction::ShowSettings, ctx), updateable_custom_item_without_checkmark(CustomAction::ShowSettings, ctx),
MenuItem::Custom(CustomMenuItem::new(
"Full Disk Access Settings...",
crate::full_disk_access::open_settings,
no_updates,
None,
)),
MenuItem::Separator, MenuItem::Separator,
updateable_custom_item_without_checkmark(CustomAction::ToggleKeybindingsPage, ctx), updateable_custom_item_without_checkmark(CustomAction::ToggleKeybindingsPage, ctx),
updateable_custom_item_without_checkmark(CustomAction::ConfigureKeybindings, ctx), updateable_custom_item_without_checkmark(CustomAction::ConfigureKeybindings, ctx),
+138
View File
@@ -0,0 +1,138 @@
//! Optional, device-local guidance for macOS Full Disk Access.
//!
//! macOS requires the user to grant this permission in System Settings. The
//! stored marker records dismissal of the guidance, never authorization.
//! A conservative filesystem preflight checks effective protected-file access;
//! macOS provides no supported API for querying the Full Disk Access toggle.
use galaxy_core::user_preferences::GetUserPreferences;
use galaxyui::modals::{AlertDialogWithCallbacks, ModalButton};
use galaxyui::AppContext;
const PROMPT_DISMISSED_KEY: &str = "FullDiskAccessPromptDismissed";
const SETTINGS_URL: &str =
"x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles";
#[derive(Clone, Copy, PartialEq, Eq)]
enum ProtectedFileAccess {
Accessible,
Denied,
Unknown,
}
/// Checks access after the macOS GUI has opened, unless reminders were disabled.
pub(crate) fn prompt_on_startup(ctx: &mut AppContext) {
if !cfg!(target_os = "macos") || prompt_dismissed(ctx) {
return;
}
let (sender, receiver) = futures::channel::oneshot::channel();
ctx.background_executor()
.spawn(async move {
let _ = sender.send(protected_file_access());
})
.detach();
let weak_app = ctx.weak_app();
ctx.foreground_executor()
.spawn(async move {
if receiver.await.ok() != Some(ProtectedFileAccess::Denied) {
return;
}
if let Some(mut app) = weak_app.upgrade() {
app.update(|ctx| {
if ctx.window_ids().next().is_some() && !prompt_dismissed(ctx) {
show_guidance(ctx);
}
});
}
})
.detach();
}
fn prompt_dismissed(ctx: &AppContext) -> bool {
match ctx
.private_user_preferences()
.read_value(PROMPT_DISMISSED_KEY)
{
Ok(Some(_)) => true,
Ok(None) => false,
Err(error) => {
log::warn!("Could not read Full Disk Access guidance preference: {error}");
true
},
}
}
/// This is an effective-access heuristic, not a supported permission query.
/// Open only the current user's database; never inspect or parse its contents.
/// Missing files and ambiguous failures must not produce a permission prompt.
fn protected_file_access() -> ProtectedFileAccess {
#[cfg(unix)]
{
use std::fs::OpenOptions;
use std::os::unix::fs::OpenOptionsExt;
let Some(home) = dirs::home_dir() else {
return ProtectedFileAccess::Unknown;
};
let path = home.join("Library/Application Support/com.apple.TCC/TCC.db");
match OpenOptions::new()
.read(true)
.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK)
.open(path)
{
Ok(file) => {
let is_regular_file = file.metadata().is_ok_and(|metadata| metadata.is_file());
drop(file);
if is_regular_file {
ProtectedFileAccess::Accessible
} else {
ProtectedFileAccess::Unknown
}
},
// EACCES may reflect ordinary POSIX permissions or ACLs, so only
// EPERM counts as protected-access denial. Neither is proof of the
// setting's value; all other errors remain unknown.
Err(error) if error.raw_os_error() == Some(libc::EPERM) => ProtectedFileAccess::Denied,
Err(_) => ProtectedFileAccess::Unknown,
}
}
#[cfg(not(unix))]
{
ProtectedFileAccess::Unknown
}
}
fn show_guidance(ctx: &mut AppContext) {
ctx.show_native_platform_modal(AlertDialogWithCallbacks::for_app(
"Protected-file access is unavailable",
"Full Disk Access is optional. Enabling it gives Galaxy, including its agents and \
commands you run, broad access to protected files on this Mac.\n\n\
To enable it, open System Settings > Privacy & Security > Full Disk Access and \
turn on Galaxy. If Galaxy is missing, click + and select the installed Galaxy app. \
macOS may ask you to quit and reopen Galaxy.\n\n\
You can continue without this permission and reopen these settings from Galaxy's menu later.",
vec![
ModalButton::for_app("Open System Settings", open_settings),
ModalButton::for_app("Not Now", |_| {}),
],
remember_dismissal,
));
}
/// Opens the macOS settings page without claiming or changing authorization.
pub(crate) fn open_settings(ctx: &mut AppContext) {
if cfg!(target_os = "macos") {
ctx.open_url(SETTINGS_URL);
}
}
fn remember_dismissal(ctx: &mut AppContext) {
if let Err(error) = ctx
.private_user_preferences()
.write_value(PROMPT_DISMISSED_KEY, true.to_string())
{
log::warn!("Could not save Full Disk Access guidance preference: {error}");
}
}
+5
View File
@@ -61,6 +61,7 @@ mod experiments;
mod external_secrets; mod external_secrets;
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
mod font_fallback; mod font_fallback;
mod full_disk_access;
pub mod galaxy_managed_paths_watcher; pub mod galaxy_managed_paths_watcher;
pub use galaxy_managed_paths_watcher as warp_managed_paths_watcher; pub use galaxy_managed_paths_watcher as warp_managed_paths_watcher;
mod global_resource_handles; mod global_resource_handles;
@@ -2826,6 +2827,10 @@ fn launch(ctx: &mut galaxyui::AppContext, app_state: Option<AppState>, launch_mo
timer.mark_interval_end("WINDOWS_CREATED"); timer.mark_interval_end("WINDOWS_CREATED");
}); });
if cfg!(target_os = "macos") && matches!(launch_mode, LaunchMode::App { .. }) {
full_disk_access::prompt_on_startup(ctx);
}
#[cfg(feature = "local_fs")] #[cfg(feature = "local_fs")]
if matches!(launch_mode, LaunchMode::App { .. }) if matches!(launch_mode, LaunchMode::App { .. })
&& PersistenceWriter::as_ref(ctx).sender().is_none() && PersistenceWriter::as_ref(ctx).sender().is_none()