Skip to content

Commit 10b9f6c

Browse files
committed
test(core): establish S0 W0 behavior baseline
1 parent fa24bd7 commit 10b9f6c

30 files changed

Lines changed: 3991 additions & 25 deletions

agent/tests/test_workspace_backend.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,14 +114,44 @@ def __init__(self, default, routes=None, **_kwargs):
114114
assert f"{SKILLS_PREFIX}/" in routes
115115
assert f"{SKILLS_PREFIX}/demo/" in routes
116116
assert f"{MEMORIES_PREFIX}/" in routes
117-
assert f"/workspace/" in routes
117+
assert "/workspace/" in routes
118118
assert skills.is_dir()
119119
assert memories.is_dir()
120120
assert Path(routes[f"{SKILLS_PREFIX}/"].kwargs["root_dir"]) == skills
121121
assert Path(routes[f"{SKILLS_PREFIX}/demo/"].kwargs["root_dir"]) == external_skill
122122
assert routes[f"{SKILLS_PREFIX}/"].kwargs["virtual_mode"] is True
123123

124124

125+
@pytest.mark.xfail(
126+
strict=True,
127+
reason="S0 baseline: the legacy global /skills mount exposes disabled managed Skills",
128+
)
129+
def test_disabled_skills_are_not_exposed_by_a_global_skills_mount(tmp_path: Path):
130+
"""Future S1 invariant: only the activation view may own /skills routes."""
131+
skills = tmp_path / "skills"
132+
133+
class FakeFs:
134+
def __init__(self, **kwargs):
135+
self.kwargs = kwargs
136+
137+
class FakeComposite:
138+
def __init__(self, default, routes=None, **_kwargs):
139+
self.default = default
140+
self.routes = routes or {}
141+
142+
factory = make_workspace_backend_factory(
143+
None,
144+
object,
145+
object,
146+
FakeComposite,
147+
filesystem_cls=FakeFs,
148+
skills_dir=skills,
149+
)
150+
151+
routes = factory(object()).routes
152+
assert f"{SKILLS_PREFIX}/" not in routes
153+
154+
125155
def test_skills_route_outside_workspace_root(tmp_path: Path):
126156
"""Regression: global skills must not be resolved under the project root."""
127157
pytest.importorskip("deepagents")
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# MisakaX Sandbox 技术选型(ADR)
2+
3+
> **用途:** 固化 MisakaX 跨平台 Agent 沙箱的技术决策、边界、适配策略和上线门槛。
4+
> **受众:** 架构师、安全、Rust/Python、构建与发布维护者。
5+
> **最后审阅 / Last reviewed:** 2026-08-01
6+
> **状态:** Proposed;需完成三个平台的 Spike Gate 后转为 Accepted。
7+
> **依据:** [`CROSS_PLATFORM_DESKTOP_SANDBOX_RESEARCH.md`](../research/CROSS_PLATFORM_DESKTOP_SANDBOX_RESEARCH.md)
8+
9+
---
10+
11+
## ADR-001:采用统一 Sandbox Broker 和平台适配器
12+
13+
### 1. 决策
14+
15+
MisakaX 采用**端口-适配器架构**实现沙箱:
16+
17+
- Rust Core 持有统一 `SandboxPolicy``SandboxBroker`、审批、审计、能力探测和会话生命周期。
18+
- 独立的、窄职责 `misakax-sandbox-runner` 建立 OS 安全边界;主 Tauri 进程不直接拼接不可信 Shell 命令。
19+
- Linux 首选 Bubblewrap + namespaces + seccomp,Landlock 作为补充/受限回退。
20+
- macOS 首选每命令 Seatbelt profile + 独立 runner + 本地网络 Broker。
21+
- Windows 首选一次性提权设置、专用 online/offline 沙箱账户、write-restricted token、ACL、防火墙和 Job Object。
22+
- Docker/Podman 实现为可选 `ContainerSandboxProvider`,不作为基础安装的强依赖。
23+
- Wasmtime 保留为未来受控 Skill 插件 Provider,不承载通用 Shell。
24+
- 用户主动 PTY 终端由 `TerminalManager` 管理,默认标记为本机权限;Agent 自动命令必须调用 `SandboxBroker`
25+
26+
### 2. 决策驱动因素
27+
28+
- 三平台必须可运行,但不要求底层机制相同。
29+
- 保持现有用户项目工具链、工作区和 Tauri 桌面体验。
30+
- 安全策略要可测试、可解释、可审计、可升级,不散落在 React/Python/命令拼接中。
31+
- 能逐步从当前逻辑路径守卫迁移,不进行一次性重写。
32+
- 能为 Skills、MCP Server、Shell、扫描器和未来自动化共用执行边界。
33+
34+
### 3. 未采用的主方案
35+
36+
| 方案 | 不作为主方案的原因 |
37+
|---|---|
38+
| 只用 Docker Desktop | 依赖虚拟化/大体积运行时;三平台安装和许可成本高;宿主工具链体验差 |
39+
| 只用 Windows Sandbox | 不支持 Windows Home;工作区/工具桥接和多会话体验不符合桌面 Agent |
40+
| Windows AppContainer/LPAC | 对开放式 Git/Python/Node/编译器工作流兼容性不足 |
41+
| 只用 Anthropic Sandbox Runtime | 当前只覆盖 macOS/Linux,无法完成 Windows 要求 |
42+
| 直接 Shell 调用 Codex CLI | 上游 CLI 协议和实现会变化;产品不能依赖用户安装或登录另一产品 |
43+
| 只用 Wasmtime | 无法透明运行任意本机命令和现有 Skill 脚本 |
44+
| 路径 guard + proxy 环境变量 | 可被直接系统调用绕过,不构成 OS 安全边界 |
45+
46+
## 4. 目标架构
47+
48+
```mermaid
49+
flowchart TB
50+
UI["React:策略、审批、状态、审计"] --> IPC["窄 Tauri IPC"]
51+
PY["Python Agent / Brokered Backend"] --> BRIDGE["带会话令牌的本地执行桥"]
52+
IPC --> APP["Rust Application Services"]
53+
BRIDGE --> APP
54+
APP --> SB["SandboxBroker"]
55+
APP --> AB["ApprovalBroker"]
56+
APP --> AUDIT["SecurityAuditLog"]
57+
SB --> POLICY["SandboxPolicy + Capability Probe"]
58+
SB --> LINUX["LinuxSandboxProvider"]
59+
SB --> MAC["MacSandboxProvider"]
60+
SB --> WIN["WindowsSandboxProvider"]
61+
SB -. optional .-> OCI["ContainerSandboxProvider"]
62+
LINUX --> RUNNER["misakax-sandbox-runner"]
63+
MAC --> RUNNER
64+
WIN --> SETUP["misakax-sandbox-setup"]
65+
WIN --> RUNNER
66+
RUNNER --> CHILD["Shell / Skill / MCP 子进程树"]
67+
CHILD --> NET["NetworkBroker:deny by default"]
68+
```
69+
70+
### 4.1 依赖方向
71+
72+
```text
73+
React UI
74+
-> IPC DTO
75+
-> Rust application service
76+
-> sandbox domain ports / policy
77+
-> platform adapters / database / runner protocol
78+
79+
Python Agent
80+
-> BrokeredWorkspaceBackend
81+
-> authenticated loopback bridge
82+
-> 同一个 Rust application service
83+
```
84+
85+
平台 API、命令行参数、Windows token、Seatbelt profile 和 Bubblewrap 参数不得出现在 UI、Skills 或 Agent 编排层。
86+
87+
## 5. 核心契约
88+
89+
以下为概念接口;实现时按仓库当时锁定的 Rust 版本调整,不在规划阶段锁死第三方小版本。
90+
91+
```rust
92+
pub trait SandboxProvider: Send + Sync {
93+
fn capabilities(&self) -> SandboxCapabilities;
94+
async fn prepare(&self, request: PrepareRequest) -> Result<PreparedSandbox>;
95+
async fn spawn(&self, request: SpawnRequest) -> Result<SandboxProcess>;
96+
async fn terminate_tree(&self, process_id: SandboxProcessId) -> Result<()>;
97+
}
98+
99+
pub struct SandboxPolicy {
100+
pub mode: SandboxMode,
101+
pub filesystem: FilesystemPolicy,
102+
pub network: NetworkPolicy,
103+
pub environment: EnvironmentPolicy,
104+
pub resources: ResourcePolicy,
105+
pub approvals: ApprovalPolicy,
106+
}
107+
108+
pub enum SandboxMode {
109+
ReadOnly,
110+
WorkspaceWrite,
111+
FullAccess,
112+
}
113+
```
114+
115+
必须使用结构化 argv 和 cwd,不允许把前端字符串直接传给 platform shell 做二次解析。确需 Shell 语法时,由明确的 `ShellCommand` 类型和平台 adapter 处理,并记录完整来源。
116+
117+
## 6. 默认策略
118+
119+
### 6.1 `read-only`
120+
121+
- 工作区和系统可读范围按平台最小映射;无任何持久写路径,仅会话临时目录可写。
122+
- 网络关闭。
123+
- 适合安全扫描、只读分析和未知 Skill 预览。
124+
125+
### 6.2 `workspace-write`(Agent 默认)
126+
127+
- 当前会话工作区可写。
128+
- `.git``.misakax``.codex``.agents`、凭据文件和用户配置默认只读/不可见。
129+
- 网络默认关闭;按域名和一次/会话范围审批。
130+
- 环境变量使用 allowlist,移除云 Key、SSH、浏览器和代理凭据;`inherit_env=false`
131+
- 限制进程数、CPU、内存、文件大小、打开句柄和执行时长。
132+
133+
### 6.3 `full-access`
134+
135+
- 仅由用户在当前操作显式批准,展示影响范围和过期时间。
136+
- 审计事件必须包含触发消息、命令摘要、工作区、批准时间和理由。
137+
- 不得被 Skill manifest 或模型输出自行请求为永久默认。
138+
139+
## 7. 平台映射
140+
141+
| 策略能力 | Linux | macOS | Windows |
142+
|---|---|---|---|
143+
| 根只读 | Bubblewrap `ro-bind` | Seatbelt deny/write rules | write-restricted token + ACL |
144+
| 工作区可写 | 精确 bind | profile allow path | sandbox SID ACL |
145+
| 子路径再保护 | 重新 ro-bind/deny | 更具体 deny | deny ACL/受限 SID 策略 |
146+
| 网络默认拒绝 | network namespace + seccomp | Seatbelt + Broker | offline 用户防火墙 |
147+
| 域名允许 | TCP/UDS Broker | localhost/UDS Broker | online 身份 + Broker/防火墙 |
148+
| 进程树 | PID namespace/cgroup/kill | process group/runner | Job Object |
149+
| 权限准备 | 通常无需 root,探测 userns | 签名/系统版本验证 | 一次性 UAC setup |
150+
| 严格失败 | 不支持 userns 时拒绝 | profile 失败时拒绝 | setup/规则不完整时拒绝 |
151+
152+
“兼容模式”只能在用户明确选择后使用,并在每次会话显示持续警告;它不能自动取代严格模式。
153+
154+
## 8. Python Sidecar 集成决策
155+
156+
### 8.1 首期
157+
158+
- 新建 `BrokeredWorkspaceBackend`,文件和 Shell 工具通过带随机会话令牌的 loopback bridge 调用 Rust。
159+
- Rust 校验会话、工作区、工具、策略和批准,Shell 交给 Sandbox Broker。
160+
- 停止对 `LocalShellBackend` 使用 `inherit_env=true`;除测试外不得直接作为生产 backend。
161+
- Skill 目录以**激活视图**挂载,只包含当前允许且启用的 Skill;禁用/过期 Skill 不可见。
162+
163+
### 8.2 强化阶段
164+
165+
- 将执行 worker 与 FastAPI 控制面分离,评估把执行 worker 整体放入沙箱。
166+
- 模型凭据由宿主侧 Model Gateway 代持;worker 获得范围受限的 loopback token,避免把 API Key 注入沙箱。
167+
- MCP 进程启动统一接入 Sandbox Broker;远程 MCP 连接由网络策略管理。
168+
169+
## 9. Tauri 与终端安全决策
170+
171+
- 删除主 WebView 的通用 `shell:allow-execute/spawn/stdin-write/kill` 权限,终端和 Agent 使用独立窄 IPC。
172+
- 收窄 FS/HTTP capability 和 scope,显式 deny 敏感 WebView 数据目录。
173+
- 配置 CSP:仅允许本地打包脚本/样式,禁止远端和动态代码;开发环境例外与生产配置分离。
174+
- 终端使用 `@xterm/xterm` + Fit addon,Rust 侧优先验证 `portable-pty 0.9.x`;依赖版本以实施时 `package.json`/`Cargo.toml` 锁定为准。
175+
- 终端会话不复用 Sandbox Process ID;类型层面区分 `LocalTerminalSession``SandboxExecution`
176+
177+
## 10. 数据与审计
178+
179+
至少记录:
180+
181+
- `execution_id`、会话、工作区、请求来源(Agent/Skill/MCP/User Terminal)。
182+
- 策略版本、provider、能力探测结果、writable/readable/denied roots 的摘要 hash。
183+
- 命令 argv 的安全摘要、cwd、开始/结束、退出码、终止原因和资源用量。
184+
- 网络请求目标、策略命中、用户批准和持续时间。
185+
- 沙箱 setup/repair/uninstall 事件和平台组件版本。
186+
187+
日志不得默认存储环境变量值、凭据、完整终端输出或用户文件内容;命令参数中疑似 Secret 应脱敏。
188+
189+
## 11. Spike Gate:ADR 转为 Accepted 的条件
190+
191+
三个平台都必须通过以下门槛,不能只在 Windows 开发机通过后宣称跨平台完成:
192+
193+
1. 工作区内创建/修改成功,工作区外写入失败。
194+
2. `.git` 保护策略按预期失败;批准后可在限定操作中放行。
195+
3. 读取 SSH/云凭据路径失败或不可见。
196+
4. 直接 Socket、DNS、HTTP、Git/SSH 在 offline 策略下均失败。
197+
5. 允许域名可用,未允许域名和重定向/解析绕过失败。
198+
6. 子进程、后台进程和崩溃后进程树被回收。
199+
7. Python、Node、Git、Rust 构建的代表性命令兼容。
200+
8. 路径含空格、Unicode、符号链接/junction、Git worktree 和长路径用例通过。
201+
9. provider 不可用时严格模式 fail closed,UI 给出可操作诊断。
202+
10. 安装包签名、升级、修复和卸载不会遗留专用账户、无效防火墙或危险 ACL。
203+
204+
## 12. 已知风险与缓解
205+
206+
| 风险 | 缓解 |
207+
|---|---|
208+
| Windows setup 复杂、需 UAC | 独立 setup helper;幂等校验、修复和卸载;明确用户说明 |
209+
| Seatbelt 接口/系统版本差异 | 支持版本矩阵、签名 helper、每版本实机测试、严格失败 |
210+
| Bubblewrap/userns 被发行版禁用 | 预检、捆绑受审版本、Landlock 能力回退、兼容模式不静默 |
211+
| 网络 Broker 成为高价值边界 | 最小协议、DNS/重定向校验、无凭据日志、模糊测试和独立审计 |
212+
| 沙箱影响构建速度 | 缓存只读映射、会话复用、基准预算;不放宽安全边界换性能 |
213+
| 第三方上游快速变化 | 内部稳定接口、固定版本/SBOM、升级安全回归,不依赖用户安装 CLI |
214+
215+
## 13. 关联文档
216+
217+
- [`CROSS_PLATFORM_DESKTOP_SANDBOX_RESEARCH.md`](../research/CROSS_PLATFORM_DESKTOP_SANDBOX_RESEARCH.md)
218+
- [`WORKSPACE_SKILLS_SECURITY_ARCHITECTURE.md`](./WORKSPACE_SKILLS_SECURITY_ARCHITECTURE.md)
219+
- [`SANDBOX_IMPLEMENTATION_PLAN.md`](../planning/SANDBOX_IMPLEMENTATION_PLAN.md)
220+
- [`SKILL_SECURITY_SCANNING_RESEARCH.md`](../research/SKILL_SECURITY_SCANNING_RESEARCH.md)

0 commit comments

Comments
 (0)