feat: 新增 stardomain 沙箱子项目脚手架(Docker + Rust 过滤层)

提供统一沙箱运行时,支持 local/sandbox 两种模式切换。Rust 层负责命令和代码的策略过滤,
Docker 层负责实际的进程隔离。包含三种预设策略:agent_exec / tool_run / untrusted。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 10:07:30 +00:00
parent 6932294ddd
commit 32bdbe77ff
13 changed files with 2021 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
/target
*.so
*.dylib
*.dll
+1547
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
[package]
name = "stardomain"
version = "0.1.0"
authors = ["zhaoxi826"]
description = "Sandbox runtime for KiloStar — Docker-based isolation with Rust filtering layer."
license = "Apache-2.0"
edition = "2024"
[lib]
name = "stardomain"
crate-type = ["cdylib", "rlib"]
[[bin]]
name = "stardomain"
path = "src/main.rs"
[dependencies]
pyo3 = { version = "0.28", features = ["extension-module"] }
pyo3-async-runtimes = { version = "0.28", features = ["tokio-runtime"] }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "process", "time"] }
bollard = "0.18"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.9"
anyhow = "1.0"
regex = "1"
clap = { version = "4", features = ["derive"] }
+14
View File
@@ -0,0 +1,14 @@
[build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"
[project]
name = "stardomain"
version = "0.1.0"
description = "Sandbox runtime for KiloStar — Docker-based isolation with Rust filtering layer."
requires-python = ">=3.11"
license = { text = "Apache-2.0" }
authors = [{ name = "zhaoxi826" }]
[tool.maturin]
features = ["pyo3/extension-module"]
+44
View File
@@ -0,0 +1,44 @@
/*
* Copyright 2026 zhaoxi826
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
/// Docker integration layer.
/// Uses bollard to manage container lifecycle for sandbox execution.
pub struct DockerBackend {
pub image: String,
pub network_disabled: bool,
pub memory_limit: u64,
pub cpu_period: u64,
pub cpu_quota: u64,
}
impl Default for DockerBackend {
fn default() -> Self {
DockerBackend {
image: "stardomain-runtime:latest".to_string(),
network_disabled: false,
memory_limit: 512 * 1024 * 1024, // 512MB
cpu_period: 100_000,
cpu_quota: 50_000, // 50% of one core
}
}
}
impl DockerBackend {
pub fn with_policy(policy_name: &str) -> Self {
let mut backend = Self::default();
if policy_name == "untrusted" {
backend.network_disabled = true;
backend.memory_limit = 128 * 1024 * 1024;
backend.cpu_quota = 25_000;
}
backend
}
}
+59
View File
@@ -0,0 +1,59 @@
/*
* Copyright 2026 zhaoxi826
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
pub mod docker;
pub mod policy;
pub mod sandbox;
use pyo3::prelude::*;
use sandbox::SandboxResult;
#[pyclass]
struct Sandbox {
mode: String,
workspace: String,
timeout: u64,
memory_limit_mb: u64,
policy_name: String,
}
#[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 {
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))
}
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))
}
}
+51
View File
@@ -0,0 +1,51 @@
/*
* Copyright 2026 zhaoxi826
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "stardomain", version, about = "KiloStar sandbox runtime")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Run a command inside the sandbox
Run {
/// Execution policy: agent_exec, tool_run, untrusted
#[arg(short, long, default_value = "agent_exec")]
policy: String,
/// Working directory inside sandbox
#[arg(short, long, default_value = "/tmp/stardomain_ws")]
workspace: String,
/// Timeout in seconds
#[arg(short, long, default_value_t = 30)]
timeout: u64,
/// The command to execute
#[arg(trailing_var_arg = true)]
cmd: Vec<String>,
},
}
fn main() {
let cli = Cli::parse();
match cli.command {
Commands::Run { policy, workspace, timeout, cmd } => {
println!("[stardomain] policy={policy}, workspace={workspace}, timeout={timeout}s");
println!("[stardomain] cmd: {:?}", cmd);
println!("[stardomain] (stub: execution not yet implemented)");
}
}
}
@@ -0,0 +1,66 @@
/*
* Copyright 2026 zhaoxi826
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
use pyo3::prelude::*;
use regex::Regex;
use super::Policy;
pub fn validate_command(command: &str, policy: &Policy) -> PyResult<()> {
let cmd_lower = command.trim().to_lowercase();
for blocked in &policy.blocked_commands {
if blocked == "*" {
return Err(PyErr::new::<pyo3::exceptions::PyPermissionError, _>(
"当前策略禁止执行任何 shell 命令"
));
}
if cmd_lower.starts_with(&blocked.to_lowercase()) {
return Err(PyErr::new::<pyo3::exceptions::PyPermissionError, _>(
format!("命令被策略禁止: {}", blocked)
));
}
}
for op in &policy.blocked_operators {
if command.contains(op.as_str()) {
return Err(PyErr::new::<pyo3::exceptions::PyPermissionError, _>(
format!("命令包含被禁止的操作符: '{}'", op)
));
}
}
Ok(())
}
pub fn validate_python(code: &str, policy: &Policy) -> PyResult<()> {
for module in &policy.blocked_imports {
let pattern = format!(r"(?m)^\s*(?:import\s+{}|from\s+{})\b", regex::escape(module), regex::escape(module));
if let Ok(re) = Regex::new(&pattern) {
if re.is_match(code) {
return Err(PyErr::new::<pyo3::exceptions::PyPermissionError, _>(
format!("禁止导入模块: {}", module)
));
}
}
}
for builtin in &policy.blocked_builtins {
let pattern = format!(r"\b{}\s*\(", regex::escape(builtin));
if let Ok(re) = Regex::new(&pattern) {
if re.is_match(code) {
return Err(PyErr::new::<pyo3::exceptions::PyPermissionError, _>(
format!("禁止使用: {}()", builtin)
));
}
}
}
Ok(())
}
+33
View File
@@ -0,0 +1,33 @@
/*
* Copyright 2026 zhaoxi826
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
pub mod filter;
pub mod preset;
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct Policy {
pub name: String,
pub blocked_commands: Vec<String>,
pub blocked_operators: Vec<String>,
pub blocked_imports: Vec<String>,
pub blocked_builtins: Vec<String>,
pub allow_network: bool,
pub max_timeout: u64,
}
pub fn get_policy(name: &str) -> Policy {
match name {
"tool_run" => preset::tool_run(),
"untrusted" => preset::untrusted(),
_ => preset::agent_exec(),
}
}
@@ -0,0 +1,81 @@
/*
* Copyright 2026 zhaoxi826
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
use super::Policy;
pub fn agent_exec() -> Policy {
Policy {
name: "agent_exec".to_string(),
blocked_commands: vec![
"rm -rf /".to_string(),
"mkfs".to_string(),
"dd ".to_string(),
"shutdown".to_string(),
"reboot".to_string(),
],
blocked_operators: vec![
"&&".to_string(),
"||".to_string(),
";".to_string(),
"`".to_string(),
"$(".to_string(),
],
blocked_imports: vec![
"ctypes".to_string(),
"socket".to_string(),
],
blocked_builtins: vec![
"exec".to_string(),
"eval".to_string(),
"compile".to_string(),
"__import__".to_string(),
],
allow_network: true,
max_timeout: 60,
}
}
pub fn tool_run() -> Policy {
Policy {
name: "tool_run".to_string(),
blocked_commands: vec![
"rm -rf /".to_string(),
"mkfs".to_string(),
"shutdown".to_string(),
"reboot".to_string(),
],
blocked_operators: vec![],
blocked_imports: vec!["ctypes".to_string()],
blocked_builtins: vec!["exec".to_string(), "eval".to_string()],
allow_network: true,
max_timeout: 120,
}
}
pub fn untrusted() -> Policy {
Policy {
name: "untrusted".to_string(),
blocked_commands: vec!["*".to_string()], // all blocked
blocked_operators: vec![
"&&".to_string(), "||".to_string(), ";".to_string(),
"`".to_string(), "$(".to_string(), ">".to_string(), "<".to_string(),
],
blocked_imports: vec![
"os".to_string(), "subprocess".to_string(), "shutil".to_string(),
"socket".to_string(), "ctypes".to_string(), "importlib".to_string(),
],
blocked_builtins: vec![
"exec".to_string(), "eval".to_string(), "compile".to_string(),
"__import__".to_string(), "open".to_string(),
],
allow_network: false,
max_timeout: 10,
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2026 zhaoxi826
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
pub struct SandboxConfig {
pub mode: SandboxMode,
pub workspace: String,
pub timeout_secs: u64,
pub memory_limit_mb: u64,
pub policy_name: String,
}
pub enum SandboxMode {
Local,
Sandbox,
}
impl Default for SandboxConfig {
fn default() -> Self {
SandboxConfig {
mode: SandboxMode::Sandbox,
workspace: "/tmp/stardomain_ws".to_string(),
timeout_secs: 30,
memory_limit_mb: 512,
policy_name: "agent_exec".to_string(),
}
}
}
@@ -0,0 +1,23 @@
/*
* Copyright 2026 zhaoxi826
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* 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;
impl Executor {
pub fn run_local(_command: &str) -> (String, String, i32) {
("".to_string(), "".to_string(), 0)
}
pub fn run_docker(_command: &str) -> (String, String, i32) {
("".to_string(), "[stardomain] Docker execution not yet implemented".to_string(), 1)
}
}
+38
View File
@@ -0,0 +1,38 @@
/*
* Copyright 2026 zhaoxi826
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
pub mod config;
pub mod executor;
use pyo3::prelude::*;
#[pyclass]
#[derive(Clone)]
pub struct SandboxResult {
#[pyo3(get)]
pub stdout: String,
#[pyo3(get)]
pub stderr: String,
#[pyo3(get)]
pub exit_code: i32,
#[pyo3(get)]
pub killed_by_timeout: bool,
}
impl SandboxResult {
pub fn stub(input: &str) -> Self {
SandboxResult {
stdout: format!("[stardomain stub] would execute: {}", input),
stderr: String::new(),
exit_code: 0,
killed_by_timeout: false,
}
}
}