修复 AGC Windows ACL 提权与文件访问边界 (#211)
### 背景 Windows 下 AGC 读取配置目录、项目文件和本地资源时,可能先于 ACL 校验阶段就因继承权限、错误 owner 或拒绝访问而失败,表现为 `拒绝访问。 (os error 5)`。 此前自定义 `--config-dir` 场景还存在 scope 无法传递到提权子进程的问题;native picker 路径也缺少进程内来源证明,递归索引可能跟随 junction/reparse point,新建文件在权限加固失败时可能留下残留文件。 ### 本次变更 1. 修复自定义 `--config-dir` 的 `managed / user-selected` scope 传递与提权票据校验。 2. 增加 native picker 文件/目录路径的短期 provenance 授权;未经过 picker 授权的任意 IPC 绝对路径不能触发 `user-selected` 自动提权。 3. 项目文件列表和索引递归改用 `symlink_metadata()`,拒绝 symlink、junction 和 Windows reparse point。 4. 统一在 metadata/read/open 前完成 ACL 准备,对 ACL 导致的 metadata 失败执行一次受控修复和重试。 5. 新建 conversation、导出包、Agent DB、资源、字体等文件在 harden 失败时关闭句柄并清理刚创建的文件。 6. 修复 UAC 提权授权票据以 `share_mode(0)` 独占打开后与 `GetNamedSecurityInfoW/SetNamedSecurityInfoW` 产生共享冲突的问题。 7. 同步 AGC Windows ACL 技术方案与安全回归测试。 ### 安全行为 - UAC 取消返回 `1223`,不会被当作成功。 - 提权子进程重新校验目标路径、scope、当前用户 SID、nonce、有效期和票据一次性消费。 - symlink、junction、reparse point 和非普通文件对象保持失败关闭。 - 自定义配置目录可以位于用户 profile 外部,但仍受 owner、DACL 和路径类型校验。 ### 验证结果 已执行并通过: ``` cargo check --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml private_path_elevation_policy_tests -- --nocapture # 10 passed cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml local_project -- --nocapture # 20 passed cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml checkpoint::security_tests -- --nocapture # 10 passed cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml private_file_write_tests -- --nocapture # 2 passed npm run check:encoding # 5554 files passed git diff --check # passed ``` 另外确认: - `origin/master` 是当前分支祖先。 - 交接文档已删除,且原本不在 Git 跟踪列表中。 - 全量 `cargo test` 未作为通过依据;此前存在与 ACL 无关的失败项。 - `cargo fmt --check` 仍受仓库已有格式漂移影响,本次未执行全仓库格式化。 - 真实 Windows UAC 点击“Yes”的手工回归仍需在目标机器上确认。 ### 手工验收 使用: ```powershell & "C:\Users\dongy\workspace\Genarrative\.worktrees\agc-windows-acl-fix\apps\ai-game-creator-shell\src-tauri\target\debug\genarrative-ai-game-creator-shell.exe" ` --config-dir "C:\Users\dongy\AppData\Local\Temp\agc-acl-live-20260830\deny-config-2" ``` 验收标准: 1. 选择“否”:进程返回 `exit code 1223`。 2. 重新启动并选择“Yes”:不再返回 `exit code 1`,ACL 修复完成后继续启动。 3. 再次启动同一配置目录:不应重复弹出修复。 4. 检查目录 owner 和 DACL,确认归当前用户所有且为私有权限。 --------- Co-authored-by: 段舒康 <kdletters@qq.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/211 Co-authored-by: 董羽秦 <suzmii@qq.com> Co-committed-by: 董羽秦 <suzmii@qq.com>
This commit was merged in pull request #211.
This commit is contained in:
@@ -169,6 +169,7 @@ Supervisor 认领该回执后,由父 run 自己为每个原 delivery 逐一创
|
||||
|
||||
- Windows AppData 安全迁移:首次创建客户端 AppData 时必须以进程 `TokenUser` SID 显式设置 owner,并写入当前用户私有 DACL,不能把可能为 Administrators 的 `TokenOwner` 当作用户身份。发现历史目录 owner 不属于当前 `TokenUser` 时,不在原目录上放宽权限,而是拒绝 reparse point / junction / symlink 后,将旧目录原子重命名到同级唯一 `.owner-mismatch-backup-*` 备份,再新建并验证当前用户 owner 与私有 DACL;迁移或备份失败必须失败关闭,不覆盖旧配置。
|
||||
- Windows 私有文件初始化:父目录已归当前 `TokenUser` 后,新建 `.agent/.manifest.json.lock`、`agent-runner.lock`、endpoint 临时文件、project-owner 诊断临时文件与 real-E2E 私有文件的 owner 仍可能采用 token 默认 owner `Administrators`。manifest 固定锁和 Runner 固定 stale lock 只有在 Windows 不共享独占句柄已取得、且句柄确认普通文件、非 reparse point、链接数为一时才允许初始化或修复为当前 `TokenUser`,随后必须再次复核句柄并按既有 owner/DACL 门禁验证;其它临时文件只允许在本进程 `create_new` 成功且仍持有同一独占句柄时初始化 `TokenUser` owner / DACL,再写入、原子安装并严格复核,初始化失败必须清理刚创建的文件。既有 durable endpoint / diagnostic 读取不得自动接管;活锁不得截断,只有 sharing / lock violation `32/33` 表示占用,access denied 等其它错误立即返回。父进程观察到 Runner 子进程退出后立即返回错误,不等待完整 30 秒 deadline。
|
||||
- Windows ACL 提权边界:自定义 `--config-dir` 的启动前置检查必须把 `managed / user-selected` scope 一并传入提权子进程,不能依赖父进程内存中的配置目录覆盖;native picker 返回的文件或项目目录在同一进程登记短时授权,后续导入 / 项目操作只对登记路径(目录可覆盖其后代)允许 `user-selected` 自动提权,直接伪造 IPC 绝对路径不得获得该能力。项目文件列表 / 索引递归逐项拒绝 symlink 与 Windows reparse point,并在 metadata / read 前先完成 ACL 准备。
|
||||
- 启动恢复和续跑边界:本条取代上一条中“只有 accepted 才可恢复”的窄口径。若进程在 Supervisor 用户消息已持久、accepted 未持久之间崩溃,只读 preflight 可以把该 `preparing` 识别为可恢复,但不改写 task/conversation;真实 resume 持有 Agent 锁后必须先幂等补写 accepted,再提升为 `pending / queued`。用户消息或 accepted conversation 已落盘而辅助审计失败时,以 conversation 为公开真相继续入队,不留下“已接收但永不执行”的任务;根终态首次公开写入的瞬时失败必须在终态投影后用相同 message ID 重试。receipt / isolated-join 等带 parent 的 Supervisor continuation 不再另写 Session 终态,只保留单一后端公开事件;`runtime-task-*` 与 `runtime-public-status-*` 共享同 run 的不透明关联摘要,秒级时间戳下多个连续任务必须按实际 run 对应的 `user -> accepted -> terminal` 顺序交错展示。
|
||||
- ready-task 启动活性:`background_task.queued`、`autonomous_ready_task.scheduled`、Runner heartbeat 或执行锁已移交都不等于 child 已启动。实际持有执行权的 Runner 必须在释放项目写锁后同步写入 child 的 running task、`turn.started` 与 started journal,再把已启动 state 和 per-Agent 执行锁交给已确认开始轮询的独立 execution worker;同步启动或 worker 接管失败时,要在仍持有执行锁期间依次把 child 和 manifest Graph 节点明确落为 failed,再释放锁并让 parent 收到调度错误。`autonomous_ready_task.scheduled` 只作诊断审计,其写入失败不能阻断 durable child 启动;external client 只 wake Runner,不在客户端抢占执行。Supervisor 进度卡通过 durable `startedAt`(旧 Run 从完整 task journal 恢复,最新 task-record fallback 保持 0)显示真实持续时间,并以父 Run 与当前关联专业 Agent 的最大事件时间计算运行态活跃度:运行超过 5 分钟无新事件时显示“运行中 · 疑似停滞”和静默时长;等待用户、等待确认、Provider retry、视觉资产、进程会话、pausing 与 paused 不误报。父 Run terminal 后,持续时间冻结在父 Run 自身最后活动,不随 child 晚到收口事件增长。消息时间统一校验为 JavaScript 可表示的 Date;越界值显示“时间未知”且不写无效 `datetime`。实时回复只显示 response stream 自己的 `updatedAt`,缺失时同样显示“时间未知”,不能借用其它 Runtime 活动时间或随前端时钟漂移。该提示只提供可观测性,不改变 Runtime/manifest 正式状态。
|
||||
- ready-task 对账取消续跑:未知工具结果仍停在 `needs-reconciliation` 且禁止自动重放;人工核对后显式取消原 child,保留 cancel tombstone,旧 child 和旧父 Run 按真实终态收口。若随后创建同 Session、同 Supervisor source、同有效任务语义的 continuation,新完成合同只对同时具有历史 `failed / needs-reconciliation`、最终 `cancelled` 和 durable tombstone 的 ready-task,把当前 manifest 对应 failed 节点恢复为 pending,并由 scheduler 创建全新 child Run。manifest 的读取、failed 筛选、每任务一次的 child journal 索引、证据重验和写回必须位于同一项目写锁域;较新的无 child 根 Run 只有在 durable journal 精确表明为旧 failed Graph 在进入调度前即失败时才能跨过,scheduler 自身失败必须阻断借用更老 tombstone。普通失败、无 tombstone、不同 source/Session/任务语义或证据冲突均保持失败关闭;不得复活旧 pending action、补造 observation 或把取消任务标成 completed。
|
||||
|
||||
Reference in New Issue
Block a user