98 lines
3.8 KiB
Markdown
98 lines
3.8 KiB
Markdown
# Agents
|
|
|
|
How sherlock decides which CLI to spawn when you type
|
|
`sherlock copilot` or `sherlock claude`, and how to add a new one.
|
|
|
|
## Supported today
|
|
|
|
| Name | Binary | MCP config flag | Notes |
|
|
|---|---|---|---|
|
|
| `copilot` | `copilot` (npm `@github/copilot`) | `--additional-mcp-config @<path>` | Augments user's `~/.copilot/mcp-config.json`. JSON shape is the canonical `.mcp.json` schema (`{"mcpServers": ...}`). |
|
|
| `claude` | `claude` (npm `@anthropic-ai/claude-code`) | `--mcp-config <path>` | Same `{"mcpServers": ...}` shape. `ANTHROPIC_API_KEY` is stripped from the child env so a personal key can't override the sherlock-managed session. |
|
|
|
|
## Routing
|
|
|
|
```
|
|
sherlock copilot [args...] ⇢ runs copilot
|
|
sherlock claude [args...] ⇢ runs claude
|
|
sherlock run copilot [args...] ⇢ same, explicit form
|
|
sherlock run <unknown> ... ⇢ exit 2 with "unknown agent"
|
|
sherlock <unknown> ⇢ exit 2 with "unknown subcommand"
|
|
```
|
|
|
|
The `run` form exists for parity with `cargo run` / `npm run`; the
|
|
bare alias is the daily-driver form.
|
|
|
|
## What sherlock does per spawn
|
|
|
|
1. `keyring.Open()` — fail fast if the OS keyring isn't available (returns `*UnavailableError` with a remediation `Hint` field).
|
|
2. Resolve the agent binary on `$PATH`. Friendly error if missing.
|
|
3. Render the per-agent MCP config to `$XDG_RUNTIME_DIR/sherlock/<agent>.mcp.json` (0600). In Phase 1 the servers map is always empty; Phase 2 populates it from `services.d/`.
|
|
4. Build the child argv with the agent-specific flag.
|
|
5. Build the child env: parent env minus per-agent forbids. MCPs spawned by the agent will reach into the OS keyring (via `internal/authn.Ensure`) on their own at startup — sherlock does not pre-authenticate anything.
|
|
6. `syscall.Exec` — sherlock disappears, the agent takes its place.
|
|
|
|
## Adding a new agent
|
|
|
|
It's a code change, deliberately. The TOML-overlay design was tried
|
|
and scrapped: each CLI has enough idiosyncrasies (auth subcommands,
|
|
permission flags, MCP config schema, env var quirks) that a Go file
|
|
per agent is honest about the surface area and gives those quirks a
|
|
real place to live.
|
|
|
|
Drop a new file in `internal/agent/`:
|
|
|
|
```go
|
|
// internal/agent/aider.go
|
|
package agent
|
|
|
|
import "gitea.alexandru.macocian.me/Charlie/sherlock/internal/mcp"
|
|
|
|
func init() { Register(&aider{}) }
|
|
|
|
type aider struct{}
|
|
|
|
func (aider) Name() string { return "aider" }
|
|
func (aider) Description() string { return "Aider AI pair programmer" }
|
|
|
|
func (a aider) Spawn(ctx Context, args []string) error {
|
|
bin, err := LookPath("aider")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Aider's MCP schema and flag would go here.
|
|
_ = bin
|
|
_ = ctx
|
|
_ = args
|
|
return nil
|
|
}
|
|
```
|
|
|
|
That's the whole API: `Name`, `Description`, `Spawn`. The CLI picks
|
|
it up automatically through the `init()` registry call; `sherlock
|
|
status` shows it; `sherlock aider ...` dispatches.
|
|
|
|
## Reusable helpers
|
|
|
|
Available to every agent implementation in this package:
|
|
|
|
| Helper | Purpose |
|
|
|---|---|
|
|
| `LookPath(name)` | `exec.LookPath` with a sherlock-friendly error message. |
|
|
| `BuildEnv(forbid, set)` | parent env minus `forbid`, plus `set`. |
|
|
| `DefaultExecer` | the package-level `Execer` (swap in tests). |
|
|
| `mcp.Render(name, servers)` | writes `{"mcpServers": ...}` to `$XDG_RUNTIME_DIR/sherlock/<name>.mcp.json`. |
|
|
|
|
If a new agent needs a third MCP-config schema, add a new `Render*`
|
|
function to `internal/mcp/` rather than open-coding JSON in the agent
|
|
file.
|
|
|
|
## What sherlock does *not* do
|
|
|
|
- Read agent config from `~/.config/sherlock/agents.d/` — that
|
|
directory does not exist.
|
|
- Hot-reload registered agents — the registry is sealed at process
|
|
start, by design (one fewer code path).
|
|
- Sandbox the agent — sherlock just `exec`s it, the agent inherits
|
|
the user's full environment minus a few targeted forbids.
|