171 lines
7.5 KiB
Go
171 lines
7.5 KiB
Go
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
|
||
}
|