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
@@ -68,6 +68,14 @@ type Capability struct {
|
||||
type Registry struct {
|
||||
mu sync.RWMutex
|
||||
caps map[string]Capability
|
||||
// store 是策略落盘载体(design/11 §3、裁决 10.4.2)。为 nil 时注册表
|
||||
// 退回纯内存态(既有行为);AttachPolicyStore 装上后,SetPolicy 系列
|
||||
// 先落库再改内存。
|
||||
store PolicyStore
|
||||
// persisted 是最近一次落盘成功的策略快照(能力名 → 标记)。注册表在
|
||||
// Register 时用它覆盖能力的初始 Policy,因此「策略在能力注册之前就已落盘」
|
||||
// 与「能力先注册、策略后落盘」两种顺序都得到同一个结果。
|
||||
persisted map[string]ToolPolicy
|
||||
}
|
||||
|
||||
// Compile-time assertion that *Registry satisfies the SDK Registrar interface.
|
||||
@@ -102,6 +110,11 @@ func (r *Registry) Register(cap Capability) error {
|
||||
if _, exists := r.caps[cap.Name]; exists {
|
||||
return fmt.Errorf("registry: register %q: %w", cap.Name, ErrDuplicate)
|
||||
}
|
||||
// 落盘策略覆盖注册时带的初始标记:DB 是策略的权威载体(裁决 10.4.2),
|
||||
// 因此重启后先 Load、再热注册(MCP 热装等)也能拿到已配置的值。
|
||||
if pol, ok := r.persisted[cap.Name]; ok {
|
||||
cap.Policy = pol
|
||||
}
|
||||
r.caps[cap.Name] = cap
|
||||
return nil
|
||||
}
|
||||
@@ -160,7 +173,17 @@ func (r *Registry) Policy(name string) (ToolPolicy, error) {
|
||||
|
||||
// SetPolicy replaces the reachability markers for the capability named name. It
|
||||
// returns ErrNotFound when no such capability is registered.
|
||||
//
|
||||
// 落盘(裁决 1.3/10.4.2):已装上 PolicyStore 时先写库,成功后才改内存——
|
||||
// 写库失败即返回错误、内存保持旧值,绝不产生「面板说保存成功、重启却复位」。
|
||||
// 顺序是「存在性读 → 落库(锁外做 SQL)→ 持锁套用」。
|
||||
func (r *Registry) SetPolicy(name string, p ToolPolicy) error {
|
||||
if _, err := r.Policy(name); err != nil {
|
||||
return fmt.Errorf("registry: set policy %q: %w", name, err)
|
||||
}
|
||||
if err := r.persist(name, p); err != nil {
|
||||
return err
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
cap, ok := r.caps[name]
|
||||
@@ -169,34 +192,35 @@ func (r *Registry) SetPolicy(name string, p ToolPolicy) error {
|
||||
}
|
||||
cap.Policy = p
|
||||
r.caps[name] = cap
|
||||
r.remember(name, p)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetAllowInGroup toggles the group-chat marker for the capability named name.
|
||||
// It returns ErrNotFound when no such capability is registered.
|
||||
// It returns ErrNotFound when no such capability is registered. 它复用
|
||||
// setPolicyField,因此同样先落库再改内存(裁决 10.4.2)。
|
||||
func (r *Registry) SetAllowInGroup(name string, allow bool) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
cap, ok := r.caps[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("registry: set allow_in_group %q: %w", name, ErrNotFound)
|
||||
}
|
||||
cap.Policy.AllowInGroup = allow
|
||||
r.caps[name] = cap
|
||||
return nil
|
||||
return r.setPolicyField(name, "allow_in_group", func(p *ToolPolicy) { p.AllowInGroup = allow })
|
||||
}
|
||||
|
||||
// SetAllowInStranger toggles the stranger-chat marker for the capability named
|
||||
// name. It returns ErrNotFound when no such capability is registered.
|
||||
// name. It returns ErrNotFound when no such capability is registered. 同
|
||||
// SetAllowInGroup:先落库再改内存。
|
||||
func (r *Registry) SetAllowInStranger(name string, allow bool) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
cap, ok := r.caps[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("registry: set allow_in_stranger %q: %w", name, ErrNotFound)
|
||||
return r.setPolicyField(name, "allow_in_stranger", func(p *ToolPolicy) { p.AllowInStranger = allow })
|
||||
}
|
||||
|
||||
// setPolicyField 是三个策略写入口共用的「读当前值 → 改一位 → SetPolicy」路径
|
||||
// (先落库再改内存,见 SetPolicy)。field 只用于错误消息里的操作名。
|
||||
func (r *Registry) setPolicyField(name, field string, apply func(*ToolPolicy)) error {
|
||||
cur, err := r.Policy(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("registry: set %s %q: %w", field, name, err)
|
||||
}
|
||||
apply(&cur)
|
||||
if err := r.SetPolicy(name, cur); err != nil {
|
||||
return fmt.Errorf("registry: set %s %q: %w", field, name, err)
|
||||
}
|
||||
cap.Policy.AllowInStranger = allow
|
||||
r.caps[name] = cap
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user