Files
goclaw/internal/providers/registry.go
T
viettranx b2c4d543aa feat(providers): add Claude CLI provider with MCP bridge (#61)
Add Claude CLI as an LLM provider (subscription-based, no API key needed).
The CLI manages session history, tool execution, and context while GoClaw
forwards messages and streams responses.

Key features:
- Claude CLI provider with session persistence (--resume)
- MCP bridge server exposing GoClaw tools to CLI via streamable-http
- Security hooks (shell deny patterns, workspace path restrictions)
- Per-session mutex preventing concurrent CLI calls
- Onboard wizard for Claude CLI setup and auth verification
- Web UI for adding/managing Claude CLI provider with auth status
- Provider registry Close() for proper shutdown cleanup

Security:
- CLI path validation (only "claude" or absolute paths from DB)
- Token auth middleware for MCP bridge endpoint
- Shell injection prevention in hook scripts (single-quoted paths)
- Relative path resolution before workspace boundary checks
- Resource leak prevention on provider replace/unregister

Co-authored-by: nhokboo <nhokboo@users.noreply.github.com>
2026-03-07 02:06:39 +07:00

81 lines
1.7 KiB
Go

package providers
import (
"fmt"
"io"
"log/slog"
"sync"
)
// Registry manages available LLM providers.
type Registry struct {
providers map[string]Provider
mu sync.RWMutex
}
func NewRegistry() *Registry {
return &Registry{
providers: make(map[string]Provider),
}
}
// Register adds a provider to the registry.
// If a provider with the same name already exists, it is closed before replacement.
func (r *Registry) Register(provider Provider) {
r.mu.Lock()
defer r.mu.Unlock()
if old, ok := r.providers[provider.Name()]; ok {
if c, ok := old.(io.Closer); ok {
c.Close()
}
}
r.providers[provider.Name()] = provider
}
// Unregister removes a provider from the registry, closing it if it implements io.Closer.
func (r *Registry) Unregister(name string) {
r.mu.Lock()
defer r.mu.Unlock()
if old, ok := r.providers[name]; ok {
if c, ok := old.(io.Closer); ok {
c.Close()
}
delete(r.providers, name)
}
}
// Get returns a provider by name.
func (r *Registry) Get(name string) (Provider, error) {
r.mu.RLock()
defer r.mu.RUnlock()
p, ok := r.providers[name]
if !ok {
return nil, fmt.Errorf("provider not found: %s", name)
}
return p, nil
}
// Close calls Close() on all providers that implement io.Closer.
func (r *Registry) Close() {
r.mu.RLock()
defer r.mu.RUnlock()
for name, p := range r.providers {
if c, ok := p.(io.Closer); ok {
if err := c.Close(); err != nil {
slog.Warn("provider close error", "name", name, "error", err)
}
}
}
}
// List returns all registered provider names.
func (r *Registry) List() []string {
r.mu.RLock()
defer r.mu.RUnlock()
names := make([]string, 0, len(r.providers))
for name := range r.providers {
names = append(names, name)
}
return names
}