feat(sandbox): Docker 一次性容器执行隔离边界(步骤 6-7)

引入两类安全边界:统一审批=授权边界(上一提交),一次性 Docker
容器=执行隔离边界(本提交)。LLM 即使执行恶意代码也只能在受限容器
内运行。

spec.rs: 新增 SandboxSpec(profile/network/memory/cpus/pids_limit/
  read_only),默认 profile=local 不启用容器。
executor.rs: 按 sandbox.profile 选择执行后端
  - local: 本地 spawn(env_clear + 白名单注入)
  - stdio: ias-stdio-runner,--network none --read-only --cap-drop ALL
    --security-opt no-new-privileges,仅 bind mount 工具二进制(ro)+tmpfs /tmp
  - network: ias-network-runner(预留自行联网工具)
  - builder: ias-tool-builder(供 tool_manager 编译)
  全局开关 IAS_DOCKER_SANDBOX=1,未启用时行为不变(向后兼容)。
tool_manager: build_tool 支持 IAS_DOCKER_BUILDER=1 在 ias-tool-builder
  容器内离线编译(--network none,挂载 tool_dir 到 /work)。
docker/: 三类 Dockerfile + build-images.sh 构建脚本。
  - stdio-runner: debian-slim + ca-certificates,无网络只读
  - network-runner: 预留联网工具
  - tool-builder: rust:1.88-slim 预缓存 serde_json,支持离线编译
spec 迁移: 17 个普通工具追加 sandbox.profile: stdio;tool_manager
  保持 local(自身在宿主操作文件系统)。

端到端验证: datetime 工具本地模式 +08:00,Docker 模式 +00:00(UTC),
  --rm 无容器残留。
This commit is contained in:
2026-06-22 11:38:42 +08:00
parent 1e33f527e0
commit 20f7c3ba9c
26 changed files with 263 additions and 30 deletions
+3
View File
@@ -15,3 +15,6 @@ params:
- name: city
required: false
desc: 城市名称,可选。限定地理编码的城市范围
sandbox:
profile: stdio
+3
View File
@@ -12,3 +12,6 @@ params:
- name: center
required: false
desc: 地图中心点经纬度坐标,格式"经度,纬度",默认天安门坐标
sandbox:
profile: stdio
@@ -18,3 +18,6 @@ params:
- name: radius
required: false
desc: 搜索半径(米),如 500、1000、3000。默认 1000 米
sandbox:
profile: stdio
@@ -12,3 +12,6 @@ params:
- name: location
required: true
desc: 经纬度坐标,格式为"经度,纬度",如"116.397,39.908"
sandbox:
profile: stdio
@@ -18,3 +18,6 @@ params:
- name: type
required: false
desc: 出行方式:driving(驾车)、walking(步行)、riding(骑行)、transit(公交)。默认 driving
sandbox:
profile: stdio
@@ -15,3 +15,6 @@ params:
- name: interests
required: false
desc: 感兴趣的关键词,逗号分隔,如"景点,美食,酒店"。默认"景点,美食"
sandbox:
profile: stdio
@@ -6,3 +6,6 @@ path: /target/release/datetime
risk_level: low
timeout_secs: 3
params: []
sandbox:
profile: stdio
@@ -9,3 +9,6 @@ params:
- name: url
required: true
desc: 网页URL
sandbox:
profile: stdio
@@ -12,3 +12,6 @@ params:
- name: memory_id
required: true
desc: 记忆ID
sandbox:
profile: stdio
@@ -12,3 +12,6 @@ params:
- name: memory_id
required: true
desc: 记忆的数字ID,从 read_memories 返回结果中获取
sandbox:
profile: stdio
@@ -9,3 +9,6 @@ timeout_secs: 3
env:
- IAS_MEMORY_DIR
params:
sandbox:
profile: stdio
@@ -15,3 +15,6 @@ params:
- name: content
required: true
desc: 更新后的新内容,覆盖原记忆
sandbox:
profile: stdio
@@ -12,3 +12,6 @@ params:
- name: content
required: true
desc: 要记录的记忆内容,用自然语言描述。如"用户住在合肥包河区联投新安里A区"
sandbox:
profile: stdio
+31 -9
View File
@@ -122,16 +122,38 @@ fn update_tool_project(tool_dir: &Path, tool_name: &str, params: &Value) -> Resu
fn build_tool(tool_dir: &Path, tool_name: &str) -> Result<(), String> {
let binary = tool_dir.join("target/release").join(tool_name);
let use_builder = std::env::var("IAS_DOCKER_BUILDER")
.ok()
.as_deref()
== Some("1");
// 用 `timeout` 限制构建时长,超时返回 124;cargo 及其子进程都会被信号终止
let output = Command::new("timeout")
.arg(BUILD_TIMEOUT_SECS.to_string())
.arg("cargo")
.arg("build")
.arg("--release")
.current_dir(tool_dir)
.output()
.map_err(|e| format!("启动 cargo 失败: {e}"))?;
// builder 容器时在隔离环境内离线编译,挂载 tool_dir 到 /work,产物写回宿主;
// 否则本地 cargo(受 timeout 限制)
let output = if use_builder {
Command::new("docker")
.arg("run").arg("--rm")
.arg("--network").arg("none")
.arg("--memory").arg("1g")
.arg("--cpus").arg("1")
.arg("--pids-limit").arg("128")
.arg("--cap-drop").arg("ALL")
.arg("--security-opt").arg("no-new-privileges")
.arg("-v").arg(format!("{}:/work", tool_dir.display()))
.arg("-w").arg("/work")
.arg("ias-tool-builder")
.arg("timeout").arg(BUILD_TIMEOUT_SECS.to_string())
.arg("cargo").arg("build").arg("--release")
.output()
} else {
Command::new("timeout")
.arg(BUILD_TIMEOUT_SECS.to_string())
.arg("cargo")
.arg("build")
.arg("--release")
.current_dir(tool_dir)
.output()
}
.map_err(|e| format!("启动构建失败: {e}"))?;
if !output.status.success() {
// 构建失败:删除可能残留的旧二进制,避免 reload 后仍调用旧版本造成"假成功"
@@ -12,3 +12,6 @@ params:
- name: location
required: true
desc: 城市名称,如"北京"、"上海"、"合肥"。支持地级市和县级市
sandbox:
profile: stdio
@@ -12,3 +12,6 @@ params:
- name: location
required: true
desc: 城市名称,如"北京"、"上海"、"合肥"。支持地级市和县级市
sandbox:
profile: stdio
@@ -12,3 +12,6 @@ params:
- name: location
required: true
desc: 城市名称,如"北京"、"上海"、"合肥"。支持地级市和县级市
sandbox:
profile: stdio
@@ -13,3 +13,6 @@ params:
desc: 搜索关键词,如"Rust 2024 edition 新特性"、"今天比特币价格"
- name: max_results
desc: 返回的最大搜索结果数量(1-20),默认5条
sandbox:
profile: stdio