diff --git a/internal/exec-plans/codex-desktop-complete-alignment-plan.md b/internal/exec-plans/codex-desktop-complete-alignment-plan.md index 745f244f6..bf345ced7 100644 --- a/internal/exec-plans/codex-desktop-complete-alignment-plan.md +++ b/internal/exec-plans/codex-desktop-complete-alignment-plan.md @@ -5,7 +5,7 @@ > upstream: `/Users/coso/Documents/dev/rust/codex`(当前观测 HEAD:`99660ab3c7b861c916e467581fa9b8723504d66b`) > checklist progress: `44/46 (95.7%)`;这是未加权任务数,不等于交付完成度,剩余项包含 Windows 平台安全、live/verifier 与正式分发硬门槛。 -> 当前主目标(2026-08-26):关闭 C-3 Windows restricted execution 的真机证据。Codex current 的 per-user Windows Firewall 非 loopback/loopback 出站阻断、offline/online sandbox identity、`LimeSandboxUsers` 普通 token ACL、短生命周期 capability SID restricted ACL、clean-runner setup、read-back、原始 DACL rollback 与七项 evidence collector 已进入唯一 current owner;Windows 真机已证明宿主账户直接 `CreateProcessAsUserW` 缺少 `SeAssignPrimaryTokenPrivilege`,`CreateProcessWithTokenW` fallback 无法可靠启动 pipe 子进程且不能承接 ConPTY,因此下一刀是在 `tool-runtime` 内按 Codex current 建立 sandbox-account command runner,再重跑真实 Windows/MSVC 矩阵。在证据通过前保持 `Planned/enforced=false`。 +> 当前主目标(2026-08-26):关闭 C-3 Windows restricted execution 的真机证据。Codex current 的 per-user Windows Firewall 非 loopback/loopback 出站阻断、offline/online sandbox identity、`LimeSandboxUsers` 普通 token ACL、短生命周期 capability SID restricted ACL、clean-runner setup/read-back、原始 DACL rollback、sandbox-account command runner 与七项 evidence collector 已进入唯一 current owner。真机已证明 runner transport/MSVC 编译成功,但 restricted child 仍在无输出前 exit `1`;当前下一刀是补齐 Codex setup 对 Windows 内置 `Users` 组成员身份的强制与 read-back,再重跑真实 Windows/MSVC 矩阵。在证据通过前保持 `Planned/enforced=false`。 > 参考原则:`/Users/coso/Documents/dev/rust/codex` 是协议、Thread/Turn/Item、runtime lifecycle、tool/approval 和 GUI 语义的直接实现参考源;Lime 只在产品范围、provider 和桌面宿主边界做显式适配,不以旧矩阵或同名 legacy API 代替对齐。 > 目标:把 Lime 的产品主链、协议字段、GUI 工作区、执行安全和真实 Desktop 证据推进到当前 Codex Desktop 语义;不恢复已退役 runtime、v0 或 `agentSession/*` 兼容主链。 @@ -68,7 +68,7 @@ Renderer typed gateway | MCP resource origin | Workspace MCP App 从 canonical Item 的 `appContext`/resource URI 建立 WebContentsView surface,并以 `originCallId` 请求同一 Thread authority;`codex_apps` 不依赖本地 Plugin 安装态 | 已完成 canonical producer/cold restore、bounded provenance、link/account revalidation、稳定组件回归和独立 Electron Gate B;证据见 `.lime/qc/gui-evidence/mcp-resource-origin-electron-gate-b/` | | Environment lifecycle | `TaskCenterEnvironmentPanel` + `useThreadEnvironmentLifecycleStatus` 已消费 `environment/status` 与 typed connected/disconnected notification,稳定 DOM 覆盖环境 identity/status | 已完成五语言、组件回归和独立 Electron Gate B:真实远端 Environment 断线与有限重连在同一 canonical Thread 可见,mock/console/page/invoke error 为 0 | -当前执行顺序:Provider capability、Queue(含重复正文视觉收口)、Project、Strict Review、D-2 MCP event stream/resource origin、D-3 Environment、Thread Revert、Browser/Computer Use requirements 产品裁决、Thread/Item current 字段/恢复复核、Agent current 聚合门禁、本机 macOS packaged candidate、`1.135.0` 正式 macOS 分发和 Windows Squirrel N-1 packaged Gate B 已关闭;C-3 Windows restricted-token、ACL/Job、ConPTY、elevated setup、offline/online account、Firewall/WFP、readiness read-back、本地 fail-closed 单测和真实 packaged evidence collector 已进入 current owner。下一刀是在真实 Windows/MSVC runner 编译并执行七项 restricted execution 安全矩阵;live provider/Pier/DeepSWE verifier 和覆盖当前工作树的正式分发同样等待外部环境。每完成一项即更新本计划与运行证据,不批量提前勾选。 +当前执行顺序:Provider capability、Queue(含重复正文视觉收口)、Project、Strict Review、D-2 MCP event stream/resource origin、D-3 Environment、Thread Revert、Browser/Computer Use requirements 产品裁决、Thread/Item current 字段/恢复复核、Agent current 聚合门禁、本机 macOS packaged candidate、`1.135.0` 正式 macOS 分发和 Windows Squirrel N-1 packaged Gate B 已关闭;C-3 Windows restricted-token、ACL/Job、ConPTY、elevated setup、offline/online account、Firewall/WFP、readiness read-back、本地 fail-closed 单测和真实 packaged evidence collector 已进入 current owner。当前先对齐内置 `Users` 组成员身份并重跑七项 Windows/MSVC 安全矩阵;矩阵 `7/7` 后再验证 packaged runner digest。live provider/Pier/DeepSWE verifier 和覆盖当前工作树的正式分发同样等待外部环境。每完成一项即更新本计划与运行证据,不批量提前勾选。 > 最新 Queue/CI 收口状态(2026-08-26):Queue 状态条已删除 submission preview 与 `thread-queue-items` 正文列表,canonical 时间线成为唯一正文 owner;五语言只保留标题、loading、数量和刷新失败状态。组件、Gate B 与 Sherpa 定向回归 25/25,`npm run i18n:check` 保持 100%。真实 Queue Gate B 已通过:`timelineMarkerCount=1`、`queueStatusContainsMarker=false`、`queueItemsPresent=false`、47 次真实 Electron IPC、`mockFallbackHitCount=0`、console/page/invoke error 均为 0,证据见 `.lime/qc/gui-evidence/thread-queue-electron-gate-b/thread-queue-electron-gate-b-summary.json`。根 `package.json`/lock importer 已声明直接 `ws@8.18.3`,修复 Quality CI Frontend Full 的 `Failed to resolve import "ws"`;Windows Shell Runtime 上限调为 60 分钟,restricted security matrix 已前移到 Sherpa/sidecar 重型阶段之前,Sherpa 清理/解压前后输出阶段日志。`smoke:agent-runtime-current-fixture` 完整通过,`npm run verify:gui-smoke` Shell-01 21/21、contracts 299 checks、typecheck、scripts/legacy governance 均通过;App Server Windows sandbox fail-closed 定向单测 4/4 通过。上述本机测试不替代 Windows restricted matrix 或 live verifier;正式 macOS 与 Squirrel 证据由独立 Release run `32864956267` 证明。 @@ -393,6 +393,7 @@ Renderer typed gateway - 2026-08-26:Windows Quality run `32954904978`(SHA `5eebd985f`)验证稳定 sandbox account 的 NUL ACL 仍不足:setup/MSVC 编译通过,七项矩阵为 `1/7`,六个 restricted child 均 spawn 后立即以 exit code `1` 结束且无输出。继续对齐 Codex current:新增 `windows_null::NullDeviceLease`,用 `Local\\LimeSandboxNullDeviceAcl` named mutex 串行授予/撤销当前 capability SID 的 NUL read/write/execute ACE,lease 在 child/descendant 完成后回收;普通 pipe child 同时移除强制 `CREATE_NO_WINDOW`,恢复常规命令 inherited-console 语义。macOS 定向 related Rust `373/373`、fmt/diff check 通过;Windows-only 编译与七项真机证据待新 SHA 重跑,计划仍保持 `44/46 (95.7%)`、`active`、`Planned/enforced=false`。 - 2026-08-26:Windows Quality run `32957046834`(SHA `e147a8731`)完成 setup/MSVC 编译,但六项 restricted case 在 child spawn 前统一返回 `CreateFileW(Windows sandbox NUL): ERROR_ACCESS_DENIED`,七项仍为 `1/7`。该证据证明低权限 sandbox-account runner 不能持有 NUL 的 `WRITE_DAC`;修复将 capability NUL lease 移至宿主,在 `CreateProcessWithLogonW` 前安装 ACE,并与 workspace ACL lease 一起由 supervisor/reaper 持有到 runner 及 descendants 退出,sandbox account 不获得全局 DACL 管理权。定向 related Rust `373/373`、fmt/diff check 通过;待下一轮 Windows schema v3 真机重跑,完成度/readiness 不变。 - 2026-08-26:Windows Quality run `32957785741`(SHA `a2a6caa85`,artifact `9602756033`)在 setup 阶段编译 `tool-runtime` 失败,七项矩阵未执行。MSVC `E0277` 明确指向 `NullDeviceLease` 持有 `LocalSid(*mut c_void)`,因此无法跨 supervisor/reaper 线程移动;修复为 lease 仅保存 capability SID 字符串,安装/撤销 ACE 时在当前线程短暂解析 `LocalSid`,不添加 `unsafe impl Send`。同时移除日志暴露的 Windows-only unused import;本机 related Rust 反向依赖链与 `tool-runtime 373/373`、rustfmt、`git diff --check` 通过。该 run 继续是 `failedStage=setup`、矩阵 `0/7`,不改变 `44/46 (95.7%)` 或 `Planned/enforced=false`。 +- 2026-08-26:Windows Quality run `32958651645`(SHA `37f385db6`,artifact `9603104216`)已通过 setup 和 MSVC 编译,schema v3 七项矩阵完整执行为 `1/7`:Everyone-write audit 通过,其余六项 restricted child 仍在无输出前 exit `1`,terminate 场景因 runner 提前结束返回 `ControlClosed`。该 run 证明 NUL lease 的 `Send` 编译问题已关闭,但 NUL ACE 未解决 child 启动。继续对照 Codex `setup_main::sandbox_users` 发现 Lime 遗漏将 sandbox account 加入 Windows 内置 `Users`(SID `S-1-5-32-545`);当前修复对本地化组名执行 setup 幂等加入、readiness 与每次 runner 启动 read-back,避免子进程缺少仅授权给内置 Users 的系统资源访问。完成度继续为 `44/46 (95.7%)`,readiness 继续 `Planned/enforced=false`。 ## 7. 阻塞与避让 diff --git a/lime-rs/crates/tool-runtime/src/windows_setup.rs b/lime-rs/crates/tool-runtime/src/windows_setup.rs index f20f6b38e..26562a924 100644 --- a/lime-rs/crates/tool-runtime/src/windows_setup.rs +++ b/lime-rs/crates/tool-runtime/src/windows_setup.rs @@ -12,9 +12,9 @@ mod accounts; #[cfg(windows)] use accounts::{ - ensure_local_account, ensure_local_group, ensure_local_group_member, - ensure_sandbox_accounts_null_device_access, validate_sandbox_accounts_null_device_access, - validate_windows_sandbox_group_membership, + ensure_builtin_users_group_member, ensure_local_account, ensure_local_group, + ensure_local_group_member, ensure_sandbox_accounts_null_device_access, + validate_sandbox_accounts_null_device_access, validate_windows_sandbox_group_membership, }; #[cfg(windows)] pub(crate) use accounts::{ @@ -144,6 +144,8 @@ pub fn run_windows_sandbox_setup( WINDOWS_SANDBOX_OFFLINE_USERNAME, )?; ensure_local_group_member(WINDOWS_SANDBOX_USERS_GROUP, WINDOWS_SANDBOX_ONLINE_USERNAME)?; + ensure_builtin_users_group_member(WINDOWS_SANDBOX_OFFLINE_USERNAME)?; + ensure_builtin_users_group_member(WINDOWS_SANDBOX_ONLINE_USERNAME)?; ensure_sandbox_accounts_null_device_access()?; let offline_blob = dpapi_protect(offline_password.as_bytes())?; diff --git a/lime-rs/crates/tool-runtime/src/windows_setup/accounts.rs b/lime-rs/crates/tool-runtime/src/windows_setup/accounts.rs index a97838915..e07e081c4 100644 --- a/lime-rs/crates/tool-runtime/src/windows_setup/accounts.rs +++ b/lime-rs/crates/tool-runtime/src/windows_setup/accounts.rs @@ -331,6 +331,11 @@ pub(super) fn ensure_local_group_member(group: &str, account: &str) -> io::Resul } } +pub(super) fn ensure_builtin_users_group_member(account: &str) -> io::Result<()> { + let group = builtin_users_group_name()?; + ensure_local_group_member(&group, account) +} + pub(crate) fn verify_windows_sandbox_group_membership(account: &str) -> io::Result<()> { if !matches!( account, @@ -349,13 +354,92 @@ pub(super) fn validate_windows_sandbox_group_membership(account: &str) -> Result .map_err(|error| format!("sandbox account SID validation failed: {error}"))?; windows_sandbox_users_group_sid() .map_err(|error| format!("sandbox users group validation failed: {error}"))?; - if local_group_contains_sid(WINDOWS_SANDBOX_USERS_GROUP, &account_sid)? { - Ok(()) - } else { - Err(format!( + if !local_group_contains_sid(WINDOWS_SANDBOX_USERS_GROUP, &account_sid)? { + return Err(format!( "Windows sandbox account {account} is not a member of {WINDOWS_SANDBOX_USERS_GROUP}" - )) + )); } + let builtin_users = builtin_users_group_name() + .map_err(|error| format!("Windows built-in Users group lookup failed: {error}"))?; + if !local_group_contains_sid(&builtin_users, &account_sid)? { + return Err(format!( + "Windows sandbox account {account} is not a member of {builtin_users}" + )); + } + Ok(()) +} + +fn builtin_users_group_name() -> io::Result { + use std::ffi::c_void; + use windows_sys::Win32::Foundation::{GetLastError, ERROR_INSUFFICIENT_BUFFER}; + use windows_sys::Win32::Security::{ + CreateWellKnownSid, LookupAccountSidW, WinBuiltinUsersSid, SID_NAME_USE, + }; + + let mut sid_size = 0; + unsafe { + CreateWellKnownSid( + WinBuiltinUsersSid, + std::ptr::null_mut(), + std::ptr::null_mut(), + &mut sid_size, + ); + } + if sid_size == 0 { + return Err(io::Error::last_os_error()); + } + let mut sid = vec![0u8; sid_size as usize]; + if unsafe { + CreateWellKnownSid( + WinBuiltinUsersSid, + std::ptr::null_mut(), + sid.as_mut_ptr() as *mut c_void, + &mut sid_size, + ) + } == 0 + { + return Err(io::Error::last_os_error()); + } + + let mut name_len = 0; + let mut domain_len = 0; + let mut sid_type: SID_NAME_USE = 0; + unsafe { + LookupAccountSidW( + std::ptr::null(), + sid.as_ptr() as *mut c_void, + std::ptr::null_mut(), + &mut name_len, + std::ptr::null_mut(), + &mut domain_len, + &mut sid_type, + ); + } + if unsafe { GetLastError() } != ERROR_INSUFFICIENT_BUFFER || name_len == 0 { + return Err(io::Error::last_os_error()); + } + let mut name = vec![0u16; name_len as usize]; + let mut domain = vec![0u16; domain_len as usize]; + if unsafe { + LookupAccountSidW( + std::ptr::null(), + sid.as_ptr() as *mut c_void, + name.as_mut_ptr(), + &mut name_len, + domain.as_mut_ptr(), + &mut domain_len, + &mut sid_type, + ) + } == 0 + { + return Err(io::Error::last_os_error()); + } + let length = name + .iter() + .position(|value| *value == 0) + .unwrap_or(name.len()); + String::from_utf16(&name[..length]) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Users group name is not UTF-16")) } pub(crate) fn windows_sandbox_users_group_sid() -> io::Result {