From 166b035977697728cb0d49823a356be5f02cdd9e Mon Sep 17 00:00:00 2001 From: robcholz <84130577+robcholz@users.noreply.github.com> Date: Sat, 7 Feb 2026 17:26:26 -0500 Subject: [PATCH] fix: fixed the symlink permission issue --- src/config.rs | 16 +----- src/instance.rs | 14 +++++- src/ssh.sh | 3 ++ src/vm_manager.rs | 125 ++++++++++++++++++++++++++++++++++++++++++++-- vibebox.toml | 2 +- 5 files changed, 139 insertions(+), 21 deletions(-) diff --git a/src/config.rs b/src/config.rs index 632f6a6..1c00d9b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -73,21 +73,7 @@ fn default_auto_shutdown_ms() -> u64 { } fn default_mounts() -> Vec { - let home = match std::env::var("HOME") { - Ok(home) => PathBuf::from(home), - Err(_) => return Vec::new(), - }; - - let mut mounts = Vec::new(); - let codex_host = home.join(".codex"); - if codex_host.exists() { - mounts.push("~/.codex:/home/vibecoder/.codex:read-write".to_string()); - } - let claude_host = home.join(".claude"); - if claude_host.exists() { - mounts.push("~/.claude:/home/vibecoder/.claude:read-write".to_string()); - } - mounts + Vec::new() } pub fn config_path(project_root: &Path) -> PathBuf { diff --git a/src/instance.rs b/src/instance.rs index 34f9c97..a652a86 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -46,6 +46,16 @@ pub(crate) struct InstanceConfig { pub(crate) vm_ipv4: Option, } +impl InstanceConfig { + pub(crate) fn ssh_user_display(&self) -> String { + if self.ssh_user.trim().is_empty() { + DEFAULT_SSH_USER.to_string() + } else { + self.ssh_user.clone() + } + } +} + fn default_ssh_user() -> String { DEFAULT_SSH_USER.to_string() } @@ -361,6 +371,7 @@ pub(crate) fn build_ssh_login_actions( project_name: &str, guest_dir: &str, key_name: &str, + home_links_script: &str, ) -> Vec { let config_guard = config.lock().expect("config mutex poisoned"); let ssh_user = config_guard.ssh_user.clone(); @@ -374,7 +385,8 @@ pub(crate) fn build_ssh_login_actions( .replace("__SUDO_PASSWORD__", &sudo_password) .replace("__PROJECT_NAME__", project_name) .replace("__KEY_PATH__", &key_path) - .replace("__VIBEBOX_SHELL_SCRIPT__", &commands::render_shell_script()); + .replace("__VIBEBOX_SHELL_SCRIPT__", &commands::render_shell_script()) + .replace("__VIBEBOX_HOME_LINKS__", home_links_script); let setup = vm::script_command_from_content("ssh_setup", &setup_script) .expect("ssh setup script contained invalid marker"); diff --git a/src/ssh.sh b/src/ssh.sh index 40ee6ea..cf15549 100644 --- a/src/ssh.sh +++ b/src/ssh.sh @@ -68,6 +68,9 @@ if [ -z "$USER_HOME" ]; then USER_HOME="/home/${SSH_USER}" fi +# Home mount links (config-driven) +__VIBEBOX_HOME_LINKS__ + # Vibebox shell commands install -d -m 755 /etc/profile.d cat > /etc/profile.d/vibebox.sh <<'VIBEBOX_SHELL_EOF' diff --git a/src/vm_manager.rs b/src/vm_manager.rs index c60ebb1..079adb0 100644 --- a/src/vm_manager.rs +++ b/src/vm_manager.rs @@ -19,8 +19,8 @@ use crate::{ config::CONFIG_PATH_ENV, instance::SERIAL_LOG_NAME, instance::{ - InstanceConfig, build_ssh_login_actions, ensure_instance_dir, ensure_ssh_keypair, - extract_ipv4, load_or_create_instance_config, write_instance_config, + DEFAULT_SSH_USER, InstanceConfig, build_ssh_login_actions, ensure_instance_dir, + ensure_ssh_keypair, extract_ipv4, load_or_create_instance_config, write_instance_config, }, session_manager::{ GLOBAL_DIR_NAME, INSTANCE_FILENAME, VM_MANAGER_PID_NAME, VM_MANAGER_SOCKET_NAME, @@ -207,6 +207,113 @@ fn is_socket_path(path: &Path) -> bool { .unwrap_or(false) } +fn prepare_mounts_and_links(mut args: vm::VmArg, ssh_user: &str) -> (vm::VmArg, String) { + let mut links = Vec::new(); + let mut mounts = Vec::with_capacity(args.mounts.len()); + for spec in args.mounts { + let (rewritten, link) = rewrite_mount_spec(&spec, ssh_user); + if let Some(link) = link { + links.push(link); + } + mounts.push(rewritten); + } + args.mounts = mounts; + let script = render_home_links_script(&links, ssh_user); + (args, script) +} + +struct HomeLink { + source: String, + target: String, +} + +fn rewrite_mount_spec(spec: &str, ssh_user: &str) -> (String, Option) { + let parts: Vec<&str> = spec.split(':').collect(); + if parts.len() < 2 || parts.len() > 3 { + return (spec.to_string(), None); + } + let host = parts[0]; + let guest = parts[1]; + let mode = parts.get(2).copied(); + + let home_prefix = format!("/home/{ssh_user}"); + let (rel, is_home) = if guest == "~" { + (String::new(), true) + } else if let Some(stripped) = guest.strip_prefix("~/") { + (stripped.to_string(), true) + } else if guest == home_prefix { + (String::new(), true) + } else if let Some(stripped) = guest.strip_prefix(&(home_prefix.clone() + "/")) { + (stripped.to_string(), true) + } else { + (String::new(), false) + }; + + if !is_home { + return (spec.to_string(), None); + } + + let root_base = "/usr/local/vibebox-mounts"; + let root_path = if rel.is_empty() { + root_base.to_string() + } else { + format!("{root_base}/{rel}") + }; + let target = if rel.is_empty() { + home_prefix + } else { + format!("{home_prefix}/{rel}") + }; + + let rewritten = match mode { + Some(mode) => format!("{host}:{root_path}:{mode}"), + None => format!("{host}:{root_path}"), + }; + + ( + rewritten, + Some(HomeLink { + source: root_path, + target, + }), + ) +} + +fn render_home_links_script(links: &[HomeLink], ssh_user: &str) -> String { + if links.is_empty() { + return String::new(); + } + let mut lines = Vec::new(); + lines.push("link_home() {".to_string()); + lines.push(" src=\"$1\"".to_string()); + lines.push(" dest=\"$2\"".to_string()); + lines.push(" if [ -L \"$dest\" ]; then".to_string()); + lines.push(" current=\"$(readlink \"$dest\" || true)\"".to_string()); + lines.push(" if [ \"$current\" != \"$src\" ]; then".to_string()); + lines.push(" rm -f \"$dest\"".to_string()); + lines.push(" fi".to_string()); + lines.push(" fi".to_string()); + lines.push(" if [ ! -e \"$dest\" ]; then".to_string()); + lines.push(" mkdir -p \"$(dirname \"$dest\")\"".to_string()); + lines.push(" ln -s \"$src\" \"$dest\"".to_string()); + lines.push(" fi".to_string()); + lines.push(format!( + " chown -h \"{ssh_user}:{ssh_user}\" \"$dest\" 2>/dev/null || true" + )); + lines.push("}".to_string()); + for link in links { + let src = shell_escape(&link.source); + let dest = shell_escape(&link.target); + lines.push(format!("link_home {src} {dest}")); + } + lines.join("\n") +} + +fn shell_escape(value: &str) -> String { + let escaped = value.replace('\'', "'\"'\"'"); + format!("'{escaped}'") +} + struct PidFileGuard { path: PathBuf, } @@ -532,6 +639,11 @@ fn run_manager_with( write_instance_config(&instance_dir.join(INSTANCE_FILENAME), &config)?; } let config = Arc::new(Mutex::new(config)); + let ssh_user = config + .lock() + .map(|cfg| cfg.ssh_user_display()) + .unwrap_or_else(|_| DEFAULT_SSH_USER.to_string()); + let (args, home_links_script) = prepare_mounts_and_links(args, &ssh_user); let ssh_guest_dir = format!("/root/{}", GLOBAL_DIR_NAME); let extra_shares = vec![DirectoryShare::new( @@ -539,8 +651,13 @@ fn run_manager_with( ssh_guest_dir.clone().into(), true, )?]; - let extra_login_actions = - build_ssh_login_actions(&config, &project_name, ssh_guest_dir.as_str(), "ssh_key"); + let extra_login_actions = build_ssh_login_actions( + &config, + &project_name, + ssh_guest_dir.as_str(), + "ssh_key", + &home_links_script, + ); let socket_path = instance_dir.join(VM_MANAGER_SOCKET_NAME); if let Ok(stream) = UnixStream::connect(&socket_path) { diff --git a/vibebox.toml b/vibebox.toml index 32cea5d..94d5bf9 100644 --- a/vibebox.toml +++ b/vibebox.toml @@ -7,4 +7,4 @@ mounts = [ ] [supervisor] -auto_shutdown_ms = 20000 +auto_shutdown_ms = 2000