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 }