🥇 export from upstream (3866546)

This commit is contained in:
sandcage-export
2026-05-22 23:07:47 +02:00
parent 37a231beb4
commit 1a31d05e7c
4 changed files with 97 additions and 15 deletions
+6 -6
View File
@@ -1,10 +1,10 @@
services: services:
claude: claude:
image: sandcage-claude:latest image: sandcage-claude:latest
working_dir: /workspace working_dir: ${SANDCAGE_CONTAINER_DIR}
user: "${SANDCAGE_UID}:${SANDCAGE_GID}" user: "${SANDCAGE_UID}:${SANDCAGE_GID}"
volumes: volumes:
- ${SANDCAGE_WORKSPACE}:/workspace - ${SANDCAGE_WORKSPACE}:${SANDCAGE_CONTAINER_DIR}
- ${SANDCAGE_HOME}/.claude:/home/agent/.claude - ${SANDCAGE_HOME}/.claude:/home/agent/.claude
- ${SANDCAGE_GLOBAL_JUSTFILE}:/home/agent/.justfile:ro - ${SANDCAGE_GLOBAL_JUSTFILE}:/home/agent/.justfile:ro
environment: environment:
@@ -14,10 +14,10 @@ services:
codex: codex:
image: sandcage-codex:latest image: sandcage-codex:latest
working_dir: /workspace working_dir: ${SANDCAGE_CONTAINER_DIR}
user: "${SANDCAGE_UID}:${SANDCAGE_GID}" user: "${SANDCAGE_UID}:${SANDCAGE_GID}"
volumes: volumes:
- ${SANDCAGE_WORKSPACE}:/workspace - ${SANDCAGE_WORKSPACE}:${SANDCAGE_CONTAINER_DIR}
- ${SANDCAGE_HOME}/.codex:/home/agent/.codex - ${SANDCAGE_HOME}/.codex:/home/agent/.codex
- ${SANDCAGE_GLOBAL_JUSTFILE}:/home/agent/.justfile:ro - ${SANDCAGE_GLOBAL_JUSTFILE}:/home/agent/.justfile:ro
environment: environment:
@@ -27,10 +27,10 @@ services:
shell: shell:
image: sandcage-base:latest image: sandcage-base:latest
working_dir: /workspace working_dir: ${SANDCAGE_CONTAINER_DIR}
user: "${SANDCAGE_UID}:${SANDCAGE_GID}" user: "${SANDCAGE_UID}:${SANDCAGE_GID}"
volumes: volumes:
- ${SANDCAGE_WORKSPACE}:/workspace - ${SANDCAGE_WORKSPACE}:${SANDCAGE_CONTAINER_DIR}
- ${SANDCAGE_HOME}/.claude:/home/agent/.claude - ${SANDCAGE_HOME}/.claude:/home/agent/.claude
- ${SANDCAGE_HOME}/.codex:/home/agent/.codex - ${SANDCAGE_HOME}/.codex:/home/agent/.codex
- ${SANDCAGE_GLOBAL_JUSTFILE}:/home/agent/.justfile:ro - ${SANDCAGE_GLOBAL_JUSTFILE}:/home/agent/.justfile:ro
+19 -1
View File
@@ -52,6 +52,8 @@ struct RawConfig {
dockerfiles: Option<HashMap<String, PathBuf>>, dockerfiles: Option<HashMap<String, PathBuf>>,
#[serde(default)] #[serde(default)]
trusted_projects: Option<Vec<PathBuf>>, trusted_projects: Option<Vec<PathBuf>>,
#[serde(default)]
container_workspace: Option<String>,
/// Absorb everything else so we can warn about unknown fields. /// Absorb everything else so we can warn about unknown fields.
#[serde(flatten)] #[serde(flatten)]
@@ -81,13 +83,15 @@ pub struct SandcageConfig {
pub dockerfiles: Option<HashMap<String, PathBuf>>, pub dockerfiles: Option<HashMap<String, PathBuf>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub trusted_projects: Option<Vec<PathBuf>>, pub trusted_projects: Option<Vec<PathBuf>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub container_workspace: Option<String>,
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Known keys — used to filter the flattened `extra` map // Known keys — used to filter the flattened `extra` map
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const KNOWN_KEYS: &[&str] = &["env", "packages", "toolchains", "mounts", "shell", "justfile", "dockerfiles", "trusted_projects"]; const KNOWN_KEYS: &[&str] = &["env", "packages", "toolchains", "mounts", "shell", "justfile", "dockerfiles", "trusted_projects", "container_workspace"];
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Validation helpers // Validation helpers
@@ -138,6 +142,7 @@ fn from_raw(raw: RawConfig) -> SandcageConfig {
justfile: raw.justfile, justfile: raw.justfile,
dockerfiles: raw.dockerfiles, dockerfiles: raw.dockerfiles,
trusted_projects: raw.trusted_projects, trusted_projects: raw.trusted_projects,
container_workspace: raw.container_workspace,
} }
} }
@@ -501,4 +506,17 @@ EDITOR = "vim"
.expect("nonexistent files should not error"); .expect("nonexistent files should not error");
assert!(cfg.shell.is_none()); assert!(cfg.shell.is_none());
} }
#[test]
fn parse_container_workspace() {
let yaml = "container_workspace: /workspace/my-app\n";
let cfg = load_from_str(yaml).expect("parse container_workspace");
assert_eq!(cfg.container_workspace.as_deref(), Some("/workspace/my-app"));
}
#[test]
fn container_workspace_defaults_to_none() {
let cfg = load_from_str("").expect("parse empty");
assert!(cfg.container_workspace.is_none());
}
} }
+69 -7
View File
@@ -118,7 +118,7 @@ fn id_flag(flag: &str) -> Result<String> {
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
} }
pub fn build_compose_env(workspace: &Path) -> Result<HashMap<String, String>> { pub fn build_compose_env(workspace: &Path, config: &SandcageConfig) -> Result<HashMap<String, String>> {
let home = dirs::home_dir().ok_or(DockerError::NoHomeDir)?; let home = dirs::home_dir().ok_or(DockerError::NoHomeDir)?;
let sandcage_home = home.join(".sandcage"); let sandcage_home = home.join(".sandcage");
@@ -130,6 +130,18 @@ pub fn build_compose_env(workspace: &Path) -> Result<HashMap<String, String>> {
(id_flag("-u")?, id_flag("-g")?) (id_flag("-u")?, id_flag("-g")?)
}; };
let container_dir = match &config.container_workspace {
Some(path) if path.starts_with('/') => path.clone(),
Some(path) => {
eprintln!(
"sandcage: warning: container_workspace '{}' is not absolute, ignoring",
path
);
default_container_dir(workspace)
}
None => default_container_dir(workspace),
};
let mut env = HashMap::new(); let mut env = HashMap::new();
env.insert("SANDCAGE_UID".into(), uid); env.insert("SANDCAGE_UID".into(), uid);
env.insert("SANDCAGE_GID".into(), gid); env.insert("SANDCAGE_GID".into(), gid);
@@ -148,10 +160,18 @@ pub fn build_compose_env(workspace: &Path) -> Result<HashMap<String, String>> {
.to_string_lossy() .to_string_lossy()
.into_owned(), .into_owned(),
); );
env.insert("SANDCAGE_CONTAINER_DIR".into(), container_dir);
Ok(env) Ok(env)
} }
fn default_container_dir(workspace: &Path) -> String {
match workspace.file_name() {
Some(name) => format!("/workspace/{}", name.to_string_lossy()),
None => "/workspace".to_string(),
}
}
fn write_compose_tempfile() -> Result<tempfile::NamedTempFile> { fn write_compose_tempfile() -> Result<tempfile::NamedTempFile> {
let mut tmp = tempfile::Builder::new() let mut tmp = tempfile::Builder::new()
.prefix("sandcage-compose-") .prefix("sandcage-compose-")
@@ -241,7 +261,7 @@ pub fn run_service(
let compose_file = write_compose_tempfile()?; let compose_file = write_compose_tempfile()?;
let compose_path = compose_file.path().to_string_lossy().into_owned(); let compose_path = compose_file.path().to_string_lossy().into_owned();
let compose_env = build_compose_env(workspace)?; let compose_env = build_compose_env(workspace, config)?;
let run_args = build_run_args(service, &compose_path, config, shell_override, extra_args); let run_args = build_run_args(service, &compose_path, config, shell_override, extra_args);
@@ -529,7 +549,7 @@ mod tests {
#[test] #[test]
fn build_compose_env_contains_required_keys() { fn build_compose_env_contains_required_keys() {
let workspace = PathBuf::from("/tmp/test-workspace"); let workspace = PathBuf::from("/tmp/test-workspace");
let env = build_compose_env(&workspace).expect("build_compose_env"); let env = build_compose_env(&workspace, &SandcageConfig::default()).expect("build_compose_env");
assert!(env.contains_key("SANDCAGE_UID"), "missing SANDCAGE_UID"); assert!(env.contains_key("SANDCAGE_UID"), "missing SANDCAGE_UID");
assert!(env.contains_key("SANDCAGE_GID"), "missing SANDCAGE_GID"); assert!(env.contains_key("SANDCAGE_GID"), "missing SANDCAGE_GID");
@@ -539,19 +559,20 @@ mod tests {
env.contains_key("SANDCAGE_GLOBAL_JUSTFILE"), env.contains_key("SANDCAGE_GLOBAL_JUSTFILE"),
"missing SANDCAGE_GLOBAL_JUSTFILE" "missing SANDCAGE_GLOBAL_JUSTFILE"
); );
assert!(env.contains_key("SANDCAGE_CONTAINER_DIR"), "missing SANDCAGE_CONTAINER_DIR");
} }
#[test] #[test]
fn build_compose_env_workspace_matches() { fn build_compose_env_workspace_matches() {
let workspace = PathBuf::from("/my/project"); let workspace = PathBuf::from("/my/project");
let env = build_compose_env(&workspace).expect("build_compose_env"); let env = build_compose_env(&workspace, &SandcageConfig::default()).expect("build_compose_env");
assert_eq!(env["SANDCAGE_WORKSPACE"], "/my/project"); assert_eq!(env["SANDCAGE_WORKSPACE"], "/my/project");
} }
#[test] #[test]
fn build_compose_env_home_ends_with_sandcage() { fn build_compose_env_home_ends_with_sandcage() {
let workspace = PathBuf::from("/tmp"); let workspace = PathBuf::from("/tmp");
let env = build_compose_env(&workspace).expect("build_compose_env"); let env = build_compose_env(&workspace, &SandcageConfig::default()).expect("build_compose_env");
assert!( assert!(
env["SANDCAGE_HOME"].ends_with(".sandcage"), env["SANDCAGE_HOME"].ends_with(".sandcage"),
"SANDCAGE_HOME should end with .sandcage, got: {}", "SANDCAGE_HOME should end with .sandcage, got: {}",
@@ -562,7 +583,7 @@ mod tests {
#[test] #[test]
fn build_compose_env_justfile_is_under_home() { fn build_compose_env_justfile_is_under_home() {
let workspace = PathBuf::from("/tmp"); let workspace = PathBuf::from("/tmp");
let env = build_compose_env(&workspace).expect("build_compose_env"); let env = build_compose_env(&workspace, &SandcageConfig::default()).expect("build_compose_env");
assert!( assert!(
env["SANDCAGE_GLOBAL_JUSTFILE"].starts_with(&env["SANDCAGE_HOME"]), env["SANDCAGE_GLOBAL_JUSTFILE"].starts_with(&env["SANDCAGE_HOME"]),
"SANDCAGE_GLOBAL_JUSTFILE should be under SANDCAGE_HOME" "SANDCAGE_GLOBAL_JUSTFILE should be under SANDCAGE_HOME"
@@ -576,7 +597,7 @@ mod tests {
#[test] #[test]
fn uid_gid_are_numeric() { fn uid_gid_are_numeric() {
let workspace = PathBuf::from("/tmp"); let workspace = PathBuf::from("/tmp");
let env = build_compose_env(&workspace).expect("build_compose_env"); let env = build_compose_env(&workspace, &SandcageConfig::default()).expect("build_compose_env");
let uid: u32 = env["SANDCAGE_UID"] let uid: u32 = env["SANDCAGE_UID"]
.parse() .parse()
@@ -799,4 +820,45 @@ mod tests {
let help_pos = args.iter().position(|a| a == "--help").unwrap(); let help_pos = args.iter().position(|a| a == "--help").unwrap();
assert!(help_pos > service_pos); assert!(help_pos > service_pos);
} }
#[test]
fn build_compose_env_container_dir_auto_derived() {
let workspace = PathBuf::from("/home/user/projects/my-app");
let config = SandcageConfig::default();
let env = build_compose_env(&workspace, &config).expect("build_compose_env");
assert_eq!(env["SANDCAGE_CONTAINER_DIR"], "/workspace/my-app");
}
#[test]
fn build_compose_env_container_dir_from_config() {
let workspace = PathBuf::from("/home/user/projects/my-app");
let config = SandcageConfig {
container_workspace: Some("/workspace/custom".to_string()),
..Default::default()
};
let env = build_compose_env(&workspace, &config).expect("build_compose_env");
assert_eq!(env["SANDCAGE_CONTAINER_DIR"], "/workspace/custom");
}
#[test]
fn build_compose_env_container_dir_ignores_relative_override() {
let workspace = PathBuf::from("/home/user/projects/my-app");
let config = SandcageConfig {
container_workspace: Some("relative/path".to_string()),
..Default::default()
};
let env = build_compose_env(&workspace, &config).expect("build_compose_env");
assert_eq!(
env["SANDCAGE_CONTAINER_DIR"], "/workspace/my-app",
"relative paths should be ignored, falling back to auto-derive"
);
}
#[test]
fn build_compose_env_container_dir_root_fallback() {
let workspace = PathBuf::from("/");
let config = SandcageConfig::default();
let env = build_compose_env(&workspace, &config).expect("build_compose_env");
assert_eq!(env["SANDCAGE_CONTAINER_DIR"], "/workspace");
}
} }
+3 -1
View File
@@ -141,7 +141,9 @@ fn build_yaml(ecosystem: Ecosystem) -> String {
# mounts:\n\ # mounts:\n\
# - /path/on/host:/path/in/container\n\ # - /path/on/host:/path/in/container\n\
\n\ \n\
# shell: zsh\n", # shell: zsh\n\
\n\
# container_workspace: /workspace/my-project\n",
ecosystem = ecosystem.as_str(), ecosystem = ecosystem.as_str(),
) )
} }