refactor(sandbox): 按威胁模型精确化——仅不受信任代码进容器

http/db 类工具的子进程是瘦壳(纯计算),危险操作全在受信任主进程,
套容器是纯开销无隔离收益。调整为:

- 开发者预写工具:移除 spec 的 sandbox: profile: stdio,回默认 local
  本地执行(仍保留 env_clear + 白名单注入)
- tool_manager 产出的工具:spec_yaml 自动写 sandbox: profile: stdio,
  不受信任代码(LLM 提供 rust_code)默认进 stdio 容器
- tool_manager 编译:仍走 builder 容器
- 移除 ias-network-runner Dockerfile(当前无工具直接联网)
- executor docker 分支仅保留 stdio/builder,删 network 分支

验证:datetime 本地执行 +08:00;tool_manager 产出工具 spec 含
sandbox: profile: stdio 且容器内执行成功;25 测试通过。
This commit is contained in:
2026-06-22 16:53:36 +08:00
parent 4a5b21a69e
commit 092f3b78f2
23 changed files with 35 additions and 61 deletions
-8
View File
@@ -1,8 +0,0 @@
# ias-network-runner — 允许联网的工具运行环境
# 供需要自行发起 HTTP 的工具使用(当前工具均通过宿主 http 消息代理联网,
# 此镜像为将来工具直接联网预留)。运行时仍可按需 --network none。
FROM debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT []
+5 -5
View File
@@ -1,16 +1,16 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# 构建三类沙箱基础镜像。无需为每个工具单独写 Dockerfile。 # 构建沙箱基础镜像。
# - ias-stdio-runner : 运行 tool_manager 产出的不受信任工具(无网络只读)
# - ias-tool-builder : tool_manager 编译 Rust 工具(离线)
# 无需为每个工具单独写 Dockerfile。
set -euo pipefail set -euo pipefail
cd "$(dirname "$0")" # docker/ cd "$(dirname "$0")" # docker/
echo "=== 构建 ias-stdio-runner(无网络只读容器)===" echo "=== 构建 ias-stdio-runner(无网络只读容器)==="
docker build -t ias-stdio-runner -f Dockerfile.stdio-runner . docker build -t ias-stdio-runner -f Dockerfile.stdio-runner .
echo "=== 构建 ias-network-runner(联网容器)==="
docker build -t ias-network-runner -f Dockerfile.network-runner .
echo "=== 构建 ias-tool-builderRust 编译容器)===" echo "=== 构建 ias-tool-builderRust 编译容器)==="
docker build -t ias-tool-builder -f Dockerfile.tool-builder . docker build -t ias-tool-builder -f Dockerfile.tool-builder .
echo "=== 完成 ===" echo "=== 完成 ==="
docker images | grep -E '^ias-(stdio|network|tool)' docker images | grep -E '^ias-(stdio|tool)'
+21 -9
View File
@@ -33,12 +33,22 @@ if spec.is_high_risk() && !approved {
## 第二层:一次性 Docker 容器(隔离边界) ## 第二层:一次性 Docker 容器(隔离边界)
### 三类基础镜像 ### 适用范围(按威胁模型精确化)
http/db 类工具的子进程是"瘦壳"——危险操作(联网、DB 读写)全在受信任的主进程里,
给它们套容器是纯开销,**没有隔离收益**。因此:
| 场景 | 是否容器隔离 | 原因 |
|------|------------|------|
| 开发者预写工具(weather/amap/... | ❌ 本地执行 | 子进程纯计算,危险操作在主进程 |
| tool_manager 产出的工具 | ✅ stdio 容器 | 含 LLM 提供的 rust_code,不受信任 |
| tool_manager 编译 | ✅ builder 容器 | cargo 执行任意 build script/proc macro |
### 两类基础镜像
| 镜像 | 用途 | 网络 | 文件系统 | | 镜像 | 用途 | 网络 | 文件系统 |
|------|------|------|----------| |------|------|------|----------|
| `ias-stdio-runner` | 普通 stdio 工具 | none | 只读根 + tmpfs /tmp | | `ias-stdio-runner` | tool_manager 产出的不受信任工具 | none | 只读根 + tmpfs /tmp |
| `ias-network-runner` | 需联网的工具(预留) | default | 只读根 + tmpfs /tmp |
| `ias-tool-builder` | tool_manager 编译 Rust 工具 | none | 可写 /workbind mount | | `ias-tool-builder` | tool_manager 编译 Rust 工具 | none | 可写 /workbind mount |
构建:`docker/build-images.sh` 构建:`docker/build-images.sh`
@@ -58,7 +68,7 @@ if spec.is_high_risk() && !approved {
```yaml ```yaml
sandbox: sandbox:
profile: stdio # local | stdio | network | builder profile: stdio # local | stdio | builder
network: none # none | default network: none # none | default
memory: 256m memory: 256m
cpus: "0.5" cpus: "0.5"
@@ -66,7 +76,9 @@ sandbox:
read_only: true read_only: true
``` ```
默认 `profile: local`(不启用容器)。现有非 tool_manager 工具均设为 `profile: stdio` 默认 `profile: local`(不启用容器)。开发者预写工具保持默认
**tool_manager 产出的工具自动写入 `sandbox: profile: stdio`**(见 tool_manager `spec_yaml`),
不受信任代码默认进容器。
### 启用沙箱 ### 启用沙箱
@@ -121,9 +133,9 @@ enum ToolRunOutcome {
|------|------| |------|------|
| tool_manager update 自身 | ❌ 拒绝(保留名) | | tool_manager update 自身 | ❌ 拒绝(保留名) |
| create_tool risk_level=low | spec 强制 high | | create_tool risk_level=low | spec 强制 high |
| datetime 本地执行 | +08:00(宿主时区) | | tool_manager 产出工具 spec | 自动含 `sandbox: profile: stdio` |
| datetime Docker 执行 | +00:00(容器 UTC,证明确实隔离 | | datetime 本地执行 | +08:00宿主时区,默认不套容器) |
| 容器残留检查 | 无(--rm 生效) | | tool_manager 产出工具 Docker 执行 | 容器内运行成功 |
| builder 容器编译 serde_json | ✅ 离线成功 | | builder 容器编译 serde_json | ✅ 离线成功 |
| cargo test | 25 passed | | cargo test | 25 passed |
@@ -138,6 +150,6 @@ enum ToolRunOutcome {
| 5 | 修 clean_expired 并发 | ✅ | | 5 | 修 clean_expired 并发 | ✅ |
| 6 | 三类 Docker 镜像 + sandbox spec | ✅ | | 6 | 三类 Docker 镜像 + sandbox spec | ✅ |
| 7 | stdio 工具迁移到一次性容器 | ✅ | | 7 | stdio 工具迁移到一次性容器 | ✅ |
| 8 | network 工具迁移 | ⏸ 保持 stdio profile(工具走宿主代理联网,最小权限不给直接网络) | | 8 | network 工具迁移 | ➖ 不需要:工具走宿主代理联网,子进程纯计算,套容器无隔离收益 |
| 9 | tool_manager build 迁移到 builder 容器 | ✅ | | 9 | tool_manager build 迁移到 builder 容器 | ✅ |
| 10 | 结构化结果替换 is_tool_error | ✅ | | 10 | 结构化结果替换 is_tool_error | ✅ |
+2 -2
View File
@@ -379,10 +379,10 @@ fn build_local_cmd(spec: &ToolSpec, binary_path: &str) -> Command {
} }
/// Docker 隔离执行:一次性容器,默认无网络、只读根、cap-drop ALL、 /// Docker 隔离执行:一次性容器,默认无网络、只读根、cap-drop ALL、
/// no-new-privileges,仅挂载工具二进制(ro+ tmpfs /tmp /// no-new-privileges,仅挂载工具二进制(ro+ tmpfs /tmp
/// 供 tool_manager 产出的不受信任工具使用(profile: stdio)。
fn build_docker_cmd(spec: &ToolSpec, binary_path: &str) -> Command { fn build_docker_cmd(spec: &ToolSpec, binary_path: &str) -> Command {
let image = match spec.sandbox.profile.as_str() { let image = match spec.sandbox.profile.as_str() {
"network" => "ias-network-runner",
"builder" => "ias-tool-builder", "builder" => "ias-tool-builder",
_ => "ias-stdio-runner", _ => "ias-stdio-runner",
}; };
+3 -3
View File
@@ -17,7 +17,7 @@ pub struct ParamSpec {
/// ///
/// ```yaml /// ```yaml
/// sandbox: /// sandbox:
/// profile: stdio # local | stdio | network | builder /// profile: stdio # local | stdio | builder
/// network: none # none | default /// network: none # none | default
/// memory: 256m /// memory: 256m
/// cpus: "0.5" /// cpus: "0.5"
@@ -27,8 +27,8 @@ pub struct ParamSpec {
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)] #[serde(default)]
pub struct SandboxSpec { pub struct SandboxSpec {
/// 沙箱画像:`local` 本地执行(默认,不启用容器);`stdio` 无网络只读容器 /// 沙箱画像:`local` 本地执行(默认,不启用容器);`stdio` 无网络只读容器
/// `network` 联网容器`builder` 构建容器(供 tool_manager /// (供 tool_manager 产出的不受信任工具)`builder` 构建容器(供 tool_manager
pub profile: String, pub profile: String,
/// 网络:`none`(默认)或 `default` /// 网络:`none`(默认)或 `default`
pub network: String, pub network: String,
-2
View File
@@ -16,5 +16,3 @@ params:
required: false required: false
desc: 城市名称,可选。限定地理编码的城市范围 desc: 城市名称,可选。限定地理编码的城市范围
sandbox:
profile: stdio
-2
View File
@@ -13,5 +13,3 @@ params:
required: false required: false
desc: 地图中心点经纬度坐标,格式"经度,纬度",默认天安门坐标 desc: 地图中心点经纬度坐标,格式"经度,纬度",默认天安门坐标
sandbox:
profile: stdio
@@ -19,5 +19,3 @@ params:
required: false required: false
desc: 搜索半径(米),如 500、1000、3000。默认 1000 米 desc: 搜索半径(米),如 500、1000、3000。默认 1000 米
sandbox:
profile: stdio
@@ -13,5 +13,3 @@ params:
required: true required: true
desc: 经纬度坐标,格式为"经度,纬度",如"116.397,39.908" desc: 经纬度坐标,格式为"经度,纬度",如"116.397,39.908"
sandbox:
profile: stdio
@@ -19,5 +19,3 @@ params:
required: false required: false
desc: 出行方式:driving(驾车)、walking(步行)、riding(骑行)、transit(公交)。默认 driving desc: 出行方式:driving(驾车)、walking(步行)、riding(骑行)、transit(公交)。默认 driving
sandbox:
profile: stdio
@@ -16,5 +16,3 @@ params:
required: false required: false
desc: 感兴趣的关键词,逗号分隔,如"景点,美食,酒店"。默认"景点,美食" desc: 感兴趣的关键词,逗号分隔,如"景点,美食,酒店"。默认"景点,美食"
sandbox:
profile: stdio
@@ -7,5 +7,3 @@ risk_level: low
timeout_secs: 3 timeout_secs: 3
params: [] params: []
sandbox:
profile: stdio
@@ -10,5 +10,3 @@ params:
required: true required: true
desc: 网页URL desc: 网页URL
sandbox:
profile: stdio
@@ -13,5 +13,3 @@ params:
required: true required: true
desc: 记忆ID desc: 记忆ID
sandbox:
profile: stdio
@@ -13,5 +13,3 @@ params:
required: true required: true
desc: 记忆的数字ID,从 read_memories 返回结果中获取 desc: 记忆的数字ID,从 read_memories 返回结果中获取
sandbox:
profile: stdio
@@ -10,5 +10,3 @@ env:
- IAS_MEMORY_DIR - IAS_MEMORY_DIR
params: params:
sandbox:
profile: stdio
@@ -16,5 +16,3 @@ params:
required: true required: true
desc: 更新后的新内容,覆盖原记忆 desc: 更新后的新内容,覆盖原记忆
sandbox:
profile: stdio
@@ -13,5 +13,3 @@ params:
required: true required: true
desc: 要记录的记忆内容,用自然语言描述。如"用户住在合肥包河区联投新安里A区" desc: 要记录的记忆内容,用自然语言描述。如"用户住在合肥包河区联投新安里A区"
sandbox:
profile: stdio
+4
View File
@@ -269,6 +269,10 @@ fn spec_yaml(tool_name: &str, params: &Value) -> String {
out.push_str("params: []\n"); out.push_str("params: []\n");
} }
// 安全约束:tool_manager 产出的工具不受信任(含 LLM 提供的 rust_code),
// 默认进 stdio 沙箱容器(无网络、只读根),防止数据外泄/越权文件读
out.push_str("sandbox:\n profile: stdio\n");
out out
} }
@@ -13,5 +13,3 @@ params:
required: true required: true
desc: 城市名称,如"北京"、"上海"、"合肥"。支持地级市和县级市 desc: 城市名称,如"北京"、"上海"、"合肥"。支持地级市和县级市
sandbox:
profile: stdio
@@ -13,5 +13,3 @@ params:
required: true required: true
desc: 城市名称,如"北京"、"上海"、"合肥"。支持地级市和县级市 desc: 城市名称,如"北京"、"上海"、"合肥"。支持地级市和县级市
sandbox:
profile: stdio
@@ -13,5 +13,3 @@ params:
required: true required: true
desc: 城市名称,如"北京"、"上海"、"合肥"。支持地级市和县级市 desc: 城市名称,如"北京"、"上海"、"合肥"。支持地级市和县级市
sandbox:
profile: stdio
@@ -14,5 +14,3 @@ params:
- name: max_results - name: max_results
desc: 返回的最大搜索结果数量(1-20),默认5条 desc: 返回的最大搜索结果数量(1-20),默认5条
sandbox:
profile: stdio