diff --git a/internal/exec-plans/codex-desktop-complete-alignment-plan.md b/internal/exec-plans/codex-desktop-complete-alignment-plan.md index 15eca0e1b..ef073c653 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、sandbox-account command runner 与七项 evidence collector 已进入唯一 current owner。真机已证明 runner transport/MSVC 编译、内置 `Users` setup/read-back、默认平台/profile RX ACL setup/read-back、`%TEMP%/%TMP%` workspace-write ACL lease 和 private desktop 路径可编译执行;最新矩阵仍为 `3/7`,说明 private desktop 不是 workspace write、offline network、allowlisted stdin 与 terminate 四个失败的单一根因。当前下一刀是先让四个真机断言稳定输出实时/终态 snapshot,再按 ACL 写入、stdio handle 和 descendant spawn 三条边界分别对照 Codex;在七项通过前保持 `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 编译、内置 `Users` setup/read-back、默认平台/profile RX ACL setup/read-back、`%TEMP%/%TMP%` workspace-write ACL lease 和 private desktop 路径可编译执行;最新矩阵仍为 `3/7`,增强 snapshot 已将四项失败统一定位为 cwd `UnauthorizedAccessException`。根因是 Lime 把 cwd 与 `%TEMP%` write roots 按祖先折叠,导致 Temp 下的已有 cwd 丢失显式 ACE;Codex 只做 exact dedup。当前修复改为先捕获所有 exact roots 的原始 DACL,再分别授权,同时保留 rollback;在七项通过前保持 `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/*` 兼容主链。 @@ -398,6 +398,7 @@ Renderer typed gateway - 2026-08-26:Windows Quality run `32961870012`(SHA `740d5b61b`,artifact `9604315141`)证明默认平台/profile RX ACL 已通过 setup/read-back 与 MSVC 编译,但 schema v3 仍为 `1/7`;六项 PowerShell child 全部在首条输出前 exit `1`,因此 read roots 不是最后一个缺口。继续对照 Codex `PermissionProfile::workspace_write -> FileSystemSpecialPath::Tmpdir -> windows_temp_env_roots` 后确认 Lime ACL plan 只给 cwd/显式 grant 可写,遗漏 child 继承的 `%TEMP%/%TMP%`;sandbox account 对宿主 Temp 只有 RX,符合 PowerShell 初始化阶段无输出失败。当前修复把已过滤 child environment 中的 Windows Temp 根纳入 workspace-write ACL lease,保持原始 DACL rollback、metadata deny、完成度 `44/46 (95.7%)` 与 `Planned/enforced=false` 不变。 - 2026-08-26:Windows Quality run `32963357155`(SHA `9af4c627a`,artifact `9604867800`)验证 `%TEMP%/%TMP%` workspace-write ACL lease 已生效,schema v3 从 `1/7` 提升至 `3/7`:`restricted_conpty_supports_stdin_resize_and_combined_output`、`restricted_execution_bounds_large_output`、`world_writable_audit_reports_everyone_write_acl` 通过;普通 pipe 的 workspace write、offline network、allowlisted stdin 与 terminate 仍失败,均表现为 PowerShell 在首条输出前 exit `1`,terminate 因 runner 已结束返回 `ControlClosed`。继续对照 Codex `windows-sandbox-rs::desktop::LaunchDesktop` 与 `process::spawn_process_with_pipes` 后确认 Lime 只填 `Winsta0\\Default` 字符串,未创建并授权 sandbox logon SID 的 desktop;当前修复新增 `windows_desktop::LaunchDesktop`,按 child token 的 logon SID 创建/授权短生命周期 desktop,并让 pipe/ConPTY 的 `STARTUPINFOEX` 复用同一 desktop,child/descendants 完成后再释放。新增 `windows-sys` 的 GDI/StationsAndDesktops features;本机 `test:rust:related` `373/373`、`cargo fmt --check`、`git diff --check` 通过,但无 Windows target,下一轮必须由 MSVC runner 验证,完成度继续 `44/46 (95.7%)`、readiness 继续 `Planned/enforced=false`。 - 2026-08-26:Windows Quality run `32965197227`(SHA `07b04bfcb`,artifact `9605628837`)已通过 elevated setup 和 MSVC 编译,schema v3 七项矩阵完整执行仍为 `3/7`:ConPTY stdin/resize/combined output、bounded large output 和 Everyone-write audit 通过,workspace write、offline account/network block、allowlisted stdin 与 terminate/job cleanup 失败。该结果排除 private desktop 是四项失败的单一根因,但不证明该安全对齐无需保留。已收紧四个失败断言:workspace 在 child 提前终态时立即打印 snapshot,offline/stdin/large-output 打印终态 `retained_output`,terminate 在发送控制前记录早退状态;不新增矩阵项、不放宽 token/ACL/firewall。本机 `npm run test:rust:related -- lime-rs/crates/tool-runtime/tests/windows_restricted_execution.rs` 退出码 `0`,反向依赖链与 `tool-runtime 373/373` 通过;Windows-only 断言编译/执行待新的 MSVC run。完成度继续 `44/46 (95.7%)`,readiness 继续 `Planned/enforced=false`。 +- 2026-08-26:Windows Quality run `32966584306`(SHA `63e4de484`)的增强 schema v3 证据仍为 `3/7`,但已把四个失败的首个共同错误定位为 PowerShell 访问 cwd 时抛出 `UnauthorizedAccessException`;stdin 与 terminate 并未进入各自控制边界。对照 Codex `effective_write_roots_for_permissions` 只做 exact dedup 后确认,Lime 在 `9af4c627a` 引入的祖先折叠会在 cwd 位于 `%TEMP%` 时删掉 cwd 本身;向 Temp 祖先新增可继承 ACE 不能代替对已存在/受保护子目录的显式授权。当前修复保留 cwd 和 Temp 两个 exact root,并在任何 ACL 变更前捕获全部原始 DACL,以避免嵌套 root snapshot 被早先授权污染。`npm run test:rust:related -- lime-rs/crates/tool-runtime/src/execution_process/windows_acl.rs lime-rs/crates/tool-runtime/tests/windows_restricted_execution.rs` 退出码 `0`,反向依赖链与 `tool-runtime 373/373` 通过;rustfmt 与 `git diff --check` 通过。该修复不改变 token、firewall、deny carveout 或 readiness,Windows-only 单测与七项矩阵仍待 MSVC 验证。 ## 7. 阻塞与避让 diff --git a/lime-rs/crates/tool-runtime/src/execution_process/windows_acl.rs b/lime-rs/crates/tool-runtime/src/execution_process/windows_acl.rs index 89a21d5b9..5bafdae25 100644 --- a/lime-rs/crates/tool-runtime/src/execution_process/windows_acl.rs +++ b/lime-rs/crates/tool-runtime/src/execution_process/windows_acl.rs @@ -74,11 +74,12 @@ pub(super) fn build_acl_plan( } read_grants.sort(); read_grants.dedup(); - write_grants = minimal_roots(write_grants); + write_grants.sort(); + write_grants.dedup(); access_denies.sort(); access_denies.dedup(); write_grants.retain(|path| access_denies.binary_search(path).is_err()); - read_grants.retain(|path| !write_grants.iter().any(|root| path.starts_with(root))); + read_grants.retain(|path| write_grants.binary_search(path).is_err()); read_grants.retain(|path| access_denies.binary_search(path).is_err()); write_denies.sort(); write_denies.dedup(); @@ -105,26 +106,6 @@ fn windows_temp_write_roots(environment: &HashMap) -> io::Result .collect() } -fn minimal_roots(mut roots: Vec) -> Vec { - roots.sort_by(|left, right| { - left.components() - .count() - .cmp(&right.components().count()) - .then_with(|| left.cmp(right)) - }); - let mut minimal = Vec::new(); - for root in roots { - if !minimal - .iter() - .any(|parent: &PathBuf| root.starts_with(parent)) - { - minimal.push(root); - } - } - minimal.sort(); - minimal -} - fn resolve_permission_path(path: &FileSystemPath, cwd: &Path) -> io::Result> { match path { FileSystemPath::Path { path } => canonical_absolute(Path::new(path)).map(Some), @@ -240,43 +221,44 @@ impl AclLease { let mut lease = Self { snapshots: Vec::new(), }; + for path in plan + .read_grants + .iter() + .chain(&plan.write_grants) + .chain(&plan.write_denies) + .chain(&plan.access_denies) + { + if let Err(error) = lease.capture(path) { + return Err(lease.rollback_after(error)); + } + } for path in plan.read_grants { - if let Err(error) = lease.capture(&path).and_then(|()| { - grant_access( - &path, - sandbox_group_sid, - capability_sid, - FILE_GENERIC_READ | FILE_GENERIC_EXECUTE, - ) - }) { + if let Err(error) = grant_access( + &path, + sandbox_group_sid, + capability_sid, + FILE_GENERIC_READ | FILE_GENERIC_EXECUTE, + ) { return Err(lease.rollback_after(error)); } } for path in plan.write_grants { - if let Err(error) = lease.capture(&path).and_then(|()| { - grant_access( - &path, - sandbox_group_sid, - capability_sid, - FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE | DELETE, - ) - }) { + if let Err(error) = grant_access( + &path, + sandbox_group_sid, + capability_sid, + FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE | DELETE, + ) { return Err(lease.rollback_after(error)); } } for path in plan.write_denies { - if let Err(error) = lease - .capture(&path) - .and_then(|()| deny_write_access(&path, capability_sid)) - { + if let Err(error) = deny_write_access(&path, capability_sid) { return Err(lease.rollback_after(error)); } } for path in plan.access_denies { - if let Err(error) = lease - .capture(&path) - .and_then(|()| deny_access(&path, sandbox_group_sid, capability_sid)) - { + if let Err(error) = deny_access(&path, sandbox_group_sid, capability_sid) { return Err(lease.rollback_after(error)); } } @@ -620,7 +602,7 @@ mod tests { } #[test] - fn workspace_acl_plan_grants_windows_temp_roots() { + fn workspace_acl_plan_keeps_nested_workspace_and_windows_temp_roots() { let root = tempfile::tempdir().expect("fixture root"); let workspace = root.path().join("workspace"); std::fs::create_dir(&workspace).expect("workspace directory"); @@ -645,7 +627,10 @@ mod tests { assert_eq!( plan.write_grants, - vec![std::fs::canonicalize(root.path()).unwrap()] + vec![ + std::fs::canonicalize(root.path()).unwrap(), + std::fs::canonicalize(&workspace).unwrap(), + ] ); } }