diff --git a/src/cli/code.rs b/src/cli/code.rs index 98d7714..769b2bd 100644 --- a/src/cli/code.rs +++ b/src/cli/code.rs @@ -84,7 +84,7 @@ async fn open_via_lima( launch_editor(editor, ssh_host, path) } -/// Incus: get VM IP address, configure SSH, then launch editor. +/// Incus: get VM IP address, configure SSH key auth, then launch editor. async fn open_via_incus( ssh_host: &str, vm_name: &str, @@ -104,15 +104,100 @@ async fn open_via_incus( let ip = extract_incus_ip(&result.stdout)?; + // Detect actual username in the VM + let uid_result = run_cmd( + "incus", + &["exec", vm_name, "--", "bash", "-lc", + "awk -F: '$3 >= 1000 && $3 < 65534 { print $1; exit }' /etc/passwd"], + ).await?; + let username = uid_result.stdout.trim(); + let username = if username.is_empty() { "dev" } else { username }; + + // Ensure SSH key-based auth is set up (inject host pubkey into VM) + ensure_ssh_key_auth(vm_name, username).await?; + // Build SSH config block for this VM + let home = dirs::home_dir().unwrap_or_default(); + let key_path = home.join(".ssh").join("id_ed25519"); + let key_fallback = home.join(".ssh").join("id_rsa"); + let identity = if key_path.exists() { + key_path.to_string_lossy().to_string() + } else if key_fallback.exists() { + key_fallback.to_string_lossy().to_string() + } else { + // Will be created by ensure_ssh_key_auth + key_path.to_string_lossy().to_string() + }; + let ssh_config = format!( - "Host {ssh_host}\n HostName {ip}\n User devbox\n StrictHostKeyChecking no\n UserKnownHostsFile /dev/null" + "Host {ssh_host}\n HostName {ip}\n User {username}\n IdentityFile {identity}\n StrictHostKeyChecking no\n UserKnownHostsFile /dev/null" ); write_ssh_config(ssh_host, &ssh_config)?; launch_editor(editor, ssh_host, path) } +/// Ensure SSH key-based auth is configured between host and Incus VM. +/// Generates a host key if needed, then injects the public key into the VM. +async fn ensure_ssh_key_auth(vm_name: &str, username: &str) -> Result<()> { + let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Cannot determine home dir"))?; + let ssh_dir = home.join(".ssh"); + + // Find or generate host SSH key + let key_path = ssh_dir.join("id_ed25519"); + let pub_path = ssh_dir.join("id_ed25519.pub"); + + if !pub_path.exists() { + let rsa_pub = ssh_dir.join("id_rsa.pub"); + if !rsa_pub.exists() { + // Generate a new key + println!("Generating SSH key for devbox..."); + std::fs::create_dir_all(&ssh_dir)?; + let status = std::process::Command::new("ssh-keygen") + .args(["-t", "ed25519", "-f"]) + .arg(&key_path) + .args(["-N", "", "-q"]) + .status()?; + if !status.success() { + bail!("Failed to generate SSH key"); + } + } + } + + // Read the public key + let pub_key_path = if pub_path.exists() { &pub_path } else { &ssh_dir.join("id_rsa.pub") }; + let pubkey = std::fs::read_to_string(pub_key_path) + .map_err(|e| anyhow::anyhow!("Cannot read SSH public key: {e}"))?; + let pubkey = pubkey.trim(); + + // Ensure sshd is enabled and the user's authorized_keys has our pubkey + let setup_cmd = format!( + "mkdir -p /home/{username}/.ssh && \ + chmod 700 /home/{username}/.ssh && \ + touch /home/{username}/.ssh/authorized_keys && \ + chmod 600 /home/{username}/.ssh/authorized_keys && \ + grep -qF '{pubkey}' /home/{username}/.ssh/authorized_keys 2>/dev/null || \ + echo '{pubkey}' >> /home/{username}/.ssh/authorized_keys && \ + chown -R $(id -u {username}):users /home/{username}/.ssh" + ); + let result = run_cmd( + "incus", + &["exec", vm_name, "--", "bash", "-lc", &setup_cmd], + ).await?; + + if result.exit_code != 0 { + eprintln!("Warning: SSH key setup may have failed: {}", result.stderr.trim()); + } + + // Ensure sshd is running + let _ = run_cmd( + "incus", + &["exec", vm_name, "--", "bash", "-lc", "systemctl enable --now sshd 2>/dev/null || systemctl enable --now ssh 2>/dev/null; true"], + ).await; + + Ok(()) +} + /// Extract the first IPv4 address from `incus list --format json` output. fn extract_incus_ip(json_output: &str) -> Result { let arr: Vec = serde_json::from_str(json_output) diff --git a/src/nix/mod.rs b/src/nix/mod.rs index 02be302..c86c800 100644 --- a/src/nix/mod.rs +++ b/src/nix/mod.rs @@ -70,7 +70,7 @@ pub async fn add_package(runtime: &dyn Runtime, sandbox_name: &str, package: &st let result = runtime .exec_cmd( sandbox_name, - &["sudo", "nix", "profile", "install", package], + &["bash", "-lc", &format!("nix profile install {package}")], false, ) .await?; @@ -84,7 +84,7 @@ pub async fn add_package(runtime: &dyn Runtime, sandbox_name: &str, package: &st let result = runtime .exec_cmd( sandbox_name, - &["sudo", "nix", "profile", "install", &flake_ref], + &["bash", "-lc", &format!("nix profile install {flake_ref}")], false, ) .await?; @@ -107,7 +107,7 @@ pub async fn remove_package( let result = runtime .exec_cmd( sandbox_name, - &["sudo", "nix", "profile", "remove", package], + &["bash", "-lc", &format!("nix profile remove {package}")], false, ) .await?; diff --git a/src/nix/rebuild.rs b/src/nix/rebuild.rs index 8a1234e..c7f247c 100644 --- a/src/nix/rebuild.rs +++ b/src/nix/rebuild.rs @@ -8,7 +8,7 @@ pub async fn nixos_rebuild(runtime: &dyn Runtime, sandbox_name: &str) -> Result< println!("Running nixos-rebuild switch..."); let result = runtime - .exec_cmd(sandbox_name, &["sudo", "nixos-rebuild", "switch"], false) + .exec_cmd(sandbox_name, &["bash", "-lc", "nixos-rebuild switch"], false) .await?; if result.exit_code != 0 { @@ -18,7 +18,7 @@ pub async fn nixos_rebuild(runtime: &dyn Runtime, sandbox_name: &str) -> Result< let rollback = runtime .exec_cmd( sandbox_name, - &["sudo", "nixos-rebuild", "switch", "--rollback"], + &["bash", "-lc", "nixos-rebuild switch --rollback"], false, ) .await; @@ -54,7 +54,7 @@ pub async fn write_state_toml( .exec_cmd( sandbox_name, &[ - "sudo", "bash", "-c", + "bash", "-lc", &format!( "mkdir -p /etc/devbox && cat > /etc/devbox/devbox-state.toml << 'DEVBOX_EOF'\n{toml_content}\nDEVBOX_EOF" ), @@ -84,7 +84,7 @@ pub async fn write_nix_file( .exec_cmd( sandbox_name, &[ - "sudo", "bash", "-c", + "bash", "-lc", &format!( "mkdir -p /etc/devbox/sets && cat > /etc/devbox/sets/{filename} << 'DEVBOX_EOF'\n{content}\nDEVBOX_EOF" ), diff --git a/src/sandbox/overlay.rs b/src/sandbox/overlay.rs index 0ace422..c0384c7 100644 --- a/src/sandbox/overlay.rs +++ b/src/sandbox/overlay.rs @@ -19,7 +19,8 @@ pub async fn diff(runtime: &dyn Runtime, sandbox_name: &str) -> Result { if change.is_dir { let result = runtime - .exec_cmd(sandbox_name, &["sudo", "mkdir", "-p", &lower_path], false) + .exec_cmd(sandbox_name, &["mkdir", "-p", &lower_path], false) .await?; if result.exit_code != 0 { eprintln!( @@ -191,14 +192,14 @@ pub async fn commit( ); if !parent.is_empty() && parent != LOWER { let _ = runtime - .exec_cmd(sandbox_name, &["sudo", "mkdir", "-p", &parent], false) + .exec_cmd(sandbox_name, &["mkdir", "-p", &parent], false) .await; } let result = runtime .exec_cmd( sandbox_name, - &["sudo", "cp", "-a", &upper_path, &lower_path], + &["cp", "-a", &upper_path, &lower_path], false, ) .await?; @@ -214,7 +215,7 @@ pub async fn commit( } ChangeStatus::Deleted => { let result = runtime - .exec_cmd(sandbox_name, &["sudo", "rm", "-rf", &lower_path], false) + .exec_cmd(sandbox_name, &["rm", "-rf", &lower_path], false) .await?; if result.exit_code != 0 { eprintln!( @@ -252,7 +253,7 @@ pub async fn discard( for path in filter_paths { let upper_path = format!("{UPPER}/{}", path.trim_start_matches('/')); let result = runtime - .exec_cmd(sandbox_name, &["sudo", "rm", "-rf", &upper_path], false) + .exec_cmd(sandbox_name, &["rm", "-rf", &upper_path], false) .await?; if result.exit_code == 0 { println!(" Discarded: {path}"); @@ -269,9 +270,8 @@ pub async fn discard( .exec_cmd( sandbox_name, &[ - "sudo", "bash", - "-c", + "-lc", &format!("rm -rf {UPPER}/* {UPPER}/.[!.]* 2>/dev/null; true"), ], false, @@ -296,7 +296,7 @@ pub async fn stash(runtime: &dyn Runtime, sandbox_name: &str) -> Result<()> { // Move upper to stash let result = runtime - .exec_cmd(sandbox_name, &["sudo", "mv", UPPER, STASH_DIR], false) + .exec_cmd(sandbox_name, &["mv", UPPER, STASH_DIR], false) .await?; if result.exit_code != 0 { @@ -305,7 +305,7 @@ pub async fn stash(runtime: &dyn Runtime, sandbox_name: &str) -> Result<()> { // Recreate empty upper directory let result = runtime - .exec_cmd(sandbox_name, &["sudo", "mkdir", "-p", UPPER], false) + .exec_cmd(sandbox_name, &["mkdir", "-p", UPPER], false) .await?; if result.exit_code != 0 { @@ -330,7 +330,7 @@ pub async fn stash_pop(runtime: &dyn Runtime, sandbox_name: &str) -> Result<()> "cp -a {STASH_DIR}/* {UPPER}/ 2>/dev/null; cp -a {STASH_DIR}/.[!.]* {UPPER}/ 2>/dev/null; true" ); let result = runtime - .exec_cmd(sandbox_name, &["sudo", "bash", "-c", &merge_cmd], false) + .exec_cmd(sandbox_name, &["bash", "-lc", &merge_cmd], false) .await?; if result.exit_code != 0 { @@ -339,7 +339,7 @@ pub async fn stash_pop(runtime: &dyn Runtime, sandbox_name: &str) -> Result<()> // Remove the stash directory let result = runtime - .exec_cmd(sandbox_name, &["sudo", "rm", "-rf", STASH_DIR], false) + .exec_cmd(sandbox_name, &["rm", "-rf", STASH_DIR], false) .await?; if result.exit_code != 0 { @@ -355,7 +355,7 @@ pub async fn has_stash(runtime: &dyn Runtime, sandbox_name: &str) -> Result/dev/null)\" ]"); let result = runtime - .exec_cmd(sandbox_name, &["bash", "-c", &check_cmd], false) + .exec_cmd(sandbox_name, &["bash", "-lc", &check_cmd], false) .await?; Ok(result.exit_code == 0) @@ -371,7 +371,7 @@ pub async fn refresh(runtime: &dyn Runtime, sandbox_name: &str) -> Result<()> { let result = runtime .exec_cmd( sandbox_name, - &["sudo", "mount", "-o", "remount", WORKSPACE], + &["bash", "-lc", &format!("mount -o remount {WORKSPACE}")], false, ) .await?; @@ -388,7 +388,7 @@ pub async fn refresh(runtime: &dyn Runtime, sandbox_name: &str) -> Result<()> { -o lowerdir={LOWER},upperdir={UPPER},workdir={WORK} {WORKSPACE}" ); let result = runtime - .exec_cmd(sandbox_name, &["sudo", "bash", "-c", &remount_cmd], false) + .exec_cmd(sandbox_name, &["bash", "-lc", &remount_cmd], false) .await?; if result.exit_code != 0 { @@ -421,7 +421,7 @@ pub async fn conflicts(runtime: &dyn Runtime, sandbox_name: &str) -> Result R LOWER, WORK, LOWER ); let result = runtime - .exec_cmd(sandbox_name, &["bash", "-c", &cmd], false) + .exec_cmd(sandbox_name, &["bash", "-lc", &cmd], false) .await?; if result.exit_code != 0 { @@ -498,7 +498,7 @@ pub async fn conflicts_quiet( upper_path, lower_path, upper_path, lower_path ); let result = runtime - .exec_cmd(sandbox_name, &["bash", "-c", &diff_cmd], false) + .exec_cmd(sandbox_name, &["bash", "-lc", &diff_cmd], false) .await?; if result.stdout.trim() == "CONFLICT" {