Files
sherlock/internal/agent/backend.go
T
Alexandru Macocian 84958c7ef3
Release / release (push) Successful in 17s
Support for local agents
2026-06-15 19:03:56 +02:00

79 lines
2.5 KiB
Go

package agent
import (
"fmt"
"sort"
"gitea.alexandru.macocian.me/amacocian/sherlock/internal/mcp"
)
// backend captures the per-CLI quirks shared by every agent that speaks
// to a given coding-assistant CLI: how the rendered MCP-config file is
// passed on the command line, and which env vars must be scrubbed from
// the child process.
//
// Built-in agents (copilot, claude) and operator-defined custom agents
// from config.toml both spawn through a backend, so a `copilot-local`
// wrapper formats its MCP config exactly like the real `copilot`.
type backend struct {
// mcpArgs returns the CLI flags that point the agent at the rendered
// MCP-config file at path.
mcpArgs func(path string) []string
// forbidEnv lists env vars stripped from the child (e.g. a personal
// ANTHROPIC_API_KEY that must not override broker-managed behavior).
forbidEnv []string
}
// backends is the set of known backend types. The keys are the values
// operators put in `[agents.<name>].backend`.
var backends = map[string]backend{
"copilot": {
// The `@` prefix tells Copilot to treat the argument as a file
// path rather than inline JSON.
mcpArgs: func(p string) []string { return []string{"--additional-mcp-config", "@" + p} },
},
"claude": {
mcpArgs: func(p string) []string { return []string{"--mcp-config", p} },
forbidEnv: []string{"ANTHROPIC_API_KEY"},
},
}
// backendNames returns the known backend type names, sorted, for error
// messages.
func backendNames() []string {
out := make([]string, 0, len(backends))
for n := range backends {
out = append(out, n)
}
sort.Strings(out)
return out
}
// spawn is the shared spawn path used by both built-in and custom
// agents. name selects the rendered MCP-config file name; command is the
// binary to exec. On success it does not return (the process is
// replaced); any returned error is a setup failure.
func (b backend) spawn(name, command string, ctx Context, args []string) error {
bin, err := LookPath(command)
if err != nil {
return err
}
mcpPath, err := mcp.Render(name, ctx.Servers)
if err != nil {
return err
}
argv := append([]string{bin}, b.mcpArgs(mcpPath)...)
argv = append(argv, args...)
return DefaultExecer.Exec(bin, argv, BuildEnv(b.forbidEnv, nil))
}
// resolveBackend returns the backend for a type name, with a friendly
// error listing the valid choices when it is unknown.
func resolveBackend(name string) (backend, error) {
b, ok := backends[name]
if !ok {
return backend{}, fmt.Errorf("unknown backend %q (want one of: %v)", name, backendNames())
}
return b, nil
}