251 lines
7.9 KiB
Go
251 lines
7.9 KiB
Go
// Package registry provides the global capability registry. Every capability a
|
|
// character can use — tools, skills, channels, MCP servers and IoT providers —
|
|
// is registered here so it is globally shared and always available to any
|
|
// character and mode (DevDocs/design/01-角色人格与行为模式.md §4).
|
|
//
|
|
// It is the single source of truth that tool dispatch (M2) and plugin/skill/MCP
|
|
// wiring (M5) read from. The registry is safe for concurrent use; it implements
|
|
// the pkg/plugins/sdk Registrar interface so plugins register through the SDK
|
|
// helpers without knowing this concrete type.
|
|
package registry
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
|
|
)
|
|
|
|
// Kind classifies a registered capability.
|
|
type Kind string
|
|
|
|
const (
|
|
// KindTool is a callable function with a JSON Schema (sdk.Tool).
|
|
KindTool Kind = "tool"
|
|
// KindSkill is a reusable workflow with steps, prompts, refs and guardrails
|
|
// (sdk.Skill).
|
|
KindSkill Kind = "skill"
|
|
// KindChannel is a platform adapter (sdk.Channel).
|
|
KindChannel Kind = "channel"
|
|
// KindMCP is a capability exposed by an MCP server (subsystem-owned).
|
|
KindMCP Kind = "mcp"
|
|
// KindIoT is an IoT device or provider capability (subsystem-owned).
|
|
KindIoT Kind = "iot"
|
|
)
|
|
|
|
// ToolPolicy holds the editable Boolean markers that gate a tool/capability in
|
|
// non-owner environments (DevDocs/design/11 §3). Both fields default to false,
|
|
// so a capability is owner-only until a marker is explicitly enabled. Policies
|
|
// are per-capability and editable at runtime (the admin panel, M10).
|
|
type ToolPolicy struct {
|
|
// AllowInGroup permits invocation from a group chat environment.
|
|
AllowInGroup bool
|
|
// AllowInStranger permits invocation from a stranger's private chat
|
|
// environment.
|
|
AllowInStranger bool
|
|
}
|
|
|
|
// Capability is a globally shared, always-available capability registered in
|
|
// the registry.
|
|
type Capability struct {
|
|
// Name is the unique capability identifier.
|
|
Name string
|
|
// Kind classifies the capability.
|
|
Kind Kind
|
|
// Description is a human-readable summary used for selection and display.
|
|
Description string
|
|
// Policy is the editable reachability markers for this capability. It
|
|
// defaults to zero, which the policy engine reads as owner-only.
|
|
Policy ToolPolicy
|
|
// Value is the underlying capability object. For KindTool/KindSkill/
|
|
// KindChannel it is the SDK interface from pkg/plugins/sdk; for KindMCP and
|
|
// KindIoT it is owned by the relevant subsystem.
|
|
Value any
|
|
}
|
|
|
|
// Registry is the thread-safe global capability registry.
|
|
type Registry struct {
|
|
mu sync.RWMutex
|
|
caps map[string]Capability
|
|
}
|
|
|
|
// Compile-time assertion that *Registry satisfies the SDK Registrar interface.
|
|
var _ sdk.Registrar = (*Registry)(nil)
|
|
|
|
// Sentinel errors returned by the registry. Use errors.Is to test for them.
|
|
var (
|
|
// ErrNotFound is returned by Get when no capability has the given name.
|
|
ErrNotFound = errors.New("registry: capability not found")
|
|
// ErrDuplicate is returned by Register when a capability with the same name
|
|
// is already registered.
|
|
ErrDuplicate = errors.New("registry: capability already registered")
|
|
// ErrInvalidArgument is returned when a capability has an empty name, an
|
|
// unknown kind, or a nil Value.
|
|
ErrInvalidArgument = errors.New("registry: invalid capability")
|
|
)
|
|
|
|
// New returns an empty, concurrency-safe registry.
|
|
func New() *Registry {
|
|
return &Registry{caps: make(map[string]Capability)}
|
|
}
|
|
|
|
// Register adds cap to the registry. It returns ErrInvalidArgument when the
|
|
// name is empty, the kind is unknown or the Value is nil, and ErrDuplicate when
|
|
// a capability with the same name already exists.
|
|
func (r *Registry) Register(cap Capability) error {
|
|
if cap.Name == "" || !validKind(cap.Kind) || cap.Value == nil {
|
|
return fmt.Errorf("registry: register %q: %w", cap.Name, ErrInvalidArgument)
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if _, exists := r.caps[cap.Name]; exists {
|
|
return fmt.Errorf("registry: register %q: %w", cap.Name, ErrDuplicate)
|
|
}
|
|
r.caps[cap.Name] = cap
|
|
return nil
|
|
}
|
|
|
|
// Get returns the capability registered under name. It returns ErrNotFound when
|
|
// no such capability is registered.
|
|
func (r *Registry) Get(name string) (Capability, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
cap, ok := r.caps[name]
|
|
if !ok {
|
|
return Capability{}, fmt.Errorf("registry: get %q: %w", name, ErrNotFound)
|
|
}
|
|
return cap, nil
|
|
}
|
|
|
|
// Remove deletes the capability registered under name. It returns ErrNotFound
|
|
// when no such capability is registered.
|
|
func (r *Registry) Remove(name string) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if _, ok := r.caps[name]; !ok {
|
|
return fmt.Errorf("registry: remove %q: %w", name, ErrNotFound)
|
|
}
|
|
delete(r.caps, name)
|
|
return nil
|
|
}
|
|
|
|
// List returns a copy of the capabilities whose Kind matches kind. An empty
|
|
// kind returns every registered capability. The returned slice is owned by the
|
|
// caller and safe to mutate.
|
|
func (r *Registry) List(kind Kind) []Capability {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
out := make([]Capability, 0, len(r.caps))
|
|
for _, cap := range r.caps {
|
|
if kind != "" && cap.Kind != kind {
|
|
continue
|
|
}
|
|
out = append(out, cap)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Policy returns the editable reachability markers for the capability named
|
|
// name. It returns ErrNotFound when no such capability is registered.
|
|
func (r *Registry) Policy(name string) (ToolPolicy, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
cap, ok := r.caps[name]
|
|
if !ok {
|
|
return ToolPolicy{}, fmt.Errorf("registry: policy %q: %w", name, ErrNotFound)
|
|
}
|
|
return cap.Policy, nil
|
|
}
|
|
|
|
// SetPolicy replaces the reachability markers for the capability named name. It
|
|
// returns ErrNotFound when no such capability is registered.
|
|
func (r *Registry) SetPolicy(name string, p ToolPolicy) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
cap, ok := r.caps[name]
|
|
if !ok {
|
|
return fmt.Errorf("registry: set policy %q: %w", name, ErrNotFound)
|
|
}
|
|
cap.Policy = p
|
|
r.caps[name] = cap
|
|
return nil
|
|
}
|
|
|
|
// SetAllowInGroup toggles the group-chat marker for the capability named name.
|
|
// It returns ErrNotFound when no such capability is registered.
|
|
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
|
|
}
|
|
|
|
// SetAllowInStranger toggles the stranger-chat marker for the capability named
|
|
// name. It returns ErrNotFound when no such capability is registered.
|
|
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)
|
|
}
|
|
cap.Policy.AllowInStranger = allow
|
|
r.caps[name] = cap
|
|
return nil
|
|
}
|
|
|
|
// RegisterTool registers t as a KindTool capability.
|
|
func (r *Registry) RegisterTool(t sdk.Tool) error {
|
|
if t == nil {
|
|
return fmt.Errorf("registry: register tool: %w", ErrInvalidArgument)
|
|
}
|
|
return r.Register(Capability{
|
|
Name: t.Name(),
|
|
Kind: KindTool,
|
|
Description: t.Description(),
|
|
Value: t,
|
|
})
|
|
}
|
|
|
|
// RegisterSkill registers s as a KindSkill capability.
|
|
func (r *Registry) RegisterSkill(s sdk.Skill) error {
|
|
if s == nil {
|
|
return fmt.Errorf("registry: register skill: %w", ErrInvalidArgument)
|
|
}
|
|
return r.Register(Capability{
|
|
Name: s.Name(),
|
|
Kind: KindSkill,
|
|
Description: s.Description(),
|
|
Value: s,
|
|
})
|
|
}
|
|
|
|
// RegisterChannel registers c as a KindChannel capability.
|
|
func (r *Registry) RegisterChannel(c sdk.Channel) error {
|
|
if c == nil {
|
|
return fmt.Errorf("registry: register channel: %w", ErrInvalidArgument)
|
|
}
|
|
return r.Register(Capability{
|
|
Name: c.Name(),
|
|
Kind: KindChannel,
|
|
Description: c.Name(),
|
|
Value: c,
|
|
})
|
|
}
|
|
|
|
// validKind reports whether k is one of the known capability kinds.
|
|
func validKind(k Kind) bool {
|
|
switch k {
|
|
case KindTool, KindSkill, KindChannel, KindMCP, KindIoT:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|