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())
}
+98 -11
View File
@@ -19,14 +19,25 @@ pub mod policy;
pub mod sandbox;
use pyo3::prelude::*;
use sandbox::SandboxResult;
fn get_runtime() -> &'static tokio::runtime::Runtime {
use std::sync::OnceLock;
static RT: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
RT.get_or_init(|| {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("Failed to create tokio runtime")
})
}
#[pyclass]
struct Sandbox {
mode: String,
workspace: String,
timeout: u64,
#[allow(dead_code)]
memory_limit_mb: u64,
policy_name: String,
}
@@ -34,26 +45,102 @@ struct Sandbox {
#[pymethods]
impl Sandbox {
#[new]
#[pyo3(signature = (mode="sandbox", workspace="/tmp/stardomain_ws", timeout=30, memory_limit_mb=512, policy="agent_exec"))]
fn new(mode: &str, workspace: &str, timeout: u64, memory_limit_mb: u64, policy: &str) -> Self {
Sandbox {
#[pyo3(signature = (mode="local", workspace="/tmp/stardomain_ws", timeout=30, memory_limit_mb=512, policy="agent_exec"))]
fn new(mode: &str, workspace: &str, timeout: u64, memory_limit_mb: u64, policy: &str) -> PyResult<Self> {
std::fs::create_dir_all(workspace).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyIOError, _>(format!("无法创建工作目录: {}", e))
})?;
Ok(Sandbox {
mode: mode.to_string(),
workspace: workspace.to_string(),
timeout,
memory_limit_mb,
policy_name: policy.to_string(),
}
})
}
fn run_command(&self, command: &str) -> PyResult<SandboxResult> {
let policy = policy::get_policy(&self.policy_name);
policy::filter::validate_command(command, &policy)?;
Ok(SandboxResult::stub(command))
let pol = policy::get_policy(&self.policy_name);
policy::filter::validate_command(command, &pol)?;
let rt = get_runtime();
match self.mode.as_str() {
"sandbox" | "docker" => {
let backend = docker::DockerBackend::with_policy(&self.policy_name);
Ok(rt.block_on(backend.run_command(command, &self.workspace, self.timeout)))
}
_ => {
Ok(rt.block_on(sandbox::executor::run_local(command, &self.workspace, self.timeout)))
}
}
}
fn run_python(&self, code: &str) -> PyResult<SandboxResult> {
let policy = policy::get_policy(&self.policy_name);
policy::filter::validate_python(code, &policy)?;
Ok(SandboxResult::stub(code))
let pol = policy::get_policy(&self.policy_name);
policy::filter::validate_python(code, &pol)?;
let rt = get_runtime();
match self.mode.as_str() {
"sandbox" | "docker" => {
let backend = docker::DockerBackend::with_policy(&self.policy_name);
Ok(rt.block_on(backend.run_python(code, &self.workspace, self.timeout)))
}
_ => {
Ok(rt.block_on(sandbox::executor::run_python_local(code, &self.workspace, self.timeout)))
}
}
}
fn run_command_async<'py>(&self, py: Python<'py>, command: String) -> PyResult<Bound<'py, PyAny>> {
let pol = policy::get_policy(&self.policy_name);
policy::filter::validate_command(&command, &pol)?;
let mode = self.mode.clone();
let workspace = self.workspace.clone();
let timeout = self.timeout;
let policy_name = self.policy_name.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let result = match mode.as_str() {
"sandbox" | "docker" => {
let backend = docker::DockerBackend::with_policy(&policy_name);
backend.run_command(&command, &workspace, timeout).await
}
_ => {
sandbox::executor::run_local(&command, &workspace, timeout).await
}
};
Ok(result)
})
}
fn run_python_async<'py>(&self, py: Python<'py>, code: String) -> PyResult<Bound<'py, PyAny>> {
let pol = policy::get_policy(&self.policy_name);
policy::filter::validate_python(&code, &pol)?;
let mode = self.mode.clone();
let workspace = self.workspace.clone();
let timeout = self.timeout;
let policy_name = self.policy_name.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let result = match mode.as_str() {
"sandbox" | "docker" => {
let backend = docker::DockerBackend::with_policy(&policy_name);
backend.run_python(&code, &workspace, timeout).await
}
_ => {
sandbox::executor::run_python_local(&code, &workspace, timeout).await
}
};
Ok(result)
})
}
}
#[pymodule]
fn stardomain(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<Sandbox>()?;
m.add_class::<SandboxResult>()?;
Ok(())
}
+47 -9
View File
@@ -8,16 +8,54 @@
* http://www.apache.org/licenses/LICENSE-2.0
*/
/// Executor: responsible for running commands either locally or in Docker.
/// This is a stub — actual Docker execution will be implemented later.
pub struct Executor;
use std::time::Duration;
use tokio::process::Command;
use tokio::time::timeout;
impl Executor {
pub fn run_local(_command: &str) -> (String, String, i32) {
("".to_string(), "".to_string(), 0)
}
use crate::sandbox::SandboxResult;
pub fn run_docker(_command: &str) -> (String, String, i32) {
("".to_string(), "[stardomain] Docker execution not yet implemented".to_string(), 1)
pub async fn run_local(
command: &str,
workspace: &str,
timeout_secs: u64,
) -> SandboxResult {
let fut = Command::new("sh")
.arg("-c")
.arg(command)
.current_dir(workspace)
.output();
match timeout(Duration::from_secs(timeout_secs), fut).await {
Ok(Ok(output)) => SandboxResult {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
exit_code: output.status.code().unwrap_or(-1),
killed_by_timeout: false,
},
Ok(Err(e)) => SandboxResult {
stdout: String::new(),
stderr: format!("执行失败: {}", e),
exit_code: -1,
killed_by_timeout: false,
},
Err(_) => SandboxResult {
stdout: String::new(),
stderr: "执行超时".to_string(),
exit_code: -1,
killed_by_timeout: true,
},
}
}
pub async fn run_python_local(
code: &str,
workspace: &str,
timeout_secs: u64,
) -> SandboxResult {
let cmd = format!("python3 -c {}", shell_escape(code));
run_local(&cmd, workspace, timeout_secs).await
}
fn shell_escape(s: &str) -> String {
format!("'{}'", s.replace('\'', "'\\''"))
}
@@ -35,4 +35,13 @@ impl SandboxResult {
killed_by_timeout: false,
}
}
pub fn error(msg: &str) -> Self {
SandboxResult {
stdout: String::new(),
stderr: msg.to_string(),
exit_code: -1,
killed_by_timeout: false,
}
}
}