@@ -19,8 +19,8 @@ use crate::{
19 config::CONFIG_PATH_ENV,
19 config::CONFIG_PATH_ENV,
20 instance::SERIAL_LOG_NAME,
20 instance::SERIAL_LOG_NAME,
21 instance::{
21 instance::{
22− InstanceConfig, build_ssh_login_actions, ensure_instance_dir, ensure_ssh_keypair,
22+ DEFAULT_SSH_USER, InstanceConfig, build_ssh_login_actions, ensure_instance_dir,
23− extract_ipv4, load_or_create_instance_config, write_instance_config,
23+ ensure_ssh_keypair, extract_ipv4, load_or_create_instance_config, write_instance_config,
25 session_manager::{
25 session_manager::{
26 GLOBAL_DIR_NAME, INSTANCE_FILENAME, VM_MANAGER_PID_NAME, VM_MANAGER_SOCKET_NAME,
26 GLOBAL_DIR_NAME, INSTANCE_FILENAME, VM_MANAGER_PID_NAME, VM_MANAGER_SOCKET_NAME,
@@ -207,6 +207,113 @@ fn is_socket_path(path: &Path) -> bool {
207 .unwrap_or(false)
207 .unwrap_or(false)
210+fn prepare_mounts_and_links(mut args: vm::VmArg, ssh_user: &str) -> (vm::VmArg, String) {
211+ let mut links = Vec::new();
212+ let mut mounts = Vec::with_capacity(args.mounts.len());
213+ for spec in args.mounts {
214+ let (rewritten, link) = rewrite_mount_spec(&spec, ssh_user);
215+ if let Some(link) = link {
218+ mounts.push(rewritten);
220+ args.mounts = mounts;
221+ let script = render_home_links_script(&links, ssh_user);
230+fn rewrite_mount_spec(spec: &str, ssh_user: &str) -> (String, Option<HomeLink>) {
231+ let parts: Vec<&str> = spec.split(':').collect();
232+ if parts.len() < 2 || parts.len() > 3 {
233+ return (spec.to_string(), None);
235+ let host = parts[0];
236+ let guest = parts[1];
237+ let mode = parts.get(2).copied();
239+ let home_prefix = format!("/home/{ssh_user}");
240+ let (rel, is_home) = if guest == "~" {
241+ (String::new(), true)
242+ } else if let Some(stripped) = guest.strip_prefix("~/") {
243+ (stripped.to_string(), true)
244+ } else if guest == home_prefix {
245+ (String::new(), true)
246+ } else if let Some(stripped) = guest.strip_prefix(&(home_prefix.clone() + "/")) {
247+ (stripped.to_string(), true)
249+ (String::new(), false)
253+ return (spec.to_string(), None);
256+ let root_base = "/usr/local/vibebox-mounts";
257+ let root_path = if rel.is_empty() {
258+ root_base.to_string()
260+ format!("{root_base}/{rel}")
262+ let target = if rel.is_empty() {
265+ format!("{home_prefix}/{rel}")
268+ let rewritten = match mode {
269+ Some(mode) => format!("{host}:{root_path}:{mode}"),
270+ None => format!("{host}:{root_path}"),
282+fn render_home_links_script(links: &[HomeLink], ssh_user: &str) -> String {
283+ if links.is_empty() {
284+ return String::new();
286+ let mut lines = Vec::new();
287+ lines.push("link_home() {".to_string());
288+ lines.push(" src=\"$1\"".to_string());
289+ lines.push(" dest=\"$2\"".to_string());
290+ lines.push(" if [ -L \"$dest\" ]; then".to_string());
291+ lines.push(" current=\"$(readlink \"$dest\" || true)\"".to_string());
292+ lines.push(" if [ \"$current\" != \"$src\" ]; then".to_string());
293+ lines.push(" rm -f \"$dest\"".to_string());
294+ lines.push(" fi".to_string());
295+ lines.push(" fi".to_string());
296+ lines.push(" if [ ! -e \"$dest\" ]; then".to_string());
297+ lines.push(" mkdir -p \"$(dirname \"$dest\")\"".to_string());
298+ lines.push(" ln -s \"$src\" \"$dest\"".to_string());
299+ lines.push(" fi".to_string());
301+ " chown -h \"{ssh_user}:{ssh_user}\" \"$dest\" 2>/dev/null || true"
303+ lines.push("}".to_string());
305+ let src = shell_escape(&link.source);
306+ let dest = shell_escape(&link.target);
307+ lines.push(format!("link_home {src} {dest}"));
312+fn shell_escape(value: &str) -> String {
313+ let escaped = value.replace('\'', "'\"'\"'");
314+ format!("'{escaped}'")
210struct PidFileGuard {
317struct PidFileGuard {
211 path: PathBuf,
318 path: PathBuf,
@@ -532,6 +639,11 @@ fn run_manager_with(
532 write_instance_config(&instance_dir.join(INSTANCE_FILENAME), &config)?;
639 write_instance_config(&instance_dir.join(INSTANCE_FILENAME), &config)?;
534 let config = Arc::new(Mutex::new(config));
641 let config = Arc::new(Mutex::new(config));
642+ let ssh_user = config
644+ .map(|cfg| cfg.ssh_user_display())
645+ .unwrap_or_else(|_| DEFAULT_SSH_USER.to_string());
646+ let (args, home_links_script) = prepare_mounts_and_links(args, &ssh_user);
536 let ssh_guest_dir = format!("/root/{}", GLOBAL_DIR_NAME);
648 let ssh_guest_dir = format!("/root/{}", GLOBAL_DIR_NAME);
537 let extra_shares = vec![DirectoryShare::new(
649 let extra_shares = vec![DirectoryShare::new(
@@ -539,8 +651,13 @@ fn run_manager_with(
539 ssh_guest_dir.clone().into(),
651 ssh_guest_dir.clone().into(),
542− let extra_login_actions =
654+ let extra_login_actions = build_ssh_login_actions(
543− build_ssh_login_actions(&config, &project_name, ssh_guest_dir.as_str(), "ssh_key");
657+ ssh_guest_dir.as_str(),
545 let socket_path = instance_dir.join(VM_MANAGER_SOCKET_NAME);
662 let socket_path = instance_dir.join(VM_MANAGER_SOCKET_NAME);
546 if let Ok(stream) = UnixStream::connect(&socket_path) {
663 if let Ok(stream) = UnixStream::connect(&socket_path) {