Skip to content
Draft
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
16 changes: 10 additions & 6 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ repository = "https://github.com/tursodatabase/agentfs"
name = "agentfs"
path = "src/main.rs"

[features]
default = []
# Force FUSE even on macOS 26+ (for testing/compatibility)
# On macOS without this feature, FSKit is used instead
force-fuse = ["dep:fuser"]

[dependencies]
agentfs-sdk = { path = "../sdk/rust" }
tokio = { version = "1", features = ["full"] }
Expand All @@ -23,17 +29,15 @@ dirs = "6"
[target.'cfg(unix)'.dependencies]
libc = "0.2"

# Linux-only dependencies for FUSE functionality
# Linux-only FUSE dependency (always included on Linux)
[target.'cfg(target_os = "linux")'.dependencies]
fuser = { version = "0.15", default-features = false, features = ["abi-7-23"] }
uuid = { version = "1", features = ["v4"] }

# macOS dependencies for FUSE functionality (requires macFUSE)
# macOS FUSE dependency (only included with force-fuse feature)
[target.'cfg(target_os = "macos")'.dependencies]
fuser = { version = "0.15", default-features = false, features = [
"abi-7-23",
"libfuse",
] }
fuser = { version = "0.15", default-features = false, features = ["abi-7-23", "libfuse"], optional = true }
uuid = { version = "1", features = ["v4"] }

# Sandbox dependencies - Linux x86_64 only (requires libunwind-ptrace)
[target.'cfg(all(target_os = "linux", target_arch = "x86_64"))'.dependencies]
Expand Down
32 changes: 30 additions & 2 deletions cli/src/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,43 @@ pub mod completions;
pub mod fs;
pub mod init;

#[cfg(any(target_os = "linux", target_os = "macos"))]
// Mount module selection:
// - Linux: always use FUSE (mount.rs)
// - macOS with force-fuse: use FUSE (mount.rs)
// - macOS without force-fuse: use FSKit (mount_fskit.rs)
// - Other platforms: use stub (mount_stub.rs)

#[cfg(target_os = "linux")]
mod mount;

#[cfg(all(target_os = "macos", feature = "force-fuse"))]
mod mount;

#[cfg(all(target_os = "macos", not(feature = "force-fuse")))]
#[path = "mount_fskit.rs"]
mod mount;

#[cfg(not(any(target_os = "linux", target_os = "macos")))]
#[path = "mount_stub.rs"]
mod mount;

// Run module selection:
// - Linux x86_64: use overlay sandbox (run.rs)
// - macOS with force-fuse: use stub (not yet supported with FUSE)
// - macOS without force-fuse: use FSKit (run_fskit.rs)
// - Other platforms: use stub (run_stub.rs)

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
mod run;
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]

#[cfg(all(target_os = "macos", not(feature = "force-fuse")))]
#[path = "run_fskit.rs"]
mod run;

#[cfg(not(any(
all(target_os = "linux", target_arch = "x86_64"),
all(target_os = "macos", not(feature = "force-fuse"))
)))]
#[path = "run_stub.rs"]
mod run;

Expand Down
7 changes: 7 additions & 0 deletions cli/src/cmd/mount.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
//! FUSE-based mount implementation.
//!
//! This module is only compiled on Linux, or on macOS when the `force-fuse` feature is enabled.
//! On macOS without `force-fuse`, the mount_fskit module is used instead.

#![cfg(any(target_os = "linux", all(target_os = "macos", feature = "force-fuse")))]

use agentfs_sdk::{AgentFS, AgentFSOptions, FileSystem, HostFS, OverlayFS};
use anyhow::Result;
use std::{os::unix::fs::MetadataExt, path::PathBuf, sync::Arc};
Expand Down
178 changes: 178 additions & 0 deletions cli/src/cmd/mount_fskit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
//! FSKit-based mount implementation for macOS 26+.
//!
//! This module is only compiled on macOS when the `force-fuse` feature is NOT enabled.
//! It uses Apple's FSKit framework for user-space filesystem mounting without kernel extensions.

#![cfg(all(target_os = "macos", not(feature = "force-fuse")))]

use agentfs_sdk::AgentFSOptions;
use anyhow::Result;
use std::path::PathBuf;
use std::process::Command;

/// Arguments for the mount command.
#[derive(Debug, Clone)]
pub struct MountArgs {
/// The agent filesystem ID or path.
pub id_or_path: String,
/// The mountpoint path.
pub mountpoint: PathBuf,
/// Automatically unmount when the process exits.
pub auto_unmount: bool,
/// Allow root to access the mount.
pub allow_root: bool,
/// Run in foreground (don't daemonize).
pub foreground: bool,
/// User ID to report for all files (defaults to current user).
pub uid: Option<u32>,
/// Group ID to report for all files (defaults to current group).
pub gid: Option<u32>,
}

/// Mount the agent filesystem using FSKit.
///
/// This requires:
/// - macOS 26 or later
/// - The AgentFS FSKit extension to be installed and enabled
pub fn mount(args: MountArgs) -> Result<()> {
// Check macOS version
if !supports_fskit()? {
anyhow::bail!(
"FSKit requires macOS 26 or later.\n\
You can use the `--features force-fuse` flag to use macFUSE instead."
);
}

// Check if extension is installed
if !is_extension_installed() {
anyhow::bail!(
"AgentFS FSKit extension is not installed.\n\
\n\
To install:\n\
1. Build the extension: cd fskit-ffi && make build-extension\n\
2. Install the extension app bundle\n\
3. Enable it via: System Settings > General > Login Items & Extensions\n\
> File System Extensions > AgentFS\n\
\n\
Alternatively, use macFUSE with: cargo build --features force-fuse"
);
}

// Resolve the database path
let db_path = resolve_db_path(&args.id_or_path)?;

// Validate mountpoint exists
if !args.mountpoint.exists() {
anyhow::bail!("Mountpoint does not exist: {}", args.mountpoint.display());
}

// Mount using FSKit's mount command
// FSKit filesystems are mounted via: mount -t <fstype> <resource> <mountpoint>
// The resource is a URL for FSGenericURLResource
let resource_url = format!("file://{}", db_path);

eprintln!("Mounting {} at {}", db_path, args.mountpoint.display());

let mut cmd = Command::new("/sbin/mount");
cmd.arg("-t").arg("agentfs")
.arg(&resource_url)
.arg(&args.mountpoint);

let output = cmd.output()?;

if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!(
"Mount failed: {}\n\
\n\
Make sure the AgentFS FSKit extension is installed and enabled.",
stderr.trim()
);
}

eprintln!("Mounted successfully!");

if args.foreground {
// Wait for unmount signal
wait_for_unmount(&args.mountpoint)?;
}

Ok(())
}

/// Resolve the database path from an ID or path.
fn resolve_db_path(id_or_path: &str) -> Result<String> {
let opts = AgentFSOptions::resolve(id_or_path)?;

if let Some(path) = opts.path {
Ok(std::fs::canonicalize(&path)?.to_string_lossy().to_string())
} else {
anyhow::bail!("Cannot mount ephemeral filesystem")
}
}

/// Check if macOS version supports FSKit (26+).
fn supports_fskit() -> Result<bool> {
let output = Command::new("sw_vers")
.arg("-productVersion")
.output()?;

if !output.status.success() {
return Ok(false);
}

let version = String::from_utf8_lossy(&output.stdout);
let major: u32 = version
.trim()
.split('.')
.next()
.and_then(|v| v.parse().ok())
.unwrap_or(0);

// FSKit with FSGenericURLResource requires macOS 26+
Ok(major >= 26)
}

/// Check if the AgentFS FSKit extension is installed.
fn is_extension_installed() -> bool {
let output = Command::new("systemextensionsctl")
.arg("list")
.output()
.ok();

if let Some(output) = output {
let stdout = String::from_utf8_lossy(&output.stdout);
// Check for our bundle identifier
stdout.contains("io.turso.agentfs") || stdout.contains("AgentFS")
} else {
false
}
}

/// Wait for the filesystem to be unmounted.
fn wait_for_unmount(mountpoint: &PathBuf) -> Result<()> {
use std::os::unix::fs::MetadataExt;

eprintln!("Running in foreground. Press Ctrl+C to unmount.");

// Get the device ID of the mounted filesystem
let mounted_dev = std::fs::metadata(mountpoint)?.dev();

// Poll for unmount
loop {
std::thread::sleep(std::time::Duration::from_secs(1));

match std::fs::metadata(mountpoint) {
Ok(meta) => {
// Check if device ID changed (unmounted)
if meta.dev() != mounted_dev {
break;
}
}
Err(_) => break, // Mountpoint gone or inaccessible
}
}

eprintln!("Unmounted.");
Ok(())
}
14 changes: 11 additions & 3 deletions cli/src/fuse.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
//! FUSE filesystem implementation for AgentFS.
//!
//! This module is only compiled on Linux, or on macOS when the `force-fuse` feature is enabled.
//! On macOS without `force-fuse`, FSKit is used instead (see mount_fskit.rs).

#![cfg(any(target_os = "linux", all(target_os = "macos", feature = "force-fuse")))]

use agentfs_sdk::{FileSystem, FsError, Stats};

use fuser::{
consts::FUSE_WRITEBACK_CACHE, FileAttr, FileType, Filesystem, KernelConfig, MountOption,
ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyOpen,
ReplyStatfs, ReplyWrite, Request,
ReplyStatfs, ReplyWrite, Request, TimeOrNow,
};
use parking_lot::Mutex;
use std::{
Expand Down Expand Up @@ -126,8 +134,8 @@ impl Filesystem for AgentFSFuse {
_uid: Option<u32>,
_gid: Option<u32>,
size: Option<u64>,
_atime: Option<fuser::TimeOrNow>,
_mtime: Option<fuser::TimeOrNow>,
_atime: Option<TimeOrNow>,
_mtime: Option<TimeOrNow>,
_ctime: Option<SystemTime>,
fh: Option<u64>,
_crtime: Option<SystemTime>,
Expand Down
7 changes: 5 additions & 2 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ mod cmd;
mod parser;
mod sandbox;

#[cfg(any(target_os = "linux", target_os = "macos"))]
// Daemon module for background mounting (FUSE only)
#[cfg(any(target_os = "linux", all(target_os = "macos", feature = "force-fuse")))]
mod daemon;

#[cfg(any(target_os = "linux", target_os = "macos"))]
// FUSE module - only on Linux or macOS with force-fuse feature
// On macOS without force-fuse, FSKit is used instead (no kernel extension needed)
#[cfg(any(target_os = "linux", all(target_os = "macos", feature = "force-fuse")))]
mod fuse;

use clap::{CommandFactory, Parser};
Expand Down
5 changes: 5 additions & 0 deletions fskit-extension/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
DerivedData/
build/
.DS_Store
*.xcuserstate
xcuserdata/
Loading