agent

package
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 28, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package agent registers coding agents and computes how to launch them against an OpenRouter model.

Index

Constants

This section is empty.

Variables

View Source
var ErrIncompatibleModel = errors.New("model may not be fully compatible with this agent")

ErrIncompatibleModel reports a pairing an agent does not fully support. It is advisory: callers confirm rather than abort.

View Source
var ErrUnknownAgent = errors.New("unknown agent")

ErrUnknownAgent is returned when a name matches no agent or alias.

View Source
var ErrUnsupportedProvider = errors.New("unsupported provider")

ErrUnsupportedProvider is the sentinel that distinguishes "this agent does not work against this provider", which is a routine fact a registry records, from a genuine construction failure, which is a bug.

Functions

func ExecArgs

func ExecArgs(c Command) (argv []string, env []string)

ExecArgs assembles the argv and environment for a Command. argv[0] is the binary path, as exec expects.

Every inherited entry whose key also appears in c.Env is dropped before c.Env is appended, so each key occurs exactly once, carrying the command's value. This dedup is required because execve(2) does not deduplicate envp: POSIX getenv returns the FIRST match, so naively appending c.Env after the inherited environment would let the inherited value win on any duplicate key - the opposite of what callers need (e.g. overriding ANTHROPIC_BASE_URL for a user who already has it exported). The relative order of the surviving inherited entries is preserved.

func Run

func Run(c Command) error

Run replaces the current process with the agent. On success it does not return: signals, job control, and TTY behavior are then identical to invoking the agent directly.

func RunWait

func RunWait(c Command) error

RunWait runs the command as a child process and waits for it — the launch path for ConfigWriter agents, whose restore must run after the session ends (syscall.Exec would replace this process and nothing after it could run). Same env merge as Run via ExecArgs; stdio is inherited. SIGINT and SIGTERM are forwarded to the child so the interactive session dies on its own terms while our restore still runs; on Windows Signal is best-effort and a failed forward is ignored. The returned error is cmd.Wait()'s — including *exec.ExitError, which main's exit-code extraction understands.

func Unsupported

func Unsupported(reason string) error

Unsupported is what a Definition's New returns when the agent cannot be pointed at the bound provider. The reason is user-facing prose and reaches the user unaltered.

Types

type Binding

type Binding struct {
	// Provider is the endpoint every launcher in this registry points at.
	Provider Provider
	// Host is the identity of the tool doing the launching.
	Host Host
	// LookPath resolves a binary on PATH. nil means exec.LookPath, which is
	// what every launcher falls back to on its own; it is here so a caller
	// can make an entire registry answer install checks from a fixture
	// rather than from the machine running the test.
	LookPath func(string) (string, error)
}

Binding is everything a launcher needs that is the same for every launcher in one tool: where the tokens go, who is doing the launching, and how a binary is found on PATH.

It exists because the eleven launchers each carried an identical Provider/Host/LookPath triple. Constructing them against a Binding is what turns the registry from a package-level literal — one provider, decided at compile time — into a value a second tool can build for its own provider.

func (Binding) Validate

func (b Binding) Validate() error

Validate reports why a Binding cannot be used. NewRegistry calls it before constructing anything, so a malformed descriptor fails at composition rather than at the first launch that happens to read the bad field.

type Claude

type Claude struct {
	// Provider is the endpoint Claude Code is pointed at. Required, with no
	// fallback: a launcher whose provider was never wired would otherwise
	// reach a default while its tests agreed it was configured correctly.
	Provider Provider
	// Host identifies this tool in the guidance attached to a rejected
	// passthrough argument, and — for droid — owns the marker stamped into
	// the agent's own settings. Required.
	Host Host
	// LookPath is injectable for tests; nil means exec.LookPath.
	LookPath func(string) (string, error)
}

Claude launches Claude Code against a model on the bound provider.

func (*Claude) CheckInstalled

func (c *Claude) CheckInstalled() bool

CheckInstalled reports whether the claude binary can be found.

func (*Claude) CheckModel

func (c *Claude) CheckModel(m catalog.Model) error

CheckModel warns when pairing Claude Code with a non-Anthropic model. OpenRouter documents that Claude Code may fail on context-management features with other providers, but it does work for many, so this is advisory rather than fatal.

An empty Provider means the catalog does not express a vendor namespace at all — a locally served model is just "qwen3-coder:30b" — and an unknown vendor is not evidence of incompatibility. Warning there would fire on every model the catalog offers, which is the same "advisory that is always on" that hermes's context floor avoids by ignoring an unknown length.

func (*Claude) Command

func (c *Claude) Command(req Request) (Command, error)

Command builds the Claude Code invocation. It is pure: nothing is written and no process is started.

func (*Claude) DisplayName

func (c *Claude) DisplayName() string

func (*Claude) InstallHint

func (c *Claude) InstallHint() string

InstallHint tells the user how to install Claude Code.

func (*Claude) Name

func (c *Claude) Name() string

type Cline

type Cline struct {
	// Provider is the endpoint this agent is pointed at. Required, with no
	// fallback — see the note on Claude.Provider.
	Provider Provider
	// Host identifies this tool in the guidance attached to a rejected
	// passthrough argument, and — for droid — owns the marker stamped into
	// the agent's own settings. Required.
	Host Host
	// LookPath is injectable for tests; nil means exec.LookPath.
	LookPath func(string) (string, error)
}

Cline launches the Cline CLI against an OpenRouter model via its native builtin openrouter provider (base URL baked in upstream). Note cline's --auto-approve defaults to TRUE upstream; per the owner decision recorded in the Phase 4 spec, the launcher does not override agent behavior defaults. Doc-verified on 3.0.51, then LIVE-verified on 3.0.52 (2026-08-09); see .superpowers/sdd/2026-08-09-tier-2-research/cline.md.

The key goes on argv via -k, and cline is a ConfigWriter. Both follow from two measurements that overturned this launcher's original env-only design (research open questions 1 and 3, resolved by running it):

  1. The interactive TUI's provider gate reads PERSISTED settings and never the environment, so an env-only launch renders "Connect a model provider to get started" no matter what the environment holds. This is the reported bug, and it applies cold or warm.
  2. The CLI client we exec does not resolve credentials or call the model. A long-lived hub daemon does (one per data dir, `--cline-hub-daemon` on a local WebSocket), and its credential chain — explicit key, then OAuth resolver, then OPENROUTER_API_KEY from ITS OWN process environment — reads the environment of whatever first spawned it. So env delivery works only while our launch is what starts the daemon; once one is running, ours is ignored and its startup key is used for every later session. Measured: a launch carrying a dummy key returned a real completion off a daemon holding the user's own key.

Together those are why the original design looked verified and still failed in use: the Phase 4a live gate ran one-shot prompts against a virgin ~/.cline, which is exactly the pair of conditions — no TUI, cold daemon — under which env-only does work.

-k is honored in both modes, and it outranks the daemon's environment and a saved providers.json key alike (all three measured). It costs argv exposure via /proc/<pid>/cmdline — accepted by owner decision, there being no working alternative — and it makes cline persist the key into its own provider store, which is what Apply exists to undo.

func (*Cline) Apply

func (c *Cline) Apply(_ Request) (func() error, error)

Apply snapshots cline's provider store and returns the restore that puts it back. It is the mirror image of droid's ConfigWriter: droid's Apply writes the agent's config itself, whereas here the AGENT does the writing — -k makes cline save our key as settings.apiKey — and our only job is to guarantee the write does not outlive the session. Implementing the capability is also what puts cline on the fork-and-wait launch path, which is the only way any restore of ours can run.

Nothing is parsed: the snapshot is raw bytes, so a provider store in a shape we do not recognise still round-trips exactly.

The Request is deliberately unused — the signal that this Apply configures nothing. Model and key both reach cline on argv (Command), so there is nothing here to derive from the request; taking it is the interface's shape, not a need of ours.

NOT SAFE against a concurrent launcher session of cline. The snapshot is taken from whatever is on disk, so two overlapping sessions interleave as: A snapshots the user's clean file, cline persists our key, B snapshots THAT (key included), A restores clean, B restores the copy holding the key — which then outlives every session, the one outcome this function exists to prevent. Serialising it needs a lock held across Apply, the run, and the restore, i.e. one owned by the launch service rather than by this method, and a lock file is a sixth write site under Landmine 6. Documented in README "Known caveats" instead, by owner decision (2026-08-16); the tool assumes one session at a time throughout.

func (*Cline) CheckInstalled

func (c *Cline) CheckInstalled() bool

CheckInstalled reports whether the cline binary can be found. npm global installs land on PATH; there is no home-dir fallback.

func (*Cline) Command

func (c *Cline) Command(req Request) (Command, error)

Command builds the cline invocation. Pure: nothing written, nothing spawned. The key goes on argv (-k) because nothing else reaches the session; see the type comment for the measurements. The env var is set as well, for the cold-start case where our client is what spawns the hub daemon and the daemon inherits our environment instead of a stray export.

func (*Cline) DisplayName

func (c *Cline) DisplayName() string

func (*Cline) InstallHint

func (c *Cline) InstallHint() string

InstallHint tells the user how to install the Cline CLI.

func (*Cline) Name

func (c *Cline) Name() string

type Codex

type Codex struct {
	// Provider is the endpoint codex is pointed at. Required, with no
	// fallback — see the note on Claude.Provider.
	Provider Provider
	// Host identifies this tool in the guidance attached to a rejected
	// passthrough argument, and — for droid — owns the marker stamped into
	// the agent's own settings. Required.
	Host Host
	// LookPath is injectable for tests; nil means exec.LookPath.
	LookPath func(string) (string, error)
}

Codex launches the OpenAI Codex CLI against an OpenRouter model. All configuration travels as -c overrides on the command line; nothing is written into ~/.codex.

func (*Codex) CheckInstalled

func (c *Codex) CheckInstalled() bool

CheckInstalled reports whether the codex binary can be found. npm global installs land on PATH, so there is no home-dir fallback.

func (*Codex) Command

func (c *Codex) Command(req Request) (Command, error)

Command builds the codex invocation. It is pure: nothing is written and no process is started. Managed overrides come before passthrough args so they apply even when the passthrough starts with a subcommand; conflicting passthrough is rejected because a later -c with the same key would win and silently point codex somewhere else.

func (*Codex) DisplayName

func (c *Codex) DisplayName() string

func (*Codex) InstallHint

func (c *Codex) InstallHint() string

InstallHint tells the user how to install Codex.

func (*Codex) Name

func (c *Codex) Name() string

type Command

type Command struct {
	Path string
	Args []string
	Env  []string
}

Command is a process to run. Env entries override any inherited environment variable of the same name; see ExecArgs for the merge.

type Compatible

type Compatible interface {
	CheckModel(catalog.Model) error
}

Compatible validates a model against agent-specific requirements. Returning an error wrapping ErrIncompatibleModel produces a confirmation prompt, not a hard failure.

type ConfigWriter

type ConfigWriter interface {
	Apply(Request) (restore func() error, err error)
}

ConfigWriter is the escape hatch for agents whose own config file has to change for the launch to work — droid since Phase 4, and cline, whose Apply exists only to snapshot and restore a file the AGENT writes (Landmine 36). An agent implementing it takes the fork-and-wait launch path so that the returned restore function can run.

type CredentialShadowCheck

type CredentialShadowCheck interface {
	ShadowedCredential() string
}

CredentialShadowCheck reports stored agent-side state that would make a launch ignore the environment this tool provides — a saved credential that outranks env vars (pi and hermes document exactly that; cline did too until its key moved to argv, see Landmine 36), or a binary generation that does not read them (legacy kimi-cli). Read-only and best-effort: implementations must never write, and must return "" (no warning) when the state is absent, unreadable, or unparseable — a detector failure must never block a launch.

type Definition

type Definition struct {
	// Name is the canonical name users type. It must equal the constructed
	// launcher's Name(); NewRegistry checks that rather than trusting it,
	// because the two are now separate sources for one string.
	Name string
	// DisplayName is the human name. It must equal the constructed
	// launcher's DisplayName(), and it is also what a rejected agent's
	// placeholder launcher reports — the reason this field exists at all,
	// since a launcher that was never constructed cannot be asked.
	DisplayName string
	// Description is the one-line summary the agents listing renders.
	Description string
	// Aliases are alternate names resolving to this agent.
	Aliases []string
	// New builds the launcher for a Binding. An error wrapping
	// ErrUnsupportedProvider means "this agent cannot be pointed at that
	// provider": the registry records the reason on Spec.Status and carries
	// on, so no new guard is needed anywhere — the existing Status machinery
	// already refuses the launch, lists the agent under --all with its
	// reason, and skips it on the TUI's root screen. Any other error is a
	// construction failure and fails the whole registry.
	New func(Binding) (Launcher, error)
}

Definition is a registry INPUT: what an agent is, independent of any provider. It is the provider-neutral half of a Spec — everything a Spec carries except the constructed Launcher and the Status that construction determines.

func Builtins

func Builtins() []Definition

Builtins returns the agent definitions this package ships, in display order. A tool composes its registry from them — MustRegistry(binding, Builtins()) for all of them, or a filtered slice — which is what lets a second tool reuse thirteen launch recipes rather than rewrite them against its own provider.

The two desktop apps are definitions like any other; they simply report that no provider can be injected into them. Their New never inspects the binding, which is the honest statement: the refusal is a fact about the app, not about where it would have been pointed.

type Droid

type Droid struct {
	// Provider is the endpoint this agent is pointed at. Required, with no
	// fallback — see the note on Claude.Provider.
	Provider Provider
	// Host identifies this tool in the guidance attached to a rejected
	// passthrough argument, and — for droid — owns the marker stamped into
	// the agent's own settings. Required.
	Host Host
	// LookPath is injectable for tests; nil means exec.LookPath.
	LookPath func(string) (string, error)
}

Droid launches Factory's droid via the ConfigWriter escape hatch — the ONE sanctioned agent-owned write (Landmine 6 as amended). Factory documents OpenRouter BYOK, but the only declaration surface is a .factory settings file: no env var, no flag, no inline config (owner decision at spec review: ConfigWriter, not unsupported). Apply writes a single marker-owned customModels entry into ~/.factory/settings.local.json (the merge-friendly local layer, never settings.json) with apiKey "${<provider key var>}" — env interpolation, so the key never touches disk — and points the default-model key at it; restore puts both back. Model selection lives in the file, NOT on argv: the entry's index-derived custom: ID is only knowable at Apply time, and Command is pure. Requires a Factory account even for BYOK. Doc-verified on 0.190.0 (2026-08-09); see .superpowers/sdd/2026-08-09-tier-2-research/droid.md.

func (*Droid) Apply

func (d *Droid) Apply(req Request) (func() error, error)

Apply upserts the marker-owned model entry and default-model key, and returns the restore that undoes exactly that. An unparseable settings file is a hard error — never clobber what we cannot understand.

NOT SAFE against a concurrent launcher session of droid, for a different reason than cline's. priorModel is captured from the file as it stands, so a second Apply that starts while our session runs records OUR marker value as the thing to restore, and also evicts our live entry (foreignDroidModels keeps only NON-marker entries, so it drops ours while adding its own). The second restore to run then finds no marker entries left to strip and writes `model` back to a `custom:<marker>-*` name that nothing defines — leaving the user a dangling reference to clear by hand. Serialising it needs a lock spanning Apply, the run and the restore, which would be a sixth Landmine 6 write site; documented in README "Known caveats" instead, by owner decision (2026-08-16).

func (*Droid) CheckInstalled

func (d *Droid) CheckInstalled() bool

CheckInstalled reports whether the droid binary can be found. The standalone installer puts it in ~/.local/bin, which the installer adds to PATH; there is no reliable secondary location.

func (*Droid) Command

func (d *Droid) Command(req Request) (Command, error)

Command builds the droid invocation: passthrough only, no -m (see the type comment), key in env for the settings file's interpolation.

func (*Droid) DisplayName

func (d *Droid) DisplayName() string

func (*Droid) InstallHint

func (d *Droid) InstallHint() string

InstallHint tells the user how to install droid. Printed, never run. Droid requires a Factory account even on the BYOK-only tier.

func (*Droid) Name

func (d *Droid) Name() string

func (*Droid) Restore added in v0.4.0

func (d *Droid) Restore() error

Restore removes this tool's marker-owned entries from droid's settings out of band, for when the launch that wrote them never ran its own restore.

It is deliberately weaker than Apply's closure. The closure captured the user's prior `model` value; nothing on disk records it. So Restore clears `model` only when it still names one of OUR entries — a dangling `custom:<marker>-N` reference is worse than an absent key, and clearing a value we did not write would be worse still.

func (*Droid) RestoreHint added in v0.4.0

func (d *Droid) RestoreHint() string

RestoreHint names the file Restore touches.

type Hermes

type Hermes struct {
	// Provider is the endpoint this agent is pointed at. Required, with no
	// fallback — see the note on Claude.Provider.
	Provider Provider
	// Host identifies this tool in the guidance attached to a rejected
	// passthrough argument, and — for droid — owns the marker stamped into
	// the agent's own settings. Required.
	Host Host
	// LookPath is injectable for tests; nil means exec.LookPath.
	LookPath func(string) (string, error)
	// contains filtered or unexported fields
}

Hermes launches Nous Research's Hermes Agent CLI against an OpenRouter model. OpenRouter is a first-class hermes provider; --provider/--model on the chat subcommand are vendor-documented per-run overrides with "no mutation to ~/.hermes/config.yaml", and CLI args outrank all config files. Doc-verified on v0.20.0 (2026-08-09); see .superpowers/sdd/2026-08-09-tier-2-research/hermes.md.

func (*Hermes) CheckInstalled

func (h *Hermes) CheckInstalled() bool

CheckInstalled reports whether the hermes binary can be found.

func (*Hermes) CheckModel

func (h *Hermes) CheckModel(m catalog.Model) error

CheckModel warns (advisory, Landmine 7) for models under hermes's context floor. Unknown context stays silent: missing catalog data is not evidence of incompatibility.

func (*Hermes) Command

func (h *Hermes) Command(req Request) (Command, error)

Command builds the hermes invocation. Pure: nothing written, nothing spawned. Managed flags ride the chat subcommand, so a passthrough that starts with a different subcommand is refused rather than silently misconfigured.

func (*Hermes) DisplayName

func (h *Hermes) DisplayName() string

func (*Hermes) InstallHint

func (h *Hermes) InstallHint() string

InstallHint tells the user how to install Hermes. Printed, never run.

func (*Hermes) Name

func (h *Hermes) Name() string

func (*Hermes) ShadowedCredential

func (h *Hermes) ShadowedCredential() string

ShadowedCredential reports stored hermes credentials that can outrank or rotate past the key this launch provides: a key line for the provider's variable in ~/.hermes/.env, or a credential pool for the provider in ~/.hermes/auth.json. Both are keyed on the provider, since hermes stores one set per provider it knows about.

type HermesDesktop added in v0.4.0

type HermesDesktop struct {
	Hermes
}

HermesDesktop launches hermes's Electron desktop app against the same provider and model the terminal launcher uses.

It is NOT a separate application with its own account — it is the hermes binary plus a subcommand, which is why it can be pointed at a provider at all where the other desktop apps could not. Everything except the argv shape is inherited: the context floor, the credential-shadow check, the binary search and the install hint are hermes's, and duplicating them here would let them drift.

The provider flags are TOP-LEVEL and must precede the subcommand. Live-verified on v0.20.5 (2026-08-28): `hermes --provider X --model Y desktop --help` exits 0, and `hermes desktop` itself declares no provider or model option, so the flags after the subcommand would be a parse error.

func (*HermesDesktop) Command added in v0.4.0

func (h *HermesDesktop) Command(req Request) (Command, error)

Command builds the desktop invocation by rewriting the terminal one: the embedded Hermes owns every guard, in its one fixed order, and the environment; this replaces only the subcommand its args are built around.

The bare-subcommand guard's error message is renamed for this launcher by setting agentName/subcommand on a COPY of the embedded Hermes — never on h.Hermes itself, which is shared across calls — and delegating to that copy's Command. Hermes.Command's guard sequence, and its position in it, stay exactly as Hermes runs them: a request that also trips an earlier guard (an unusable API key, a conflicting --model) reports that guard's error here exactly as it would through plain Hermes, rather than this launcher's argv rewrite pre-empting it with the wrong failure.

func (*HermesDesktop) DisplayName added in v0.4.0

func (h *HermesDesktop) DisplayName() string

func (*HermesDesktop) Name added in v0.4.0

func (h *HermesDesktop) Name() string

type Host

type Host struct {
	// Name is the command users type. It appears in the guidance attached to
	// a rejected passthrough argument ("<Name> manages the model; pick it
	// with <Name> <agent> -m"), so it is the binary's invocable name rather
	// than a prose title.
	Name string

	// Marker identifies entries this tool owns inside an AGENT's own
	// configuration: droid's customModels displayName, and the
	// "custom:<Marker>-<n>" selection ID derived from it.
	//
	// It is a separate field from Name, and must be treated as persisted
	// data rather than a label. droid's Apply strips only entries whose
	// displayName equals it, so changing Marker orphans every entry a
	// previous version of this tool left in a user's
	// ~/.factory/settings.local.json — each one preserved forever as
	// "foreign", plus a possibly-dangling default-model reference they have
	// to clear by hand. Changing it is a data migration, not a rename.
	Marker string
}

Host is the identity of the tool doing the launching. Every user-facing string this package produces that names a TOOL — rather than an agent or a provider — comes from here, and so does the token stamped into the one agent-owned config this package writes.

func (Host) Validate

func (h Host) Validate() error

Validate reports why a Host cannot be used.

type Installable

type Installable interface {
	CheckInstalled() bool
	InstallHint() string
}

Installable reports whether the agent's binary is present.

type Installer

type Installer interface {
	EnsureInstalled() error
}

Installer can install the agent. Implemented by no agent in Phase 1; callers must confirm before invoking it.

type Kimi

type Kimi struct {
	// Provider is the endpoint this agent is pointed at. Required, with no
	// fallback — see the note on Claude.Provider.
	Provider Provider
	// Host identifies this tool in the guidance attached to a rejected
	// passthrough argument, and — for droid — owns the marker stamped into
	// the agent's own settings. Required.
	Host Host
	// LookPath is injectable for tests; nil means exec.LookPath.
	LookPath func(string) (string, error)
}

Kimi launches Moonshot AI's Kimi Code CLI against an OpenRouter model via the KIMI_MODEL_* env family, which synthesizes a provider+model in memory ("nothing is written back to the config file" — vendor docs) and outranks config.toml; only a -m flag beats it, which we never pass and reject in passthrough.

Deliberately NOT ported from ollama: its `kimi --config '<json>'` with provider type "openai_legacy" targets the deprecated legacy Python kimi-cli. Kimi Code CLI has neither the flag nor the type — porting it would repeat Landmine 18 (see the Phase 4 spec and .superpowers/sdd/2026-08-09-tier-2-research/kimi.md). Doc-verified on kimi-code 0.34.0, KIMI_MODEL_* channel present since 0.6.0 (2026-08-09).

func (*Kimi) CheckInstalled

func (k *Kimi) CheckInstalled() bool

CheckInstalled reports whether a kimi binary can be found.

func (*Kimi) Command

func (k *Kimi) Command(req Request) (Command, error)

Command builds the kimi invocation. Pure: nothing written, nothing spawned. KIMI_MODEL_MAX_CONTEXT_SIZE comes from the catalog; when the catalog does not know the context length, the variable is omitted so kimi's documented default applies instead of a fabricated zero.

func (*Kimi) DisplayName

func (k *Kimi) DisplayName() string

func (*Kimi) InstallHint

func (k *Kimi) InstallHint() string

InstallHint tells the user how to install Kimi Code CLI. Printed, never run. Windows additionally needs Git for Windows (kimi uses Git Bash as its shell backend).

func (*Kimi) Name

func (k *Kimi) Name() string

func (*Kimi) ShadowedCredential

func (k *Kimi) ShadowedCredential() string

ShadowedCredential flags a legacy-only install: the deprecated Python kimi-cli ignores KIMI_MODEL_* entirely, so a launch would silently run on the user's Moonshot account instead of OpenRouter. Pure path heuristic — executing the binary to ask its version would violate launch purity. A PATH hit is trusted (the Kimi Code installer renames legacy shims to kimi-legacy); only a uv-tools-dir resolution with no Kimi Code install alongside is confidently legacy.

type Launcher

type Launcher interface {
	Name() string
	DisplayName() string
	Command(Request) (Command, error)
}

Launcher computes the process that runs an agent against a model. Implementations MUST be pure: no file writes, no network, no spawning.

type OMP

type OMP struct {
	// Provider is the endpoint this agent is pointed at. Required, with no
	// fallback — see the note on Claude.Provider.
	Provider Provider
	// Host identifies this tool in the guidance attached to a rejected
	// passthrough argument, and — for droid — owns the marker stamped into
	// the agent's own settings. Required.
	Host Host
	// LookPath is injectable for tests; nil means exec.LookPath.
	LookPath func(string) (string, error)
}

OMP launches Oh My Pi against an OpenRouter model. OpenRouter is a built-in omp provider with the base URL baked in upstream; the model selector is "openrouter/<slug>" — the prefix IS the provider selection in omp's dialect (unlike its ancestor pi, which takes --provider plus a bare slug). Nothing is written; ollama's models.yml write existed only because Ollama is a custom provider there.

Known, documented, NOT runtime-detected: omp's stored credentials (~/.omp/agent/agent.db, sqlite) outrank the env key — "env vars are a fallback, not an override". No sqlite dependency for one advisory; the caveat lives in the spec and README. Doc-verified on 17.2.11 (2026-08-09); see .superpowers/sdd/2026-08-09-tier-2-research/omp.md.

func (*OMP) CheckInstalled

func (o *OMP) CheckInstalled() bool

CheckInstalled reports whether the omp binary can be found.

func (*OMP) Command

func (o *OMP) Command(req Request) (Command, error)

Command builds the omp invocation. Pure: nothing written, nothing spawned. Passthrough --api-key stays allowed: it is the user's explicit, documented override of omp's stored-credential precedence.

func (*OMP) DisplayName

func (o *OMP) DisplayName() string

func (*OMP) InstallHint

func (o *OMP) InstallHint() string

InstallHint tells the user how to install Oh My Pi. Printed, never run.

func (*OMP) Name

func (o *OMP) Name() string

type OpenClaw

type OpenClaw struct {
	// Provider is the endpoint this agent is pointed at. Required, with no
	// fallback — see the note on Claude.Provider.
	Provider Provider
	// Host identifies this tool in the guidance attached to a rejected
	// passthrough argument, and — for droid — owns the marker stamped into
	// the agent's own settings. Required.
	Host Host
	// LookPath is injectable for tests; nil means exec.LookPath.
	LookPath func(string) (string, error)
}

OpenClaw launches OpenClaw against an OpenRouter model. Interactive sessions run `openclaw tui --local` — the embedded runtime, no gateway or daemon — with the model in a LAUNCHER-OWNED config file passed via OPENCLAW_CONFIG_PATH (openclaw's tui has no model flag or env var). That file is write site #3 of the amended Landmine 6; it lives under OUR config dir, holds only the model ref, and replaces the user's own openclaw config for the session (owner-approved at spec review — a launched session deliberately does not load their channels/plugins). One-shot `agent exec` passthrough needs no file: --model plus --auth-env-only compose config in memory. Doc-verified on 2026.7.1-2 (2026-08-09); see .superpowers/sdd/2026-08-09-tier-2-research/openclaw.md.

func (*OpenClaw) CheckInstalled

func (o *OpenClaw) CheckInstalled() bool

CheckInstalled reports whether an openclaw (or legacy clawdbot) binary can be found. npm global installs land on PATH.

func (*OpenClaw) Command

func (o *OpenClaw) Command(req Request) (Command, error)

Command builds the openclaw invocation. Pure: the staged file is declared by StagedFiles and written by the launch service, never here.

func (*OpenClaw) DisplayName

func (o *OpenClaw) DisplayName() string

func (*OpenClaw) InstallHint

func (o *OpenClaw) InstallHint() string

InstallHint tells the user how to install OpenClaw. Printed, never run.

func (*OpenClaw) Name

func (o *OpenClaw) Name() string

func (*OpenClaw) ShadowedCredential

func (o *OpenClaw) ShadowedCredential() string

ShadowedCredential reports stored OpenClaw auth profiles for the bound provider: a prior onboard/OAuth stores a key that participates in auth rotation, and its precedence against the env key is undocumented — surface it.

func (*OpenClaw) StagedFiles

func (o *OpenClaw) StagedFiles(req Request) ([]StagedFile, error)

StagedFiles declares the launcher-owned model config for interactive launches. Pure: returns data; launch.Service.Launch writes it. No secret goes in — the key travels in env only — so the mode is 0644.

type OpenCode

type OpenCode struct {
	// Provider is the endpoint this agent is pointed at. Required, with no
	// fallback — see the note on Claude.Provider.
	Provider Provider
	// Host identifies this tool in the guidance attached to a rejected
	// passthrough argument, and — for droid — owns the marker stamped into
	// the agent's own settings. Required.
	Host Host
	// LookPath is injectable for tests; nil means exec.LookPath.
	LookPath func(string) (string, error)
}

OpenCode launches opencode against an OpenRouter model. The entire config travels inline in OPENCODE_CONFIG_CONTENT; opencode's native openrouter provider reads the provider's key variable. Nothing is written to disk — in particular not opencode's model-state file, which ollama's integration edits and we deliberately do not.

func (*OpenCode) CheckInstalled

func (o *OpenCode) CheckInstalled() bool

CheckInstalled reports whether the opencode binary can be found.

func (*OpenCode) Command

func (o *OpenCode) Command(req Request) (Command, error)

Command builds the opencode invocation. It is pure: nothing is written and no process is started.

func (*OpenCode) DisplayName

func (o *OpenCode) DisplayName() string

func (*OpenCode) InstallHint

func (o *OpenCode) InstallHint() string

InstallHint tells the user how to install OpenCode. Printed, never run.

func (*OpenCode) Name

func (o *OpenCode) Name() string

type Pi

type Pi struct {
	// Provider is the endpoint this agent is pointed at. Required, with no
	// fallback — see the note on Claude.Provider.
	Provider Provider
	// Host identifies this tool in the guidance attached to a rejected
	// passthrough argument, and — for droid — owns the marker stamped into
	// the agent's own settings. Required.
	Host Host
	// LookPath is injectable for tests; nil means exec.LookPath.
	LookPath func(string) (string, error)
}

Pi launches the pi coding agent (earendil-works/pi) against an OpenRouter model. OpenRouter is a built-in pi provider with the base URL baked in upstream, so the launch is two flags plus one env var; nothing is written. Doc-verified on pi 0.84.1 (2026-08-09); see .superpowers/sdd/2026-08-09-tier-2-research/pi.md.

func (*Pi) CheckInstalled

func (p *Pi) CheckInstalled() bool

CheckInstalled reports whether the pi binary can be found.

func (*Pi) Command

func (p *Pi) Command(req Request) (Command, error)

Command builds the pi invocation. Pure: nothing written, nothing spawned. The slug passes through verbatim — pi's catalog keys models by bare OpenRouter slugs; the provider is selected by --provider, never by an provider-prefixed model reference (that is omp's dialect, not pi's).

func (*Pi) DisplayName

func (p *Pi) DisplayName() string

func (*Pi) InstallHint

func (p *Pi) InstallHint() string

InstallHint tells the user how to install pi. The legacy @mariozechner/pi-coding-agent npm package is deprecated; install only the earendil-works one.

func (*Pi) Name

func (p *Pi) Name() string

func (*Pi) ShadowedCredential

func (p *Pi) ShadowedCredential() string

ShadowedCredential reports pi's documented precedence trap: a credential in ~/.pi/agent/auth.json (e.g. from "/login openrouter") outranks the provider's key variable, so the session would bill that stored account instead of the key this launch provides.

type PlatformSupported

type PlatformSupported interface {
	Supported() error
}

PlatformSupported reports whether the agent can run on this platform.

type Pool added in v0.4.0

type Pool struct {
	// Provider is the endpoint this agent is pointed at. Required, with no
	// fallback — see the note on Claude.Provider.
	Provider Provider
	// Host identifies this tool in the guidance attached to a rejected
	// passthrough argument. Required.
	Host Host
	// LookPath is injectable for tests; nil means exec.LookPath.
	LookPath func(string) (string, error)
}

Pool launches Poolside's coding agent against a provider-hosted model.

Poolside documents OpenRouter as a supported endpoint by name, and its standalone mode is entirely environment-driven: POOLSIDE_STANDALONE_BASE_URL is what SELECTS that mode, and the vendor documents the environment as being read BEFORE settings.yaml and credentials.json. That precedence is why this launcher is zero-touch and why it needs no CredentialShadowCheck — a saved Poolside credential cannot outrank what we pass. Live-verified on v1.0.16 (2026-08-28) against OpenRouter.

The model travels as POOLSIDE_STANDALONE_MODEL rather than on argv. Ollama's launcher passes -m and never sets the documented variable; the documented one is what was verified here.

func (*Pool) CheckInstalled added in v0.4.0

func (p *Pool) CheckInstalled() bool

CheckInstalled reports whether the pool binary can be found.

func (*Pool) Command added in v0.4.0

func (p *Pool) Command(req Request) (Command, error)

Command builds the pool invocation. Pure: nothing written, nothing spawned. Every managed value rides the environment, so the passthrough is forwarded untouched apart from the flags we own.

func (*Pool) DisplayName added in v0.4.0

func (p *Pool) DisplayName() string

func (*Pool) InstallHint added in v0.4.0

func (p *Pool) InstallHint() string

InstallHint tells the user how to install pool. Printed, never run — and the installer requires accepting Poolside's EULA, which is the user's to accept, not ours.

func (*Pool) Name added in v0.4.0

func (p *Pool) Name() string

type Provider

type Provider struct {
	// ID is this provider's token as it appears on an agent's command line
	// and inside agent configuration: cline's -P, pi's and hermes's
	// --provider, codex's model_providers.<ID>.*, the "<ID>/<slug>" model
	// reference omp, opencode and openclaw take, and the key a stored
	// credential check looks for in an agent's own auth file. Lowercase.
	ID string
	// DisplayName is the human name: codex's model_providers.<ID>.name, and
	// the subject of every "an X API key is required".
	DisplayName string

	// BaseURL is the OpenAI-compatible root, and it INCLUDES whatever version
	// segment the vendor publishes ("https://openrouter.ai/api/v1",
	// "http://127.0.0.1:11434/v1"). Clients speaking that protocol append
	// only a method path — "/chat/completions", "/responses" — never a
	// version. Empty means the provider has no OpenAI-compatible surface.
	BaseURL string
	// AnthropicBaseURL is the Anthropic-Messages root, and it EXCLUDES a
	// version segment: Claude Code appends its own, so a /v1 here produces
	// /api/v1/v1/messages and breaks the launch. OpenRouter's two roots
	// therefore differ by exactly that segment — https://openrouter.ai/api
	// against https://openrouter.ai/api/v1 — which is Landmine 1.
	//
	// The two live side by side here on purpose. The invariant used to be two
	// constants in two packages that never referenced each other, so the
	// "these are the same URL, DRY them up" refactor could look reasonable
	// from either one alone. It is also a refactor that is RIGHT for other
	// providers — a local server's roots are host:11434 and host:11434/v1,
	// direct Anthropic's are identical — which is what made it tempting.
	// Adjacent fields under one comment, plus Validate's suffix check below,
	// is the strongest form this invariant has ever had.
	//
	// Empty means the provider has no Anthropic-compatible surface, which is
	// the ordinary case for a local OpenAI-compatible server, and which makes
	// Claude Code unlaunchable against it.
	AnthropicBaseURL string

	// APIKeyEnv is the environment variable the credential travels in, and
	// the name agents are told to read it from: codex's
	// model_providers.<ID>.env_key and droid's "${...}" interpolation both
	// need the NAME regardless of the value. Always set, even when
	// RequiresAPIKey is false.
	APIKeyEnv string
	// RequiresAPIKey reports whether the user must supply a key. False for a
	// local server, in which case a launch carries PlaceholderKey.
	RequiresAPIKey bool
	// PlaceholderKey is the credential sent when RequiresAPIKey is false. It
	// must be non-empty: several agents refuse an endpoint with no credential
	// at all, and Claude Code falls back to authenticating against Anthropic
	// directly when its credential slot is empty (Landmine 2).
	PlaceholderKey string
	// KeysURL is where a user obtains a key, quoted in the no-key error.
	KeysURL string

	// ModelPrefix is prepended to a model ID for agents that select a
	// provider through the model reference itself ("openrouter/" turns
	// anthropic/claude-opus-4.6 into openrouter/anthropic/claude-opus-4.6).
	// Empty means the agent is told the provider some other way.
	ModelPrefix string
	// WireAPI is codex's model_providers.<ID>.wire_api value. "responses" for
	// any endpoint serving OpenAI's newer protocol; "chat" only for one that
	// proxies /chat/completions alone. Read Landmine 18 before changing it:
	// "chat" is rejected at config-load time by codex >= 0.146.1, and the
	// value here is live-verified, not inferred from documentation.
	WireAPI string
}

Provider is the endpoint a launcher points an agent at. Every launcher in this package is parameterized by one, so that the same thirteen recipes serve OpenRouter, a locally served model, a direct vendor API, or a self-hosted gateway without being rewritten.

It carries no catalog and no credentials. It is the answer to "where does this agent send tokens, and what does it call that place", nothing more.

func (Provider) Credential

func (p Provider) Credential(agentName, userKey string) (string, error)

Credential returns the value a launch sends. userKey is what the host resolved from its own configuration; it is ignored for a provider that needs no user key, which sends PlaceholderKey instead.

The returned credential is never empty on the success path — see PlaceholderKey for why that matters.

func (Provider) EnvEntry

func (p Provider) EnvEntry(key string) string

EnvEntry returns the "NAME=value" entry carrying the credential.

func (Provider) EnvRef

func (p Provider) EnvRef() string

EnvRef returns the shell-style interpolation of the key variable, which is what droid writes into its settings file so the key never touches disk.

func (Provider) ModelRef

func (p Provider) ModelRef(modelID string) string

ModelRef returns the model reference an agent that selects a provider through the model name expects. Whether an agent takes this form or a bare slug plus a --provider flag is a fact about the AGENT, not the provider: pi and omp are pointed at the same OpenRouter and disagree.

func (Provider) UpperID

func (p Provider) UpperID() string

UpperID is the provider ID in the form agents use to build per-provider environment variable names (hermes reads <ID>_BASE_URL). Dashes become underscores, since a dash cannot appear in an environment variable name.

func (Provider) Validate

func (p Provider) Validate() error

Validate reports why a Provider cannot be used. It is called at registry construction so a misconfigured descriptor fails before any launch, in the same spirit as the nil-Launcher panic in buildIndex.

type Qwen

type Qwen struct {
	// Provider is the endpoint this agent is pointed at. Required, with no
	// fallback — see the note on Claude.Provider.
	Provider Provider
	// Host identifies this tool in the guidance attached to a rejected
	// passthrough argument, and — for droid — owns the marker stamped into
	// the agent's own settings. Required.
	Host Host
	// LookPath is injectable for tests; nil means exec.LookPath.
	LookPath func(string) (string, error)
}

Qwen launches Qwen Code against an OpenRouter model through its generic OpenAI-protocol auth: OPENAI_BASE_URL/OPENAI_API_KEY/OPENAI_MODEL env vars plus the MANDATORY --auth-type openai flag. Without the flag, qwen-code resolves auth from the user's persisted settings or its qwen-oauth default, both of which silently ignore every OPENAI_* env var (upstream issue #891) — the launch would look configured and run against the wrong backend. Doc-verified on 0.21.8 (2026-08-09); see .superpowers/sdd/2026-08-09-tier-2-research/qwen.md.

func (*Qwen) CheckInstalled

func (q *Qwen) CheckInstalled() bool

CheckInstalled reports whether the qwen binary can be found.

func (*Qwen) Command

func (q *Qwen) Command(req Request) (Command, error)

Command builds the qwen invocation. Pure: nothing written, nothing spawned. Both OPENAI_API_KEY and the provider's own key variable carry it: the generic openai auth path reads the former, qwen-code's dedicated OpenRouter recipe documents the latter.

func (*Qwen) DisplayName

func (q *Qwen) DisplayName() string

func (*Qwen) InstallHint

func (q *Qwen) InstallHint() string

InstallHint tells the user how to install Qwen Code. Printed, never run.

func (*Qwen) Name

func (q *Qwen) Name() string

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry is a set of agents bound to one provider. It is a value rather than package state so that a second tool — same thirteen recipes, different provider — builds its own instead of mutating a global.

func MustRegistry

func MustRegistry(b Binding, defs []Definition) *Registry

MustRegistry is NewRegistry for a composition root, where a bad registry is a programmer error in a literal and failing before main() does any work is the correct outcome. A library must not panic on a caller's slice, which is why NewRegistry returns an error and this wrapper is separate; the precedent is cli.NewRootCmdWith's nil-Service panic.

func NewRegistry

func NewRegistry(b Binding, defs []Definition) (*Registry, error)

NewRegistry resolves definitions against a binding.

A definition whose New reports ErrUnsupportedProvider is registered unsupported, carrying its reason: that is the whole mechanism by which an agent that cannot reach a given provider is explained rather than missing. Any other error from New fails the registry, because it means the definition is wrong, not the pairing.

func NewRegistryFromSpecs

func NewRegistryFromSpecs(specs []*Spec) (*Registry, error)

NewRegistryFromSpecs indexes specs that are already built, rejecting the programmer errors a registry literal can contain: a missing name, a nil Launcher, and any collision between names and aliases.

It is exported because building a Spec directly is what a consumer's tests need — an adversarial description, a launcher that reports itself uninstalled — without a Provider, a Host, or the machine's PATH being involved. Production code should use NewRegistry.

func (*Registry) Installed

func (r *Registry) Installed(s *Spec) bool

Installed reports whether the agent's binary is present. Agents that do not implement Installable are assumed present.

It is a method for the same reason List and Lookup are: a consumer injects one *Registry and gets all three answers from it, rather than three separately overridable function fields that can disagree about which registry they are talking about.

func (*Registry) List

func (r *Registry) List() []*Spec

List returns every registered agent in display order.

func (*Registry) Lookup

func (r *Registry) Lookup(name string) (*Spec, error)

Lookup resolves a canonical name or alias.

type Request

type Request struct {
	Model     catalog.Model
	APIKey    string
	ExtraArgs []string
	// StageDir is the directory launcher-owned files declared by StagedFiles
	// must live under. It is supplied by the caller and never guessed by a
	// launcher: the same value is what launch.Service.Launch validates staged
	// paths against, so the boundary being enforced and the boundary the
	// launcher computed against cannot drift apart. A launcher that needs it
	// and is handed an empty value must refuse rather than fall back —
	// filepath.Join("", "x.json") is a path in the working directory, which
	// is a write outside the sanctioned dir (Landmine 6).
	StageDir string
}

Request is everything a launcher needs to build its command.

type Restorable added in v0.4.0

type Restorable interface {
	Restore() error
	// RestoreHint names, in one line, what Restore touches. It is shown by
	// the restore command so a user can see which file changed.
	RestoreHint() string
}

Restorable undoes this tool's writes to an agent's own config OUT OF BAND — without having applied anything in this process.

ConfigWriter's restore is a closure captured at Apply time, which is what makes it exact and also what makes it unavailable once the launch that created it is gone: a process killed with SIGKILL, or a desktop app that outlives the command that started it, leaves the agent configured with no closure left to call. Restorable is the answer to that, and it reconstructs what to undo from the file plus Host.Marker rather than from captured state.

It is therefore allowed to be WEAKER than the matching closure, and droid is the worked example: the closure restores the user's prior model selection, while Restore can only remove a selection that still points at one of ours, because nothing on disk records what was there before.

Implementations MUST be idempotent — restoring an agent that was never configured is a success, not an error, since `restore --all` walks every implementation and most will have nothing to do — and MUST NOT remove configuration this tool did not write.

type Spec

type Spec struct {
	Name    string
	Aliases []string
	// Launcher is required: NewRegistryFromSpecs rejects a nil one, since
	// every caller (newLaunchCmds, the agents listing, Installed)
	// dereferences it unconditionally. An agent the Binding's provider
	// cannot serve still gets one — a placeholder that names the agent and
	// errors if it is ever asked for a Command.
	Launcher    Launcher
	Description string
	Status      Status
	// Provider is the bound provider's DisplayName, carried on the Spec
	// because the planner's CheckSupported has nothing else to read it from:
	// it takes a *Spec, and the refusal it produces has to name the provider
	// the agent could not be pointed at. Hardcoding a vendor there is what
	// this field replaced, and a second tool's refusal named the wrong
	// company until it existed.
	//
	// Empty is legitimate — NewRegistryFromSpecs builds Specs with no Binding
	// at all, for consumers' tests over adversarial entries — and
	// UnsupportedAgentError renders that case without a name rather than with
	// a blank one.
	Provider string
}

Spec is a registry entry: one Definition resolved against one Binding.

type Staged

type Staged interface {
	StagedFiles(Request) ([]StagedFile, error)
}

Staged is implemented by launchers that need launcher-owned files at launch time. StagedFiles MUST be pure, like Command. Distinct from ConfigWriter on purpose: Staged writes OUR files (idempotent overwrite, no undo, syscall.Exec handoff unaffected); ConfigWriter writes an AGENT'S file (backup and restore required, forces fork-and-wait). Do not merge them — the distinction is the amended Landmine 6 in type form.

type StagedFile

type StagedFile struct {
	Path     string
	Contents []byte
	Mode     os.FileMode
}

StagedFile is a launcher-owned file a launch needs on disk — openclaw's model config is the canonical case. Declared as data so Command stays pure; launch.Service.Launch materializes it. Staged files live under this tool's own config dir and must never contain secrets.

type Status

type Status struct {
	Supported bool
	Reason    string
}

Status records whether an agent can be pointed at the bound provider. Unsupported agents stay registered so their absence is explained rather than silent.

type UnsupportedProviderError

type UnsupportedProviderError struct {
	// Reason is user-facing prose explaining what cannot be done. The
	// registry copies it to Spec.Status.Reason unchanged.
	Reason string
}

UnsupportedProviderError carries the human reason an agent cannot be pointed at the bound provider.

It is a typed error rather than a wrapped string because the registry stores the reason on Spec.Status verbatim, and that value is rendered to users by `agents --all` and by UnsupportedAgentError. Recovering it from err.Error() would prepend the sentinel's own text to every one of those messages.

func (*UnsupportedProviderError) Error

func (e *UnsupportedProviderError) Error() string

func (*UnsupportedProviderError) Unwrap

func (e *UnsupportedProviderError) Unwrap() error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL