a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
179 lines
4.5 KiB
Go
179 lines
4.5 KiB
Go
package agents
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// Registry holds the set of adapters `gortex init` will run. Callers
|
|
// Register each adapter explicitly (from cmd/gortex/init.go) rather
|
|
// than via sub-package init() hooks so that unit tests can build a
|
|
// registry with a controlled subset without a global side-effect.
|
|
//
|
|
// Methods are safe for concurrent reads; Register should only be
|
|
// called during startup.
|
|
type Registry struct {
|
|
mu sync.RWMutex
|
|
adapters []Adapter
|
|
byName map[string]Adapter
|
|
}
|
|
|
|
// NewRegistry returns an empty Registry.
|
|
func NewRegistry() *Registry {
|
|
return &Registry{byName: make(map[string]Adapter)}
|
|
}
|
|
|
|
// Register adds an adapter. Panics on duplicate names — duplicates
|
|
// indicate a programming error (two packages registering the same
|
|
// name), not a user mistake, so a panic is appropriate.
|
|
func (r *Registry) Register(a Adapter) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
name := a.Name()
|
|
if _, dup := r.byName[name]; dup {
|
|
panic(fmt.Sprintf("agents.Registry: duplicate adapter name %q", name))
|
|
}
|
|
r.byName[name] = a
|
|
r.adapters = append(r.adapters, a)
|
|
}
|
|
|
|
// All returns adapters in registration order. Callers should not
|
|
// mutate the returned slice.
|
|
func (r *Registry) All() []Adapter {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
out := make([]Adapter, len(r.adapters))
|
|
copy(out, r.adapters)
|
|
return out
|
|
}
|
|
|
|
// Names returns the sorted list of registered adapter names. Used by
|
|
// the --agents flag's help text and the unknown-name error message.
|
|
func (r *Registry) Names() []string {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
out := make([]string, 0, len(r.byName))
|
|
for name := range r.byName {
|
|
out = append(out, name)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// Lookup returns the adapter with the given name, or nil when no
|
|
// adapter is registered under that name.
|
|
func (r *Registry) Lookup(name string) Adapter {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
return r.byName[name]
|
|
}
|
|
|
|
// Filter returns the subset of adapters selected by allow/skip lists.
|
|
// allowCSV:
|
|
// - "" (empty) or "auto" selects every registered adapter (the
|
|
// default: let Detect decide which ones run)
|
|
// - comma-separated list of names selects only those
|
|
// - any unknown name produces an error
|
|
//
|
|
// skipCSV removes names from the selection after allowCSV applies.
|
|
// Unknown skip names are also hard errors — silent skips mask typos.
|
|
//
|
|
// The returned slice preserves registration order so the user sees a
|
|
// stable run sequence.
|
|
func (r *Registry) Filter(allowCSV, skipCSV string) ([]Adapter, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
|
|
allow := parseCSV(allowCSV)
|
|
skip := parseCSV(skipCSV)
|
|
|
|
// Validate names up front; we'd rather report all typos at once.
|
|
var unknown []string
|
|
for _, n := range allow {
|
|
if n == "auto" {
|
|
continue
|
|
}
|
|
if _, ok := r.byName[n]; !ok {
|
|
unknown = append(unknown, n)
|
|
}
|
|
}
|
|
for _, n := range skip {
|
|
if _, ok := r.byName[n]; !ok {
|
|
unknown = append(unknown, n)
|
|
}
|
|
}
|
|
if len(unknown) > 0 {
|
|
sort.Strings(unknown)
|
|
return nil, fmt.Errorf("unknown agent name(s): %s; available: %s",
|
|
strings.Join(unknown, ", "),
|
|
strings.Join(r.sortedNamesLocked(), ", "))
|
|
}
|
|
|
|
// Build the allowed set. Empty allowCSV or the "auto" sentinel
|
|
// means "every registered adapter" — Detect() then decides which
|
|
// ones actually run.
|
|
var allowSet map[string]struct{}
|
|
autoMode := len(allow) == 0
|
|
for _, n := range allow {
|
|
if n == "auto" {
|
|
autoMode = true
|
|
continue
|
|
}
|
|
}
|
|
if !autoMode {
|
|
allowSet = make(map[string]struct{}, len(allow))
|
|
for _, n := range allow {
|
|
allowSet[n] = struct{}{}
|
|
}
|
|
}
|
|
|
|
skipSet := make(map[string]struct{}, len(skip))
|
|
for _, n := range skip {
|
|
skipSet[n] = struct{}{}
|
|
}
|
|
|
|
out := make([]Adapter, 0, len(r.adapters))
|
|
for _, a := range r.adapters {
|
|
name := a.Name()
|
|
if allowSet != nil {
|
|
if _, ok := allowSet[name]; !ok {
|
|
continue
|
|
}
|
|
}
|
|
if _, skipped := skipSet[name]; skipped {
|
|
continue
|
|
}
|
|
out = append(out, a)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *Registry) sortedNamesLocked() []string {
|
|
out := make([]string, 0, len(r.byName))
|
|
for name := range r.byName {
|
|
out = append(out, name)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// parseCSV splits a comma-separated list and trims whitespace. Empty
|
|
// input and all-whitespace input yield a nil slice.
|
|
func parseCSV(s string) []string {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
parts := strings.Split(s, ",")
|
|
out := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
p = strings.TrimSpace(p)
|
|
if p != "" {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|