Files
Agent-Squad/squad/registry.go
weichenandCursor 8e987bf535
ci / vet-and-test (pull_request) Failing after 1s
ci / docker-checkout-hostgateway (pull_request) Failing after 3s
Persist step approvals and guard squad HTTP entry points
Why: A step confirmation cancelled the whole run, approvals and repair budget vanished on restart, and the HTTP server accepted anonymous and oversized requests.
Evidence: GOWORK=off go vet ./... && GOWORK=off go test -count=1 ./... && GOWORK=off go test -race ./squad/... passed. scripts/submodule-consistency.sh ok.
Impact: Approve and skip apply to one step; runstate.json and unix flock survive restart; wakeup leaves permission holds pending; serve defaults to loopback and requires a token off-loopback.
Refs: Q1 Q2 Q3 Q4 Q5 Q8 Q9 Q14 Q15 Q16
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 01:48:10 +00:00

186 lines
5.5 KiB
Go

package squad
import (
"fmt"
"sort"
"strings"
"sync"
)
// RoleRegistry 角色注册中心:空表起步,无任何预设角色。
// 线程安全;角色可来自配置文件、HTTP API、CLI 或规划器运行期派生。
type RoleRegistry struct {
mu sync.RWMutex
roles map[string]RoleSpec
// OnChange 可选钩子:角色注册/派生后回调(store 层用于落盘)。
OnChange func(spec RoleSpec) error
}
// NewRoleRegistry 创建空注册表。
func NewRoleRegistry() *RoleRegistry {
return &RoleRegistry{roles: make(map[string]RoleSpec)}
}
// Register 注册或更新角色;Role 为空或 BashPermission 非法时返回错误。
func (r *RoleRegistry) Register(spec RoleSpec) error {
if !ValidID(spec.Role) {
return invalidID(spec.Role)
}
switch spec.BashPermission {
case "", "deny", "ask", "allow":
default:
return fmt.Errorf("bash_permission must be allow/ask/deny, got %q", spec.BashPermission)
}
if spec.BashPermission == "" {
spec.BashPermission = "deny"
}
if spec.Source == "" {
spec.Source = RoleSourceRegistered
}
if spec.Name == "" {
spec.Name = spec.Role
}
r.mu.Lock()
r.roles[spec.Role] = spec
r.mu.Unlock()
if r.OnChange != nil {
if err := r.OnChange(spec); err != nil {
return err
}
}
return nil
}
// Get 按 Key 获取角色。
func (r *RoleRegistry) Get(key string) (RoleSpec, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
spec, ok := r.roles[key]
return spec, ok
}
// List 列出全部角色(按 Role 排序,输出稳定)。
func (r *RoleRegistry) List() []RoleSpec {
r.mu.RLock()
defer r.mu.RUnlock()
out := make([]RoleSpec, 0, len(r.roles))
for _, spec := range r.roles {
out = append(out, spec)
}
sort.Slice(out, func(i, j int) bool { return out[i].Role < out[j].Role })
return out
}
// Remove 删除角色;不存在时返回 false。
func (r *RoleRegistry) Remove(key string) bool {
r.mu.Lock()
_, ok := r.roles[key]
if ok {
delete(r.roles, key)
}
r.mu.Unlock()
return ok
}
// Recommend 按查询文本推荐最贴切角色:Keywords 命中计 10 分/词,
// Responsibilities 命中计 5 分;无命中返回 false(不猜测兜底角色)。
func (r *RoleRegistry) Recommend(query string) (RoleSpec, bool) {
q := strings.ToLower(query)
var best RoleSpec
bestScore := 0
for _, spec := range r.List() {
score := 0
for _, kw := range spec.Keywords {
if kw != "" && strings.Contains(q, strings.ToLower(kw)) {
score += 10
}
}
for _, resp := range spec.Responsibilities {
if resp != "" && strings.Contains(q, strings.ToLower(resp)) {
score += 5
}
}
if score > bestScore {
best, bestScore = spec, score
}
}
return best, bestScore > 0
}
// DeriveRequest 派生角色请求:以 BaseRole 为基线叠加领域特化。
type DeriveRequest struct {
RoleKey string `json:"role_key"`
Name string `json:"name,omitempty"`
BaseRole string `json:"base_role,omitempty"`
Backend string `json:"backend,omitempty"`
Model string `json:"model,omitempty"`
ToolProfile string `json:"tool_profile,omitempty"`
Tools []string `json:"tools,omitempty"`
Skills []string `json:"skills,omitempty"`
BashPermission string `json:"bash_permission,omitempty"`
DomainTopic string `json:"domain_topic,omitempty"`
Responsibilities []string `json:"responsibilities,omitempty"`
ForbiddenActions []string `json:"forbidden_actions,omitempty"`
Instructions string `json:"instructions,omitempty"`
}
// Derive 派生特设角色:继承基线角色的 Backend/Model/Tools/指令,
// 叠加领域职责与禁忌;BaseRole 缺省取注册表中第一个角色(无角色则内联最小基线)。
func (r *RoleRegistry) Derive(req DeriveRequest) (RoleSpec, error) {
if strings.TrimSpace(req.RoleKey) == "" {
return RoleSpec{}, fmt.Errorf("role_key cannot be empty")
}
base, hasBase := r.Get(req.BaseRole)
if !hasBase {
list := r.List()
if len(list) > 0 {
base = list[0]
}
}
spec := RoleSpec{
Role: req.RoleKey,
Name: req.Name,
Source: RoleSourceDerived,
BaseRole: base.Role,
Backend: firstNonEmpty(req.Backend, base.Backend),
Model: firstNonEmpty(req.Model, base.Model),
ToolProfile: firstNonEmpty(req.ToolProfile, base.ToolProfile),
Tools: firstNonEmptySlice(req.Tools, base.Tools),
Skills: firstNonEmptySlice(req.Skills, base.Skills),
BashPermission: firstNonEmpty(req.BashPermission, base.BashPermission),
Responsibilities: append([]string{}, req.Responsibilities...),
ForbiddenActions: append([]string{}, req.ForbiddenActions...),
Instructions: firstNonEmpty(req.Instructions, base.Instructions),
InstructionsFile: base.InstructionsFile,
}
if spec.Name == "" {
spec.Name = fmt.Sprintf("%s专员", req.RoleKey)
}
if len(spec.Responsibilities) == 0 && req.DomainTopic != "" {
spec.Responsibilities = append(spec.Responsibilities, fmt.Sprintf("专责推演与论证【%s】", req.DomainTopic))
}
spec.ForbiddenActions = append(spec.ForbiddenActions,
"严禁违规越权执行未经规划的破坏性命令", "严禁直接代用户做终审拍板")
if err := r.Register(spec); err != nil {
return RoleSpec{}, err
}
return spec, nil
}
// firstNonEmptySlice 首个非空切片,否则回退后者。
func firstNonEmptySlice(a, b []string) []string {
if len(a) > 0 {
return append([]string{}, a...)
}
return append([]string{}, b...)
}
func firstNonEmpty(vals ...string) string {
for _, v := range vals {
if v != "" {
return v
}
}
return ""
}