From 9d450eca7c963e6552c7a04b3bb23756f251c326 Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:11:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=9D=E7=95=99=E9=A1=B9=E7=9B=AE=E4=B8=8A?= =?UTF-8?q?=E4=B8=8B=E6=96=87=E8=AF=BB=E5=8F=96=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 保留文件打开读取和验证指纹 OS 正文 补充工具桥与证据错误收口约定 --- .../src/agent/direct_project_context.rs | 45 ++++++++++--------- ...GC命令错误结构化与错误报告口径-2026-10-01.md | 1 + ...€�里程碑】AGC错误具体文本展示-2026-10-04.md | 1 + 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_context.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_context.rs index cf06f63b0..939dfb184 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_context.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_context.rs @@ -237,50 +237,53 @@ fn normalize_requests(root: &Path, arguments: &Value) -> Result Ok(files) } -fn bounded_bytes(root: &Path, raw: &str) -> Result, &'static str> { - let target = resolve_game_agent_read_path(root, raw).map_err(|_| "unsafe-project-path")?; +fn bounded_bytes(root: &Path, raw: &str) -> Result, String> { + let target = + resolve_game_agent_read_path(root, raw).map_err(|_| "unsafe-project-path".to_string())?; let path = if let Some(relative) = target.project_relative.as_deref() { if relative.is_empty() { - return Err("not-safe-regular-file"); + return Err("not-safe-regular-file".to_string()); } - reject_agent_runtime_private_control_path(relative).map_err(|_| "private-control-path")?; - reject_sensitive_project_file_read(relative).map_err(|_| "sensitive-path")?; + reject_agent_runtime_private_control_path(relative) + .map_err(|_| "private-control-path".to_string())?; + reject_sensitive_project_file_read(relative).map_err(|_| "sensitive-path".to_string())?; if should_skip_project_snapshot_path(relative) { - return Err("excluded-project-path"); + return Err("excluded-project-path".to_string()); } // 检查每段目录,避免父目录 junction/symlink 绕过叶子 O_NOFOLLOW。 let mut component = root.to_path_buf(); for segment in relative.split('/') { component.push(segment); - let metadata = std::fs::symlink_metadata(&component).map_err(|_| "file-not-found")?; + let metadata = std::fs::symlink_metadata(&component) + .map_err(|error| format!("file-not-found:{error}"))?; if metadata.file_type().is_symlink() || windows_metadata_is_reparse_point(&metadata) { - return Err("linked-path"); + return Err("linked-path".to_string()); } } component } else { if external_read_is_protected(&target.display) { - return Err("sensitive-path"); + return Err("sensitive-path".to_string()); } reject_external_read_links(&target.absolute)?; target.absolute }; let (file, metadata) = open_project_snapshot_regular_file(&path, "项目批量读取") - .map_err(|_| "not-safe-regular-file")?; + .map_err(|error| format!("not-safe-regular-file:{error}"))?; if metadata.len() > MAX_FILE_BYTES as u64 { - return Err("file-too-large"); + return Err("file-too-large".to_string()); } read_bounded(file) } -fn read_bounded(reader: impl Read) -> Result, &'static str> { +fn read_bounded(reader: impl Read) -> Result, String> { let mut bytes = Vec::new(); reader .take(MAX_FILE_BYTES as u64 + 1) .read_to_end(&mut bytes) - .map_err(|_| "file-read-failed")?; + .map_err(|error| format!("file-read-failed:{error}"))?; if bytes.len() > MAX_FILE_BYTES { - return Err("file-grew-over-limit"); + return Err("file-grew-over-limit".to_string()); } Ok(bytes) } @@ -320,7 +323,7 @@ struct ReadResult { } fn read_file(root: &Path, input: &FileRequest, item_budget: usize) -> ReadResult { - let error = |code| ReadResult { + let error = |code: String| ReadResult { value: json!({"path":input.path,"status":"error","code":code}), source_hash: None, }; @@ -331,16 +334,16 @@ fn read_file(root: &Path, input: &FileRequest, item_budget: usize) -> ReadResult let source_hash = sha256(&bytes); let text = match std::str::from_utf8(&bytes) { Ok(text) if !text.contains('\0') => text, - _ => return error("not-utf8-text"), + _ => return error("not-utf8-text".to_string()), }; let lines: Vec<_> = text.split_inclusive('\n').collect(); let safe_text = safe_source_text(root, text); let safe_lines: Vec<_> = safe_text.split_inclusive('\n').collect(); if safe_lines.len() != lines.len() { - return error("redaction-line-boundary-error"); + return error("redaction-line-boundary-error".to_string()); } if input.start_line > lines.len().saturating_add(1) { - return error("line-out-of-range"); + return error("line-out-of-range".to_string()); } let start = input.start_line - 1; let requested_count = input.max_lines.min(lines.len().saturating_sub(start)); @@ -361,7 +364,7 @@ fn read_file(root: &Path, input: &FileRequest, item_budget: usize) -> ReadResult value, source_hash: Some(source_hash), }, - None => error("line-exceeds-response-budget"), + None => error("line-exceeds-response-budget".to_string()), } } @@ -571,7 +574,7 @@ mod tests { let mut reader = std::io::Cursor::new(vec![b'a'; MAX_FILE_BYTES * 2]); assert_eq!( read_bounded(&mut reader).unwrap_err(), - "file-grew-over-limit" + "file-grew-over-limit".to_string() ); assert_eq!(reader.position(), MAX_FILE_BYTES as u64 + 1); } @@ -595,7 +598,7 @@ mod tests { ] { assert_eq!( bounded_bytes(&root, &path.to_string_lossy()), - Err("linked-path") + Err("linked-path".to_string()) ); } std::fs::write(base.join("ordinary.txt"), "ordinary").unwrap(); diff --git a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md index e066ee91c..d30d636a6 100644 --- a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md +++ b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md @@ -118,6 +118,7 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接 - 浏览器健康检查的发现、临时目录、配置、DevTools 版本请求和清理失败保留底层错误/超时阶段;稳定浏览器码用于分类,但不能覆盖诊断正文。 - WebView 普通错误出口遇到 Tauri 的结构化对象时,优先展示 `message/detail/reason/code`,再有界序列化对象;不得把对象转成 `[object Object]` 后回落“未提供具体错误正文”。 - Direct 工具桥图片读取和真实试玩验证证据的 OS 读取/哈希错误保留底层原因;路径越界、私有路径和安全拒绝仍使用稳定安全码。 +- `agc_read_project_context` 的安全拒绝继续只给稳定码,但受控文件打开/读取/复核的 OS 错误要附在错误码后,不能把 `file-read-failed` 当成完整原因。 - 浏览器启动/DevTools 握手与 Codex model-catalog 子进程失败保留 stderr、退出状态、解析错误和阶段;稳定机器码仍用于分类,但不能单独成为用户可见正文。 - Node/npm 环境探测和 Web scaffold 构建持续排空并保存有界 stdout/stderr 尾部,失败返回退出状态、超时阶段和脱敏正文;环境预检回执同时下发 `code` 与 `diagnostic`。 - HTTP 409 只有明确包含泥点不足事实时才映射为 `paidCreditsInsufficient`;Claude Code 的普通 409 冲突保留为 `upstreamFailed`。 diff --git a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md index df5ddfe51..2df3632d6 100644 --- a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md +++ b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md @@ -21,6 +21,7 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时, - 浏览器健康检查不再把发现、配置、DevTools 版本和清理失败只返回机器码;阶段码保留,同时带底层错误或明确的超时预算。 - WebView 结构化对象错误保留 `message/detail/reason/code` 或有界 JSON 正文,避免 `[object Object]` 触发普通错误兜底。 - Direct 工具桥图片与真实试玩证据读取/哈希错误保留 OS 正文,安全边界拒绝继续使用稳定码。 +- `agc_read_project_context` 保留 `file-read-failed` 等稳定码,同时在能取得时附带文件打开/读取/复核的 OS 正文。 - Transport / Stream / IPC / host process / memory exhaustion 的回归测试与错误事件证据。 ## 不做