-
Notifications
You must be signed in to change notification settings - Fork 0
Agent Backends
The crate installs a plugin into a coding agent through an AgentBackend. One binary can target
several agents; install loops the configured agents list and reconciles each one. 25 backends
ship: Claude Code, copilot-cli (both plugin-native), and 23 config-merge backends. All 24 non-CC
backends were verified against their real shipping binary on 2026-07-16 (copilot-cli's native
rewrite 2026-07-18).
For the harness-first at-a-glance (which backend translates what, config paths, scopes) see Harness comparison; for the capability-first depth (MCP shapes + fidelity, hook events, native Claude-Code-config interop) see Capabilities. This page holds the trait mechanics, backend-specific detail, and how to add your own.
pub enum BackendState { Absent, Healthy, Disabled, NeedsRepair }
pub trait AgentBackend {
fn id(&self) -> &'static str;
fn detect(&self) -> bool; // is this agent installed?
fn capabilities(&self) -> Capabilities; // plugins / mcp / hooks / statusline / scopes
fn probe(&self, plugin: &Plugin, scope: &Scope, source: &Source) -> Result<BackendState>; // self_heal's input
fn reconcile(&self, plugin: &Plugin, desired: &Desired, scope: &Scope) -> Result<Outcome>;
fn remove(&self, plugin: &Plugin, scope: &Scope, source: &Source) -> Result<Outcome>;
fn forget(&self, plugin: &Plugin, scope: &Scope) -> Result<()> { Ok(()) } // defaulted, see below
fn report(&self, plugin: &Plugin, source: &Source) -> DoctorReport;
}reconcile is the seam. Every lifecycle op (install, update, self_heal) reduces to a
reconcile with a different desired state. probe classifies the plugin's current state so
self_heal can run a marker × state table per agent; it renders from the source argument
(the same one self_heal resolves for reconcile), and a backend writing more than one surface
classifies each into an Option<BackendState> and folds them through report::compose. Each
backend defines what "converged" means:
- Claude Code: marketplace present, plugin installed, version at or above the embedded one, files on disk.
- A config-merge agent: its config file matches what the plugin declares.
capabilities() lets setup report a partial fit ("this agent hosts MCP servers, not hooks")
instead of dropping features silently. The trait is unsealed: an external crate can write an
AgentBackend for an agent this crate does not ship.
forget is the teardown counterpart to remove, and defaults to doing nothing — which is right
for any backend whose writes are all config-merge, since remove already takes those back.
Implement it when a backend writes into a file the user owns and the tool does not carry, the
way all four status line backends write their slot. Such a write
outlives the tool's registry and the tool binary itself, so uninstall calls forget on every path
(an undetected tool included) before clearing agentgear's marker, and a failure keeps that marker
rather than dropping the only copy of what the write displaced.
Two shapes:
-
Claude Code and copilot-cli. Both use the target tool's own plugin-management CLI
(
claude plugin/copilot plugin): materialize the embedded tree, add or update the marketplace source, install or update the plugin, read the result back through the CLI's own list command. Full mcp, hooks, commands, agents, and skills, since the tool copies the whole tree itself, so there is no per-surface translation. Claude Code has one write outside that model: a host-declared status line goes into thestatusLinekey of the user's ownsettings.json, since no plugin tree carries one. copilot-cli is user-scope only (no--scopeon thecopilotCLI) and can't pin a GitHub ref (owner/repo@refis misparsed; only the bare repo registers, tracking copilot's default branch). Claude Code details: How It Works. -
The 23 config-merge agents. None have a plugin or marketplace concept, so each backend
read-modify-writes the tool's own config file: mcp servers keyed by name, hooks/commands/agent
defs translated under a plugin-name-prefixed path. Only the entries agentgear wrote get touched,
so the user's own config survives. A backend writes only when
detect()finds the tool installed and it has a surface at the target scope.
The deep cross-harness tables that used to live here now sit on the capability pages, one home per surface:
- Environment overrides — the config-dir env var each backend honors, for redirecting a hermetic run: Detection § environment overrides.
- Hook event mapping — the CC-event → per-harness-event map, the renames, and the per-backend hook notes: Hooks § event map.
- MCP shapes — the stdio body per tool, the remote dialects, and the fidelity verdicts: MCP.
See Capabilities for the full index.
A host that uses the derive customizes through the #[plugin(..)] attributes plus
install_into for runtime agent selection. Every knob configures the whole plugin. None
reshapes one backend: nothing overrides how a backend renders (a different MCP command for
codex) or drops a surface for one backend (skipping hooks on cursor). Each backend writes what
its target tool supports, gated by detection and the ${CLAUDE_PLUGIN_ROOT} portability filter.
instructions_fn and statusline_fn are the only method overrides the derive exposes, because it
emits the sole impl PluginHost. For anything past the attributes, write impl PluginHost by hand. Every trait
item is public: supply the five consts (NAME, MARKETPLACE, VERSION, DEFAULT_SOURCE,
AGENTS) and embedded_blob(), and the lifecycle methods (install, update, self_heal,
doctor, …) come with the trait. You give up three derive-only guarantees:
- the compile error when the
build.rsguard is missing; - the
agents-vs-feature check; - the baked
include_bytes!tree: a hand-writtenembedded_blob()returns&[], so such a host installs fromSource::PathorSource::GitHub, notSource::Embedded.
AgentBackend is the whole seam: seven methods plus a defaulted forget, one registry arm. A host opts a plugin into an
in-crate backend by naming its id in the derive's agents = [...] list.
- Add a feature in
Cargo.tomland a module undersrc/agents/, one file per backend.agents/amp.rs(mcp-only) is the smallest real template to copy. - Implement the trait against the shared writers:
confedit(atomic, BOM-safe json/toml/yaml read-modify-write),mcpjson/mcptoml(the common server shapes). Keep the invariants every backend holds: write only entries the plugin owns;removedeletes exactly whatreconcilewrote; a secondreconcileis a trueNoOp; skip a surface the tool lacks instead of writing under a guessed key. - Add the id arm in
backend_for(src/agents/mod.rs) and a hermetic lifecycle test against a tempHOME(copy anycrates/host-fixture/tests/<id>.rs).
The trait is unsealed: every method, probe, reconcile, remove, report included,
is implementable from outside the crate, not only the read-only ones a thin wrapper could reach.
Capabilities is a plain struct, so a field added to it is a breaking change for an external impl
that constructs one — statusline is the first, and pre-1.0 the crate spends that rather than
reshaping the struct.
An external backend maps its own failures to Error::Backend { agent, detail } instead of
borrowing an in-crate variant's meaning, builds report() from the now-pub
DoctorReport::from_checks (or from_error on an upfront failure), and renders from
Plugin::components(&source), the same harness-agnostic IR the in-crate backends parse from,
rather than walking the plugin tree by hand. Field-level detail on both is on
Types and errors.
The id registry backend_for resolves stays closed, so the derive's agents = [...] still cannot
name an external backend. It never joins the locked, stamped install/self_heal fan-out; it
runs beside that fan-out instead, driven directly:
use agentgear::{AgentBackend, Desired, PluginHost, Scope, Source};
let plugin = MyHost::descriptor();
MyHost::install(Scope::User, Source::Embedded)?; // the built-in agents
let source = Source::Embedded;
let components = plugin.components(&source)?; // the parsed plugin IR
let desired = Desired { source: source.clone(), reenable: true };
MyToolBackend.reconcile(&plugin, &desired, &Scope::User)?; // yours, rendering from `components`
MyToolBackend.remove(&plugin, &Scope::User, &source)?; // symmetric with reconcile