fix: 限定卸载范围并要求真实核验证据引用 (#14)

This commit is contained in:
2026-09-19 12:59:01 +08:00
committed by GitHub
parent 779ef2712d
commit b29360ed8d
3 changed files with 17 additions and 3 deletions

View File

@@ -1,7 +1,7 @@
--- ---
name: 应用安装管理 name: 应用安装管理
description: 安装、升级、卸载或检查应用;读取固定资料,复用执行工具核验后记录结果。处理 ApplicationManagement 请求;旧服务 RegistryCatalogAcquisition 请求按需读取兼容指南。 description: 安装、升级、卸载或检查应用;读取固定资料,复用执行工具核验后记录结果。处理 ApplicationManagement 请求;旧服务 RegistryCatalogAcquisition 请求按需读取兼容指南。
version: "1.5.1" version: "1.5.2"
requiredClientVersion: "10.0.170" requiredClientVersion: "10.0.170"
type: procedural type: procedural
risk_level: high risk_level: high
@@ -36,4 +36,6 @@ Agent 理解环境并执行;已有工具提供能力;Agent Service 只可靠
5. 超时、断线和结果不明时先观察,不重放有副作用的命令;同一安装同时存在其他修改任务时不并发执行。核验只说明实际检查过的能力。 5. 超时、断线和结果不明时先观察,不重放有副作用的命令;同一安装同时存在其他修改任务时不并发执行。核验只说明实际检查过的能力。
6. 结果明确后提交一次带时间的观察,material 必须原样复用成功 resolve 的 data.material(包括 action 与 snapshot);首装结束也不能改成 manage。不预写 installing/reinstalling/uninstalling,不拼装 pending 状态或完整收据,不直接编辑 installed-entries.json。版本必须与本次固定资料一致;发现不同版本应先确认正确资料,不能伪报核验成功。 6. 结果明确后提交一次带时间的观察,material 必须原样复用成功 resolve 的 data.material(包括 action 与 snapshot);首装结束也不能改成 manage。不预写 installing/reinstalling/uninstalling,不拼装 pending 状态或完整收据,不直接编辑 installed-entries.json。版本必须与本次固定资料一致;发现不同版本应先确认正确资料,不能伪报核验成功。
7. 记账失败只重读并处理记录,不重复安装/卸载。记录修订冲突要重新观察,不能只换 expectedRevision。异常时按需读取 [恢复原则](references/recovery.md)。 7. 记账失败只重读并处理记录,不重复安装/卸载。记录修订冲突要重新观察,不能只换 expectedRevision。异常时按需读取 [恢复原则](references/recovery.md)。
8. 最后说明软件的实际结果与记录是否提交成功;二者可能不同。进度留在当前会话/既有任务;不得承诺无人执行的后续修复或“所有失败都已回滚”。 8. 卸载仅移除已核验的应用包/安装目录;不能因为位于同一前缀就递归清理父目录。未知文件、同级数据、凭据和日志默认保留;额外删除必须逐项说明并另获确认。清理空父目录只用非递归删除,非空就保留。
9. 证据引用使用工具实际返回的调用 ID、真实会话日志位置或本次已保存的核验报告路径,不编造 session:…:step:… 标识;没有可追溯引用就先保存不含秘密的报告。
10. 最后说明软件的实际结果与记录是否提交成功;二者可能不同。进度留在当前会话/既有任务;不得承诺无人执行的后续修复或“所有失败都已回滚”。

View File

@@ -47,6 +47,8 @@ const body = {
限制:location 最多 2048 字符、不能含换行/NUL;evidenceRefs 1–8 个,每项最多 512 字符;notes 最多 2000 字符。不填 runtimeStatus、operationId、pending 字段或 catalogReceipt。证据引用由 Agent 报告,服务端校验结构和记录一致性,不声称验证引用内容或软件真实安全。 限制:location 最多 2048 字符、不能含换行/NUL;evidenceRefs 1–8 个,每项最多 512 字符;notes 最多 2000 字符。不填 runtimeStatus、operationId、pending 字段或 catalogReceipt。证据引用由 Agent 报告,服务端校验结构和记录一致性,不声称验证引用内容或软件真实安全。
evidenceRefs 不是自由命名的步骤标题。只填实际存在的工具调用 ID、会话日志位置,或已用 Write/执行工具保存的核验报告路径;不能自行编造 `session:…:step:…`。报告保存包版本、实际操作结果和时间,不含凭据;保存失败则如实报告记账前提未满足。
成功返回新的 revision、replayed 和当前记录摘要。相同 requestId+相同 body 的当前结果可重复读取,且不重写时间;同 ID 不同 body 拒绝。旧提交已被后续修订取代时,不换 ID/修订强行重提,也不承诺任意历史请求的全局去重。 成功返回新的 revision、replayed 和当前记录摘要。相同 requestId+相同 body 的当前结果可重复读取,且不重写时间;同 ID 不同 body 拒绝。旧提交已被后续修订取代时,不换 ID/修订强行重提,也不承诺任意历史请求的全局去重。
## 重读和失败 ## 重读和失败
@@ -55,4 +57,4 @@ const body = {
提交响应丢失:先重读;当前 requestId 相同可用原正文重试。修订变化:观察实际软件再决定是否发起新的核验,禁止只替换 expectedRevision。写入失败:说明软件可能已修改但记录未完成,不重执行安装。具体恢复见 recovery.md。 提交响应丢失:先重读;当前 requestId 相同可用原正文重试。修订变化:观察实际软件再决定是否发起新的核验,禁止只替换 expectedRevision。写入失败:说明软件可能已修改但记录未完成,不重执行安装。具体恢复见 recovery.md。
应用正常停止后无需改观察记录。卸载默认保留用户数据和凭据;删除这些材料需要独立确认。注册内部服务也属于单独任务,应用安装不派生新的内部 MCP/HTTP 服务。 应用正常停止后无需改观察记录。卸载默认保留用户数据和凭据;删除这些材料需要独立确认。先检查实际安装位置及其父目录边界,使用包管理器卸载或只删除已确认归属的应用目录;不得把整个自定义前缀当作可递归删除对象。只检查默认用户数据路径不能证明前缀内没有数据。未知/后来加入的文件一律保留;空父目录可用非递归删除,非空就停止清理。注册内部服务也属于单独任务,应用安装不派生新的内部 MCP/HTTP 服务。

View File

@@ -666,6 +666,16 @@ test('首装观察示例复用 resolver material,不将 install 改为 manage'
assert.match(recovery, /响应未知不能如此处理/) assert.match(recovery, /响应未知不能如此处理/)
}) })
test('卸载不扩大到未知父目录,证据必须实际存在', async () => {
const skill = await readFile(skillPath, 'utf8')
const api = await readFile(join(here, '..', 'references', 'recording-api.md'), 'utf8')
assert.match(skill, /不能因为位于同一前缀就递归清理父目录/)
assert.match(skill, /非递归删除,非空就保留/)
assert.match(api, /只检查默认用户数据路径不能证明前缀内没有数据/)
assert.match(api, /不能自行编造/)
assert.match(api, /已用 Write\/执行工具保存的核验报告路径/)
})
test('Skill contract has no fixed official/local Registry fallback and resolves before execution', async () => { test('Skill contract has no fixed official/local Registry fallback and resolves before execution', async () => {
const skill = await readFile(join(here, '..', 'references', 'legacy-services.md'), 'utf8') const skill = await readFile(join(here, '..', 'references', 'legacy-services.md'), 'utf8')
assert.doesNotMatch(skill, /registry\/official\/entries/) assert.doesNotMatch(skill, /registry\/official\/entries/)