package restrict import ( "strings" "unicode/utf8" ) // SensitiveOp 是 design/11 §4「敏感清单」里的操作类别。它只用于审计与前端 // 小字(「xxx 想要干什么,我审查后批准了」),不参与身份/权限判定——身份始终 // 由 channel 层 actor 元数据 + 白名单决定(design/11 §0 红线)。 type SensitiveOp string const ( // SensitiveExecCommand 执行命令(terminal)。 SensitiveExecCommand SensitiveOp = "执行命令" // SensitiveWriteFile 修改文件(file 工具的 write 动作)。 SensitiveWriteFile SensitiveOp = "修改文件" // SensitiveNetwork 网络访问。只有会改变外部状态的写方法算敏感: // GET/HEAD/OPTIONS 属「检索」,可自主(design/11 §4)。 SensitiveNetwork SensitiveOp = "网络访问" ) // sensitiveSummaryRunes 截断动作摘要:代审小字与审计只需一眼看出「想干什么」, // 同时限制进入前端/日志的文本块大小(也降低把长参数里的秘密整段搬进日志的机会)。 const sensitiveSummaryRunes = 80 // SensitiveCall 判定一次工具调用是否属敏感操作(design/11 §4)。 // // 判定完全结构化:只看工具名 + 参数,绝不由模型文本推断(design/11 §0 红线)。 // 返回操作类别、动作摘要与是否敏感。清单与项目实际的工具面对应如下: // // terminal → 执行命令(命令原文进摘要) // file (action=write) → 修改文件(路径进摘要) // 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「非敏感(读文件 / 检索 / 查记忆)可自主」一致。 // // 未知工具(含 MCP 工具)同样不敏感:工具面已由 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 case "http": 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 default: return "", "", false } } // summarize 拼接「动作 + 目标」摘要并按 sensitiveSummaryRunes 截断(不切开 // 多字节字符);目标为空时只返回动作。 func summarize(action, target string) string { target = 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 }