Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 5 additions & 78 deletions desktop/src-tauri/src/managed_agents/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,85 +9,9 @@ use crate::managed_agents::{
AcpAvailabilityStatus, AcpRuntimeCatalogEntry, AuthStatus, CommandAvailabilityInfo,
};

pub(crate) struct KnownAcpRuntime {
pub id: &'static str,
pub label: &'static str,
pub commands: &'static [&'static str],
pub aliases: &'static [&'static str],
pub avatar_url: &'static str,
/// Legacy MCP server binary field. Vestigial — all agents now use the bundled CLI.
/// directly. Will be removed when runtime discovery is simplified.
pub mcp_command: Option<&'static str>,
/// Whether to enable MCP hook tools (`_Stop`, `_PostCompact`) for this agent.
pub mcp_hooks: bool,
/// CLI binary that indicates partial install (e.g. `"claude"` when `claude-agent-acp` is missing).
pub underlying_cli: Option<&'static str>,
/// Shell commands to install the runtime CLI itself (run sequentially).
pub cli_install_commands: &'static [&'static str],
/// Windows-specific CLI install commands (e.g. PowerShell installers).
/// When non-empty on Windows, these are used instead of `cli_install_commands`.
#[allow(dead_code)] // read only on Windows via cli_install_commands_for_os()
pub cli_install_commands_windows: &'static [&'static str],
/// Shell commands to install the ACP adapter (run sequentially, after CLI).
pub adapter_install_commands: &'static [&'static str],
/// Link to docs/repo for manual instructions.
pub install_instructions_url: &'static str,
/// Human-readable hint about installing the CLI binary.
pub cli_install_hint: &'static str,
/// Human-readable hint about installing the ACP adapter.
pub adapter_install_hint: &'static str,
/// Harness-specific skill discovery directory (e.g. `.goose/skills`).
/// `Some(dir)` → Buzz creates a symlink at `<nest>/<dir>/buzz-cli`
/// pointing to the canonical `.agents/skills/buzz-cli`. `None` → this
/// runtime reads the canonical path directly or has no skill support.
pub skill_dir: Option<&'static str>,
/// Whether this runtime handles model switching via ACP protocol natively.
/// Currently unused — env var injection runs unconditionally regardless of
/// this value. Retained as scaffolding for when ACP model switching matures.
#[allow(dead_code)]
pub supports_acp_model_switching: bool,
pub model_env_var: Option<&'static str>,
pub provider_env_var: Option<&'static str>,
pub provider_locked: bool,
pub default_env: &'static [(&'static str, &'static str)],
pub config_file_path: Option<&'static str>,
#[allow(dead_code)] // reserved for format-based dispatch when readers are unified
pub config_file_format: Option<&'static str>,
pub supports_acp_native_config: bool, // tier 1a: config/read+write
pub thinking_env_var: Option<&'static str>,
/// Env var for normalizing `max_output_tokens`. `None` when the harness
/// does not have a first-class env var for this field (config-file only).
pub max_tokens_env_var: Option<&'static str>,
/// Env var for normalizing `context_limit`. `None` when not applicable.
pub context_limit_env_var: Option<&'static str>,
/// Normalized field keys that must be set for this harness to function.
/// Used by the config bridge to mark fields as required in the UI.
/// Keys match the camelCase names used in `NormalizedConfig` (e.g. "model", "provider").
pub required_normalized_fields: &'static [&'static str],
/// Human-readable hint shown in Doctor when the runtime is available but not
/// authenticated. `None` for runtimes that have no login step (goose, buzz-agent).
pub login_hint: Option<&'static str>,
/// CLI args for probing authentication status. `args[0]` is the binary name;
/// the remainder are the subcommand. `None` for runtimes with no login step.
pub auth_probe_args: Option<&'static [&'static str]>,
}
mod runtime_metadata;

impl KnownAcpRuntime {
/// Return the CLI install commands for the current platform.
///
/// On Windows, returns `cli_install_commands_windows` when non-empty,
/// falling back to the default `cli_install_commands`. On other platforms
/// always returns `cli_install_commands`.
pub fn cli_install_commands_for_os(&self) -> &[&str] {
#[cfg(windows)]
{
if !self.cli_install_commands_windows.is_empty() {
return self.cli_install_commands_windows;
}
}
self.cli_install_commands
}
}
pub(crate) use runtime_metadata::KnownAcpRuntime;

const GOOSE_AVATAR_URL: &str = "https://goose-docs.ai/img/logo_dark.png";
const CLAUDE_CODE_AVATAR_URL: &str = "https://anthropic.gallerycdn.vsassets.io/extensions/anthropic/claude-code/2.1.77/1773707456892/Microsoft.VisualStudio.Services.Icons.Default";
Expand Down Expand Up @@ -1318,6 +1242,9 @@ pub fn discover_acp_runtimes() -> Vec<AcpRuntimeCatalogEntry> {
binary_path,
default_args,
mcp_command: runtime.mcp_command.map(str::to_string),
model_env_var: runtime.model_env_var.map(str::to_string),
provider_env_var: runtime.provider_env_var.map(str::to_string),
thinking_env_var: runtime.thinking_env_var.map(str::to_string),
install_hint,
install_instructions_url: runtime.install_instructions_url.to_string(),
can_auto_install,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/// Static capabilities and installation metadata for a known ACP runtime.
pub(crate) struct KnownAcpRuntime {
pub id: &'static str,
pub label: &'static str,
pub commands: &'static [&'static str],
pub aliases: &'static [&'static str],
pub avatar_url: &'static str,
/// Legacy MCP server binary field. Vestigial — all agents now use the bundled CLI
/// directly. Will be removed when runtime discovery is simplified.
pub mcp_command: Option<&'static str>,
/// Whether to enable MCP hook tools (`_Stop`, `_PostCompact`) for this agent.
pub mcp_hooks: bool,
/// CLI binary that indicates partial install (e.g. `"claude"` when `claude-agent-acp` is missing).
pub underlying_cli: Option<&'static str>,
/// Shell commands to install the runtime CLI itself (run sequentially).
pub cli_install_commands: &'static [&'static str],
/// Windows-specific CLI install commands (e.g. PowerShell installers).
/// When non-empty on Windows, these are used instead of `cli_install_commands`.
#[allow(dead_code)] // read only on Windows via cli_install_commands_for_os()
pub cli_install_commands_windows: &'static [&'static str],
/// Shell commands to install the ACP adapter (run sequentially, after CLI).
pub adapter_install_commands: &'static [&'static str],
/// Link to docs/repo for manual instructions.
pub install_instructions_url: &'static str,
/// Human-readable hint about installing the CLI binary.
pub cli_install_hint: &'static str,
/// Human-readable hint about installing the ACP adapter.
pub adapter_install_hint: &'static str,
/// Harness-specific skill discovery directory (e.g. `.goose/skills`).
/// `Some(dir)` → Buzz creates a symlink at `<nest>/<dir>/buzz-cli`
/// pointing to the canonical `.agents/skills/buzz-cli`. `None` → this
/// runtime reads the canonical path directly or has no skill support.
pub skill_dir: Option<&'static str>,
/// Whether this runtime handles model switching via ACP protocol natively.
/// Currently unused — env var injection runs unconditionally regardless of
/// this value. Retained as scaffolding for when ACP model switching matures.
#[allow(dead_code)]
pub supports_acp_model_switching: bool,
pub model_env_var: Option<&'static str>,
pub provider_env_var: Option<&'static str>,
pub provider_locked: bool,
pub default_env: &'static [(&'static str, &'static str)],
pub config_file_path: Option<&'static str>,
#[allow(dead_code)] // reserved for format-based dispatch when readers are unified
pub config_file_format: Option<&'static str>,
pub supports_acp_native_config: bool, // tier 1a: config/read+write
pub thinking_env_var: Option<&'static str>,
/// Env var for normalizing `max_output_tokens`. `None` when the harness
/// does not have a first-class env var for this field (config-file only).
pub max_tokens_env_var: Option<&'static str>,
/// Env var for normalizing `context_limit`. `None` when not applicable.
pub context_limit_env_var: Option<&'static str>,
/// Normalized field keys that must be set for this harness to function.
/// Used by the config bridge to mark fields as required in the UI.
/// Keys match the camelCase names used in `NormalizedConfig` (e.g. "model", "provider").
pub required_normalized_fields: &'static [&'static str],
/// Human-readable hint shown in Doctor when the runtime is available but not
/// authenticated. `None` for runtimes that have no login step (goose, buzz-agent).
pub login_hint: Option<&'static str>,
/// CLI args for probing authentication status. `args[0]` is the binary name;
/// the remainder are the subcommand. `None` for runtimes with no login step.
pub auth_probe_args: Option<&'static [&'static str]>,
}

impl KnownAcpRuntime {
/// Return the CLI install commands for the current platform.
///
/// On Windows, returns `cli_install_commands_windows` when non-empty,
/// falling back to the default `cli_install_commands`. On other platforms
/// always returns `cli_install_commands`.
pub fn cli_install_commands_for_os(&self) -> &[&str] {
#[cfg(windows)]
{
if !self.cli_install_commands_windows.is_empty() {
return self.cli_install_commands_windows;
}
}
self.cli_install_commands
}
}
6 changes: 6 additions & 0 deletions desktop/src-tauri/src/managed_agents/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,12 @@ pub struct AcpRuntimeCatalogEntry {
pub binary_path: Option<String>,
pub default_args: Vec<String>,
pub mcp_command: Option<String>,
/// Environment variable used to apply the initial model, when supported.
pub model_env_var: Option<String>,
/// Environment variable used to apply the selected LLM provider, when supported.
pub provider_env_var: Option<String>,
/// Environment variable used to apply thinking effort, when supported.
pub thinking_env_var: Option<String>,
pub install_hint: String,
pub install_instructions_url: String,
/// true when at least one automated install step is available
Expand Down
77 changes: 77 additions & 0 deletions desktop/src/features/agents/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Agent Configuration — Contributor Rules

Scope: `desktop/src/features/agents/` (config surfaces, shared config renderer,
and the agent config core). Read this before changing how harness / provider /
model / effort configuration is modeled, rendered, persisted, or applied.

Plan of record: `Buzz/Harness-Provider-Model.md` in Morgan's Obsidian vault
(PR sequence, decisions log). PRs: #2140 (rename), #2148 (flag reduction),
#2156 (honest model states), #2158 (Agent Config Core).

## The one rule

**Harness capability facts have exactly one source: the Rust runtime catalog.**
`KnownAcpRuntime` (`desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs`)
declares each harness's model/provider/effort env keys and capabilities. Spawn
applies them; `AcpRuntimeCatalogEntry` exposes them over IPC; and
`lib/agentConfigCore.ts` projects them into field descriptors. The frontend
never maintains a rival copy of this table.

If you need a new capability fact (a new env key, a native option, a "supports
X" flag): add it to `KnownAcpRuntime` first, expose it on
`AcpRuntimeCatalogEntry`, then project it through the core. Do not shortcut
with a TypeScript lookup table or an id comparison in a component.

## Rules

1. **No hardcoded harness-ID checks in render code.** `runtime.id === "claude"`
belongs in `deriveAgentConfigFieldModel` (once, with a named reason), never
in a component. Components ask the field model what exists
(`hasRenderableAgentConfigField`, `getRenderableEffortField`).
2. **Effort reads/writes go through the descriptor.** Use the effort
descriptor's `currentPersistence` key — never a raw
`BUZZ_AGENT_THINKING_EFFORT` literal in UI code. `currentPersistence` is
where the value lives *today*; `targetApplication` is how the harness
*should* receive it. They intentionally differ until PR 2.7 migrates
Goose/Claude — do not "fix" one to match the other without doing the
migration work.
3. **Field absence has a named reason, not a boolean.** Codex effort is
`ownedByModelId`; Claude effort is `deferredUntilNativeOptionsAvailable`.
New absences get new named reasons in `AgentConfigOmission` /
`render` — never a `showX` prop.
4. **The clearing policy is the named types.** `onContextChange:
"resetDependentValues"` (user changed harness/provider → dependent values
reset everywhere) vs `onCatalogMismatch: "explainOnly" | "onboardingCleanup"`
(an async catalog miss never silently erases saved state outside
onboarding's named cleanup). Do not add mutation booleans like
`clearInvalidModel`; extend the policy types.
5. **"Metadata unknown" ≠ "harness lacks the capability".** Passing
`runtime: undefined` to the core means fields won't render. Surfaces must
gate on the runtime catalog query settling (loading/error states) rather
than letting fields silently vanish — see `AgentDefaultsEditor` /
`DefaultConfigStep` for the pattern.
6. **One canonical behavior, disclosure presets for visibility.** Behavior
flags were deliberately killed in #2148 (`CANONICAL_CONFIG_BEHAVIORS`).
Surface differences are expressed via the `disclosure` preset, not new
boolean props.
7. **Onboarding does not change.** `onboarding-agent-defaults.spec.ts` is the
acceptance gate for anything touching the shared renderer.

## The tests that enforce this

- `lib/agentConfigCore.test.mjs` — field model per harness × scope, clearing
policy. Update when the capability model changes.
- `ui/agentConfigFieldsContract.test.mjs` — canonical behaviors + disclosure
presets. If this fails, you probably reintroduced a per-surface flag.
- `desktop/tests/e2e/onboarding-agent-defaults.spec.ts` — onboarding behavior
pin (35 tests).
- Rust: `runtime_metadata_env_vars` tests pin spawn-time key application.

## Keep this file true

**If you change how agent configuration is modeled, rendered, persisted,
applied, or cleared — update this file in the same PR.** A rule that no longer
matches the code is worse than no rule; a new pattern that isn't written down
here will be broken by the next agent that never learns it existed. Reviewers:
treat a config-behavior diff without a matching AGENTS.md diff (or an explicit
"no rules changed" note) as incomplete.
Loading
Loading