Skip to content

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

Merged
su-fen merged 26 commits into
mainfrom
feature/sandbox-unified
Aug 19, 2026
Merged

feat(sandbox): 统一 OS 沙箱与 Windows workspace-write 后端#530
su-fen merged 26 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 (networked write fence) and AppContainer (offline, kernel network denial) without elevation.
  • Linux bwrap is resolved to an absolute system path; workspace PATH entries are never used as the wrapper.
  • McpManager stdio create/update/enable is refused while the OS sandbox is on.

Screenshots / Preview

Default command mode

composer auto mode

Command mode menu

mode dropdown

Sandbox mode selected

sandbox active

Windows composer with workspace-write sandbox

Windows sandbox composer

Windows 下拉截图已撤下:旧图仍显示 sandboxOffline 不可用,且文案承诺 ~/.ssh 不可读。AppContainer 落地后这两点都过时了,需在真 Windows 主机上重拍(含 sandboxDescNoReadMask)。

Verification

  • GUI MCP / shell targeted Node tests — passed
  • Rust cargo test --lib sandbox29 passed, including Darwin temp-parent layout, bwrap PATH pinning, and Unix workspace-write E2E
  • git diff --check — passed

Windows FFI is compile-checked and covered by pure-logic tests, but a real Windows host/Tauri launcher E2E is still required before release.

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 文案与文档的其余部分。若某条判断有误请指出。

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.
@StackCairn
StackCairn marked this pull request as draft August 19, 2026 07:47
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

PR governance checks passed. Awaiting human review.

coder-hhx and others added 2 commits August 19, 2026 16:13
Two conflicts, both from files that main also touched:

- crates/agent-gui/src-tauri/Cargo.toml: our windows-sys feature list is a
  superset of main's, so keep ours and add main's new [dev-dependencies].
- crates/agent-gateway/web/src/lib/proto/gen/proto/v2/gateway_pb.ts: generated
  base64 descriptor, not hand-mergeable. gateway.proto itself merged cleanly
  (our ChatRequest.command_safety_mode plus main's TrajectoryFetch messages), so
  the generated Go and TS output was regenerated from the merged schema with the
  repo-pinned buf 1.71.0 / protoc-gen-go 1.36.11 instead of resolving by hand.

Verified: cargo check --tests, cargo test --lib (921 passed), go build for the
proto/protocol/session packages, tsc --noEmit for both frontends,
test:gui (2112 passed) and test:webui (619 passed).
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>
@su-fen

su-fen commented Aug 19, 2026

Copy link
Copy Markdown
Member

跟进审查结论,已在 96c124bf 修完当时列出的全部问题。

P1

  1. macOS TMPDIR=/tmp 写围栏塌缩writable_temp_dirs 不再对任意 TMPDIR 做 parent()。只有 Darwin /var/folders/<xx>/<rand>/T(含 /private/var/folders/.../T)才提升到父级;/$HOME 以及会包住敏感目录的临时根直接丢弃,避免 Seatbelt last-match-wins 把整盘(含 ~/.ssh)重新放行。
  2. McpManager create/update/enable 仍能种无沙箱 stdio 进程assertRuntimeSpawnAllowed 现在也拦配置写入。沙箱开启时拒绝 stdio 的 create / update / enable;http/sse 与 disable/delete 不受影响。下一轮 registry 不能再靠模型写入的 stdio server 绕过围栏。
  3. Linux 相对名 bwrap 走 PATH — 探测和 wrap 都解析绝对路径:优先 /usr/bin/bwrap/usr/local/bin/bwrap,再只搜绝对 PATH 目录,并跳过工作区(node_modules/.bin.venv/bin 等)。工作区内的同名二进制永远不会当包装器。

P2

  1. 选择器 null 能力仍承诺「~/.ssh 不可读」capability === null 改为保守按「无读掩蔽」处理。WebUI 一直拿不到桌面 mechanism,因此会走 sandboxDescNoReadMask,不再给出过强承诺。
  2. Bash 工具描述在 Windows 联网沙箱仍声称凭据目录被掩蔽 — 仅在非 Windows、或 Windows 断网(AppContainer)时才写 credential dirs are masked

P3

  1. Windows 下拉截图过时 — 已删除 docs/images/sandbox-mode/windows-selector-dropdown.png(图上仍是 sandboxOffline 禁用 + 旧断网文案 + 读掩蔽承诺)。composer 截图保留。真 Windows 主机 E2E 时需要重拍下拉(offline 可用 + sandboxDescNoReadMask)。

验证

  • cargo test --lib sandbox:29 passed(含 Darwin temp parent、bwrap PATH、Unix workspace-write E2E)
  • mcp-manager-tools / shell-tools Node 测试:60 passed

Windows 受限令牌 / AppContainer / CreateProcess* 仍缺真机 E2E,这一条审查时就标了,不在这次代码修复范围内。

su-fen and others added 2 commits August 19, 2026 18:17
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>
…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.
@su-fen

su-fen commented Aug 19, 2026

Copy link
Copy Markdown
Member

跟进 Windows 沙箱里 PowerShell 启动即死(日志写 BCrypt.dll 加载失败、exit_code=-532462766),已在 5fd878d7 修。

这不是本机 pwsh 坏了。启动器的 CreateProcessAsUserW 已经成功;崩在 WRITE_RESTRICTED 第二遍写检查,拦住了 CNG/CAPI 初始化时对用户证书库的写入。CAPI 返回 NTE_PROVIDER_DLL_FAIL,PowerShell/.NET 再包装成 CLR 0xE0434352(即 -532462766),日志就写成「BCrypt.dll 加载失败」——DLL 映射其实是成功的。同令牌下 cmd.exe 能活,是因为它不走这条初始化。这和 Git Bash 的 0xC0000142(default DACL / msys 内核对象)不是同一类问题。

改动

  1. 窄 CNG 用户写面 — 启动器在 CreateProcess* 前用同一个 fence_sidHKCU\...\SystemCertificates / Cryptography 以及 %APPDATA%\Microsoft\Crypto|Protect%LOCALAPPDATA%\Microsoft\CryptnetUrlCache 盖可继承写 ACE。不放开整个 HKCU / AppData,也不 stamp HKLM(需管理员)。单项失败只告警,不把整个沙箱判死。
  2. 探测回退0xE0434352 / 0x8009001D 视为启动即死。pwsh 仍崩则跳到下一候选;cmd 不走 CNG,作为兜底。此前探测只认 NTSTATUS loader 三码,会把已经崩溃的 pwsh 当成可用。
  3. 拒绝 Store/MSIX WindowsApps\pwsh.exe — 受限令牌下 CreateProcessAsUserW 对打包二进制返回 5,和 BCrypt 无关,不能当沙箱 shell。

验证

  • cargo test --lib -- runtime::sandbox:: runtime::shell_runner::tests::sandbox_probe:28 passed(含 CNG 写面范围、WindowsApps 路径、CLR -532462766 探测裁决)
  • Windows CreateProcess* / 真机 pwsh 仍缺 E2E。请在 Windows 沙箱里跑一条 exit 0:预期 pwsh 能活过 CNG init;若某台机器还必须写 HKLM 证书库,应看到探测跳过并落到 cmd,而不是再把 0xE0434352 回给用户。

coder-hhx and others added 3 commits August 19, 2026 18:30
su-fen's 5fd878d lands the same 0xE0434352 bug with a better diagnosis, so both
conflicting files are resolved wholesale in their favour and the merged tree is
byte-identical to origin.

Why mine stepped aside: I had placed the denial on the \Device\CNG device object
and concluded it was unfixable, so a53b3f1 only reordered cmd.exe to the front.
The real denial is on the user certificate store (HKCU\...\SystemCertificates and
%APPDATA%\Microsoft\Crypto), which is fixable - they stamp a narrow write ACE
there and keep pwsh working, with the probe left as a safety net. Forcing cmd.exe
first would now be a regression: it permanently downgrades the shell even though
pwsh is usable again. Their version is also strictly more complete (it catches
NTE_PROVIDER_DLL_FAIL 0x8009001D, which I missed, and refuses WindowsApps
binaries) and carries equivalent tests.

a53b3f1 is kept on the scratch/cmd-first-reorder branch for reference only; it
is not intended to land.

Verified after the merge: cargo test --lib (928 passed), test:gui (2115 passed),
rustfmt parses both Windows-gated files. Cross-compiling to
x86_64-pc-windows-msvc still needs MSVC headers this host lacks, so the Win32
paths remain unverified here.
…e 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.
@su-fen

su-fen commented Aug 19, 2026

Copy link
Copy Markdown
Member

已推送修复提交 561af38efix(sandbox): preserve workspace access across Windows modes)。

根因

断网 AppContainer 之前直接向工作区写入 package SID ACE。该 ACL 会与普通联网 Low-IL 沙箱的访问模型冲突,导致后续 Bash / ManagedProcess 甚至无法列目录,而不只是“禁止写入”。

修复

  • 断网模式改为使用“每工作区唯一”的自定义 capability SID,不授予任何网络 capability;AppContainer / WFP 仍负责断网。
  • 启动时精确撤销旧 AppContainer package SID ACE,自动迁移已经被旧实现写入 ACL 的工作区。
  • 普通“沙箱”模式继续使用非 restricted 的 Low-IL duplicated primary token,保留网络访问。
  • 只有“沙箱·断网”模式阻断网络;两种模式均可按策略读写工作区。
  • 同步覆盖 Bash、PowerShell、ManagedProcess、shell session/runner 等执行路径,并补充模式切换与旧 ACL 迁移回归测试。

验证

  • cargo check -p liveagent --tests:通过。
  • Windows 沙箱定向真机测试:15/15 通过。
  • 真实工作区 D:\code\project\test-project\001
    • 普通沙箱下 cmd、Git Bash、PowerShell 均可读写,HTTPS 返回 200。
    • 断网沙箱仍可读写工作区,DNS/外网失败,curl exit 6。
    • 从断网切回普通沙箱后读写与联网仍正常。
  • git diff --check:通过。
  • cargo test -p liveagent --lib:894 passed / 22 failed。失败集中在现有 Windows/Linux 路径假设、hook shell 选择和全套并行运行时的 MSYS 命名空间冲突;随后重新运行 Windows 定向套件仍为 15/15 通过,因此这里不将整库测试描述为全绿。

@su-fen
su-fen marked this pull request as ready for review August 19, 2026 17:27
@su-fen
su-fen merged commit ff7b689 into main Aug 19, 2026
9 checks passed
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