feat(capability,gateway): 工具策略落盘 + 主代理代审真接线(裁决 1.1 核对/1.3/10.4.2/13.4.3)
- 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:
1 parent
ed5bd92e1b
commit
63236c9b00
20 files changed
+1709
-24
No files matched your search
@@ -0,0 +1,73 @@
|
||||
package restrict
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestSensitiveCall 是 design/11 §4 敏感清单的判定表:执行命令/修改文件/
|
||||
// 网络写方法敏感,读文件/检索/纯计算自主。
|
||||
func TestSensitiveCall(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
args map[string]any
|
||||
wantOp SensitiveOp
|
||||
wantIn string // 摘要应包含的片段(wantSensitive 时)
|
||||
sensitive bool
|
||||
}{
|
||||
{name: "terminal", args: map[string]any{"command": "rm -rf /tmp/x"}, wantOp: SensitiveExecCommand, wantIn: "rm -rf /tmp/x", sensitive: true},
|
||||
{name: "terminal", args: nil, wantOp: SensitiveExecCommand, wantIn: "terminal", sensitive: true},
|
||||
{name: "file", args: map[string]any{"action": "write", "path": "/data/a.md"}, wantOp: SensitiveWriteFile, wantIn: "/data/a.md", sensitive: true},
|
||||
{name: "file", args: map[string]any{"action": "read", "path": "/data/a.md"}},
|
||||
{name: "file", args: map[string]any{"action": "list"}},
|
||||
{name: "file", args: map[string]any{"action": "search", "query": "x"}},
|
||||
{name: "http", args: map[string]any{"method": "POST", "url": "https://example.com/hook"}, wantOp: SensitiveNetwork, wantIn: "example.com/hook", sensitive: true},
|
||||
{name: "http", args: map[string]any{"method": "post", "url": "https://example.com/hook"}, wantOp: SensitiveNetwork, sensitive: true},
|
||||
{name: "http", args: map[string]any{"url": "https://example.com"}}, // method 缺省 = GET
|
||||
{name: "http", args: map[string]any{"method": "GET", "url": "https://example.com"}},
|
||||
{name: "http", args: map[string]any{"method": "HEAD"}},
|
||||
{name: "knowledge", args: map[string]any{"query": "设计文档"}},
|
||||
{name: "web-search", args: map[string]any{"query": "x"}},
|
||||
{name: "web-fetch", args: map[string]any{"url": "https://example.com"}},
|
||||
{name: "session_read", args: map[string]any{"sessionId": "web"}},
|
||||
{name: "calculator", args: map[string]any{"expression": "1+1"}},
|
||||
{name: "mcp:some-server/tool", args: map[string]any{"anything": true}},
|
||||
{name: "unknown-tool", args: nil},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
op, detail, sensitive := SensitiveCall(tc.name, tc.args)
|
||||
if sensitive != tc.sensitive {
|
||||
t.Fatalf("SensitiveCall(%s, %v) sensitive = %v, want %v", tc.name, tc.args, sensitive, tc.sensitive)
|
||||
}
|
||||
if !tc.sensitive {
|
||||
if op != "" || detail != "" {
|
||||
t.Fatalf("SensitiveCall(%s) 非敏感时不应给出类别/摘要,得到 (%q, %q)", tc.name, op, detail)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if op != tc.wantOp {
|
||||
t.Fatalf("SensitiveCall(%s) op = %q, want %q", tc.name, op, tc.wantOp)
|
||||
}
|
||||
if tc.wantIn != "" && !strings.Contains(detail, tc.wantIn) {
|
||||
t.Fatalf("SensitiveCall(%s) detail = %q, want it to contain %q", tc.name, detail, tc.wantIn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSensitiveCall_SummaryIsClipped 锁定动作摘要的截断:长参数不许整段
|
||||
// 进入小字/审计(同时不切开多字节字符)。
|
||||
func TestSensitiveCall_SummaryIsClipped(t *testing.T) {
|
||||
t.Parallel()
|
||||
long := strings.Repeat("昔", 200)
|
||||
_, detail, sensitive := SensitiveCall("terminal", map[string]any{"command": long})
|
||||
if !sensitive {
|
||||
t.Fatal("terminal 应判定为敏感")
|
||||
}
|
||||
if got := len([]rune(detail)); got > sensitiveSummaryRunes+len("terminal ")+1 {
|
||||
t.Fatalf("摘要 rune 数 = %d,未按 %d 截断: %q", got, sensitiveSummaryRunes, detail)
|
||||
}
|
||||
if !strings.HasSuffix(detail, "…") {
|
||||
t.Fatalf("被截断的摘要应以省略号结尾: %q", detail)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user