Files
Cyrene_v2/internal/capability/restrict/sensitive.go
T

171 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package restrict
import (
"strings"
"unicode/utf8"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
)
// SensitiveOp 是 design/11 §4「敏感清单」里的操作类别。它只用于审计与前端
// 小字(「xxx 想要干什么,我审查后批准了」),不参与身份/权限判定——身份始终
// 由 channel 层 actor 元数据 + 白名单决定(design/11 §0 红线)。
type SensitiveOp string
const (
// SensitiveExecCommand 执行命令(terminal)。
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 截断动作摘要:代审小字与审计只需一眼看出「想干什么」,
// 同时限制进入前端/日志的文本块大小。它只管**长度** —— 秘密由 summarize 里的
// trace.RedactText 负责打码,不靠截断「顺手甩掉」。
const sensitiveSummaryRunes = 80
// SensitiveCall 判定一次工具调用是否属敏感操作(design/11 §4)。
//
// 判定完全结构化:只看工具名 + 参数(以及参数里的路径落点),绝不由模型文本
// 推断(design/11 §0 红线)。判据分三层:
//
// 1. 具名工具的参数判据(`file` / `http` / `terminal`,见下表);
// 2. 按**路径落点**细化 `file` 的类别(配置件 / 数据存储件 / 密钥件,
// 见 sensitive_paths.go);
// 3. 其余工具(MCP / 插件 / 生态新增)按**工具名**的名词+动词词族判定,
// 并对「存储类工具 + 写动作参数」补一条参数判据(见 sensitive_names.go),
// 判不出即非敏感。
//
// 具名工具的参数判据:
//
// terminal → 执行命令(命令原文进摘要)
// file (action=write) → 落点是配置件 ⇒ 改配置与权限;落点是数据存储件 ⇒
// 写数据;其余 ⇒ 修改文件(路径进摘要)
// file (read/list/search) → 落点是密钥件或数据存储件 ⇒ 访问密钥与隐私;
// 其余 ⇒ 非敏感(design/11 §4「读文件可自主」)
// http (method=POST) → 网络访问(URL 进摘要)
//
// 其余情况一律**不敏感**:knowledge / web-search / web-fetch(检索)、
// calculator / datetime / text / json / markdown / random(纯计算)、
// vision / ocr / session_read(只读)——与 design/11 §4「非敏感(读文件 /
// 检索 / 查记忆)可自主」一致。
//
// 判不准的新工具同样不敏感:工具面已由 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":
return fileCall(args)
case "http":
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
}
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 截断(不切开
// 多字节字符);目标为空时只返回动作。
//
// 目标先过 trace.RedactText 打码:这个摘要会进审计与前端小字两个读面,而
// terminal 的 command / http 的 url 里常常就带着凭据(`curl -H 'api_key=…'`、
// `?token=…`),截断并不能让它们变得安全。RedactText 自身已按同一档(80 rune)
// 截断,后面的 clipRunes 保留是为了让长度上限继续由本包的
// sensitiveSummaryRunes 掌握 —— 两处同档,叠加不改变结果。
func summarize(action, target string) string {
target = trace.RedactText(strings.TrimSpace(target))
if target == "" {
return action
}
return action + " " + clipRunes(target, sensitiveSummaryRunes)
}
// clipRunes 把 s 截到至多 max 个 rune,超出时追加省略号;max 非正时原样返回。
func clipRunes(s string, max int) string {
if max <= 0 || utf8.RuneCountInString(s) <= max {
return s
}
return string([]rune(s)[:max]) + "…"
}
// argString 从工具参数里取一个字符串字段;参数表缺失或类型不符返回 ""。
func argString(args map[string]any, key string) string {
if args == nil {
return ""
}
s, _ := args[key].(string)
return s
}