- 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 触发代审 + 审计有行有端到端断言
111 lines
3.9 KiB
Go
111 lines
3.9 KiB
Go
package gateway
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"io"
|
||
"log/slog"
|
||
"path/filepath"
|
||
"testing"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/auth"
|
||
capreg "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/db"
|
||
)
|
||
|
||
// 本文件锁定裁决 1.3/10.4.2 的管理面一侧:面板的「工具策略」保存
|
||
// (POST /api/v1/admin/plugins/policy)真的**先落库再改内存**——重启后策略仍在。
|
||
// 落盘载体是 tool_policies 表(迁移 0012_tool_policy),由 registry 的
|
||
// PolicyStore 钩子写入;这里用真实 SQLite 库走完整 HTTP 路径。
|
||
|
||
// policyEnv 接线一个「策略已接存储」的能力注册表的网关,返回服务端地址、
|
||
// JWT 服务与库连接(用例直接查 tool_policies 断言落盘)。
|
||
func policyEnv(t *testing.T) (string, *auth.JWTService, *sql.DB) {
|
||
t.Helper()
|
||
ctx := context.Background()
|
||
log := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||
conn, err := db.Open(ctx, filepath.Join(t.TempDir(), "cyrene.db"), log)
|
||
if err != nil {
|
||
t.Fatalf("open sqlite: %v", err)
|
||
}
|
||
t.Cleanup(func() { conn.Close() })
|
||
if err := db.MigrateUp(ctx, conn, log); err != nil {
|
||
t.Fatalf("MigrateUp: %v", err)
|
||
}
|
||
store, err := capreg.NewSQLitePolicyStore(conn, log)
|
||
if err != nil {
|
||
t.Fatalf("policy store: %v", err)
|
||
}
|
||
reg := capreg.New()
|
||
if err := reg.AttachPolicyStore(ctx, store); err != nil {
|
||
t.Fatalf("AttachPolicyStore: %v", err)
|
||
}
|
||
if err := reg.RegisterTool(stubTool{}); err != nil {
|
||
t.Fatalf("register stub tool: %v", err)
|
||
}
|
||
_, ts, jwt := buildEnv(t, nil, Deps{Registry: reg})
|
||
return ts.URL, jwt, conn
|
||
}
|
||
|
||
// TestAdminSetPolicyPersistsToToolPolicies 锁定「面板保存 → 真落库」:
|
||
// 走真实 HTTP 端点写入后,tool_policies 里有对应的行。
|
||
func TestAdminSetPolicyPersistsToToolPolicies(t *testing.T) {
|
||
t.Parallel()
|
||
ctx := context.Background()
|
||
url, jwt, conn := policyEnv(t)
|
||
owner := signAny(t, jwt, "acct", auth.RoleOwner)
|
||
|
||
resp, body := doReq(t, "POST", url+"/api/v1/admin/plugins/policy",
|
||
map[string]any{"name": "echo", "allowInGroup": true, "allowInStranger": false}, owner)
|
||
if resp.StatusCode != 200 {
|
||
t.Fatalf("POST policy status = %d, body = %s", resp.StatusCode, body)
|
||
}
|
||
var (
|
||
group bool
|
||
stranger bool
|
||
)
|
||
if err := conn.QueryRowContext(ctx,
|
||
`SELECT allow_in_group, allow_in_stranger FROM tool_policies WHERE name = 'echo'`).
|
||
Scan(&group, &stranger); err != nil {
|
||
t.Fatalf("tool_policies 里没有 echo 的策略行(未落盘?): %v", err)
|
||
}
|
||
if !group || stranger {
|
||
t.Fatalf("落盘策略 = {group:%v stranger:%v}, want {true false}", group, stranger)
|
||
}
|
||
}
|
||
|
||
// TestAdminSetPolicySurvivesRestart 跨「重启」验证:写完策略后另外构造一个
|
||
// 注册表重新 Load,标记仍在(这正是裁决 1.3 要求消灭的「重启即失」)。
|
||
func TestAdminSetPolicySurvivesRestart(t *testing.T) {
|
||
t.Parallel()
|
||
ctx := context.Background()
|
||
url, jwt, conn := policyEnv(t)
|
||
owner := signAny(t, jwt, "acct", auth.RoleOwner)
|
||
|
||
if resp, body := doReq(t, "POST", url+"/api/v1/admin/plugins/policy",
|
||
map[string]any{"name": "echo", "allowInGroup": false, "allowInStranger": true}, owner); resp.StatusCode != 200 {
|
||
t.Fatalf("POST policy status = %d, body = %s", resp.StatusCode, body)
|
||
}
|
||
|
||
// 模拟重启:新注册表 + 新 store,只有 Load 一条路能拿到刚才的标记。
|
||
log := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||
store, err := capreg.NewSQLitePolicyStore(conn, log)
|
||
if err != nil {
|
||
t.Fatalf("policy store (2): %v", err)
|
||
}
|
||
rebooted := capreg.New()
|
||
if err := rebooted.AttachPolicyStore(ctx, store); err != nil {
|
||
t.Fatalf("AttachPolicyStore (2): %v", err)
|
||
}
|
||
if err := rebooted.RegisterTool(stubTool{}); err != nil {
|
||
t.Fatalf("RegisterTool (2): %v", err)
|
||
}
|
||
pol, err := rebooted.Policy("echo")
|
||
if err != nil {
|
||
t.Fatalf("Policy (2): %v", err)
|
||
}
|
||
if pol.AllowInGroup || !pol.AllowInStranger {
|
||
t.Fatalf("重启后策略 = %+v, want {group:false stranger:true}", pol)
|
||
}
|
||
}
|