on main help sign in
fix: fixed the symlink permission issue
166b035robcholz
src/config.rs+1−15
@@ -73,21 +73,7 @@ fn default_auto_shutdown_ms() -> u64 {
73}
73}
74
74
75fn default_mounts() -> Vec<String> {
75fn default_mounts() -> Vec<String> {
76− let home = match std::env::var("HOME") {
77− Ok(home) => PathBuf::from(home),
78− Err(_) => return Vec::new(),
79− };
80−
81− let mut mounts = Vec::new();
76+ Vec::new()
82− let codex_host = home.join(".codex");
83− if codex_host.exists() {
84− mounts.push("~/.codex:/home/vibecoder/.codex:read-write".to_string());
85− }
86− let claude_host = home.join(".claude");
87− if claude_host.exists() {
88− mounts.push("~/.claude:/home/vibecoder/.claude:read-write".to_string());
89− }
90− mounts
91}
77}
92
78
93pub fn config_path(project_root: &Path) -> PathBuf {
79pub fn config_path(project_root: &Path) -> PathBuf {
src/instance.rs+13−1
@@ -46,6 +46,16 @@ pub(crate) struct InstanceConfig {
46 pub(crate) vm_ipv4: Option<String>,
46 pub(crate) vm_ipv4: Option<String>,
47}
47}
48
48
49+impl InstanceConfig {
50+ pub(crate) fn ssh_user_display(&self) -> String {
51+ if self.ssh_user.trim().is_empty() {
52+ DEFAULT_SSH_USER.to_string()
53+ } else {
54+ self.ssh_user.clone()
55+ }
56+ }
57+}
58+
49fn default_ssh_user() -> String {
59fn default_ssh_user() -> String {
50 DEFAULT_SSH_USER.to_string()
60 DEFAULT_SSH_USER.to_string()
51}
61}
@@ -361,6 +371,7 @@ pub(crate) fn build_ssh_login_actions(
361 project_name: &str,
371 project_name: &str,
362 guest_dir: &str,
372 guest_dir: &str,
363 key_name: &str,
373 key_name: &str,
374+ home_links_script: &str,
364) -> Vec<LoginAction> {
375) -> Vec<LoginAction> {
365 let config_guard = config.lock().expect("config mutex poisoned");
376 let config_guard = config.lock().expect("config mutex poisoned");
366 let ssh_user = config_guard.ssh_user.clone();
377 let ssh_user = config_guard.ssh_user.clone();
@@ -374,7 +385,8 @@ pub(crate) fn build_ssh_login_actions(
374 .replace("__SUDO_PASSWORD__", &sudo_password)
385 .replace("__SUDO_PASSWORD__", &sudo_password)
375 .replace("__PROJECT_NAME__", project_name)
386 .replace("__PROJECT_NAME__", project_name)
376 .replace("__KEY_PATH__", &key_path)
387 .replace("__KEY_PATH__", &key_path)
377− .replace("__VIBEBOX_SHELL_SCRIPT__", &commands::render_shell_script());
388+ .replace("__VIBEBOX_SHELL_SCRIPT__", &commands::render_shell_script())
389+ .replace("__VIBEBOX_HOME_LINKS__", home_links_script);
378 let setup = vm::script_command_from_content("ssh_setup", &setup_script)
390 let setup = vm::script_command_from_content("ssh_setup", &setup_script)
379 .expect("ssh setup script contained invalid marker");
391 .expect("ssh setup script contained invalid marker");
380
392
src/ssh.sh+3
@@ -68,6 +68,9 @@ if [ -z "$USER_HOME" ]; then
68 USER_HOME="/home/${SSH_USER}"
68 USER_HOME="/home/${SSH_USER}"
69fi
69fi
70
70
71+# Home mount links (config-driven)
72+__VIBEBOX_HOME_LINKS__
73+
71# Vibebox shell commands
74# Vibebox shell commands
72install -d -m 755 /etc/profile.d
75install -d -m 755 /etc/profile.d
73cat > /etc/profile.d/vibebox.sh <<'VIBEBOX_SHELL_EOF'
76cat > /etc/profile.d/vibebox.sh <<'VIBEBOX_SHELL_EOF'
src/vm_manager.rs+121−4
@@ -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,
24 },
24 },
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)
208}
208}
209
209
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 {
216+ links.push(link);
217+ }
218+ mounts.push(rewritten);
219+ }
220+ args.mounts = mounts;
221+ let script = render_home_links_script(&links, ssh_user);
222+ (args, script)
223+}
224+
225+struct HomeLink {
226+ source: String,
227+ target: String,
228+}
229+
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);
234+ }
235+ let host = parts[0];
236+ let guest = parts[1];
237+ let mode = parts.get(2).copied();
238+
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)
248+ } else {
249+ (String::new(), false)
250+ };
251+
252+ if !is_home {
253+ return (spec.to_string(), None);
254+ }
255+
256+ let root_base = "/usr/local/vibebox-mounts";
257+ let root_path = if rel.is_empty() {
258+ root_base.to_string()
259+ } else {
260+ format!("{root_base}/{rel}")
261+ };
262+ let target = if rel.is_empty() {
263+ home_prefix
264+ } else {
265+ format!("{home_prefix}/{rel}")
266+ };
267+
268+ let rewritten = match mode {
269+ Some(mode) => format!("{host}:{root_path}:{mode}"),
270+ None => format!("{host}:{root_path}"),
271+ };
272+
273+ (
274+ rewritten,
275+ Some(HomeLink {
276+ source: root_path,
277+ target,
278+ }),
279+ )
280+}
281+
282+fn render_home_links_script(links: &[HomeLink], ssh_user: &str) -> String {
283+ if links.is_empty() {
284+ return String::new();
285+ }
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());
300+ lines.push(format!(
301+ " chown -h \"{ssh_user}:{ssh_user}\" \"$dest\" 2>/dev/null || true"
302+ ));
303+ lines.push("}".to_string());
304+ for link in links {
305+ let src = shell_escape(&link.source);
306+ let dest = shell_escape(&link.target);
307+ lines.push(format!("link_home {src} {dest}"));
308+ }
309+ lines.join("\n")
310+}
311+
312+fn shell_escape(value: &str) -> String {
313+ let escaped = value.replace('\'', "'\"'\"'");
314+ format!("'{escaped}'")
315+}
316+
210struct PidFileGuard {
317struct PidFileGuard {
211 path: PathBuf,
318 path: PathBuf,
212}
319}
@@ -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)?;
533 }
640 }
534 let config = Arc::new(Mutex::new(config));
641 let config = Arc::new(Mutex::new(config));
642+ let ssh_user = config
643+ .lock()
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);
535
647
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(),
540 true,
652 true,
541 )?];
653 )?];
542− let extra_login_actions =
654+ let extra_login_actions = build_ssh_login_actions(
655+ &config,
656+ &project_name,
543− build_ssh_login_actions(&config, &project_name, ssh_guest_dir.as_str(), "ssh_key");
657+ ssh_guest_dir.as_str(),
658+ "ssh_key",
659+ &home_links_script,
660+ );
544
661
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) {
vibebox.toml+1−1
@@ -7,4 +7,4 @@ mounts = [
7]
7]
8
8
9[supervisor]
9[supervisor]
10−auto_shutdown_ms = 20000
10+auto_shutdown_ms = 2000