Skip to content
Merged
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ nix = { version = "0.31.1", features = ["signal", "process", "feature", "user"]
inquire = "0.9.3"
console = "0.16.2"
vfs = "0.12"
thiserror = "2"

[dev-dependencies]
tokio = { version = "1.49", features = ["full", "test-util"] }
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod config;
pub mod dlp;
pub mod keys;
pub mod onboard;
pub mod platform;
pub mod process;
pub mod proxy;
pub mod tui;
Expand Down
99 changes: 4 additions & 95 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use tracing::{debug, info, warn};

use clawshell::cli::{Cli, Commands};
use clawshell::config::Config;
use clawshell::platform;
use clawshell::process;
use clawshell::tui;
use clawshell::{AppState, build_router};
Expand Down Expand Up @@ -465,7 +466,7 @@ fn cmd_onboard() -> Result<(), Box<dyn std::error::Error>> {
if user_exists {
tui::print_step_done(1, TOTAL_STEPS, "System user already exists");
} else {
let status = create_system_user("clawshell")?;
let status = platform::create_system_user("clawshell")?;
if !status.success() {
tui::print_error("Failed to create 'clawshell' user.");
std::process::exit(1);
Expand All @@ -492,11 +493,7 @@ fn cmd_onboard() -> Result<(), Box<dyn std::error::Error>> {
// Step 3: Set permissions and ownership
tui::print_step(3, TOTAL_STEPS, "Setting permissions and ownership...");

let chown_spec = if cfg!(target_os = "macos") {
"clawshell:staff"
} else {
"clawshell:clawshell"
};
let chown_spec = platform::clawshell_chown_spec();

if let Err(e) = std::process::Command::new("chmod")
.args(["0700", &config_dir.to_string_lossy()])
Expand Down Expand Up @@ -941,7 +938,7 @@ fn cmd_uninstall(skip_confirm: bool) -> Result<(), Box<dyn std::error::Error>> {
.unwrap_or(false);

if user_exists {
let status = delete_system_user("clawshell")?;
let status = platform::delete_system_user("clawshell")?;
if status.success() {
tui::print_success("System user removed.");
} else {
Expand Down Expand Up @@ -976,83 +973,6 @@ fn cmd_version() {
println!(" {bullet} Streaming support (SSE pass-through)");
}

/// Create a system user, using platform-appropriate commands.
fn create_system_user(name: &str) -> Result<std::process::ExitStatus, Box<dyn std::error::Error>> {
if cfg!(target_os = "macos") {
create_macos_system_user(name)
} else {
Ok(std::process::Command::new("useradd")
.args([
"--system",
"--no-create-home",
"--shell",
"/usr/sbin/nologin",
name,
])
.status()?)
}
}

/// Create a hidden system user on macOS using dscl.
fn create_macos_system_user(
name: &str,
) -> Result<std::process::ExitStatus, Box<dyn std::error::Error>> {
let output = std::process::Command::new("dscl")
.args([".", "-list", "/Users", "UniqueID"])
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
let used_uids: Vec<u32> = stdout
.lines()
.filter_map(|line| line.split_whitespace().last()?.parse().ok())
.collect();
let uid = (400..500)
.rev()
.find(|u| !used_uids.contains(u))
.ok_or("No available system UID in 400-499 range")?;

let user_path = format!("/Users/{name}");
let uid_str = uid.to_string();

let dscl = |args: &[&str],
desc: &str|
-> Result<std::process::ExitStatus, Box<dyn std::error::Error>> {
let status = std::process::Command::new("dscl").args(args).status()?;
if !status.success() {
eprintln!("Warning: failed to {desc} for '{name}'");
}
Ok(status)
};

dscl(&[".", "-create", &user_path], "create user record")?;
dscl(
&[".", "-create", &user_path, "UniqueID", &uid_str],
"set UID",
)?;
dscl(
&[".", "-create", &user_path, "PrimaryGroupID", "20"],
"set GID",
)?;
dscl(
&[".", "-create", &user_path, "UserShell", "/usr/bin/false"],
"set shell",
)?;
dscl(
&[".", "-create", &user_path, "RealName", "ClawShell Service"],
"set real name",
)?;
let status = dscl(
&[".", "-create", &user_path, "NFSHomeDirectory", "/var/empty"],
"set home directory",
)?;

// Hide the user from the login window
let _ = std::process::Command::new("dscl")
.args([".", "-create", &user_path, "IsHidden", "1"])
.status();

Ok(status)
}

/// Start ClawShell directly by spawning a child process (no service manager).
fn start_clawshell_direct(
toml_config_path: &std::path::Path,
Expand Down Expand Up @@ -1082,14 +1002,3 @@ fn start_clawshell_direct(
tui::print_info("PID", &pid.to_string());
Ok(())
}

/// Delete a system user, using platform-appropriate commands.
fn delete_system_user(name: &str) -> Result<std::process::ExitStatus, Box<dyn std::error::Error>> {
if cfg!(target_os = "macos") {
Ok(std::process::Command::new("dscl")
.args([".", "-delete", &format!("/Users/{name}")])
.status()?)
} else {
Ok(std::process::Command::new("userdel").arg(name).status()?)
}
}
90 changes: 8 additions & 82 deletions src/onboard.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::platform;
use crate::tui;

use serde_json::Value;
Expand Down Expand Up @@ -660,11 +661,7 @@ pub fn backup_openclaw_config(openclaw_path: &Path) -> Result<PathBuf, Box<dyn s
std::fs::set_permissions(&backup_path, std::fs::Permissions::from_mode(0o000))?;

// Chown the backup to the clawshell user
let chown_spec = if cfg!(target_os = "macos") {
"clawshell:staff"
} else {
"clawshell:clawshell"
};
let chown_spec = platform::clawshell_chown_spec();
let _ = std::process::Command::new("chown")
.args([chown_spec, &backup_path.to_string_lossy()])
.status();
Expand Down Expand Up @@ -801,11 +798,7 @@ pub const LAUNCHD_PLIST_PATH: &str = "/Library/LaunchDaemons/com.clawshell.daemo

/// Return the platform-appropriate service file path.
pub fn autostart_service_path() -> &'static str {
if cfg!(target_os = "macos") {
LAUNCHD_PLIST_PATH
} else {
SYSTEMD_SERVICE_PATH
}
platform::autostart_service_path()
}

/// Generate a systemd unit file for the ClawShell daemon.
Expand Down Expand Up @@ -898,100 +891,33 @@ pub fn install_autostart_service(
exe_path: &Path,
config_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let content = if cfg!(target_os = "macos") {
generate_launchd_plist(exe_path, config_path)
} else {
generate_systemd_unit(exe_path, config_path)
};
let content = platform::autostart_service_content(exe_path, config_path);

let service_path = autostart_service_path();
let root = crate::process::physical_root();
let vfs_path = root.join(service_path.trim_start_matches('/'))?;
install_autostart_service_vfs(&vfs_path, &content)?;

if cfg!(target_os = "macos") {
// Unload if already loaded so launchd picks up the new plist.
let _ = std::process::Command::new("launchctl")
.args(["unload", service_path])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
let _ = std::process::Command::new("chown")
.args(["root:wheel", service_path])
.status();
let _ = std::process::Command::new("chmod")
.args(["0644", service_path])
.status();
// Loading is done separately via start_autostart_service().
} else {
let status = std::process::Command::new("systemctl")
.args(["daemon-reload"])
.status()?;
if !status.success() {
return Err("systemctl daemon-reload failed".into());
}
let status = std::process::Command::new("systemctl")
.args(["enable", "clawshell.service"])
.status()?;
if !status.success() {
return Err("systemctl enable failed".into());
}
}
platform::install_autostart_post_write(service_path)?;

Ok(())
}

/// Start the auto-start service via the platform service manager.
pub fn start_autostart_service() -> Result<(), Box<dyn std::error::Error>> {
let service_path = autostart_service_path();

if cfg!(target_os = "macos") {
let status = std::process::Command::new("launchctl")
.args(["load", service_path])
.status()?;
if !status.success() {
return Err(format!("launchctl load failed (exit code {})", status).into());
}
} else {
let status = std::process::Command::new("systemctl")
.args(["start", "clawshell.service"])
.status()?;
if !status.success() {
return Err(format!("systemctl start failed (exit code {})", status).into());
}
}

platform::start_autostart_service(service_path)?;
Ok(())
}

/// Remove the auto-start service from the real filesystem and disable it.
pub fn remove_autostart_service() -> Result<(), Box<dyn std::error::Error>> {
let service_path = autostart_service_path();

if cfg!(target_os = "macos") {
let _ = std::process::Command::new("launchctl")
.args(["unload", service_path])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
} else {
let _ = std::process::Command::new("systemctl")
.args(["disable", "clawshell.service"])
.status();
let _ = std::process::Command::new("systemctl")
.args(["stop", "clawshell.service"])
.status();
}
platform::remove_autostart_service(service_path)?;

let root = crate::process::physical_root();
let vfs_path = root.join(service_path.trim_start_matches('/'))?;
remove_autostart_service_vfs(&vfs_path)?;

if !cfg!(target_os = "macos") {
let _ = std::process::Command::new("systemctl")
.args(["daemon-reload"])
.status();
}
platform::remove_autostart_post_delete()?;

Ok(())
}
Expand Down
78 changes: 78 additions & 0 deletions src/platform/linux.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use super::{Error, command_output, command_status, ensure_success};
use std::path::Path;
use std::process::{Command, ExitStatus};

pub fn clawshell_chown_spec() -> &'static str {
"clawshell:clawshell"
}

pub fn pid_file_abs_path() -> &'static str {
"/run/clawshell/clawshell.pid"
}

pub fn pid_file_vfs_rel_path() -> &'static str {
"run/clawshell/clawshell.pid"
}

pub fn autostart_service_path() -> &'static str {
"/etc/systemd/system/clawshell.service"
}

pub fn autostart_service_content(exe_path: &Path, config_path: &Path) -> String {
crate::onboard::generate_systemd_unit(exe_path, config_path)
}

pub fn create_system_user(name: &str) -> Result<ExitStatus, Error> {
let mut command = Command::new("useradd");
command.args([
"--system",
"--no-create-home",
"--shell",
"/usr/sbin/nologin",
name,
]);
command_status(&mut command, "useradd")
}

pub fn delete_system_user(name: &str) -> Result<ExitStatus, Error> {
let mut command = Command::new("userdel");
command.arg(name);
command_status(&mut command, "userdel")
}

pub fn install_autostart_post_write(_service_path: &str) -> Result<(), Error> {
let mut daemon_reload = Command::new("systemctl");
daemon_reload.args(["daemon-reload"]);
let output = command_output(&mut daemon_reload, "systemctl daemon-reload")?;
ensure_success("systemctl daemon-reload", output)?;

let mut enable = Command::new("systemctl");
enable.args(["enable", "clawshell.service"]);
let output = command_output(&mut enable, "systemctl enable clawshell.service")?;
ensure_success("systemctl enable clawshell.service", output)?;

Ok(())
}

pub fn start_autostart_service(_service_path: &str) -> Result<(), Error> {
let mut start = Command::new("systemctl");
start.args(["start", "clawshell.service"]);
let output = command_output(&mut start, "systemctl start clawshell.service")?;
ensure_success("systemctl start clawshell.service", output)?;
Ok(())
}

pub fn remove_autostart_service(_service_path: &str) -> Result<(), Error> {
let _ = Command::new("systemctl")
.args(["disable", "clawshell.service"])
.status();
let _ = Command::new("systemctl")
.args(["stop", "clawshell.service"])
.status();
Ok(())
}

pub fn remove_autostart_post_delete() -> Result<(), Error> {
let _ = Command::new("systemctl").args(["daemon-reload"]).status();
Ok(())
}
Loading