SuperAPI AI 工具集

guarding-destructive-operations

编程与开发

查看项目

更新于 10-16 04:09

技能介绍

用途

Guarding Destructive Operations 面向不可逆操作的防护设计,涵盖删除、覆盖、重写历史,以及把调用者提供的名称解析为文件系统路径等场景。其核心论点是:实现本身通常简短且在其作者设想的配置下是正确的,问题不在删除动作本身,而在于没有任何检查确认目标是否就是作者所设想的那个对象。

两大类反复出现的风险

  • 影响范围(Blast radius):操作仅在专门的单一用途目标中正确,在混杂目标中则会造成灾难性后果。
  • 边界逃逸(Boundary escape):调用者提供的名称与基目录拼接后,落到基目录之外。

拒绝而非警告

发现前置条件被违反的破坏性操作应返回错误并保持状态不变。警告无人阅读,而 --force 逃生舱会把防护降格为文档。文档指出,不提供覆盖开关的正当理由是:不存在操作者希望该命令删除无关已跟踪文件的场景;若日后真出现此类需求,应是一个另行命名的新命令,而不是在此命令上增加布尔开关。

把防护放在第一次变更之前

只有当拒绝发生在任何内容改变之前,“拒绝”才成立。需要沿函数逐句查找第一个有副作用的语句——它往往不是那个显眼的删除。内存状态的清空、分支检出,以及对正要保留的文件的读取后重写,都算变更。文档以一个重建流程为例,列出安全顺序:拒绝 detached HEAD/歧义状态、ensureDedicatedRepo()(新防护,尚无变更)、读取要保留的状态文件、CheckoutOrphan()、RemoveAllTrackedFiles()、ClearAllProgressCounts()。防护放在第 2 步,被拒绝的运行会让工作树保持逐字节一致。此外,在重写之前也应拒绝歧义状态:假设使用命名分支的历史重写应在开头拒绝 detached HEAD,而不是静默重建错误的 ref。

按结构分类,而非字符串前缀

防护需要回答“每个已跟踪路径是否都在该命令所拥有的目录内?”。对裸目录名使用 HasPrefix/startswith 是错误的答案,却能在你想到要写的每个测试里看起来正确。文档给出反例与正例:

// 坏——放行 .state-backup/notes.md 与 .stateful.txt func owned(path string) bool { return strings.HasPrefix(path, ".state") }

// 好——目录本身,或确实位于其下 func owned(path string) bool { return path == ".state" || strings.HasPrefix(path, ".state/") }

Python 中对应的区分是 Path(p) == base or base in Path(p).parents,而不是 p.startswith(str(base))。任何把路径当字符串比较的地方,都要问:一个名字更长的兄弟路径会怎样?

名称校验与解析后包含性是两个检查

当公共参数选择某个文件(基线名、模板 id、报告 slug)时,需按顺序执行两项检查。文档给出示例代码:

NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")

def _validate_name(name: str) -> str: if ".." in name or not NAME_RE.match(name): raise ValueError(f"invalid name: {name!r}") return name

def _resolve(name: str, base: Path) -> Path: candidate = (base / f"{_validate_name(name)}.json").resolve() if not candidate.is_relative_to(base.resolve()): raise ValueError(f"name escapes {base}: {name!r}") return candidate

包含性检查与正则并不冗余。正则阻止 .. 片段与绝对路径;只有 resolve() 加包含性检查能阻止目录内指向外部的符号链接——一个从头到尾匹配 ^[A-Za-z0-9] 的名称仍可能解析到任何位置。加载器、保存器,以及公共方法仍直接调用的任何路径构建辅助函数,都必须走这两项检查。单个未走检查的辅助函数就会重新引入整个漏洞。若无此防护,隐式后缀(name + ".json")加上绝对路径参数,会读取磁盘上任何恰好以此后缀结尾的文件。

预览看不到破坏性路径上的防护

若真实运行会走到防护,而 --dry-run 提前短路,预览便会欣然描述一个真实运行会拒绝的操作。文档认为这是可辩护的设计——破坏性路径才是必须安全的那条——但需明确说明:预览会显示在禁止重建的仓库中执行完整重建,只有真实调用才报错。任何意在提前警示操作者的内容都必须放在预览阶段或 CLI 参数校验层,有意重复而非移动。文档建议在 PR 描述中写明你选择了哪种方式;审阅者无法从 diff 判断 dry-run 缺口是否有意为之。

了解调用方如何处理你的拒绝

收紧校验只有在新的错误能到达某人时才有帮助。上层两帧处的宽泛 except Exception 会把硬拒绝变成普通结果形状。文档示例中,一个本就存在的 except Exception 会捕获你的 ValueError 并返回 {"error": ..., "features": {}, "flags": {...}}。文档指出这往往没问题——一个从不抛出的 API 边界是真实契约,拒绝会以正常的错误形状响应呈现,而非协议错误。但要有意检查:若调用方看到的就是这种形状,集成测试应断言 result["error"],而不是 pytest.raises。

测试

  • 分别变异防护的每一半。仅削弱正则,确认 .. 夹具失败;仅削弱包含性检查,确认符号链接夹具失败。一个合并测试在任一半被删除时都会通过,因而两者都证明不了。
  • 显式编写兄弟名称夹具。一个仓库跟踪 .state/data.json 的测试对 HasPrefix(path, ".state") 也会通过;能区分实现的测试应跟踪 .state-backup/old.json 并断言操作被拒绝。
  • 拒绝时断言没有任何变更,而不只是返回了错误:分支未变、已跟踪文件列表未变、内存中的进度计数器未变。这才是真正的断言。
  • 既有夹具会成为被拒绝的形状。破坏性路径的旧测试常构建得较宽松——例如仓库在状态目录旁还跟踪 app.txt,因为当时没人在意。加上防护后,这样的测试不再到达它原本覆盖的代码,而是转为测试拒绝。应把这些夹具收紧到允许的形状,并把拒绝用例作为新测试添加,否则会静默失去对破坏性路径的覆盖。
  • 用最窄的钩子复现失败。当需要让某个特定操作失败以测试恢复路径时,要精确针对它——一个拒绝该类所有操作的一刀切钩子会在比你试图到达的路径更早处中止运行,测试证明的就是别的东西。

检查清单

  • 每个不可逆操作都在代码中声明其前置条件,而非仅在文档中
  • 违反时返回错误;没有 --force,没有警告后继续
  • 防护在第一个有副作用的语句之前运行
  • 歧义状态(detached HEAD、目标缺失、多个候选)在开头即被拒绝
  • 路径/归属分类按结构判断,绝不用裸字符串前缀
  • 调用者提供的名称经过校验并做解析路径包含性检查
  • 每个入口点都走两项检查,包括内部辅助函数
  • dry-run/预览缺口是有意的并已说明
  • 测试独立变异防护的每一半
  • 同时存在兄弟名称(x-backup/)与符号链接两种夹具
  • 拒绝测试断言目标未变,而不只是抛出了错误

限制

文档未提供该技能所依托的具体框架、语言版本、安装方式或平台信息,亦未说明任何收费、价格或效果数据,这些内容暂未提供。