feat(security): 敏感清单补齐 design/11 §4 的 7 类(写数据/对外发消息/访问密钥/改配置,结构化判据)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

This commit is contained in:
AskaEth committed 2026-10-05 11:53:17 +08:00
1 parent 509dc412eb
commit 85abb90067
6 files changed
+813 -18

No files matched your search

+90 -17
View File
@@ -15,51 +15,124 @@ const (
SensitiveExecCommand SensitiveOp = "执行命令"
// SensitiveWriteFile 修改文件(file 工具的 write 动作)。
SensitiveWriteFile SensitiveOp = "修改文件"
// SensitiveWriteData 写数据:对数据存储的写入(数据库/存储文件的写、
// 记忆与索引类工具的写动作)。
SensitiveWriteData SensitiveOp = "写数据"
// SensitiveOutboundMessage 对外发消息:把内容投出本进程的发送类工具
// (邮件/短信/渠道消息/Webhook)。
SensitiveOutboundMessage SensitiveOp = "对外发消息"
// SensitiveSecretAccess 访问密钥与隐私:读写凭据、密钥材料与隐私数据
// (`.env` / `models.json` / 渠道 token / SSH 私钥 / 本地数据库)。
SensitiveSecretAccess SensitiveOp = "访问密钥与隐私"
// SensitiveConfigChange 改配置与权限:改动实例配置与权限边界
// (模型与渠道配置、MCP/插件清单、策略与角色定义)。
SensitiveConfigChange SensitiveOp = "改配置与权限"
// SensitiveNetwork 网络访问。只有会改变外部状态的写方法算敏感:
// GET/HEAD/OPTIONS 属「检索」,可自主(design/11 §4)。
SensitiveNetwork SensitiveOp = "网络访问"
)
// SensitiveOps 按 design/11 §4 的顺序列出**全部 7 类**敏感操作。它是该清单在
// 代码里的对账口径:测试断言 7 类齐全、且每一类都至少有一条结构化判据能命中。
var SensitiveOps = []SensitiveOp{
SensitiveExecCommand,
SensitiveWriteFile,
SensitiveWriteData,
SensitiveOutboundMessage,
SensitiveSecretAccess,
SensitiveConfigChange,
SensitiveNetwork,
}
// sensitiveSummaryRunes 截断动作摘要:代审小字与审计只需一眼看出「想干什么」,
// 同时限制进入前端/日志的文本块大小(也降低把长参数里的秘密整段搬进日志的机会)。
const sensitiveSummaryRunes = 80
// SensitiveCall 判定一次工具调用是否属敏感操作(design/11 §4)。
//
// 判定完全结构化:只看工具名 + 参数,绝不由模型文本推断(design/11 §0 红线)。
// 返回操作类别、动作摘要与是否敏感。清单与项目实际的工具面对应如下:
// 判定完全结构化:只看工具名 + 参数(以及参数里的路径落点),绝不由模型文本
// 推断(design/11 §0 红线)。判据分三层:
//
// 1. 具名工具的参数判据(`file` / `http` / `terminal`,见下表);
// 2. 按**路径落点**细化 `file` 的类别(配置件 / 数据存储件 / 密钥件,
// 见 sensitive_paths.go);
// 3. 其余工具(MCP / 插件 / 生态新增)按**工具名**的名词+动词词族判定,
// 并对「存储类工具 + 写动作参数」补一条参数判据(见 sensitive_names.go),
// 判不出即非敏感。
//
// 具名工具的参数判据:
//
// terminal → 执行命令(命令原文进摘要)
// file (action=write) → 修改文件(路径进摘要)
// file (action=write) → 落点是配置件 ⇒ 改配置与权限;落点是数据存储件 ⇒
// 写数据;其余 ⇒ 修改文件(路径进摘要)
// file (read/list/search) → 落点是密钥件或数据存储件 ⇒ 访问密钥与隐私;
// 其余 ⇒ 非敏感(design/11 §4「读文件可自主」)
// http (method=POST) → 网络访问(URL 进摘要)
//
// 其余情况一律**不敏感**:file 的 read/list/search、http 的 GET/HEAD/OPTIONS、
// knowledge / web-search / web-fetch(检索)、calculator / datetime / text /
// json / markdown / random(纯计算)、vision / ocr / session_read(只读)——
// 与 design/11 §4「非敏感(读文件 / 检索 / 查记忆)可自主」一致。
// 其余情况一律**不敏感**:knowledge / web-search / web-fetch(检索)、
// calculator / datetime / text / json / markdown / random(纯计算)、
// vision / ocr / session_read(只读)——与 design/11 §4「非敏感(读文件 /
// 检索 / 查记忆)可自主」一致。
//
// 未知工具(含 MCP 工具)同样不敏感:工具面已由 registry 的策略闸门管理,
// 这里不新增「未知即拦截」的硬边界,否则任何新工具都会在主人会话里被拦死。
// 判不准的新工具同样不敏感:工具面已由 registry 的策略闸门管理,这里不新增
// 「未知即拦截」的硬边界,否则任何新工具都会在主人会话里被拦死。判据刻意保守
// 的理由同上:误判把正常工具拉进代审会污染审计与前端小字。
func SensitiveCall(name string, args map[string]any) (SensitiveOp, string, bool) {
switch name {
case "terminal":
return SensitiveExecCommand, summarize("terminal", argString(args, "command")), true
case "file":
if argString(args, "action") != "write" {
return "", "", false
}
return SensitiveWriteFile, summarize("file write", argString(args, "path")), true
return fileCall(args)
case "http":
method := strings.ToUpper(strings.TrimSpace(argString(args, "method")))
if method == "" || method == "GET" || method == "HEAD" || method == "OPTIONS" {
return "", "", false
return httpCall(args)
}
if op, ok := matchNameRule(name); ok {
return op, summarize(name, argTarget(args)), true
}
if op, ok := matchStoreAction(name, args); ok {
return op, summarize(name, argTarget(args)), true
}
return "", "", false
}
// fileCall 按 `file` 的动作与**落点路径**给出类别(design/11 §4 的四类判据:
// 修改文件 / 写数据 / 改配置与权限 / 访问密钥与隐私)。
//
// 未知或缺失 action 保持「不敏感」:真实工具会直接拒绝这种调用(
// plugins/tools/io/file.go 的 switch 落到 default),它不是一次真实的副作用。
func fileCall(args map[string]any) (SensitiveOp, string, bool) {
action := argString(args, "action")
target := argString(args, "path")
switch action {
case "write":
switch {
case isConfigArtifact(target):
return SensitiveConfigChange, summarize("file write", target), true
case isDataStoreArtifact(target):
return SensitiveWriteData, summarize("file write", target), true
default:
return SensitiveWriteFile, summarize("file write", target), true
}
return SensitiveNetwork, summarize("http "+method, argString(args, "url")), true
case "read", "list", "search":
if isSecretArtifact(target) || isDataStoreArtifact(target) {
return SensitiveSecretAccess, summarize("file "+action, target), true
}
return "", "", false
default:
return "", "", false
}
}
// httpCall 按 HTTP 方法给出类别(design/11 §4):只有会改变外部状态的写方法
// 算敏感,GET/HEAD/OPTIONS 与缺省方法(=GET)属检索,可自主。
func httpCall(args map[string]any) (SensitiveOp, string, bool) {
method := strings.ToUpper(strings.TrimSpace(argString(args, "method")))
if method == "" || method == "GET" || method == "HEAD" || method == "OPTIONS" {
return "", "", false
}
return SensitiveNetwork, summarize("http "+method, argString(args, "url")), true
}
// summarize 拼接「动作 + 目标」摘要并按 sensitiveSummaryRunes 截断(不切开
// 多字节字符);目标为空时只返回动作。
func summarize(action, target string) string {