Skip to content

feat(sandbox): Windows 免管理员 workspace-write 沙箱后端 - #518

Closed
coder-hhx wants to merge 2 commits into
feature/rust-embed-cubesandboxfrom
feature/windows-sandbox
Closed

feat(sandbox): Windows 免管理员 workspace-write 沙箱后端#518
coder-hhx wants to merge 2 commits into
feature/rust-embed-cubesandboxfrom
feature/windows-sandbox

Conversation

@coder-hhx

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

Copy link
Copy Markdown
Collaborator

叠加 PR:base 是 #505 的分支 feature/rust-embed-cubesandbox,不是 main。等 #505 合入后本 PR 的 base 会自动回落到 main。这样 Windows 代码不拖累基础沙箱 PR 的合入节奏。

Closes #520

概述

#505 的基础 OS 沙箱之上,实现 Windows 端免管理员写围栏,替换原先 sandbox.rs windows 分支的 unsupported 桩。免管理员、零配置、fail-closed,语义为 workspace-write(广读、写限工作区+临时目录)。

sandbox 模式在 Windows 放开;sandboxOffline(断网+敏感目录读掩蔽)免管理员无法实现,保持 fail-closed 禁用

机制(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:
    1. (OI)(CI) 继承写 ACE(合成 workspace SID)到 write_root + 工作区内临时目录
    2. WRITE_RESTRICTED 受限令牌(合成 SID 作 restricting SID ⇒ 广读、写受限)
    3. CreateProcessAsUserW 拉真实 shell,WaitForSingleObject(INFINITE) 后透传退出码
    4. 任何失败退 127,绝不静默直跑
  • 进程树 kill:launcher 阻塞等待使 tracked PID 存活,taskkill /T 沿完整 PPID 链级联;额外 Job Object KILL_ON_JOB_CLOSE 兜底 ⇒ 多一跳不破坏 cancel/kill 语义。

能力契约 / 前端门控

  • SandboxCapability 新增 network_control: boolean,Rust snake_case → 双端 TS adapter 贯通
  • windows capability() 返回 supported:true, mechanism:"restricted-token", network_control:false;mac/Linux 保持 network_control:true
  • CommandSafetyModeSelector 改为按 network_control 单独禁用 sandboxOffline,sandbox 仍可用;i18n 新增 chat.safety.sandboxOfflineUnavailable(en/zh),移除过期的 sandboxUnavailableWindows

截图 / preview

说明:前端门控在 Windows 能力契约(network_control:false)下的实际渲染。

命令执行方式下拉 —— 沙箱 可选并被选中(✓);沙箱·断网 因 Windows 免管理员方案无法断网而 fail-closed 置灰,附提示「当前平台不支持断网沙箱(Windows 免管理员方案无法断网)」:

Windows 命令执行方式下拉

输入框已应用沙箱 —— 选中后 composer 显示绿色「沙箱」药丸:

输入框沙箱药丸

安全加固(采纳两轮 review)

  • resolve_program_in_path:lpApplicationName 用 PATH 内绝对路径解析,工作区 cwd 绝不被搜索(挡 planted-binary 向量),argv[0] 保留原始命令名
  • ensure_write_ace 跳过 null-DACL(避免留下 deny-all 空 DACL)
  • setup_fenced_temp 拒绝 reparse point(挡 junction/symlink 逃逸)
  • inheritable_std_handlesINVALID_HANDLE_VALUE 守卫

验证

  • ✅ Windows 编译:probe crate cargo check --target x86_64-pc-windows-msvc 零错误(#[cfg(windows)] FFI 全类型检查通过)
  • ✅ Rust host:cargo test --lib sandbox 12/12(launcher arg round-trip / 合成 SID / 命令行引用 / resolve_program_in_path)
  • ✅ 前端:GUI 715 / WebUI 578 测试(含 i18n parity)+ 双端 tsc --noEmit + biome 全绿

⚠️ 合入前必读:未做真机验证

token/ACE/CreateProcessAsUserW 这段 unsafe FFI 仅到编译级验证(Tauri crate 无法在 macOS 上交叉编译,ring/sqlite 需 MSVC)。从未在真 Windows 上功能测试,写围栏是否真的生效、广读是否真的不破工具链,都还是未知。

合入前需在真 Windows 机器上验证:工作区内写成功 / 区外写被拒 / 工具链目录可读 / sandboxOffline 置灰。因为特性 opt-in + fail-closed,落地代码在没人主动选 sandbox 前不改变任何行为。

残留(不在本 PR)

  • `bInheritHandles=TRUE` 目前是 blanket 继承;STARTUPINFOEX+PROC_THREAD_ATTRIBUTE_HANDLE_LIST 精确化待真机验证后再做(已在模块 doc 注明)
  • 真正的 Windows 断网层(Codex 路线:Job Object + WFP + 提权装 dedicated user)是后续独立工作

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

PR governance checks passed. Awaiting human review.

@StackCairn
StackCairn marked this pull request as draft August 16, 2026 15:10
在基础 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>
@coder-hhx
coder-hhx force-pushed the feature/windows-sandbox branch from a12797a to 8d70769 Compare August 16, 2026 16:37
…518

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coder-hhx
coder-hhx marked this pull request as ready for review August 16, 2026 16:56
@coder-hhx

Copy link
Copy Markdown
Collaborator Author

Superseded by unified PR #530: #530

@coder-hhx coder-hhx closed this Aug 17, 2026
su-fen added a commit that referenced this pull request Aug 19, 2026
* feat(sandbox): 对话框内命令执行方式选择器 + OS 级沙箱执行

新增命令执行方式(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>

* docs: 命令执行方式选择器截图(auto/下拉/沙箱态)

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

* fix(sandbox): reject sensitive workspaces and preserve isolated process 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>

* fix(gateway): 透传 commandSafetyMode 至远端会话链路,修复沙箱模式被忽略 (PR505 P1#1)

远端 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>

* feat(sandbox): Windows 免管理员 workspace-write 沙箱后端

在基础 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>

* docs(sandbox): add Windows-state selector & composer screenshots for #518

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

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

* fix(sandbox): enforce fences for resumable shell sessions

* fix(sandbox): enforce policy across all shell execution paths

* feat(sandbox): Windows offline backend via AppContainer + fix Git Bash 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).

* docs(ui): update Windows sandboxOffline copy for AppContainer backend

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.

* fix(sandbox): close bypass paths and enforce the fence from the backend

Address PR review findings on the unified sandbox work.

- MCP: McpManager no longer doubles as an unfenced process spawn primitive.
  test/tools/restart/diagnose now refuse stdio runtime probes while the OS
  sandbox is active (both inline and persisted servers), before any IPC.
  http/sse probes and the Settings UI path are unaffected.
- Cron: bash tasks used to run with sandbox_options=None, i.e. a persistent,
  restart-surviving bypass. They now share the same backend floor as Bash and
  ManagedProcess.
- Backend floor: sandbox flags from the renderer can only tighten, never relax.
  The backend re-reads the persisted commandSafetyMode at the command boundary
  and takes the stricter side; failure to read it is fail-closed. Applied to
  shell_run, shell_session_start, managed_process_start and cron.
- Windows capability is now probed at runtime instead of hardcoding
  supported: true. Both backends build a real security context (restricted
  token + default DACL, AppContainer SID derivation), so the fail-closed guard
  in wrap_command is actually reachable on Windows.
- validate_workspace normalizes verbatim prefixes (\\?\, \\?\UNC\) and folds
  case on Windows before the lexical comparison; mismatched prefix forms used
  to make the fence check silently pass.
- Unrecognized commandSafetyMode values now fail closed to "ask" instead of
  silently downgrading to "auto" (Rust and frontend, same semantics).
- The Windows networked backend fences writes only, so the mode copy no longer
  promises credential read masking on that platform.
- The self-reexec launcher calls validate_workspace too, keeping both entry
  points symmetric. Documented why write ACEs are grant-only.
- Command safety mode from remote/queued snapshots is clamped to the stricter
  of itself and the local setting, so a stale snapshot cannot widen it.

* fix(sandbox): close remaining TMPDIR, MCP, and bwrap bypasses

Keep Darwin temp parent promotion on /var/folders/*/*/T only, refuse
McpManager stdio create/update/enable while the OS sandbox is on, and
resolve bwrap to a system absolute path outside the workspace.
Treat unknown capability as lacking read masking and stop telling the
model that Windows networked sandbox hides ~/.ssh.

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

* fix(sandbox): unblock PowerShell CNG init under WRITE_RESTRICTED

The 0xE0434352 / BCrypt.dll crash is a write-fence denial on the user
certificate store, not a broken pwsh install. Stamp a narrow CNG ACE
surface, treat CLR crypto-init exits as probe failures, and refuse
WindowsApps binaries.

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

* fix(sandbox): recognise managed-runtime startup death and prefer cmd.exe on Windows

pwsh dies with 0xE0434352 under the Windows sandbox. That code is a CLR
unhandled exception, not a loader failure: the PE loader and the native CLR came
up fine, then managed init failed reaching BCrypt/CNG, which a WRITE_RESTRICTED
token cannot open for write. So the pwsh install is fine; the sandbox is the
cause, and there is no warmup window to work around it (Chromium's trick needs
the process to start with a normal token and drop privileges itself, whereas we
build the restricted token in the parent and CreateProcessAsUserW a fresh shell).

- Classify 0xE0434352 as sandbox-incompatible. The probe only matched the three
  loader NTSTATUS values, so it judged a guaranteed-dead pwsh "usable": the
  fallback chain never advanced, every command failed with the same code, and the
  wrong verdict was cached process-wide until restart. Kept the loader set as its
  own predicate so diagnostics can still tell the two failure layers apart.
- Order cmd.exe first when sandboxed. It is the only fully native candidate;
  Git Bash needs the MSYS runtime, pwsh is CoreCLR and powershell.exe is .NET
  Framework, so the old comment claiming the non-Git-Bash candidates were
  "native PE, always usable" was wrong. This also drops the ~6s worst case of
  probing three doomed candidates on the first command. Unsandboxed order is
  unchanged. All three spawn paths (Bash, shell sessions, ManagedProcess) share
  spawn_platform_shell_command, so they all pick this up.
- Translate 0xE0434352 into an actionable bilingual hint, alongside the existing
  loader ones, and document the limitation on the module.
- This covers both backends: the probe verdict is mechanism-agnostic and its
  cache is keyed by (program, mechanism), so pwsh is judged independently under
  restricted-token and AppContainer.

Verified: cargo test --lib (922 passed), cargo clippy --tests clean for both
files. The Windows-gated code is parse-checked via rustfmt only - cross-checking
x86_64-pc-windows-msvc needs MSVC headers that this host lacks. AppContainer's
actual CNG behaviour is still unconfirmed on real hardware; cmd.exe-first is the
conservative choice there and can be narrowed to the restricted-token backend if
pwsh turns out to work under AppContainer.

* style(sandbox): separate the trailing doc paragraph from the exit-code list

5fd878d's new doc comment ends a bullet list with an unindented follow-up line,
so rustdoc folds it into the last item and clippy::doc_lazy_continuation fires.
Blank doc line only; no wording or logic change.

* fix(sandbox): preserve workspace access across Windows modes

* fix(sandbox): gate Unix temp helpers by platform

* style(composer): align the sandbox dropdown with model and branch selectors

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

* fix(stt): close duplicate-sequence sockets before adapter teardown

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: su-fen <715041@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant