保留项目上下文读取错误

保留文件打开读取和验证指纹 OS 正文

补充工具桥与证据错误收口约定
This commit is contained in:
kdletters
2026-10-05 19:11:37 +08:00
parent f2946aaeca
commit 9d450eca7c
3 changed files with 26 additions and 21 deletions
@@ -237,50 +237,53 @@ fn normalize_requests(root: &Path, arguments: &Value) -> Result<Vec<FileRequest>
Ok(files)
}
fn bounded_bytes(root: &Path, raw: &str) -> Result<Vec<u8>, &'static str> {
let target = resolve_game_agent_read_path(root, raw).map_err(|_| "unsafe-project-path")?;
fn bounded_bytes(root: &Path, raw: &str) -> Result<Vec<u8>, 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<Vec<u8>, &'static str> {
fn read_bounded(reader: impl Read) -> Result<Vec<u8>, 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();
@@ -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`。
@@ -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 的回归测试与错误事件证据。
## 不做