🥇 export from upstream (3866546)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user