🥇 export from upstream (3866546)
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
services:
|
||||
claude:
|
||||
image: sandcage-claude:latest
|
||||
working_dir: /workspace
|
||||
working_dir: ${SANDCAGE_CONTAINER_DIR}
|
||||
user: "${SANDCAGE_UID}:${SANDCAGE_GID}"
|
||||
volumes:
|
||||
- ${SANDCAGE_WORKSPACE}:/workspace
|
||||
- ${SANDCAGE_WORKSPACE}:${SANDCAGE_CONTAINER_DIR}
|
||||
- ${SANDCAGE_HOME}/.claude:/home/agent/.claude
|
||||
- ${SANDCAGE_GLOBAL_JUSTFILE}:/home/agent/.justfile:ro
|
||||
environment:
|
||||
@@ -14,10 +14,10 @@ services:
|
||||
|
||||
codex:
|
||||
image: sandcage-codex:latest
|
||||
working_dir: /workspace
|
||||
working_dir: ${SANDCAGE_CONTAINER_DIR}
|
||||
user: "${SANDCAGE_UID}:${SANDCAGE_GID}"
|
||||
volumes:
|
||||
- ${SANDCAGE_WORKSPACE}:/workspace
|
||||
- ${SANDCAGE_WORKSPACE}:${SANDCAGE_CONTAINER_DIR}
|
||||
- ${SANDCAGE_HOME}/.codex:/home/agent/.codex
|
||||
- ${SANDCAGE_GLOBAL_JUSTFILE}:/home/agent/.justfile:ro
|
||||
environment:
|
||||
@@ -27,10 +27,10 @@ services:
|
||||
|
||||
shell:
|
||||
image: sandcage-base:latest
|
||||
working_dir: /workspace
|
||||
working_dir: ${SANDCAGE_CONTAINER_DIR}
|
||||
user: "${SANDCAGE_UID}:${SANDCAGE_GID}"
|
||||
volumes:
|
||||
- ${SANDCAGE_WORKSPACE}:/workspace
|
||||
- ${SANDCAGE_WORKSPACE}:${SANDCAGE_CONTAINER_DIR}
|
||||
- ${SANDCAGE_HOME}/.claude:/home/agent/.claude
|
||||
- ${SANDCAGE_HOME}/.codex:/home/agent/.codex
|
||||
- ${SANDCAGE_GLOBAL_JUSTFILE}:/home/agent/.justfile:ro
|
||||
|
||||
@@ -52,6 +52,8 @@ struct RawConfig {
|
||||
dockerfiles: Option<HashMap<String, PathBuf>>,
|
||||
#[serde(default)]
|
||||
trusted_projects: Option<Vec<PathBuf>>,
|
||||
#[serde(default)]
|
||||
container_workspace: Option<String>,
|
||||
|
||||
/// Absorb everything else so we can warn about unknown fields.
|
||||
#[serde(flatten)]
|
||||
@@ -81,13 +83,15 @@ pub struct SandcageConfig {
|
||||
pub dockerfiles: Option<HashMap<String, PathBuf>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
@@ -138,6 +142,7 @@ fn from_raw(raw: RawConfig) -> SandcageConfig {
|
||||
justfile: raw.justfile,
|
||||
dockerfiles: raw.dockerfiles,
|
||||
trusted_projects: raw.trusted_projects,
|
||||
container_workspace: raw.container_workspace,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,4 +506,17 @@ EDITOR = "vim"
|
||||
.expect("nonexistent files should not error");
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ fn id_flag(flag: &str) -> Result<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 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")?)
|
||||
};
|
||||
|
||||
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();
|
||||
env.insert("SANDCAGE_UID".into(), uid);
|
||||
env.insert("SANDCAGE_GID".into(), gid);
|
||||
@@ -148,10 +160,18 @@ pub fn build_compose_env(workspace: &Path) -> Result<HashMap<String, String>> {
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
);
|
||||
env.insert("SANDCAGE_CONTAINER_DIR".into(), container_dir);
|
||||
|
||||
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> {
|
||||
let mut tmp = tempfile::Builder::new()
|
||||
.prefix("sandcage-compose-")
|
||||
@@ -241,7 +261,7 @@ pub fn run_service(
|
||||
let compose_file = write_compose_tempfile()?;
|
||||
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);
|
||||
|
||||
@@ -529,7 +549,7 @@ mod tests {
|
||||
#[test]
|
||||
fn build_compose_env_contains_required_keys() {
|
||||
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_GID"), "missing SANDCAGE_GID");
|
||||
@@ -539,19 +559,20 @@ mod tests {
|
||||
env.contains_key("SANDCAGE_GLOBAL_JUSTFILE"),
|
||||
"missing SANDCAGE_GLOBAL_JUSTFILE"
|
||||
);
|
||||
assert!(env.contains_key("SANDCAGE_CONTAINER_DIR"), "missing SANDCAGE_CONTAINER_DIR");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_compose_env_workspace_matches() {
|
||||
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");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_compose_env_home_ends_with_sandcage() {
|
||||
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!(
|
||||
env["SANDCAGE_HOME"].ends_with(".sandcage"),
|
||||
"SANDCAGE_HOME should end with .sandcage, got: {}",
|
||||
@@ -562,7 +583,7 @@ mod tests {
|
||||
#[test]
|
||||
fn build_compose_env_justfile_is_under_home() {
|
||||
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!(
|
||||
env["SANDCAGE_GLOBAL_JUSTFILE"].starts_with(&env["SANDCAGE_HOME"]),
|
||||
"SANDCAGE_GLOBAL_JUSTFILE should be under SANDCAGE_HOME"
|
||||
@@ -576,7 +597,7 @@ mod tests {
|
||||
#[test]
|
||||
fn uid_gid_are_numeric() {
|
||||
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"]
|
||||
.parse()
|
||||
@@ -799,4 +820,45 @@ mod tests {
|
||||
let help_pos = args.iter().position(|a| a == "--help").unwrap();
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +141,9 @@ fn build_yaml(ecosystem: Ecosystem) -> String {
|
||||
# mounts:\n\
|
||||
# - /path/on/host:/path/in/container\n\
|
||||
\n\
|
||||
# shell: zsh\n",
|
||||
# shell: zsh\n\
|
||||
\n\
|
||||
# container_workspace: /workspace/my-project\n",
|
||||
ecosystem = ecosystem.as_str(),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user