use crate::cmd::completions::Shell;
use clap::{Parser, Subcommand};
use clap_complete::{
engine::ValueCompleter, ArgValueCompleter, CompletionCandidate, PathCompleter,
};
use std::path::PathBuf;
/// Mount backend type
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum MountBackend {
/// FUSE filesystem (Linux only)
Fuse,
/// NFS over localhost
Nfs,
}
// Platform-specific default: FUSE on Linux, NFS elsewhere
#[allow(clippy::derivable_impls)]
impl Default for MountBackend {
fn default() -> Self {
#[cfg(target_os = "linux")]
{
MountBackend::Fuse
}
#[cfg(not(target_os = "linux"))]
{
MountBackend::Nfs
}
}
}
impl std::fmt::Display for MountBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MountBackend::Fuse => write!(f, "fuse"),
MountBackend::Nfs => write!(f, "nfs"),
}
}
}
#[derive(Parser, Debug)]
#[command(name = "secafs")]
#[command(version = env!("SECAFS_VERSION"))]
#[command(about = "SecAFS - Secure Agent Filesystem (PostgreSQL)", long_about = None)]
pub struct Args {
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand, Debug)]
pub enum Command {
/// Manage shell completions
Completions {
#[command(subcommand)]
command: CompletionsCommand,
},
/// Initialize a new agent filesystem database in PostgreSQL
Init {
/// PostgreSQL connection URL (e.g. postgres://user:pass@host/dbname)
#[arg(env = "SECAFS_POSTGRES_URL")]
postgres_url: String,
/// Base directory for overlay filesystem (copy-on-write)
#[arg(long)]
base: Option<PathBuf>,
/// Command to execute after initialization (mounts the filesystem, runs command, unmounts)
#[arg(short = 'c', long = "command")]
command: Option<String>,
/// Backend to use for mounting when using -c (default: fuse on Linux, nfs on macOS)
#[arg(long, default_value_t = MountBackend::default())]
backend: MountBackend,
},
/// Filesystem operations
Fs {
/// PostgreSQL connection URL (e.g. postgres://user:pass@host/dbname)
#[arg(env = "SECAFS_POSTGRES_URL", add = ArgValueCompleter::new(id_or_path_completer))]
postgres_url: String,
#[command(subcommand)]
command: FsCommand,
},
/// Run a command in the sandboxed environment.
///
/// Uses copy-on-write overlay backed by a PostgreSQL database.
/// All changes are captured in a per-session database, leaving
/// the original files untouched.
Run {
/// Allow write access to additional directories (can be specified multiple times)
#[arg(long = "allow", value_name = "PATH")]
allow: Vec<PathBuf>,
/// Disable default allowed directories (~/.config, ~/.cache, ~/.local, ~/.npm, etc.)
#[arg(long = "no-default-allows")]
no_default_allows: bool,
/// Use experimental ptrace-based syscall interception sandbox
#[arg(long = "experimental-sandbox")]
experimental_sandbox: bool,
/// Enable strace-like output for system calls
/// Only used with --experimental-sandbox
#[arg(long = "strace")]
strace: bool,
/// Session identifier for sharing delta layer across multiple runs.
/// If not provided, a unique session ID is generated for each run.
#[arg(long = "session", value_name = "ID")]
session: Option<String>,
/// Allow other system users to access this mount (requires /etc/fuse.conf
/// user_allow_other; use cautiously)
#[arg(long = "system")]
system: bool,
/// PostgreSQL connection URL for the delta layer
/// (defaults to postgres://localhost/secafs_session_<id>)
#[arg(long, env = "SECAFS_POSTGRES_URL")]
postgres_url: Option<String>,
/// Command to execute (defaults to bash on Linux, zsh on macOS)
command: Option<PathBuf>,
/// Arguments for the command
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
/// Execute a command with a SecAFS filesystem mounted.
///
/// Mounts the specified SecAFS to a temporary directory, runs the command
/// with that directory as the working directory, then automatically unmounts.
#[cfg(unix)]
Exec {
/// PostgreSQL connection URL
#[arg(value_name = "POSTGRES_URL", env = "SECAFS_POSTGRES_URL", add = ArgValueCompleter::new(id_or_path_completer))]
postgres_url: String,
/// Command to execute
#[arg(value_name = "COMMAND")]
command: PathBuf,
/// Arguments for the command
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
/// Backend to use for mounting (default: fuse on Linux, nfs on macOS)
#[arg(long, default_value_t = MountBackend::default())]
backend: MountBackend,
},
/// Mount a SecAFS filesystem using FUSE (or list mounts if no args)
Mount {
/// PostgreSQL connection URL (if omitted, lists current mounts)
#[arg(value_name = "POSTGRES_URL", add = ArgValueCompleter::new(id_or_path_completer))]
id_or_path: Option<String>,
/// Mount point directory
#[arg(value_name = "MOUNTPOINT", add = ArgValueCompleter::new(PathCompleter::dir()))]
mountpoint: Option<PathBuf>,
/// Automatically unmount on exit
#[arg(short = 'a', long)]
auto_unmount: bool,
/// Allow root user to access filesystem
#[arg(long)]
allow_root: bool,
/// Allow other system users to access this mount (requires /etc/fuse.conf
/// user_allow_other; use cautiously)
#[arg(long = "system")]
system: bool,
/// Run in foreground (don't daemonize)
#[arg(short = 'f', long)]
foreground: bool,
/// User ID to report for all files (defaults to current user)
#[arg(long)]
uid: Option<u32>,
/// Group ID to report for all files (defaults to current group)
#[arg(long)]
gid: Option<u32>,
/// Backend to use for mounting
#[arg(long, default_value_t = MountBackend::default())]
backend: MountBackend,
},
/// Show differences between base filesystem and delta (overlay mode only)
Diff {
/// PostgreSQL connection URL
#[arg(value_name = "POSTGRES_URL", env = "SECAFS_POSTGRES_URL", add = ArgValueCompleter::new(id_or_path_completer))]
id_or_path: String,
},
/// Display agent action timeline from tool call audit log
Timeline {
/// PostgreSQL connection URL
#[arg(env = "SECAFS_POSTGRES_URL", add = ArgValueCompleter::new(id_or_path_completer))]
id_or_path: String,
/// Limit number of entries to display
#[arg(long, default_value = "100")]
limit: i64,
/// Filter by tool name
#[arg(long)]
filter: Option<String>,
/// Filter by status (pending/success/error)
#[arg(long, value_parser = ["pending", "success", "error"])]
status: Option<String>,
/// Output format
#[arg(long, default_value = "table", value_parser = ["table", "json"])]
format: String,
},
/// Start an NFS server to export a SecAFS filesystem over the network
/// (deprecated: use `secafs serve nfs` instead)
#[cfg(unix)]
Nfs {
/// PostgreSQL connection URL
#[arg(value_name = "POSTGRES_URL", env = "SECAFS_POSTGRES_URL", add = ArgValueCompleter::new(id_or_path_completer))]
id_or_path: String,
/// IP address to bind to
#[arg(long, default_value = "127.0.0.1")]
bind: String,
/// Port to listen on
#[arg(long, default_value = "11111")]
port: u32,
},
/// Start an MCP server exposing filesystem and key-value store tools
/// (deprecated: use `secafs serve mcp` instead)
McpServer {
/// PostgreSQL connection URL
#[arg(value_name = "POSTGRES_URL", env = "SECAFS_POSTGRES_URL", add = ArgValueCompleter::new(id_or_path_completer))]
id_or_path: String,
/// Tools to expose (comma-separated). If not provided, all tools are exposed.
#[arg(long, value_delimiter = ',')]
tools: Option<Vec<String>>,
},
/// Serve a SecAFS filesystem via different protocols
Serve {
#[command(subcommand)]
command: ServeCommand,
},
/// List active SecAFS run sessions
Ps,
/// Prune unused resources
Prune {
#[command(subcommand)]
command: PruneCommand,
},
/// Migrate database schema to the current version
Migrate {
/// PostgreSQL connection URL
#[arg(env = "SECAFS_POSTGRES_URL", add = ArgValueCompleter::new(id_or_path_completer))]
id_or_path: String,
/// Preview migration without applying changes
#[arg(long)]
dry_run: bool,
/// Downgrade to a specific schema version (e.g. "0.5")
#[arg(long = "target-version")]
target_version: Option<String>,
},
}
#[derive(Subcommand, Debug)]
pub enum FsCommand {
/// List files in the filesystem
Ls {
/// Path to list (default: /)
#[arg(default_value = "/")]
fs_path: String,
},
/// Display file contents
Cat {
/// Path to the file in the filesystem
file_path: String,
},
/// Write file content
Write {
/// Path to the file in the filesystem
file_path: String,
/// Content of the file
content: String,
},
}
#[derive(Subcommand, Debug)]
pub enum ServeCommand {
/// Start an NFS server to export a SecAFS filesystem over the network
#[cfg(unix)]
Nfs {
/// PostgreSQL connection URL
#[arg(value_name = "POSTGRES_URL", env = "SECAFS_POSTGRES_URL", add = ArgValueCompleter::new(id_or_path_completer))]
id_or_path: String,
/// IP address to bind to
#[arg(long, default_value = "127.0.0.1")]
bind: String,
/// Port to listen on
#[arg(long, default_value = "11111")]
port: u32,
},
/// Start an MCP server exposing filesystem and key-value store tools
Mcp {
/// PostgreSQL connection URL
#[arg(value_name = "POSTGRES_URL", env = "SECAFS_POSTGRES_URL", add = ArgValueCompleter::new(id_or_path_completer))]
id_or_path: String,
/// Tools to expose (comma-separated). If not provided, all tools are exposed.
#[arg(long, value_delimiter = ',')]
tools: Option<Vec<String>>,
},
/// Start the JSON-RPC daemon managing per-volume FUSE mounts
#[cfg(target_os = "linux")]
Api {
/// Unix socket path (default: $XDG_RUNTIME_DIR/secafs/secafs.sock)
#[arg(long)]
socket: Option<std::path::PathBuf>,
/// Postgres connection URL (default: $SECAFS_POSTGRES_URL env)
#[arg(long, env = "SECAFS_POSTGRES_URL")]
pg_url: String,
/// Host-side directory where per-conversation mount points are created
/// (default: $XDG_STATE_HOME/secafs/mounts)
#[arg(long)]
mount_root: Option<std::path::PathBuf>,
},
}
#[derive(Subcommand, Debug)]
pub enum PruneCommand {
/// Unmount unused SecAFS mount points
Mounts {
/// Skip confirmation prompt and unmount immediately
#[arg(long)]
force: bool,
},
}
#[derive(Subcommand, Debug, Clone, Copy)]
pub enum CompletionsCommand {
/// Install shell completions to your shell rc file
Install {
/// Shell to install completions for (defaults to current shell)
#[arg(value_enum)]
shell: Option<Shell>,
},
/// Uninstall shell completions from your shell rc file
Uninstall {
/// Shell to uninstall completions for (defaults to current shell)
#[arg(value_enum)]
shell: Option<Shell>,
},
/// Print instructions for manual installation
Show,
}
fn id_or_path_completer(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
let mut completions = vec![];
let path_completer = PathCompleter::any();
let mut path_completions = path_completer.complete(current);
completions.append(&mut path_completions);
completions
}