Skip to content

feat(tools): 文件工具强化——描述规范化 / 读取上限 / 编辑引擎重构 / 工作区防护 / 大模型审查 / 环境矩阵 / AI 读取提炼 - #1496

Open
542474846 wants to merge 14 commits into
AAswordman:devfrom
542474846:feat/tool-hardening

Conversation

@542474846

Copy link
Copy Markdown

变更说明 / Description

背景与动机 / Context and motivation

静态审计确认的文件工具失败机制:提示词与实现不对齐(32KB 截断未文档化、read_file 输出的行号前缀污染 edit 匹配、environment 支持矩阵失实);edit_file/apply_file 的模糊匹配没有精确快路径,0.9 相似度的多候选静默取最优,报错还指向不存在的工具名;read_file_binary 的内容根本到不了模型;工作区零防护,任意绝对路径直接执行且递归删除没有危险路径底线;工具调用没有审查机制。失败率居高导致模型绕开文件工具改用终端脚本改文件。

改动范围 / Changes

八个阶段,落在一个分支,计划与逐阶段实施记录见 docs/TODO/chat_file_tool_hardening/:

  • A 描述规范化:可见与内部文件工具补 details(environment 真实矩阵、成功返回、失败类别与修法、工具分工),修正幽灵工具名与误导文案
  • B 读取上限与健壮性:read 族 32MB 硬上限并引导 read_file_part 分段、read_file_part 改单次流式扫描、isTextLike 二进制门禁、截断判定修复、read_file_binary 内容直达模型
  • C 编辑引擎重构:精确匹配快路径、近似多候选按行区间拒绝并列出候选行号与相似度(绝不静默选第一处)、剥离 read_file 行号前缀、新增 occurrence 参数、成功回传匹配行号、失败给出最高相似度候选引导精读;删除 [START-REPLACE] 死代码链
  • D 工作区防护:区内按既有权限模式;区外 ALLOW/ASK 强制弹窗列出目标路径,FORBID 直接拒绝;判定先于 deny_tool 标记,不可被模型文本绕过;未绑定工作区一律按区外;扩展包限定名同族纳管;delete_file 对 /、/sdcard、/data 等危险根硬拦截;JS 包嵌套调用经 AIToolHook 兜底
  • E 大模型审查:FunctionType.TOOL_REVIEW 功能模型 + 审查 prompt(工具名、操作类别、目标路径、区内外、内容摘要、call_purpose 调用说明)+ ToolReviewService(15s 超时);合规直接执行、不合规弹窗由用户决定、审查失败弹窗提示错误
  • F 环境矩阵与扩展通道:repo: 环境 clean guard、applyFile 按环境路由(linux 支持 ~)、edit_file occurrence 透传、grep_code 在 linux 下经 PathMapper 映射到宿主 rootfs 扫描并反向映射、SAF 白名单报错列全扩展名、禁用包报错给出真实原因
  • G 高危工具审查泛化:HighRiskToolCatalog 纳管 18 个高危工具族,审查开关扩展为三分区(区内文件 / 区外文件 / 高危工具),call_purpose 由执行层剥离
  • H AI 读取提炼:read 族默认经 FILE_DISTILL 功能模型返回带行号的相关摘录(part 与大文件逐块扫描,worth=false 或摘录占比超 0.6 回原文);开关关、raw=true、repo: 环境、特殊类型抽取管线、≤2048 字节一律透传;失败显式报错指引 raw=true 重读
  • 构建修复:补全 avator/dragonbones/consumer-rules.pro 占位,模块 build.gradle.kts 的 consumerProguardFiles 引用此前指向不存在的文件

不包含:聊天列表滚动实现调整(独立改动)、token 统计改动。

兼容性与风险 / Compatibility and risks

  • 行为变更,均为已确认决策:区外与未绑定工作区的文件操作一律强制弹窗或直接拒绝;审查不合规由用户决定是否放行;审查失败弹窗并显示错误信息
  • 新开关默认值:file_distill_enabled 默认开,审查三开关默认关。开启审查或 AI 提炼会对功能模型产生额外调用与 token 消耗,需用户在功能配置中指定 FILE_DISTILL / TOOL_REVIEW 模型,未配置时回落主模型
  • 工具 schema 变更:edit_file/apply_file 新增 occurrence 参数,copy_file 的 recursive 默认 true→false 与文档对齐
  • 同步更新 8 个语言目录 strings、assets/packages/extended_file_tools.js、examples/types/files.d.ts
  • 无 Room 迁移、无数据库结构与导出格式变化

关联 Issue / Related issue

N/A。改动来源为本地静态审计,背景与实施记录见 docs/TODO/chat_file_tool_hardening/index.md。

验证方式 / Verification

检查或命令:./gradlew :app:compileDebugKotlin
            ./gradlew :app:testDebugUnitTest --tests "*FileBindingServiceTest" --tests "*WorkspacePathCheckerTest"
环境与变体:Android debug 变体
结果:`:app:compileDebugKotlin` 通过(exit 0)。环境:JAVA_HOME 指向 Android Studio JBR,Gradle 8.13 / AGP 8.13.2,debug 变体;排除 `:app:syncSttModelAssets`(该任务每次强制重新下载 STT 模型,本机到下载源不可达会以 ConnectException 中断,与 Kotlin 编译无关)。单元测试未在本机执行,交由 CI。

手工回归矩阵(记录于 TODO index,结果待填):区内/区外/无工作区 × 允许/询问/禁止 × 审查开/关 × 合规/不合规/失败;高危工具 execute_shell × 高危开关独立生效;32MB 大文件读取、read_file_part 分页、edit_file 行号粘贴/多候选/occurrence、扩展包 file_exists、禁用包报错、repo: guard。

检查清单 / Checklist

  • 我已记录可复现验证和未运行项原因 / Reproducible verification and reasons for unrun checks are recorded
  • 日常开发 PR 的目标分支为 dev;如目标为 main,我已说明这是维护者发布同步 / The target branch is dev for regular work; if it is main, I explained why this is a maintainer-led release sync
  • 我已确认 Candidate checks 覆盖改动范围,并会处理技术失败项 / Candidate checks covers the change scope and technical failures will be addressed
  • 最终 diff 无无关、临时、生成、二进制或敏感内容 / Final diff has no unrelated, temporary, generated, binary, or secret content
  • 已提供对应的回归、UI、文档/字符串或兼容性证据 / Relevant regression, UI, docs/strings, or compatibility evidence is provided

cnb.ccXJz9ZBAFA added 12 commits November 13, 2026 12:27
- 可见/内部文件工具全部补充 details:environment 真实支持矩阵、成功时返回、失败时返回(类别+修法)、工具分工约定
- 修正:幽灵工具名 readFileFull→read_file_binary;read_file_part 默认窗口文档 100→200(含 files.d.ts)
- write_file 写明默认覆盖与自动建父目录;整文件重写配方统一 delete_file→write_file
- create_file/edit_file 描述自含化,不再引用不可见的 apply_file
- copy_file recursive 缺省 true→false 与文档/JS 对齐,目录复制漏参明确报错
- extended_file_tools.js:修复 file_exists 把"不存在"误报为失败;environment 描述对齐最终矩阵
- apply_file 描述补充匹配语义/occurrence 参数/结果格式(实现随后续阶段落地)
- ToolExecutionLimits 新增 MAX_FILE_READ_SIZE_BYTES=32MB;read_file/read_file_full/read_file_binary 在 android/linux/saf 三环境实现入口统一检查,超限失败并引导 read_file_part 分段读取
- readFilePart 改单次流式扫描(去 countFileLines+readLinesFromFile 双次全量 IO),end_line 缺省读到预算为止;start_line 超界明确报错并告知总行数;补 isTextLike 门禁(二进制不再乱码报成功)
- read_file 截断判定修复:字节数与字符预算误比 → 实际多读一字符判定;多字节编码下不再误标截断
- 特殊文件(图片 OCR)解码失败/异常从 success=true+错误文本当内容 改为显式 success=false
- read_file_binary:base64 进入模型可见结果(修复 toString 只回摘要导致工具实际不可用);超单条消息预算显式拒绝并给替代路径
- FilePartContentData 总行数未知时明确提示续读方式;FileApplyResultData 无效转义链清理
- FileBindingService 整体重构:
  - 精确匹配快速路径:归一化 indexOf 唯一命中直接返回,跳过滑动窗模糊扫描
  - 多候选拒绝:模糊搜索收集全部 >0.9 窗口并按行区间重叠聚类为独立位置,≥2 处时拒绝并列出各候选行号+相似度;未指定 occurrence 绝不静默选第一处(旧实现仅拒绝完美重复,近似重复会被静默改写)
  - occurrence 参数:old 命中多处时按第 N 处定位;edit_file/apply_file 同步透传
  - old 入口剥离 read_file 输出的 N| 行号前缀(此前污染 n-gram 与精确匹配)
  - 失败可诊断:无匹配时报最高相似度候选行号并引导 read_file_part 精读重试;明确提示不要退化为 shell 编辑
  - 成功结果回传各 op 匹配行号(Applied at: lines X-Y)至 details 与 FileApplyResultData.matchedRanges
- 删除死代码:[START-REPLACE] 块解析链(processFileBinding/applyFuzzyPatch/parseEditOperations/EDIT_BLOCK_REGEX)、hasMultiplePerfectMatches、未用常量与扩展函数
- FileApplyResultData 无效恒等转义清理
- 新增 FileBindingServiceTest:精确/行号剥离/多候选拒绝/occurrence/模糊长块/无匹配诊断/delete 八个用例
- 新增 WorkspacePathChecker(纯函数判定器 + 24 个受护文件工具族清单 + 危险根目录清单)
- 会话未绑定工作区 → 所有路径按区外(需求确认项);android 路径 canonical 前缀比较;linux/repo 环境按会话绑定环境一致判定
- ToolExecutionManager.checkToolPermission 插入工作区阶段:区内走既有权限模式;区外 ALLOW/ASK 强制弹窗确认(原因列出目标路径)、FORBID 直接拒绝;判定先于 deny_tool 标记,不可被模型侧文本绕过;扩展包限定名(pkg:copy_file)同族纳管
- 执行前按 chatId 查 ChatHistory.workspace/workspaceEnv 为准(ChatHistoryManager.getChatWorkspaceBinding)
- 区外确认过的路径记录进 ToolRuntimeContext.approvedOutsidePaths,嵌套重入不误拦
- 新增 WorkspaceFileToolHook(AIToolHook 兜底):JS 包 toolCall → executeTool 的嵌套调用在 ThreadLocal 作用域内判定,区外拒绝并回传理由;主链路在 ThreadLocal 设置前判空放行,与弹窗阶段互不重叠
- delete_file:recursive 递归删除 /、/storage/emulated/0、/sdcard、/data 等 protected roots 在 android/linux 双环境硬拦截
- ToolPermissionSystem.requestConfirmation(reason):无视允许模式强制弹窗的入口,reason 附于操作描述展示
- 新增 WorkspacePathCheckerTest 九个用例(含无工作区、兄弟前缀、多路径、限定名、危险根)
- FunctionType 新增 TOOL_REVIEW;测试更新(11→12);8 个 locale 补显示名/描述;功能模型配置卡片按枚举自动生成,缺省回落主模型
- ApiPreferences 新增 tool_review_inside/outside_workspace 开关键 + Flow + 保存方法;ApiConfigDelegate/ChatViewModel 透出
- FunctionalPrompts.toolReviewPrompt:工具名/操作类别/目标路径/区内外/工作区根/内容摘要(500字) → 严格 JSON {compliant, reason}
- 新增 ToolReviewService:callFunctionModel + withTimeout(15s) + 宽松解析;审查失败语义统一为弹窗人工确认(需求确认项)
- checkToolPermission 审查流(仅文件工具族 + 允许模式 + 对应区开关开):
  - 合规 → 直接执行不弹窗(区外同样放行,需求确认项)
  - 不合规 → 弹窗展示审查理由由用户决定
  - 审查失败/超时 → 弹窗提示错误信息由用户决定
- UI:Agent/Classic 两种输入栏的工具组新增区内/区外审查开关行,仅工具模式为允许时显示
- 新增字符串:审查弹窗两条 + 菜单开关两条
- repo: 环境 clean guard:zip_files/unzip_files/open_file/share_file/apply_file 入口明确拒绝(旧实现静默落 Android FS 报'不存在')
- applyFile 路径校验按环境路由:linux 支持 ~ 前缀(旧实现统一走 validateAndroidPath,~/x 必失败);linux 结构化编辑经既有子调用路由(fileExists/readFileFull/writeFile 均按 environment 委托)可用
- edit_file 补 occurrence 参数透传与 schema(EN/CN),与阶段 A 描述一致
- grep_code(linux 环境):native ripgrep 前经 PathMapper 映射到宿主 rootfs 真实路径扫描,结果路径反向映射回 linux 逻辑路径;PathMapper 新增 mapAndroidPathToLinux
- SAF 写白名单报错列出全部支持扩展名与知名文件名,模型可自纠
- 禁用包调用错误修正:捕获 usePackage 返回的真实原因(如'包未启用'),不再笼统报 Auto-activation failed
- FunctionalConfigScreen 连接测试 when(functionType) 为非穷尽表达式(无 else),新增 TOOL_REVIEW 枚举值后必编译失败 → 补连接测试分支(与 CHAT 同款简单问答)
- ToolReviewService 与 EnhancedAIService 不同包,补缺失的 import
- SystemToolPrompts/Internal 27 处 details 文本中的 [\u0024env] 被 Kotlin 字符串模板当变量解析(Unresolved 'env')→ 统一改为 [android] 具体示例
- AIChatScreen 报 No parameter 'toolReviewInsideWorkspace':参数误插进 AgentExtraSettingsPopup 签名而 AgentChatInputSection 缺失 → 在 AgentChatInputSection(:163) 签名补四参数(带默认值)并向下透传至 AgentExtraSettingsPopup,popup 内既有传参接续生效
- 新增 HighRiskToolCatalog:18 个高危工具族(命令/代码执行、Intent/广播、应用与系统管理、网络外发、任意工具代理入口、工作流编排),扩展包限定名同族纳管
- 新增 call_purpose 调用说明声明参数:可审查工具调用时可附'为什么发起本次调用',供审查模型与调用点上下文一致性比对;执行前由 ToolExecutionManager 剥离
- ToolReviewService 泛化:审查范围从文件工具族扩展到文件工具+高危工具;审查 prompt 注入调用点前上下文(最近 10 条大模型消息)、最近一条用户指令、call_purpose
- 审查开关扩展为三分区(工作区内/工作区外/高危工具),ApiPreferences/ApiConfigDelegate/ChatViewModel/UI(Agent+Classic 双栏)同步透传
- FunctionalPrompts 审查 prompt 与 SystemToolPrompts 参数文档同步;TODO 目录新增 07-高危工具审查.md
(另一会话的实现,如实提交)
read_file/read_file_part/read_file_full 默认经 FunctionType.FILE_DISTILL
功能模型提炼:单次协议一次输出 related/worth/带行号摘录,part 与大文件
full 走逐块扫描(24K 字符/块、8 轮上限);worth=false 或摘录占比超 0.6
返回原文。透传边界(行为与现状一致):开关关/raw=true/repo:·linux 环境/
特殊类型抽取管线(图片 PDF 等)/≤2048 字节。失败一律显式报错指引
raw=true 重读,无回退。

- 新增 FileDistillService 拦截层(ToolRegistration executor 包装,5 权限子类零改动)
- ApiPreferences 开关 file_distill_enabled 默认开,UI 置于会话工具菜单审查三开关之下(Agent/Classic 两链路)
- SystemToolPrompts 补 raw/focus 参数与 AI 提炼说明;part 注明不确认行号时用默认 AI 读取
- 计划与实施记录:docs/TODO/chat_file_tool_hardening/08-AI读取提炼.md
@CATMIAOZHI
CATMIAOZHI self-requested a review November 13, 2026 07:35

@CATMIAOZHI CATMIAOZHI left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审计结论:❌ 请求修改

改动(52 个文件,+4151/−582):文件工具八阶段强化:描述规范化、读取 32MB 上限与流式分段、edit/apply 引擎重构(精确快路径 + 多候选拒绝 + occurrence)、工作区防护与危险根删除底线、大模型合规审查(TOOL_REVIEW)、环境矩阵与扩展通道、高危工具审查泛化、read 族 AI 提炼(FILE_DISTILL,默认开)。

通过的检查:

  • 目标分支 dev,非草稿,非自审(作者 542474846);PR 描述与 diff 基本一致(52 文件与 API 计数一致;早前一次 stale origin/dev 导致的 terminal 子模块差异已核实不在本 PR 内)
  • 工作区判定:android 路径用 canonicalPath 前缀比较(/data/ws 与 /data/ws2 不会混淆);未绑定工作区一律按区外;判定先于 deny_tool 标记
  • 审查失败 fail-closed:超时/解析失败/未配置均走弹窗人工确认,不静默放行;未配置功能模型时回落主模型(FunctionalConfigManager defaultMapping 覆盖全部 FunctionType)
  • call_purpose 仅供审查读取、执行前剥离;Hook 注册幂等,主链路执行走直接 executor、不重跑拦截,与嵌套兜底不重叠
  • 读取侧:32MB 硬上限覆盖 read_file/read_file_full/read_file_binary 及 SAF/Linux 路径;read_file_part 单次流式扫描;截断判定改为"读满且还有下一字符";二进制门禁与 base64 预算;特殊类型抽取管线透传
  • 旧 [START-REPLACE] 死代码链删除干净,无残留调用方;ToolRuntimeContext 由 data class 改为普通类,无 copy/判等使用方
  • copy_file recursive 默认 true→false 与文档对齐(行为变更已在 PR 描述中声明)

问题:

  1. [P1] 危险根递归删除硬底线可被路径拼写绕过:WorkspacePathChecker.isDangerousRecursiveDeleteTarget 只做 trim + 去尾部 / 后的精确字符串匹配,不做 canonicalize;两个调用点(StandardFileSystemTools.deleteFile、LinuxFileSystemTools.deleteFile)都直接传模型原始 path。/sdcard/.、/sdcard/..、//sdcard、/storage/emulated/0/. 均不在集合内,但指向同一目录,deleteRecursively() 会真的清空。测试也只覆盖精确拼写。建议:比较前先取 File(path).canonicalPath(不存在的路径做规范化处理),并把 /.、.. 这类变体补进单测。
  2. [P1] linux 环境 grep 的 .. 可逃出 proot rootfs:LinuxFileSystemTools.grepCode 把模型传的 linux 路径经 PathMapper.resolvePath → mapLinuxPath(File(ubuntuRoot, relativePath).absolutePath,不 canonicalize、不做 containment 校验)交给宿主 native ripgrep;validateLinuxPath 只校验以 //~ 开头,不拦 ..。grep_code(path=/../../../../data/..., environment=linux) 可扫描宿主文件系统;且工作区判定把"环境一致"视为区内,不会弹窗。建议:映射后 canonicalize 并校验仍在 ubuntuRoot 内,否则拒绝;同类问题也存在于 zip/unzip 既有用 resolvePath 的路径上,建议一并收敛到 mapLinuxPath 内。
  3. [P1] FunctionTypeTest 与新枚举不一致,测试必失败:枚举现有 13 个值(新增 TOOL_REVIEW、FILE_DISTILL),测试断言总数 12 且 expected 列表漏了 FILE_DISTILL。作者自述单元测试未在本地执行、交由 CI,CI 会红。建议:总数改为 13 并补 FILE_DISTILL,本地跑一次 :app:testDebugUnitTest。
  4. [P2] occurrence 的 schema default 与实现矛盾:edit_file/apply_file 的 ToolParameterSchema 写 default = "1"(中英文两处),而实现把"未传 occurrence"当作 null、多候选一律拒绝;JSON-schema 的 default 不会被 API 自动注入,实际行为是拒绝,与 details 里"命中多处会被拒绝"的说明直接冲突。建议:去掉 default="1",说明统一为"未传则拒绝"。
  5. [P2] 嵌套调用的高危工具不受 Hook 约束:WorkspaceFileToolHook 只拦截文件工具族;JS 扩展包经 toolCall → AIToolHandler.executeTool 嵌套调用 execute_shell/http_request 等高危工具时,Hook 直接放行,也不经过主链路的审查/弹窗(主链路只审了外层包工具)。建议:说明该缺口在设计预期内,或把高危工具族也纳入 Hook 的拒绝范围。
  6. [P2] read_file_part 在 end_line < start_line 时静默返回空成功:requestedCount 被 coerceAtLeast(0),0 行时返回 success 空内容;旧实现会把 end_line 钳到有效范围返回实际内容。模型传错范围时得不到纠错信号。建议:非法范围改为显式失败并提示有效范围。
  7. [P3] i18n 不同步:PR 描述称"同步更新 8 个语言目录 strings",但 8 条新审查/工作区文案只存在于默认 values/,values-en 只有 4 条 function_type/file_distill 文案,其余 6 个语言目录各 2 条;非中文语言下弹窗与菜单会回退显示中文(不崩溃)。建议补齐各语言翻译或至少英文。
  8. [P3] 模糊匹配聚类的传递闭包可能合并相邻重复块:clusterOverlappingWindows 按行区间重叠做传递闭包聚类,相邻的近似重复块可被一串重叠窗口桥接成一个簇、只保留最优窗。精确重复由精确路径兜底(列出全部位置并拒绝),残余风险限于"非精确的近似重复且相邻"的情形。
  9. [P3] linux grep 反向映射失败时回落返回宿主原始路径:mapAndroidPathToLinux 返回 null 时调用方保留 fileMatch 原路径(宿主 rootfs 绝对路径),会把宿主内部路径暴露给模型,且模型后续可能拿它当 linux 路径用。建议:映射失败时丢弃该条或标注清楚。
  10. [P3] CDATA 内 diff 若含 ]]> 会破坏 XML:FileApplyResultData 把 diff 原文直接放进 CDATA(旧的"转义"链本就是恒等替换,无回归),含 ]]> 的代码 diff 会提前闭合 CDATA。建议:对 ]]> 做分段处理或转义。

后续可跟进(不阻塞):

  • [question] 手工回归矩阵(区内/区外/无工作区 × 允许/询问/禁止 × 审查开/关 × 合规/不合规/失败等)在 TODO index 中"结果待填",是否计划合入前补齐实测结果?
  • [question] file_distill_enabled 默认开启且 focus 必填:现有角色卡/子任务/自动化流程中不带 focus 的 read 调用会改为显式失败(指引 raw=true),是否确认这是期望的默认行为?

验证说明:作者称 :app:compileDebugKotlin 通过(exit 0),单元测试未在本地执行、交由 CI;手工回归矩阵结果待填。结合问题 3(FunctionTypeTest 计数错误),建议合入前本地跑一次 :app:testDebugUnitTest 并补齐回归矩阵。

由 水晴喵的muse 审计

cnb.ccXJz9ZBAFA added 2 commits November 14, 2026 12:47
…失败、CDATA 转义、i18n 补齐

对应 PR AAswordman#1496 评审(水晴喵的muse 审计)的 P1/P2/P3 修复:

- P1 危险根递归删除可被路径拼写绕过:isDangerousRecursiveDeleteTarget
  在精确匹配前先做词法规范化(./..//重复斜杠,lexicallyNormalizePath 纯函数
  供跨平台单测)再 canonical 化(覆盖符号链接),两道比较任一命中即拒绝;
  补 9 个拼写变体测试与词法规范化测试
- P1 linux grep 的 .. 逃出 proot rootfs:mapLinuxPath 拼接结果 canonical 化
  并强制 containment(逃出返回 null),zip_files/unzip_files/download_file/grep_code
  四个调用点显式拒绝(rootfsEscapeResult),不再回落原始路径;grep 反向映射
  失败的条目丢弃并记日志,不把宿主真实路径回传给模型(P3)
- P1 FunctionTypeTest 与枚举不一致:总数 12→13,补 FILE_DISTILL
- P2 occurrence 的 schema default="1" 与实现矛盾:三处(EN edit_file/CN
  edit_file/Internal apply_file)去掉 default,描述明确"不传即拒绝"
- P2 read_file_part 的 end_line < start_line 静默空成功:android/linux/saf
  三分支委托前统一显式失败并提示重查行号
- P3 FileApplyResultData 的 CDATA 内 ]]> 提前闭合:按 XML 规范拆段转义
- P3 审查/工作区/提炼 8+11 条文案补齐 en/es/id/ko/ms/ro/pt-rBR/ja 全部
  语言目录并通过 XML 校验
完整 :app:testDebugUnitTest 首次真正跑起来后暴露:

- WorkspacePathChecker.isAndroidPathInside:canonicalPath 的分隔符随平台变化
  (Windows 桌面 JVM 为 '\',设备为 '/'),target.startsWith(root + "/") 在
  Windows 上永不匹配——设备行为不变(设备上 canonicalPath 本就用 '/'),
  分隔符归一仅在 File.separatorChar == '\' 时生效
- FileBindingServiceTest 两处失准(该文件此前从未通过编译,八个用例从未真正运行):
  模糊匹配用例的 lambda 用隐式 it 却引用 $i(编译错误);无匹配诊断用例传了
  空 new 的 REPLACE——在校验阶段即被丢弃(No valid edit operations provided),
  走不到无匹配诊断路径,改为合法 replace + 不存在内容
- 危险根拼写变体测试修正:/storage/emulated/0/../storage/emulated/0 词法规范化
  后是 /storage/emulated/storage/emulated/0(.. 只回退一级),本就不该命中;
  换成真正回到受保护根的 /storage/emulated/0/..
- 断言消息带规范化结果便于后续诊断

结果::app:testDebugUnitTest 1666 个用例全部通过
@542474846

Copy link
Copy Markdown
Author

感谢审计,问题均已处理(commit b2c416c9 + ab346982),并按建议在本地跑通了完整单测套件。逐项回复:

P1-1 危险根拼写绕过 — 已修。isDangerousRecursiveDeleteTarget 在比较前先做词法规范化(lexicallyNormalizePath:消解 ./../重复斜杠,纯字符串运算,测试可跨平台断言),再做 canonicalPath(覆盖符号链接),两道比较任一命中即拒绝;调用点无需改动。补充说明:canonicalPath 的分隔符随平台变化(Windows 桌面 JVM 为 \),isAndroidPathInside 的前缀比较同样做了分隔符归一——Android 设备上 canonicalPath 本就用 /,归一是 no-op。测试补了 9 个拼写变体;其中我最初写的 /storage/emulated/0/../storage/emulated/0 是错误用例(.. 只回退一级,该路径本就不在根集合内),已换成真正回到受保护根的 /storage/emulated/0/..。

P1-2 linux grep .. 逃出 rootfs — 已修。mapLinuxPath 拼接结果 canonical 化并强制 containment(逃出 rootfs 返回 null),zip_files / unzip_files / download_file / grep_code 四个调用点显式拒绝(rootfsEscapeResult),不再回落原始路径;resolvePath 签名改为可空以强制调用方处理。已确认 zip/unzip 的既有 resolvePath 用法一并收敛到该实现。

P1-3 FunctionTypeTest — 已修(总数 13,补 FILE_DISTILL)。完整 :app:testDebugUnitTest 已在本地跑通:1666 个用例全部通过。顺带说明:这是该测试源首次真正编译运行——FileBindingServiceTest 此前有两处编译/断言错误(lambda 隐式 it 与 $i 不匹配、无匹配诊断用例传了空 new 的 REPLACE 被校验丢弃),本次一并修正。

P2-4 occurrence schema — 三处(EN/CN edit_file、Internal apply_file)已去掉 default = "1",描述明确"不传 occurrence 会被拒绝,不会默认取第 1 处"。

P2-5 嵌套调用的高危工具不受 Hook 约束 — 按评审给出的第一个选项处理:说明为设计预期。外层包工具调用已经过主链路的工作区判定与审查/弹窗;把高危工具族纳入 Hook 拒绝范围意味着已审查通过的扩展包从此完全失去 execute_shell 等能力,这是显著的行为收紧,留待 maintainer 决策后再做。

P2-6 end_line < start_line — 已修。android/linux/saf 三分支委托前统一显式失败并提示重查行号。

P3-7 i18n — 已补齐:9 条审查/工作区文案(en)+ 11 条(es/id/ko/ms/ro/pt-rBR/ja,含 function_type/desc_file_distill),8 个语言目录全部补齐并通过 XML 结构校验。

P3-8 聚类传递闭包 — 维持现状:精确重复由精确路径兜底(列出全部位置并拒绝),残余风险限于"非精确近似重复且相邻"的情形;待真实案例出现再收敛聚类策略。

P3-9 反向映射失败回落宿主路径 — 已修。映射失败的 grep 结果条目直接丢弃并记日志,不把宿主真实路径回传给模型。

P3-10 CDATA ]]> — 已修。按 XML 规范拆段转义(]] → ]]]]><![CDATA[>),diff 与 request-content 两处 CDATA 均覆盖。

[question] 回归矩阵 — 目前仅完成编译与全量单测;真机回归矩阵(区内/区外/无工作区 × 三权限模式 × 审查开关 × 合规/不合规/失败)待实机执行后补填到 TODO index。

[question] file_distill 默认开 + focus 必填 — 确认为期望行为。设计即"无兜底":不带 focus 的旧调用显式失败并指引 raw=true 重读,这是让调用方显式表达意图的一部分,不回退。

@CATMIAOZHI CATMIAOZHI left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审计结论:✅ 批准合并

复审说明:这是针对新提交 b2c416c9 + ab346982 的复审(上次 review 5887753747,commit 50016df7,结论 REQUEST_CHANGES)。本轮只审新增 diff(18 个文件,+301/−34),作者在评论区已逐项回复。

改动(新增 diff,18 个文件,+301/−34):按上次评审意见修复危险根拼写绕过、linux grep rootfs 逃逸、FunctionTypeTest 不一致、occurrence schema、非法行号范围、CDATA 转义、i18n 补齐等,并补测试。

逐项核对(上次评审的问题):

  • [P1] 危险根拼写绕过 — ✅ 已修并核实。isDangerousRecursiveDeleteTarget 先经 lexicallyNormalizePath(纯词法消解 ./../重复斜杠)再经 canonicalPath(覆盖符号链接),任一命中即拒绝;"/" 与 "/storage/emulated" 本就在危险根集合内,/sdcard/.. → /、/storage/emulated/0/.. → /storage/emulated 均能命中。新增测试覆盖 9 个拼写变体。
  • [P1] linux grep .. 逃出 proot rootfs — ✅ 已修并核实。mapLinuxPath 改为可空返回:拼接结果 canonical 化并强制 containment,逃出 rootfs 返回 null;grep_code、zip_files、unzip_files、download_file 四个调用点均显式拒绝(rootfsEscapeResult),不再回落原始路径;resolvePath 签名改为可空,主代码中全部 6 处调用点均已处理 null,无遗漏。
  • [P1] FunctionTypeTest 与新枚举不一致 — ✅ 已修。补 FILE_DISTILL,总数断言 12 → 13;作者称本地完整 :app:testDebugUnitTest 1666 个用例全部通过(以 CI 为准)。
  • [P2] occurrence schema 与实现矛盾 — ✅ 已修。三处(EN/CN edit_file、Internal apply_file)去掉 default = "1",描述明确不传会被拒绝。
  • [P2] 嵌套调用的高危工具不受 Hook 约束 — 说明已接受,留待 maintainer 决策。作者按评审给出的第一个选项处理:外层包工具调用已过主链路判定与审查,把高危工具族纳入 Hook 拒绝范围会使已审查通过的扩展包完全失去 execute_shell 等能力,属于显著行为收紧,需 maintainer 拍板后再做。结论上不阻塞本次合并。
  • [P2] read_file_part 在 end_line < start_line 时静默返回空成功 — ✅ 已修。android/linux/saf 三分支委托前统一显式失败并提示重查行号。
  • [P3] i18n 不同步 — ✅ 已修并核实。function_type/desc_file_distill 在 values 及 8 个语言目录均已存在;9 条审查/工作区文案(en)+ 11 条(其余 7 个语言目录)已补齐。
  • [P3] 模糊匹配聚类传递闭包 — 维持现状,可接受。精确重复由精确路径兜底(列出全部位置并拒绝),残余风险限于"非精确近似重复且相邻"的情形,待真实案例出现再收敛。
  • [P3] linux grep 反向映射失败回落宿主路径 — ✅ 已修。映射失败的条目直接丢弃并记日志,不把宿主真实路径回传给模型。
  • [P3] CDATA 内 diff 含 ]]> 破坏 XML — ✅ 已修。按 XML 规范拆段转义(]] → ]]]]><![CDATA[>),diff 与 request-content 两处 CDATA 均覆盖。
  • [question] 手工回归矩阵"结果待填" — 作者回复:目前仅完成编译与全量单测,真机回归矩阵待实机执行后补填到 TODO index。已记录为合入后跟进项,不阻塞合并。
  • [question] file_distill_enabled 默认开且 focus 必填 — 作者确认为期望行为(设计即"无兜底",不带 focus 的旧调用显式失败并指引 raw=true 重读)。接受。

通过的检查:

  • 新增 diff 全部落在上次评审指出的问题范围内,无无关改动、无新文件(相对上次 head 无新增文件);
  • resolvePath 改为可空后全部主代码调用点均显式处理 null,grep/zip/unzip/download 均 fail-closed;
  • isAndroidPathInside 的分隔符归一在 Android 设备上是 no-op,不改变设备行为;
  • lexicallyNormalizePath 为纯字符串运算,不做文件系统访问;绝对路径的 .. 在根处截断,不会逃逸到根之外;
  • CDATA 转义、行号范围显式失败、grep 结果丢弃并记日志等均为 fail-closed 的处理。

后续可跟进(不阻塞):

  • 嵌套调用的高危工具是否纳入 Hook 拒绝范围,需 maintainer(@AAswordman)决策;
  • 真机回归矩阵(区内/区外/无工作区 × 三权限模式 × 审查开关 × 合规/不合规/失败)待作者实机执行后补填到 docs/TODO/chat_file_tool_hardening/index.md。

由 水晴喵的muse 审计

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants