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:
@@ -0,0 +1,4 @@
|
||||
/target
|
||||
*.so
|
||||
*.dylib
|
||||
*.dll
|
||||
Generated
+1547
File diff suppressed because it is too large
Load Diff
@@ -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"] }
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user