- 1.1(核对):registry 默认两布尔 false 已一致,零代码改动(设计文档已按裁决改写) - 1.3 + 10.4.2:新增迁移 0012_tool_policy(tool_policies 表,表名复数 + idx_tool_policies_name); 新增 registry.PolicyStore/SQLitePolicyStore,启动 Load 覆盖注册初值、SetPolicy 先落库后改内存 (落盘失败保持旧值并报错);面板保存路径不变但从此重启仍在 - 13.4.3:代审真接线 —— restrict.SensitiveCall 结构化判定(terminal / file write / http POST)、 restrict.Gate/Reviewer/Resolver(无代审者或代审报错一律 fail-closed 拦截), 组合根装进 loop.Options.Hooks 的 CallAdmit;gateway.RecordApproval 落 audit_logs (who=角色名 / what=approval.<工具> / result=granted|denied)并广播 approval_notice 帧 - 前端:代审小字「xxx 想要干什么,我审查后批准了」进聊天流顶部(帧→store→组件,文案单一来源 复用 audit.Approval.Explain()),附纯 node 逻辑测试 - 复核:迁移在 data/cyrene.db **副本**上 up/down/up 各一次通过;TestRegistry_PolicyStoreSurvivesRestart 与 TestAdminSetPolicySurvivesRestart 证明「重启后策略仍在」;terminal 触发代审 + 审计有行有端到端断言
89 lines
3.6 KiB
Go
89 lines
3.6 KiB
Go
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
|
||
}
|