mod backend;
#[cfg(feature = "docker_sandbox")]
mod docker_bollard;
#[cfg(not(feature = "docker_sandbox"))]
mod docker_stub;
mod runner;
pub use backend::{SandboxRunRequest, SyncDefaultSandboxBackend};
#[cfg(feature = "docker_sandbox")]
pub use docker_bollard::BollardSandboxBackend;
#[cfg(not(feature = "docker_sandbox"))]
pub use docker_stub::BollardSandboxBackend;
pub use runner::{
SandboxToolRunnerConfig, ToolInvocationLine, tool_runner_internal_main,
write_runner_config_json, write_runner_config_json_with_allowed_commands,
};
use std::path::{Path, PathBuf};
use std::time::Duration;
use crate::cm_config::{AgentConfig, SyncDefaultToolSandboxMode};
use self::runner::write_runner_config_json as write_runner_cfg_default;
pub fn default_sync_default_sandbox_backend() -> std::sync::Arc<dyn SyncDefaultSandboxBackend> {
std::sync::Arc::new(BollardSandboxBackend)
}
pub fn docker_sandbox_enabled(cfg: &AgentConfig) -> bool {
cfg.sync_tool_sandbox.sync_default_tool_sandbox_mode == SyncDefaultToolSandboxMode::Docker
}
pub async fn run_tool_in_docker(
backend: &std::sync::Arc<dyn SyncDefaultSandboxBackend>,
cfg: &AgentConfig,
effective_working_dir: &Path,
cfg_json_path: PathBuf,
inv: ToolInvocationLine,
) -> Result<String, String> {
if !docker_sandbox_enabled(cfg) {
return Err("内部错误:未启用 Docker 沙盒".to_string());
}
let image = cfg
.sync_tool_sandbox
.sync_default_tool_sandbox_docker_image
.trim();
if image.is_empty() {
return Err("错误:sync_default_tool_sandbox_docker_image 为空".to_string());
}
let exe = std::env::current_exe().map_err(|e| format!("current_exe:{}", e))?;
let work_canon = effective_working_dir
.canonicalize()
.map_err(|e| format!("规范工作区路径失败:{}", e))?;
let work_s = work_canon.to_string_lossy();
let exe_s = exe.to_string_lossy();
let cfg_in_container = "/run/crabmate-tool-runner.json";
let crabmate_in_container = "/crabmate";
let network = cfg
.sync_tool_sandbox
.sync_default_tool_sandbox_docker_network
.trim();
let network_mode = if network.is_empty() {
None
} else {
Some(network.to_string())
};
let binds = vec![
format!("{}:/workspace:rw", work_s),
format!("{}:{}:ro", exe_s, crabmate_in_container),
format!(
"{}:{}:ro",
cfg_json_path.to_string_lossy(),
cfg_in_container
),
];
let env = vec![format!("CM_TOOL_RUNNER_CONFIG_FILE={}", cfg_in_container)];
let cmd = vec![
crabmate_in_container.to_string(),
"tool-runner-internal".to_string(),
];
let stdin_payload = format!(
"{}\n",
serde_json::to_string(&inv).map_err(|e| format!("{}", e))?
)
.into_bytes();
let timeout_secs = cfg
.sync_tool_sandbox
.sync_default_tool_sandbox_docker_timeout_secs
.max(1);
let user = cfg
.sync_tool_sandbox
.sync_default_tool_sandbox_docker_user
.as_docker_user_string()
.map(str::to_string);
let req = SandboxRunRequest {
image: image.to_string(),
network_mode,
binds,
env,
working_dir: "/workspace".to_string(),
cmd,
stdin_payload,
timeout: Duration::from_secs(timeout_secs),
user,
};
let out = backend.run_isolated(req).await;
let _ = std::fs::remove_file(&cfg_json_path);
let bytes = out?;
String::from_utf8(bytes).map_err(|e| format!("工具输出非 UTF-8:{}", e))
}
pub async fn run_sync_default_in_docker(
backend: &std::sync::Arc<dyn SyncDefaultSandboxBackend>,
cfg: &AgentConfig,
effective_working_dir: &Path,
tool_name: &str,
args_json: &str,
) -> Result<String, String> {
let cfg_path = write_runner_cfg_default(cfg)?;
let inv = ToolInvocationLine {
kind: "sync_default".to_string(),
tool: Some(tool_name.to_string()),
args_json: args_json.to_string(),
};
run_tool_in_docker(backend, cfg, effective_working_dir, cfg_path, inv).await
}