diff --git a/src/runtime/incus.rs b/src/runtime/incus.rs index f934998..9f57000 100644 --- a/src/runtime/incus.rs +++ b/src/runtime/incus.rs @@ -217,27 +217,9 @@ impl Runtime for IncusRuntime { let vm = Self::vm_name(name); if interactive { - // For interactive sessions (shell attach, etc.), run as the - // non-root user with correct HOME and working directory. - let uid_str = Self::detect_vm_uid(&vm).await.unwrap_or("1000".to_string()); - let home_env = Self::detect_vm_home(&vm, &uid_str).await; - - let mut args = vec![ - "exec".to_string(), - vm, - "--user".to_string(), - uid_str, - "--cwd".to_string(), - "/workspace".to_string(), - "--env".to_string(), - home_env, - "--".to_string(), - ]; - for c in cmd { - args.push(c.to_string()); - } - let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); - run_interactive("incus", &arg_refs).await + let mut args = vec!["exec", &vm, "--"]; + args.extend_from_slice(cmd); + run_interactive("incus", &args).await } else { let mut args = vec!["exec", &vm, "--"]; args.extend_from_slice(cmd); @@ -245,6 +227,32 @@ impl Runtime for IncusRuntime { } } + /// Execute an interactive command as the non-root user. + /// Incus exec defaults to root, so we detect the first UID >= 1000 + /// and set --user, HOME, and CWD for proper user sessions. + async fn exec_as_user(&self, name: &str, cmd: &[&str]) -> Result { + let vm = Self::vm_name(name); + let uid_str = Self::detect_vm_uid(&vm).await.unwrap_or("1000".to_string()); + let home_env = Self::detect_vm_home(&vm, &uid_str).await; + + let mut args = vec![ + "exec".to_string(), + vm, + "--user".to_string(), + uid_str, + "--cwd".to_string(), + "/workspace".to_string(), + "--env".to_string(), + home_env, + "--".to_string(), + ]; + for c in cmd { + args.push(c.to_string()); + } + let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); + run_interactive("incus", &arg_refs).await + } + async fn destroy(&self, name: &str) -> Result<()> { let vm = Self::vm_name(name); // Stop first (ignore errors if already stopped) diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index e31ce2e..25f3538 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -126,4 +126,11 @@ pub trait Runtime: Send + Sync { /// Update mount points for an existing sandbox. /// Stops the VM, updates mounts in the config, and restarts. async fn update_mounts(&self, name: &str, mounts: &[Mount]) -> Result<()>; + + /// Execute an interactive command as the non-root user. + /// Used for shell attach — defaults to exec_cmd with interactive=true. + /// Runtimes like Incus override this to set --user, HOME, and CWD. + async fn exec_as_user(&self, name: &str, cmd: &[&str]) -> Result { + self.exec_cmd(name, cmd, true).await + } } diff --git a/src/sandbox/mod.rs b/src/sandbox/mod.rs index 8404ad9..377e672 100644 --- a/src/sandbox/mod.rs +++ b/src/sandbox/mod.rs @@ -237,7 +237,7 @@ impl SandboxManager { if layout == "plain" { println!("Attaching to sandbox '{name}'..."); let shell = Self::probe_shell(runtime.as_ref(), name).await; - runtime.exec_cmd(name, &[&shell, "-l"], true).await?; + runtime.exec_as_user(name, &[&shell, "-l"]).await?; return Ok(()); } @@ -249,7 +249,7 @@ impl SandboxManager { // No Zellij — fall back to raw shell println!("Attaching to sandbox '{name}'..."); let shell = Self::probe_shell(runtime.as_ref(), name).await; - runtime.exec_cmd(name, &[&shell, "-l"], true).await?; + runtime.exec_as_user(name, &[&shell, "-l"]).await?; return Ok(()); } @@ -317,7 +317,7 @@ impl SandboxManager { // Reattach to existing live session println!("Reattaching to sandbox '{name}'..."); runtime - .exec_cmd(name, &["zellij", "attach", &session_name], true) + .exec_as_user(name, &["zellij", "attach", &session_name]) .await?; } else { // Create new named session with layout. @@ -336,10 +336,9 @@ impl SandboxManager { println!("Attaching to sandbox '{name}' (layout: {effective_layout})..."); runtime - .exec_cmd( + .exec_as_user( name, &["zellij", "--config", &config_path, "--layout", &layout_path], - true, ) .await?; }