7.2 KiB
Mount Tilde Expansion Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Expand ~ in mount paths to the user's home directory at Docker invocation time, so mounts work on all platforms without relying on shell expansion.
Architecture: A single pure function expand_mount_path in docker.rs replaces ~ with dirs::home_dir() and normalizes Windows separators. Called from build_run_args where mounts become -v flags. Config files keep the portable ~ form.
Tech Stack: Rust, dirs crate (already a dependency)
Task 1: Add expand_mount_path function with TDD
Files:
-
Modify:
crates/sandcage/src/docker.rs:227-233(call site) -
Modify:
crates/sandcage/src/docker.rs(add function + tests at end of file) -
Step 1: Write the failing tests for
expand_mount_path
Add these tests inside the existing #[cfg(test)] mod tests block in docker.rs:
#[test]
fn expand_mount_path_expands_tilde_slash() {
let home = dirs::home_dir().unwrap();
let home_str = home.to_string_lossy().replace('\\', "/");
let result = expand_mount_path("~/.ssh:/home/agent/.ssh:ro");
assert_eq!(result, format!("{home_str}/.ssh:/home/agent/.ssh:ro"));
}
#[test]
fn expand_mount_path_expands_bare_tilde() {
let home = dirs::home_dir().unwrap();
let home_str = home.to_string_lossy().replace('\\', "/");
let result = expand_mount_path("~:/container");
assert_eq!(result, format!("{home_str}:/container"));
}
#[test]
fn expand_mount_path_ignores_absolute() {
let result = expand_mount_path("/absolute:/container:ro");
assert_eq!(result, "/absolute:/container:ro");
}
#[test]
fn expand_mount_path_ignores_relative() {
let result = expand_mount_path("./relative:/container");
assert_eq!(result, "./relative:/container");
}
#[test]
fn expand_mount_path_ignores_no_colon() {
let result = expand_mount_path("no-colon");
assert_eq!(result, "no-colon");
}
- Step 2: Run tests to verify they fail
Run: cargo test -p sandcage expand_mount_path
Expected: compilation error — expand_mount_path is not defined.
- Step 3: Implement
expand_mount_path
Add this function in docker.rs, just above build_run_args:
fn expand_mount_path(mount: &str) -> String {
let Some(colon_pos) = mount.find(':') else {
return mount.to_string();
};
let host_path = &mount[..colon_pos];
let rest = &mount[colon_pos..];
let expanded = if host_path == "~" {
match dirs::home_dir() {
Some(home) => home.to_string_lossy().into_owned(),
None => return mount.to_string(),
}
} else if let Some(suffix) = host_path.strip_prefix("~/") {
match dirs::home_dir() {
Some(home) => format!("{}/{suffix}", home.to_string_lossy()),
None => return mount.to_string(),
}
} else {
return mount.to_string();
};
let normalized = expanded.replace('\\', "/");
format!("{normalized}{rest}")
}
- Step 4: Run tests to verify they pass
Run: cargo test -p sandcage expand_mount_path
Expected: all 5 tests PASS.
- Step 5: Commit
git add crates/sandcage/src/docker.rs
git commit -m "✨ Add expand_mount_path for cross-platform tilde expansion"
Task 2: Wire expand_mount_path into build_run_args and update existing tests
Files:
-
Modify:
crates/sandcage/src/docker.rs:229-231(call site) -
Modify:
crates/sandcage/src/docker.rs(existing tests at lines 866-921) -
Step 1: Replace
mount.clone()withexpand_mount_path(mount)inbuild_run_args
Change lines 229-231 from:
if mount.contains(':') {
args.push("-v".to_string());
args.push(mount.clone());
}
To:
if mount.contains(':') {
args.push("-v".to_string());
args.push(expand_mount_path(mount));
}
- Step 2: Update
build_run_args_with_mountstest
The test at line 866 currently asserts literal ~/.ssh strings. Replace the assertions:
#[test]
fn build_run_args_with_mounts() {
let home = dirs::home_dir().unwrap();
let home_str = home.to_string_lossy().replace('\\', "/");
let config = SandcageConfig {
mounts: Some(vec![
"~/.ssh:/home/agent/.ssh:ro".to_string(),
"~/.gitconfig:/home/agent/.gitconfig:ro".to_string(),
]),
..Default::default()
};
let args = build_run_args("claude", "/tmp/compose.yml", &config, false, &[]);
assert!(args.contains(&"-v".to_string()), "should contain -v flag");
let expected_ssh = format!("{home_str}/.ssh:/home/agent/.ssh:ro");
let expected_git = format!("{home_str}/.gitconfig:/home/agent/.gitconfig:ro");
assert!(args.contains(&expected_ssh), "SSH mount should be expanded");
assert!(args.contains(&expected_git), "gitconfig mount should be expanded");
let service_pos = args.iter().position(|a| a == "claude").unwrap();
let first_v = args.iter().position(|a| a == "-v").unwrap();
assert!(first_v < service_pos, "-v flags must come before the service name");
}
- Step 3: Update
build_run_args_mounts_with_env_and_extra_argstest
The test at line 902 asserts literal ~/.ssh. Replace the mount assertion:
#[test]
fn build_run_args_mounts_with_env_and_extra_args() {
let home = dirs::home_dir().unwrap();
let home_str = home.to_string_lossy().replace('\\', "/");
let mut env = HashMap::new();
env.insert("FOO".to_string(), "bar".to_string());
let config = SandcageConfig {
env: Some(env),
mounts: Some(vec!["~/.ssh:/home/agent/.ssh:ro".to_string()]),
..Default::default()
};
let args = build_run_args("claude", "/tmp/compose.yml", &config, false, &["--resume".to_string()]);
let service_pos = args.iter().position(|a| a == "claude").unwrap();
let env_pos = args.iter().position(|a| a == "FOO=bar").unwrap();
let expected_mount = format!("{home_str}/.ssh:/home/agent/.ssh:ro");
let mount_pos = args.iter().position(|a| *a == expected_mount).unwrap();
let resume_pos = args.iter().position(|a| a == "--resume").unwrap();
assert!(env_pos < service_pos, "env before service");
assert!(mount_pos < service_pos, "mounts before service");
assert!(resume_pos > service_pos, "extra args after service");
}
- Step 4: Run the full test suite
Run: cargo test -p sandcage
Expected: all tests PASS.
- Step 5: Commit
git add crates/sandcage/src/docker.rs
git commit -m "🐛 Expand tilde in mount paths before passing to Docker"
Task 3: Clean up the literal ~ directory artifact
- Step 1: Remove the literal
~directory from the project root
rm -rf "~"
Verify it's gone:
ls -la "~" 2>&1
Expected: "No such file or directory"
- Step 2: Verify
.gitignoredoesn't need updating
Check if the ~ directory was tracked by git:
git status
If it shows as untracked (it should, since git wouldn't track a ~ directory), no .gitignore change is needed.