Skip to content

feat(sandbox): 统一 OS 沙箱与 Windows workspace-write 后端 - #530

Open
coder-hhx wants to merge 15 commits into
mainfrom
feature/sandbox-unified
Open

feat(sandbox): 统一 OS 沙箱与 Windows workspace-write 后端#530
coder-hhx wants to merge 15 commits into
mainfrom
feature/sandbox-unified

Conversation

@coder-hhx

@coder-hhx coder-hhx commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Closes #506
Closes #520

Summary

将基础 OS 沙箱与 Windows workspace-write 后端合并为一个面向 main 的 PR,并修复模型驱动 shell 的可恢复 Bash 绕过问题。

根因是 chat 的 Bash 在启用 resumable session 时走 shell_session_start,旧链路没有传递 sandbox policy,Rust 最终以 sandbox_spec=None 启动普通 shell。RANDOM&&、Git Bash 和 D:/... 路径本身都不是绕过技巧;缺失的策略参数才是漏洞。

现在一次性 Bash、resumable Bash 和 ManagedProcess 共用同一个平台 spawn funnel。resumable session 初始启动和后续 ProcessWait 都保持同一隔离进程,响应返回实际生效的机制,便于审计。

本 PR supersedes #505#518;两个旧 PR 已关闭。

Coverage Matrix

Entry point Policy propagation Result
Desktop Chat / Agent Dev useSendChatTurn -> runAgentConversationTurn -> builtin registry sandbox / sandboxOffline reaches every shell tool
Worktree subagent child registry rebuild inherits policy and uses child worktree as fence root Bash and ManagedProcess remain fenced
Readonly subagent execution-time allowlist excludes shell and mutation tools no Bash surface
Gateway direct request protobuf/JSON command_safety_mode -> desktop listener -> turn mode preserved
Gateway queued request queue snapshot and dequeue/replay preserve the mode mode preserved
Cron Auto Prompt registry now receives the system command safety mode model-driven prompt shell is covered

Explicit user-authored cron scripts and hooks remain outside this model-driven Bash contract and retain their existing explicit-script semantics.

Change Scope

  • Shared command safety mapping is centralized in sandboxPolicy.ts.
  • shell_run, shell_session_start, and managed_process_start always send explicit sandbox and sandbox_allow_network booleans.
  • Rust IPC arguments are required; omitted policy data fails at the command boundary instead of silently becoming unsandboxed.
  • macOS uses Seatbelt (sandbox-exec); Linux uses bubblewrap (bwrap).
  • Windows uses a restricted token, inherited workspace-write ACEs, and a Job Object without elevation.
  • Windows sandboxOffline remains disabled and fails closed because this backend cannot enforce network isolation.

Screenshots / Preview

Default command mode

composer auto mode

Command mode menu

mode dropdown

Sandbox mode selected

sandbox active

Windows capability menu

Windows command mode dropdown

Windows composer with workspace-write sandbox

Windows sandbox composer

Verification

  • pnpm --filter liveagent exec tsc --noEmit — passed
  • GUI frontend suite — 1897 passed, 0 failed
  • Shell / registry / Gateway targeted Node tests — passed
  • Rust sandbox unit tests — 16 passed
  • Rust shell session tests — 17 passed, including Unix workspace-write E2E and fail-closed backend-unavailable coverage
  • Gateway WebUI socket tests — 38 passed
  • Gateway Go suite — go test ./... passed
  • git diff --check — passed

The Unix E2E proves workspace writes succeed while sibling writes fail and the response reports the active mechanism. Windows FFI is compile-checked and covered by pure-logic tests, but a real Windows host/Tauri launcher E2E is still required before release; this macOS environment cannot provide that evidence.

Pre-submit Checklist

coder-hhx and others added 12 commits August 16, 2026 15:38
新增命令执行方式(commandSafetyMode),在聊天输入栏工具条切换,单一互斥维度:
- 逐条审批(ask):非只读工具每次执行前都请求批准
- 自动执行(auto,默认):按设置中的工具权限策略直接执行
- 沙箱(sandbox):Bash/ManagedProcess 在 OS 级沙箱内执行
- 沙箱·断网(sandboxOffline):在沙箱基础上禁止命令联网

沙箱实现(runtime/sandbox.rs):
- macOS:/usr/bin/sandbox-exec + 动态 Seatbelt profile(allow-default +
  全盘写 deny + 工作区/临时目录写 allow + 敏感目录读 deny + 工作区 re-allow,
  规则顺序有单测锁定)
- Linux:bwrap(--ro-bind / / + 工作区 bind + tmpfs 掩蔽敏感目录 +
  可选 --unshare-net),带真实可用性探测
- Windows:暂不支持(受限令牌 + Job Object + WFP 路线待实现),选择器中
  沙箱两项禁用并提示,执行层 fail-closed 兜底
- 掩蔽范围:~/.ssh、~/.aws、~/.gnupg、~/.config/gh、~/.liveagent
- 拦截点:spawn_platform_shell_command 唯一漏斗(Bash + ManagedProcess 共用),
  沙箱包裹失败直接报错,绝不静默降级
- Hook/Cron 脚本为用户显式配置的自动化,不套沙箱

配套:
- ShellRunResponse 新增 sandbox 字段,Bash 结果头部显示 sandbox: seatbelt
- Bash 工具描述在沙箱开启时告知模型围栏规则
- system_sandbox_capability 命令 + adapters 探测(WebUI 返回 null 透传)
- 设置持久化(含保存白名单)、GUI/WebUI settings 双份同步、i18n 四 block 同加

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…besandbox

# Conflicts:
#	crates/agent-gateway/web/src/app/GatewayApp.tsx
#	crates/agent-gateway/web/src/i18n/config.ts
#	crates/agent-gateway/web/src/lib/settings/index.ts
#	crates/agent-gui/src-tauri/src/lib.rs
#	crates/agent-gui/src/i18n/config.ts
#	crates/agent-gui/src/lib/settings/index.ts
#	crates/agent-ui/src/pages/chat/ChatComposerBar.tsx
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ss lifecycle

Address two P1 review blockers on the sandbox write fence:

P1#2 — Workspace root could re-expose sensitive dirs. The write fence
re-allows write_root last (macOS re-allow rule, Linux --bind after --tmpfs),
so a workspace of $HOME or / would re-open ~/.ssh, ~/.liveagent, etc. Add a
fail-closed validate_workspace() in the shared wrap_command choke point:
reject any workspace that encloses/equals a sensitive dir, and any workspace
nested inside a credential dir (~/.ssh/.aws/.gnupg/.config/gh). The app-config
dir (~/.liveagent) is exempt in the descendant direction so the default
workspace ~/.liveagent/default-project still runs.

P1#3 — isolated ManagedProcess lifecycle was broken on Linux. bwrap always
added --die-with-parent, killing isolated long-lived processes when LiveAgent
exits. Thread an `isolated` flag through SandboxSpec (default false for the
Bash tool path, true for isolated managed processes) and omit
--die-with-parent when set, mirroring the existing conditional --unshare-net.

Adds cross-platform tests for workspace validation and the bwrap flag.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
远端 WebUI / 队列 / 编辑重发的请求经网关下发时,useSendChatTurn 硬编码使用
本地 settings.system.commandSafetyMode,忽略了请求自带的安全模式,导致远端
选定的沙箱模式无法生效。

- proto: ChatRequest 新增 command_safety_mode 字段(regen Go/TS/Rust)
- Go 网关: NormalizeCommandSafetyMode 白名单归一(非法/空→"")并全链路透传
- WebUI: buildGatewaySystemSettings / payload / adapters 补齐字段
- 桌面端: 网关事件 → bridge override → useSendChatTurn 优先级链
  (override ?? 网关直带 ?? 本地设置),fail-closed 回落本地而非默认 auto
- 队列: QueuedChatTurn 顶层携带 commandSafetyMode,保留入队时的安全模式

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…besandbox

# Conflicts:
#	crates/agent-gui/src-tauri/src/commands/runtime/shell.rs
#	crates/agent-gui/src/lib/tools/shellTools.ts
#	crates/agent-gui/src/pages/ChatPage.tsx
#	crates/agent-ui/src/pages/chat/ChatComposerBar.tsx
在基础 OS 沙箱之上实现 Windows 端写围栏,替换原先的 unsupported 桩。
免管理员、零配置、fail-closed,语义为 workspace-write。

机制(Option A,自我再执行 launcher):
- sandbox.rs windows `wrap_command` 返回 (current_exe,
  ["__sandbox_exec","--write-root",root,"--",program,args...])
- windows_sandbox.rs `run_sandbox_launcher_if_requested()` 作为 run()
  首行(Tauri 初始化前)识别 marker:盖 (OI)(CI) 继承写 ACE(合成
  workspace SID)+ 建 WRITE_RESTRICTED 受限令牌(合成 SID 作 restricting
  SID ⇒ 广读、写限工作区)+ CreateProcessAsUserW 拉真实 shell,
  WaitForSingleObject(INFINITE) 后透传退出码;任何失败退 127,绝不直跑。
- Job Object KILL_ON_JOB_CLOSE 兜底;launcher 阻塞等待使 taskkill /T
  沿完整 PPID 链级联,进程树 kill 不受多一跳影响。

能力/门控:
- capability() 返回 supported:true, mechanism:"restricted-token",
  network_control:false。sandbox 模式在 Windows 放开;sandboxOffline
  (断网 + 读掩蔽免管理员做不到)保持 fail-closed 禁用,执行层对
  !allow_network 直接报错兜底(设置可能自 macOS 同步)。
- 前端 CommandSafetyModeSelector 改为按 network_control 单独禁用
  sandboxOffline,sandbox 仍可用;i18n 增 sandboxOfflineUnavailable。
- SandboxCapability 增 network_control:boolean,Rust→双端 TS adapter 贯通。

安全加固(采纳两轮 review):
- resolve_program_in_path:lpApplicationName 用 PATH 内绝对路径解析,
  工作区 cwd 绝不被搜索(挡 planted-binary),argv[0] 保留原名。
- ensure_write_ace 跳过 null-DACL(避免 deny-all)、setup_fenced_temp
  拒绝 reparse point、INVALID_HANDLE_VALUE 守卫。

验证:probe crate cargo check --target x86_64-pc-windows-msvc 零错误;
host cargo test --lib sandbox 12/12;GUI 715 / WebUI 578 前端测试 + 双端
tsc + biome 全绿。⚠️ token/ACE/CreateProcessAsUserW 的 unsafe FFI 仅编译
级验证,从未在真 Windows 上功能测试,合入前需真机验证边界。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…518

沙箱可选并被选中(✓),沙箱·断网因 Windows 免管理员方案无法断网而 fail-closed 禁用。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…besandbox

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	crates/agent-gateway/web/src/lib/proto/gen/proto/v2/gateway_pb.ts
coder-hhx and others added 3 commits August 17, 2026 23:09
…h DLL-init failure

Two Windows sandbox changes, both admin-free:

1. Fix Git Bash dying with 0xC0000142 (STATUS_DLL_INIT_FAILED) under the
   restricted-token sandbox: append a logon-SID GENERIC_ALL ACE to the
   restricted token's default DACL (Chromium AddSidToDefaultDacl pattern)
   so msys/cygwin named kernel objects pass the write-restricted second
   access check; set lpDesktop explicitly; replace bInheritHandles=TRUE
   with STARTUPINFOEXW + PROC_THREAD_ATTRIBUTE_HANDLE_LIST (3 std handles
   only); translate loader NTSTATUS exit codes into readable stderr hints.
   shell_runner additionally probes each candidate once per (shell,
   mechanism) under sandbox mode and falls through to pwsh/powershell/cmd
   on loader-level incompatibility instead of returning a dead shell.

2. sandboxOffline now works on Windows: zero-capability AppContainer
   (CreateProcessW + SECURITY_CAPABILITIES) gives kernel-enforced network
   denial including loopback via WFP, no elevation needed; write fence
   reuses the inheritable-ACE stamping with the AC SID as trustee, and AC
   default-deny read masks sensitive dirs as a side benefit. Offline env
   overlay (blackhole proxy, CARGO_NET_OFFLINE, PIP_NO_INDEX,
   NPM_CONFIG_OFFLINE) added as defense in depth.

Launcher protocol gains --net on|off and --isolated; isolated processes
now skip the KILL_ON_JOB_CLOSE job object across the re-exec boundary
(previously the flag was dropped, matching bwrap --die-with-parent
semantics on Linux).
network_control is now true on Windows, so the capability-driven selector
enables sandboxOffline there; drop the hardcoded "Windows can't sever
network" from the unavailable hints (kept generic for platforms that
still lack network control) and refresh the CommandSafetyMode comment.
@xiaozhou26

Copy link
Copy Markdown
Contributor

c125b582 做了一轮针对沙箱边界的审查,以下问题均已对照源码核实。按严重度分组。


一、沙箱围栏可被绕过

1. 模型可经 MCP 工具拿到无围栏的进程 spawn 原语crates/agent-gui/src-tauri/src/commands/integration/mcp.rs:102

build_stdio_command 最终走到裸 Command::new(program),不带任何 SandboxSpec。而 createMcpManagerToolsbuiltinRegistry.ts:234-240 是无条件注册的,且不接收 params.sandbox。模型调用 mcp_manageraction:"test" 时可传入自由格式的 command/argsmcpManagerTools.ts:118-119)——这条路径不写配置、不持久化,因此 runtimeScope !== "chat" 的守卫不生效。

更关键的是 runAgentConversationTurn.ts:606 的 ask 升级只在 safetyMode === "ask" 时触发,所以选了 sandbox / sandboxOffline 的用户不会收到任何提示。净效果是:选择最严格的模式围住了 shell 工具,却留下一个同样通用的进程 spawn 入口——在 sandboxOffline 下尤其矛盾,该模式的全部意义就是内核级断网。

2. cron bash 任务完全不受沙箱约束,且重启后仍在crates/agent-gui/src-tauri/src/services/automation/scheduler.rs:519

run_shell_script 转发给 run_shell_script_with_envs 时尾部两参写死 &[], Noneshell_runner.rs:880-887),即 sandbox_options 恒为 None。cron 工具在 builtinRegistry.ts:230 同样未接收 params.sandbox,也无 ask 升级。

用户为了约束一个不可信任务而选择 sandbox,结果 shell 被围住、cron 没有:模型创建一个 bash 任务,调度器在 :519 以无沙箱方式触发,脚本对整个文件系统和网络拥有完整用户权限。持久化让它比一次性逃逸更严重——任务会跨应用重启存活,在沙箱会话早已结束后继续触发。

3. 沙箱开关完全由渲染进程的两个 bool 决定,后端无独立下限crates/agent-gui/src-tauri/src/commands/runtime/shell.rs:36

sandbox.then_some(SandboxOptions { allow_network: sandbox_allow_network }) 直接采信前端传值,后端从不回查已持久化的 settings.system.commandSafetyMode 来建立最低标准。任何使 sandbox=false 抵达该命令的路径都能完全绕过围栏。

仓库里已有正确范式可循:ssh_local_forward 在服务端重新解析持久化配置,而不是信任调用方。考虑到 CLAUDE.md 明确"桌面端是工具唯一执行处",沙箱下限应当落在 Rust 侧,从而不论请求源自哪个 host 都能强制生效。commands/runtime/process.rs:23-25 是同一形态。


二、fail-open:失效路径未按 fail-closed 收敛

4. Windows capability() 无运行时探测,模块自身的 fail-closed 守卫在该平台恒不可达crates/agent-gui/src-tauri/src/runtime/sandbox.rs:611

其余平台都做真实探测:macOS 在 :421-439 stat /usr/bin/sandbox-exec,Linux 在 :503-547 实际 exec bwrap。Windows 直接返回硬编码的 supported: true, network_control: true, reason: None

理解注释的论证是两个后端免依赖免提权、恒可用。但若 CreateRestrictedToken 或 AppContainer profile 在 spawn 时失败(组策略、EDR hook、受限 SKU),:404-410 的守卫因为 capability().supported 从来不为 false 而永不触发。叠加 shell_runner.rs:708-716 把 NTSTATUS 加载器失败(0xC0000142/0xC0000135/0xC0000022)归类为"可用",一个在施加任何限制前就死掉的 launcher 仍会被标记为候选正常——命令随后无约束执行,而 ShellRunResponse.sandboxshell_runner.rs:1011)照常上报 restricted-token,UI 照常显示沙箱徽标。这与同文件 :8 自述的原则直接冲突:"沙箱被请求而平台机制不可用时直接报错,绝不静默降级为无沙箱执行"。

5. validate_workspacestarts_with 比较可能带 verbatim 前缀的路径,Windows 下会静默失效crates/agent-gui/src-tauri/src/runtime/sandbox.rs:329

canonical_or_self:316)是 canonicalize(path).unwrap_or_else(|_| path.to_path_buf()):路径存在时返回 \\?\C:\...,不存在时返回原始路径。而 Path::starts_with 是纯词法的组件比较,不做前缀归一。

于是当 write_root 存在但某个敏感目录不存在(或反之,例如机器上有 ~/.ssh 而无 ~/.aws),两侧操作数携带不同前缀形态,dir.starts_with(&root) 即使目录真的在工作区内也会返回 false。按 :319-326 的文档注释,这个函数的全部目的就是拒绝把工作区设为 home 或 /,以免 write_root 的后置 re-allow 重新暴露凭据。比较一旦 fail-open,该守卫被跳过,沙箱会对 ~/.ssh~/.aws~/.liveagent(内含 config.sqlite,存放 provider API key)重新放行读写。:727-758 的单测只用了 Unix 形态路径,因此 CI 覆盖不到。

6. 未识别的安全模式静默降级为 auto(最宽松的非 ask 值),且会被写回磁盘crates/agent-gui/src-tauri/src/commands/config/settings/system.rs:477

.filter(|v| matches!(*v, "ask"|"auto"|"sandbox"|"sandboxOffline")).unwrap_or("auto") 没有错误分支也没有日志。system_value_with_defaults 会在保存路径上执行它(:497-506),而保存实现是删除全部 system key 再重新插入(:511-538),本 PR 又把 SYSTEM_COMMAND_SAFETY_MODE_KEY 加进了该 key 列表——所以这不只是读取期的默认值,而是破坏性地持久化

任何未来新增的模式值、任何回退到旧版本、任何手改配置的笔误,都会解析成宽松的 auto 而非显式失败。对于一个全部意义在于约束的设置,未识别值应当向严格侧(ask)收敛,与 sandbox.rs:8 自述的 fail-closed 原则保持一致。


三、UI 安全承诺与 Windows 联网后端实现不符

7. 两种语言的文案都承诺"~/.ssh 等敏感目录不可读",但 Windows 联网后端(restricted-token)没有实现任何读掩蔽crates/agent-ui/src/i18n/translations/zhCNCommon.ts:250

macOS 用 (deny file-read* (subpath …))sandbox.rs:469-472),Linux 用 --tmpfs 掩蔽(:577-581),文案在这两个平台成立。但 Windows 的 WRITE_RESTRICTED 令牌在读/执行上完全跳过 restricting-SID 检查(windows_sandbox.rs:12-14,133,292),ensure_write_ace 只签发 GRANT_ACCESS 且明确从不移除 ACE(:20, :719-791)——windows_sandbox.rs:33-34 本身就把读掩蔽标注为"联网后端缺失项"。

问题在于文案把一个已知缺口转成了肯定的安全承诺:Windows 用户读到该描述,据此认为凭据受保护,于是在 sandbox 模式下运行不可信的模型生成代码。这段代码可以随意读取 ~/.ssh/id_rsa%USERPROFILE%\.aws\credentials~/.liveagent/config.sqlite(provider API key),而这恰恰是联网后端,可直接外传。建议在读掩蔽补齐前,让该文案随平台/后端分叉。


四、Windows 后端实现细节

8. __sandbox_exec launcher 未复用 validate_workspace,且写 ACE 只授不撤crates/agent-gui/src-tauri/src/runtime/windows_sandbox.rs:890

validate_workspace 只在 wrap 路径的 sandbox.rs:404-411 调用;re-exec 出来的 launcher 进程经 win::execute:866-891)到达 ensure_write_ace:719-791)时并不重做该校验。同一个 write_root 在两个入口点上前置条件不一致,属于 fail-closed 的不对称,且会随任一侧演进而继续漂移。

另外 ensure_write_ace 通过 SetNamedSecurityInfoW 授予 FILE_GENERIC_READ|WRITE|EXECUTE|DELETE,带 OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE:752-755),而全文件没有任何 REVOKE_ACCESS / DeleteAce。结果是每个在 sandbox 模式下用过的工作区都会永久累积一条可继承 ACE,对应一个不再映射到任何活跃主体的合成 S-1-5-21 SID,此后该目录下新建的每个文件都会继承它。用户会在文件夹权限里看到无法解析的 SID,且卸载 LiveAgent 不会清理。


五、设计问题(非缺陷,供讨论)

9. 安全模式优先级链缺少 max-strictness 钳制crates/agent-gui/src/pages/chat/runtime/useSendChatTurn.ts:342

overrides ?? gatewayBridgeRequest ?? settings.system.commandSafetyMode 是首个非空值胜出。注释写明远端 WebUI 选定的模式经此生效是有意设计(P1#1),这点理解。

但链路上没有任何一处与本地设置做严格度比较。WebUI 经 gatewaySocketShared.ts:473 把自己快照里的值送上 proto ChatRequest.command_safety_mode,于是一个刻意选择了 sandboxOffline 的桌面用户,其选择会被任何浏览器会话的陈旧快照静默降级为 auto。鉴于 CLAUDE.md 的核心不变式是"桌面端是工具唯一执行处",这里是远端取值在决定本地进程的约束强度。是否考虑改成取远端与本地中更严格的一方?这样既保留远端收紧的能力,又不允许远端放松。


审查覆盖沙箱边界相关改动;未逐行审查 UI 文案与文档的其余部分。若某条判断有误请指出。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants