feat(stardomain): 实现完整的沙箱执行引擎(local + Docker 双模式)

local 模式通过 tokio::process 真实执行命令,Docker 模式通过 bollard 创建容器运行,
两种模式均支持超时强杀、输出捕获和策略过滤。同时提供同步和异步 Python 接口。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 11:07:39 +00:00
parent 32bdbe77ff
commit 0bbd08638c
6 changed files with 331 additions and 28 deletions
+175 -8
View File
@@ -8,25 +8,34 @@
* http://www.apache.org/licenses/LICENSE-2.0
*/
/// Docker integration layer.
/// Uses bollard to manage container lifecycle for sandbox execution.
use std::time::Duration;
use bollard::Docker;
use bollard::container::{
Config, CreateContainerOptions, StartContainerOptions,
WaitContainerOptions, LogsOptions, RemoveContainerOptions,
};
use bollard::models::HostConfig;
use futures_util::StreamExt;
use tokio::time::timeout;
use crate::sandbox::SandboxResult;
pub struct DockerBackend {
pub image: String,
pub network_disabled: bool,
pub memory_limit: u64,
pub cpu_period: u64,
pub cpu_quota: u64,
pub memory_limit: i64,
pub cpu_period: i64,
pub cpu_quota: i64,
}
impl Default for DockerBackend {
fn default() -> Self {
DockerBackend {
image: "stardomain-runtime:latest".to_string(),
image: "python:3.13-slim".to_string(),
network_disabled: false,
memory_limit: 512 * 1024 * 1024, // 512MB
memory_limit: 512 * 1024 * 1024,
cpu_period: 100_000,
cpu_quota: 50_000, // 50% of one core
cpu_quota: 50_000,
}
}
}
@@ -41,4 +50,162 @@ impl DockerBackend {
}
backend
}
pub async fn run_command(
&self,
command: &str,
workspace: &str,
timeout_secs: u64,
) -> SandboxResult {
let docker = match Docker::connect_with_local_defaults() {
Ok(d) => d,
Err(e) => return SandboxResult::error(&format!("Docker 连接失败: {}", e)),
};
let container_name = format!("stardomain_{}", uuid_short());
let host_config = HostConfig {
memory: Some(self.memory_limit),
cpu_period: Some(self.cpu_period),
cpu_quota: Some(self.cpu_quota),
network_mode: if self.network_disabled {
Some("none".to_string())
} else {
None
},
binds: Some(vec![format!("{}:/workspace", workspace)]),
..Default::default()
};
let config = Config {
image: Some(self.image.clone()),
cmd: Some(vec![
"sh".to_string(),
"-c".to_string(),
command.to_string(),
]),
working_dir: Some("/workspace".to_string()),
host_config: Some(host_config),
..Default::default()
};
let create_opts = CreateContainerOptions {
name: &container_name,
platform: None,
};
if let Err(e) = docker.create_container(Some(create_opts), config).await {
return SandboxResult::error(&format!("创建容器失败: {}", e));
}
if let Err(e) = docker
.start_container(
&container_name,
None::<StartContainerOptions<String>>,
)
.await
{
let _ = cleanup(&docker, &container_name).await;
return SandboxResult::error(&format!("启动容器失败: {}", e));
}
let wait_fut = wait_for_container(&docker, &container_name);
let result = match timeout(Duration::from_secs(timeout_secs), wait_fut).await
{
Ok(r) => r,
Err(_) => {
let _ = docker
.kill_container(
&container_name,
None::<bollard::container::KillContainerOptions<String>>,
)
.await;
let logs = collect_logs(&docker, &container_name).await;
let _ = cleanup(&docker, &container_name).await;
return SandboxResult {
stdout: logs.0,
stderr: logs.1,
exit_code: -1,
killed_by_timeout: true,
};
}
};
let logs = collect_logs(&docker, &container_name).await;
let _ = cleanup(&docker, &container_name).await;
SandboxResult {
stdout: logs.0,
stderr: logs.1,
exit_code: result,
killed_by_timeout: false,
}
}
pub async fn run_python(
&self,
code: &str,
workspace: &str,
timeout_secs: u64,
) -> SandboxResult {
let escaped = code.replace('\'', "'\\''");
let cmd = format!("python3 -c '{}'", escaped);
self.run_command(&cmd, workspace, timeout_secs).await
}
}
async fn wait_for_container(docker: &Docker, name: &str) -> i32 {
let opts = WaitContainerOptions { condition: "not-running" };
let mut stream = docker.wait_container(name, Some(opts));
if let Some(Ok(response)) = stream.next().await {
response.status_code as i32
} else {
-1
}
}
async fn collect_logs(docker: &Docker, name: &str) -> (String, String) {
let opts = LogsOptions::<String> {
stdout: true,
stderr: true,
..Default::default()
};
let mut stream = docker.logs(name, Some(opts));
let mut stdout = String::new();
let mut stderr = String::new();
while let Some(Ok(output)) = stream.next().await {
match output {
bollard::container::LogOutput::StdOut { message } => {
stdout.push_str(&String::from_utf8_lossy(&message));
}
bollard::container::LogOutput::StdErr { message } => {
stderr.push_str(&String::from_utf8_lossy(&message));
}
_ => {}
}
}
(stdout, stderr)
}
async fn cleanup(
docker: &Docker,
name: &str,
) -> Result<(), bollard::errors::Error> {
docker
.remove_container(
name,
Some(RemoveContainerOptions {
force: true,
..Default::default()
}),
)
.await
}
fn uuid_short() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let t = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
format!("{:x}{:x}", t.as_secs(), t.subsec_nanos())
}