feat(capability,gateway): 工具策略落盘 + 主代理代审真接线(裁决 1.1 核对/1.3/10.4.2/13.4.3)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 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 触发代审 + 审计有行有端到端断言
This commit is contained in:
AskaEth committed 2026-10-04 12:30:16 +08:00
1 parent ed5bd92e1b
commit 63236c9b00
20 files changed
+1709 -24

No files matched your search

+88
View File
@@ -0,0 +1,88 @@
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
}