Some checks failed
Build and Test / Build Server (Linux) (pull_request) Failing after 7m12s
Build and Test / Build Agent (Windows) (pull_request) Successful in 14m56s
Build and Test / Security Audit (pull_request) Successful in 7m57s
Build and Test / Build Summary (pull_request) Has been skipped
Address the SPEC-018 Phase 1 code review (reports/2026-06-03-spec018-review.md): - Bug 2 (config.rs): stop agent_id churn on every restart. The embedded-config path always wins in Config::load, so the saved agent_id was never read back. Add Config::persisted_agent_id() and reuse a prior id from the TOML; only mint a new UUID when none exists. - Bug 1 (main.rs): remove the non-functional in-process fallback in run_permanent_agent_managed. A managed agent's cak_ store is SYSTEM-only ACL'd, so a non-elevated in-process run cannot authenticate (load_cak permission-denied, or enroll C1 read-back failure). Return an actionable "install elevated" error instead of pretending to provide an agent; update the misleading comments. - Issue 6 (startup.rs): replace the fragile transmute::<HANDLE, HKEY> with the windows crate's typed HKEY out-param; add SAFETY comments. cargo check -p guruconnect --target x86_64-pc-windows-msvc passes clean. Deferred lower-severity items tracked in #8. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
313 lines
8.8 KiB
Rust
313 lines
8.8 KiB
Rust
//! Startup persistence for the agent
|
|
//!
|
|
//! Handles adding/removing the agent from Windows startup.
|
|
|
|
use anyhow::Result;
|
|
use tracing::{info, warn};
|
|
|
|
#[cfg(windows)]
|
|
use windows::core::PCWSTR;
|
|
#[cfg(windows)]
|
|
use windows::Win32::System::Registry::{
|
|
RegCloseKey, RegDeleteValueW, RegOpenKeyExW, RegSetValueExW, HKEY, HKEY_CURRENT_USER, KEY_WRITE,
|
|
REG_SZ,
|
|
};
|
|
|
|
const STARTUP_KEY: &str = r"Software\Microsoft\Windows\CurrentVersion\Run";
|
|
const STARTUP_VALUE_NAME: &str = "GuruConnect";
|
|
|
|
/// Add the current executable to Windows startup
|
|
#[cfg(windows)]
|
|
pub fn add_to_startup() -> Result<()> {
|
|
use std::ffi::OsStr;
|
|
use std::os::windows::ffi::OsStrExt;
|
|
|
|
// Get the path to the current executable
|
|
let exe_path = std::env::current_exe()?;
|
|
let exe_path_str = exe_path.to_string_lossy();
|
|
|
|
info!("Adding to startup: {}", exe_path_str);
|
|
|
|
// Convert strings to wide strings
|
|
let key_path: Vec<u16> = OsStr::new(STARTUP_KEY)
|
|
.encode_wide()
|
|
.chain(std::iter::once(0))
|
|
.collect();
|
|
let value_name: Vec<u16> = OsStr::new(STARTUP_VALUE_NAME)
|
|
.encode_wide()
|
|
.chain(std::iter::once(0))
|
|
.collect();
|
|
let value_data: Vec<u16> = OsStr::new(&*exe_path_str)
|
|
.encode_wide()
|
|
.chain(std::iter::once(0))
|
|
.collect();
|
|
|
|
// SAFETY: FFI into the Win32 registry API. `key_path`/`value_name`/`value_data`
|
|
// are NUL-terminated wide strings that outlive the calls. `RegOpenKeyExW`
|
|
// writes the opened key into `hkey`; we only use it after confirming success,
|
|
// and always pair it with `RegCloseKey`.
|
|
unsafe {
|
|
let mut hkey = HKEY::default();
|
|
|
|
// Open the Run key. RegOpenKeyExW takes a `*mut HKEY` out-param.
|
|
let result = RegOpenKeyExW(
|
|
HKEY_CURRENT_USER,
|
|
PCWSTR(key_path.as_ptr()),
|
|
0,
|
|
KEY_WRITE,
|
|
&mut hkey,
|
|
);
|
|
|
|
if result.is_err() {
|
|
anyhow::bail!("Failed to open registry key: {:?}", result);
|
|
}
|
|
|
|
// Set the value
|
|
let data_bytes =
|
|
std::slice::from_raw_parts(value_data.as_ptr() as *const u8, value_data.len() * 2);
|
|
|
|
let set_result = RegSetValueExW(
|
|
hkey,
|
|
PCWSTR(value_name.as_ptr()),
|
|
0,
|
|
REG_SZ,
|
|
Some(data_bytes),
|
|
);
|
|
|
|
let _ = RegCloseKey(hkey);
|
|
|
|
if set_result.is_err() {
|
|
anyhow::bail!("Failed to set registry value: {:?}", set_result);
|
|
}
|
|
}
|
|
|
|
info!("Successfully added to startup");
|
|
Ok(())
|
|
}
|
|
|
|
/// Remove the agent from Windows startup
|
|
#[cfg(windows)]
|
|
pub fn remove_from_startup() -> Result<()> {
|
|
use std::ffi::OsStr;
|
|
use std::os::windows::ffi::OsStrExt;
|
|
|
|
info!("Removing from startup");
|
|
|
|
let key_path: Vec<u16> = OsStr::new(STARTUP_KEY)
|
|
.encode_wide()
|
|
.chain(std::iter::once(0))
|
|
.collect();
|
|
let value_name: Vec<u16> = OsStr::new(STARTUP_VALUE_NAME)
|
|
.encode_wide()
|
|
.chain(std::iter::once(0))
|
|
.collect();
|
|
|
|
// SAFETY: FFI into the Win32 registry API. `key_path`/`value_name` are
|
|
// NUL-terminated wide strings that outlive the calls. `RegOpenKeyExW` writes
|
|
// the opened key into `hkey`; we only use it after confirming success, and
|
|
// always pair it with `RegCloseKey`.
|
|
unsafe {
|
|
let mut hkey = HKEY::default();
|
|
|
|
let result = RegOpenKeyExW(
|
|
HKEY_CURRENT_USER,
|
|
PCWSTR(key_path.as_ptr()),
|
|
0,
|
|
KEY_WRITE,
|
|
&mut hkey,
|
|
);
|
|
|
|
if result.is_err() {
|
|
warn!("Failed to open registry key for removal: {:?}", result);
|
|
return Ok(()); // Not an error if key doesn't exist
|
|
}
|
|
|
|
let delete_result = RegDeleteValueW(hkey, PCWSTR(value_name.as_ptr()));
|
|
|
|
let _ = RegCloseKey(hkey);
|
|
|
|
if delete_result.is_err() {
|
|
warn!("Registry value may not exist: {:?}", delete_result);
|
|
} else {
|
|
info!("Successfully removed from startup");
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Full uninstall: remove from startup and delete the executable
|
|
#[cfg(windows)]
|
|
pub fn uninstall() -> Result<()> {
|
|
use std::ffi::OsStr;
|
|
use std::os::windows::ffi::OsStrExt;
|
|
use windows::Win32::Storage::FileSystem::{MoveFileExW, MOVEFILE_DELAY_UNTIL_REBOOT};
|
|
|
|
info!("Uninstalling agent");
|
|
|
|
// First remove from startup
|
|
let _ = remove_from_startup();
|
|
|
|
// Get the path to the current executable
|
|
let exe_path = std::env::current_exe()?;
|
|
let exe_path_str = exe_path.to_string_lossy();
|
|
|
|
info!("Scheduling deletion of: {}", exe_path_str);
|
|
|
|
// Convert path to wide string
|
|
let exe_wide: Vec<u16> = OsStr::new(&*exe_path_str)
|
|
.encode_wide()
|
|
.chain(std::iter::once(0))
|
|
.collect();
|
|
|
|
// Schedule the file for deletion on next reboot
|
|
// This is necessary because the executable is currently running
|
|
unsafe {
|
|
let result = MoveFileExW(
|
|
PCWSTR(exe_wide.as_ptr()),
|
|
PCWSTR::null(),
|
|
MOVEFILE_DELAY_UNTIL_REBOOT,
|
|
);
|
|
|
|
if result.is_err() {
|
|
warn!(
|
|
"Failed to schedule file deletion: {:?}. File may need manual removal.",
|
|
result
|
|
);
|
|
} else {
|
|
info!("Executable scheduled for deletion on reboot");
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Install the SAS service if the binary is available
|
|
/// This allows the agent to send Ctrl+Alt+Del even without SYSTEM privileges
|
|
// Not yet wired into the CLI; retained as the SAS service management API.
|
|
#[allow(dead_code)]
|
|
#[cfg(windows)]
|
|
pub fn install_sas_service() -> Result<()> {
|
|
info!("Attempting to install SAS service...");
|
|
|
|
// Check if the SAS service binary exists alongside the agent
|
|
let exe_path = std::env::current_exe()?;
|
|
let exe_dir = exe_path
|
|
.parent()
|
|
.ok_or_else(|| anyhow::anyhow!("No parent directory"))?;
|
|
let sas_binary = exe_dir.join("guruconnect-sas-service.exe");
|
|
|
|
if !sas_binary.exists() {
|
|
// Also check in Program Files
|
|
let program_files =
|
|
std::path::PathBuf::from(r"C:\Program Files\GuruConnect\guruconnect-sas-service.exe");
|
|
if !program_files.exists() {
|
|
warn!("SAS service binary not found");
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
// Run the install command
|
|
let sas_path = if sas_binary.exists() {
|
|
sas_binary
|
|
} else {
|
|
std::path::PathBuf::from(r"C:\Program Files\GuruConnect\guruconnect-sas-service.exe")
|
|
};
|
|
|
|
let output = std::process::Command::new(&sas_path)
|
|
.arg("install")
|
|
.output();
|
|
|
|
match output {
|
|
Ok(result) => {
|
|
if result.status.success() {
|
|
info!("SAS service installed successfully");
|
|
} else {
|
|
let stderr = String::from_utf8_lossy(&result.stderr);
|
|
warn!("SAS service install failed: {}", stderr);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
warn!("Failed to run SAS service installer: {}", e);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Uninstall the SAS service
|
|
// Not yet wired into the CLI; retained as the SAS service management API.
|
|
#[allow(dead_code)]
|
|
#[cfg(windows)]
|
|
pub fn uninstall_sas_service() -> Result<()> {
|
|
info!("Attempting to uninstall SAS service...");
|
|
|
|
// Try to find and run the uninstall command
|
|
let paths = [
|
|
std::env::current_exe()
|
|
.ok()
|
|
.and_then(|p| p.parent().map(|d| d.join("guruconnect-sas-service.exe"))),
|
|
Some(std::path::PathBuf::from(
|
|
r"C:\Program Files\GuruConnect\guruconnect-sas-service.exe",
|
|
)),
|
|
];
|
|
|
|
for path in paths.iter().flatten() {
|
|
if path.exists() {
|
|
let output = std::process::Command::new(path).arg("uninstall").output();
|
|
|
|
if let Ok(result) = output {
|
|
if result.status.success() {
|
|
info!("SAS service uninstalled successfully");
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
warn!("SAS service binary not found for uninstall");
|
|
Ok(())
|
|
}
|
|
|
|
/// Check if the SAS service is installed and running
|
|
// Not yet wired into the CLI; retained as the SAS service management API.
|
|
#[allow(dead_code)]
|
|
#[cfg(windows)]
|
|
pub fn check_sas_service() -> bool {
|
|
use crate::sas_client;
|
|
sas_client::is_service_available()
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
pub fn add_to_startup() -> Result<()> {
|
|
warn!("Startup persistence not implemented for this platform");
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
pub fn remove_from_startup() -> Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
pub fn uninstall() -> Result<()> {
|
|
warn!("Uninstall not implemented for this platform");
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
pub fn install_sas_service() -> Result<()> {
|
|
warn!("SAS service only available on Windows");
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
pub fn uninstall_sas_service() -> Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
pub fn check_sas_service() -> bool {
|
|
false
|
|
}
|