Skip to content
Closed
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
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,7 @@ jobs:
run: |
rustup target add x86_64-apple-darwin
- uses: tauri-apps/tauri-action@v0
env:
APPLE_SIGNING_IDENTITY: "-"
with:
args: ${{ matrix.args }}
1 change: 1 addition & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ jobs:
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_SIGNING_IDENTITY: "-"
with:
releaseId: ${{ needs.create-release.outputs.release_id }}
args: ${{ matrix.args }}
Expand Down
47 changes: 46 additions & 1 deletion src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ struct AppState {
uv_info: UvInfo,
next_id: IdType,
children: std::collections::HashMap<IdType, SpawnedProcessState>,
should_offer_tcc_reset: bool,
}

enum SpawnedProcessState {
Expand Down Expand Up @@ -458,6 +459,28 @@ fn kill_process_group(state: State<'_, Mutex<AppState>>, id: IdType) {
// tracing::info!("Process group {id} has exited");
}

#[tauri::command]
fn is_macos() -> bool {
cfg!(target_os = "macos")
}

#[tauri::command]
fn get_should_offer_tcc_reset(state: State<'_, Mutex<AppState>>) -> bool {
state.lock().unwrap().should_offer_tcc_reset
}

#[tauri::command]
fn run_tccutil_reset() -> Result<(), String> {
#[cfg(target_os = "macos")]
{
std::process::Command::new("/usr/bin/tccutil")
.args(["reset", "SystemPolicyAllFiles", "org.strawlab.Sunbeam"])
.output()
.map_err(|e| e.to_string())?;
}
Ok(())
}

/// Pick a free TCP port on localhost by binding to port 0 and reading back the
/// assigned port. The listener is closed immediately, so there is a small race
/// window before Jupyter binds the port, but this is acceptable in practice.
Expand Down Expand Up @@ -942,6 +965,24 @@ pub fn run() {
// data, before we read or populate any defaults below.
migrate_store(&store, &default_working_dir);

// On macOS, detect whether this is a new build since the last run.
// If so, the frontend will offer to reset TCC permissions, which
// can accumulate stale grants when an unsigned app is updated.
#[cfg(target_os = "macos")]
let should_offer_tcc_reset = {
let current_version = app.package_info().version.to_string();
let last_seen = store
.get("lastKnownBuildVersion")
.and_then(|v| v.as_str().map(str::to_owned));
// Only offer on genuine updates — suppress on first install
// (no prior grants exist yet, so nothing useful to reset).
let offer = matches!(&last_seen, Some(v) if v != &current_version);
store.set("lastKnownBuildVersion", serde_json::json!(current_version));
offer
};
#[cfg(not(target_os = "macos"))]
let should_offer_tcc_reset = false;

// // Note that values must be serde_json::Value instances,
// // otherwise, they will not be compatible with the JavaScript bindings.
// store.set("some-key", json!({ "value": 5 }));
Expand All @@ -960,6 +1001,7 @@ pub fn run() {
uv_info,
next_id: Default::default(),
children: Default::default(),
should_offer_tcc_reset,
}));
Ok(())
})
Expand All @@ -975,7 +1017,10 @@ pub fn run() {
parse_pyproject_toml,
process_group_state,
kill_process_group,
spawn_uv
spawn_uv,
is_macos,
get_should_offer_tcc_reset,
run_tccutil_reset,
])
.build(tauri::generate_context!())
.expect("error while building tauri application");
Expand Down
50 changes: 46 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Sun, Package, Play, FolderOpen, Check, Copy, Loader2, ChevronDown, Chev

import { invoke, Channel } from "@tauri-apps/api/core";
import "./App.css";
import { open } from '@tauri-apps/plugin-dialog';
import { open, ask } from '@tauri-apps/plugin-dialog';
import { openUrl } from '@tauri-apps/plugin-opener';

type SpawnedProcessEvent = {
Expand Down Expand Up @@ -526,6 +526,7 @@ function App() {
const [configLoaded, setConfigLoaded] = useState(false);

const [workingDir, setWorkingDir] = useState("");
const [isMacos, setIsMacos] = useState(false);

// Custom package input state
const [customPackageName, setCustomPackageName] = useState("");
Expand All @@ -544,11 +545,13 @@ function App() {
const [uploadMessage, setUploadMessage] = useState<string>('');

useEffect(() => {
// Load initial configuration
// Load initial configuration and check for macOS TCC reset offer
Promise.all([
invoke<string>("get_cwd"),
invoke<PackageConfig>("get_package_config")
]).then(([cwd, config]) => {
invoke<PackageConfig>("get_package_config"),
invoke<boolean>("is_macos"),
invoke<boolean>("get_should_offer_tcc_reset"),
]).then(async ([cwd, config, macos, shouldReset]) => {
setWorkingDir(cwd);
setPythonVersion(config.pythonVersion);

Expand All @@ -566,6 +569,21 @@ function App() {

setPackages(packageMap);
setConfigLoaded(true);
setIsMacos(macos);

if (shouldReset) {
const confirmed = await ask(
"Sunbeam has been updated. Due to a known macOS behavior with unsigned apps, " +
"file permissions from the previous version may not carry over correctly, " +
"which can cause repeated permission prompts.\n\n" +
"Would you like Sunbeam to reset its file access now? macOS will ask you " +
"to re-grant access the next time it's needed.",
{ title: "Reset file permissions?", okLabel: "Reset Now", cancelLabel: "Not Now" }
);
if (confirmed) {
await invoke("run_tccutil_reset");
}
}
});
}, []);

Expand Down Expand Up @@ -684,6 +702,16 @@ function App() {
});
};

const handleManualTccReset = async () => {
const confirmed = await ask(
"This will reset Sunbeam's file access permissions. macOS will ask you to re-grant access the next time it's needed.",
{ title: "Reset file permissions?", okLabel: "Reset Now", cancelLabel: "Cancel" }
);
if (confirmed) {
await invoke("run_tccutil_reset");
}
};

const selectWorkingDir = async () => {

const dirHandle = await open({
Expand Down Expand Up @@ -1065,6 +1093,20 @@ function App() {
Launch Jupyter Lab
</button>
</div>

{/* macOS file permission reset — only shown on macOS */}
{isMacos && (
<div className="mt-4 pt-4 border-t border-gray-100 flex items-center justify-between">
<p className="text-xs text-gray-400">Having repeated permission prompts?</p>
<button
onClick={handleManualTccReset}
className="text-xs text-gray-400 hover:text-amber-600 underline transition"
title="Reset Sunbeam's macOS file access permissions (Documents, Downloads)"
>
Reset macOS file permissions
</button>
</div>
)}
</div>
</div>

Expand Down
Loading