Files
Cyrene_v2/internal/services/gateway/admin_policy_persist_test.go
AskaEth 63236c9b00
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
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 触发代审 + 审计有行有端到端断言
2026-10-04 12:30:16 +08:00

111 lines
3.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}