Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4b36534
Allow hiding agent harness options
klopez4212 Jul 23, 2026
b160141
Keep hidden harness defaults in sync
klopez4212 Jul 23, 2026
4a049a3
Honor harness visibility for implicit starts
klopez4212 Jul 23, 2026
b48a8c2
Scope defaults to visible harness fallbacks
klopez4212 Jul 23, 2026
c0f2602
Apply harness visibility to every agent start
klopez4212 Jul 23, 2026
d2de493
Preserve explicit hidden harness starts
klopez4212 Jul 23, 2026
33d285e
Preserve hidden harness defaults when editing
klopez4212 Jul 23, 2026
4e82da3
Filter implicit runtime fallbacks centrally
klopez4212 Jul 23, 2026
93cf670
Scope hidden harness defaults by dialog mode
klopez4212 Jul 23, 2026
d9b164f
Fix hidden agent harness fallbacks
klopez4212 Jul 23, 2026
642fedc
Block hidden harness provisioning bypasses
klopez4212 Jul 23, 2026
616e9d7
Preserve visible harness fallbacks
klopez4212 Jul 24, 2026
bb5b033
Complete hidden harness provisioning policy
klopez4212 Jul 24, 2026
b8d8fb2
Merge remote-tracking branch 'origin/main' into kennylopez-agent-harn…
klopez4212 Jul 24, 2026
4ba301d
Address harness visibility review feedback
klopez4212 Jul 24, 2026
0a0fba2
Fix harness visibility smoke fixture
klopez4212 Jul 24, 2026
5a50252
Use harness labels for current options
klopez4212 Jul 24, 2026
87ebbf4
Filter hidden harnesses from remaining pickers
klopez4212 Jul 24, 2026
4ad4ee7
Merge remote-tracking branch 'origin/main' into kennylopez-agent-harn…
klopez4212 Jul 24, 2026
26ca6e0
Filter hidden runtimes from welcome readiness
klopez4212 Jul 24, 2026
d9c2b08
Preserve hidden Welcome runtime aliases
klopez4212 Jul 24, 2026
f62fb40
Merge remote-tracking branch 'origin/main' into kennylopez-agent-harn…
klopez4212 Jul 25, 2026
1f634af
Merge remote-tracking branch 'origin/main' into kennylopez-agent-harn…
klopez4212 Jul 25, 2026
eb015bc
Handle hidden runtimes during Welcome kickoff
klopez4212 Jul 25, 2026
c77ad4f
Respect hidden harnesses during onboarding
klopez4212 Jul 25, 2026
1e70b78
Merge remote-tracking branch 'origin/main' into kennylopez-agent-harn…
klopez4212 Jul 25, 2026
33a0a60
Align hidden harness fallback readiness
klopez4212 Jul 25, 2026
44b276f
Merge remote-tracking branch 'origin/main' into kennylopez-agent-harn…
klopez4212 Jul 25, 2026
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
6 changes: 4 additions & 2 deletions desktop/src-tauri/src/commands/agents_deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,16 @@ pub(crate) fn resolve_deploy_model_provider<'a>(
.persona_id
.as_deref()
.and_then(|pid| personas.iter().find(|p| p.id == pid));
let (global_model, global_provider) =
crate::managed_agents::global_model_provider_for_record(record, personas, global);
let model = live_persona
.and_then(|p| p.model.as_deref())
.or(record.model.as_deref())
.or(global.model.as_deref());
.or(global_model);
let provider = live_persona
.and_then(|p| p.provider.as_deref())
.or(record.provider.as_deref())
.or(global.provider.as_deref());
.or(global_provider);
(model, provider)
}

Expand Down
37 changes: 37 additions & 0 deletions desktop/src-tauri/src/commands/agents_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,43 @@ fn deploy_resolver_falls_back_to_global_when_persona_and_record_have_none() {
assert_eq!(provider, Some("global-prov"));
}

#[test]
fn deploy_resolver_ignores_defaults_from_a_different_implicit_runtime() {
let mut record = bare_agent_record(Some("p1"), None, None);
record.agent_command_override = Some("goose".to_string());
let personas = vec![persona_record("p1", None, None)];
let global = crate::managed_agents::GlobalAgentConfig {
model: Some("auto".to_string()),
provider: Some("relay-mesh".to_string()),
preferred_runtime: Some("buzz-agent".to_string()),
..Default::default()
};

assert_eq!(
resolve_deploy_model_provider(&record, &personas, &global),
(None, None)
);
}

#[test]
fn deploy_resolver_ignores_mismatched_defaults_without_a_stored_override() {
let mut record = bare_agent_record(Some("p1"), None, None);
record.agent_command = "buzz-agent".to_string();
record.agent_command_override = None;
let personas = vec![persona_record("p1", None, None)];
let global = crate::managed_agents::GlobalAgentConfig {
model: Some("global-model".to_string()),
provider: Some("global-provider".to_string()),
preferred_runtime: Some("goose".to_string()),
..Default::default()
};

assert_eq!(
resolve_deploy_model_provider(&record, &personas, &global),
(None, None)
);
}

#[test]
fn normalize_relay_mesh_rejects_empty_model_ref() {
let config = RelayMeshConfig {
Expand Down
10 changes: 5 additions & 5 deletions desktop/src-tauri/src/managed_agents/discovery/overrides.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,10 @@ pub fn apply_agent_command_update(
/// `resolvePersonaRuntime` (frontend), which produces a divergent command in two
/// distinct cases that the backend MUST tell apart:
///
/// - DELIBERATE OVERRIDE (`harness_override` true): the user explicitly picked a
/// runtime command in UI that exposes a runtime selector. This is a real pin
/// and is preserved when it differs from the command inheritance would spawn,
/// including installed aliases such as `claude-code-acp`.
/// - PINNED SELECTION (`harness_override` true): the frontend selected a runtime
/// that must survive persona inheritance. This includes explicit user choices,
/// installed aliases such as `claude-code-acp`, and visible implicit fallbacks
/// for runtime-less personas.
/// - MISSING-RUNTIME FALLBACK (`harness_override` false): the persona's runtime
/// isn't installed locally, so `resolvePersonaRuntime` substitutes a fallback
/// default. This is NOT a pin — baking it would freeze the agent on the fallback
Expand All @@ -125,7 +125,7 @@ pub fn apply_agent_command_update(
/// so the persona stays authoritative.
///
/// `isOverridden` from `resolvePersonaRuntime` cannot distinguish these — it is
/// `true` for BOTH — so the caller must thread the explicit user-intent bit.
/// `true` for BOTH — so the caller must thread the pinning decision.
///
/// Persona-less creates (`persona_id` is `None`, e.g. the standalone
/// CreateAgentDialog) have no persona to inherit, so the picked command is always a
Expand Down
16 changes: 1 addition & 15 deletions desktop/src-tauri/src/managed_agents/discovery/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,21 +419,6 @@ fn create_time_override_none_when_persona_runtime_not_installed() {
);
}

#[test]
fn create_time_override_some_when_user_deliberately_overrides_installed_runtime() {
// Case 2 + deliberate override: the persona's `claude` runtime IS
// available, but the user explicitly picked `codex` in a deploy dialog's
// runtime selector ("overriding persona preferences"), so the frontend
// sends `codex-acp` with `harness_override` true. This is a real pin and
// MUST be preserved — returning `None` would silently swallow the
// deliberate override and inherit `claude` on spawn.
let personas = vec![persona_with_runtime("p1", Some("claude"))];
assert_eq!(
create_time_agent_command_override(Some("p1"), &personas, Some("codex-acp"), true),
Some("codex-acp".to_string())
);
}

#[test]
fn create_time_override_none_when_persona_runtime_installed() {
// Case 2: the persona's runtime is available, so `resolvePersonaRuntime`
Expand Down Expand Up @@ -610,6 +595,7 @@ fn apply_agent_command_update_concrete_pin_keeps_materialized_runtime() {
// ── probe_codex_acp_major_version ─────────────────────────────────────────────

mod managed_path_resolution;
mod provisioning_overrides;

#[cfg(unix)]
#[test]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use super::{create_time_agent_command_override, persona_with_runtime};

#[test]
fn preserves_deliberate_override_of_installed_runtime() {
let personas = vec![persona_with_runtime("p1", Some("claude"))];
assert_eq!(
create_time_agent_command_override(Some("p1"), &personas, Some("codex-acp"), true),
Some("codex-acp".to_string())
);
}

#[test]
fn pins_visible_fallback_for_runtime_less_persona() {
let personas = vec![persona_with_runtime("p1", None)];
assert_eq!(
create_time_agent_command_override(Some("p1"), &personas, Some("goose"), true),
Some("goose".to_string())
);
}
74 changes: 56 additions & 18 deletions desktop/src-tauri/src/managed_agents/global_config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,20 +204,60 @@ pub fn save_global_agent_config(app: &AppHandle, config: &GlobalAgentConfig) ->
atomic_write_json_restricted(&path, &payload)
}

/// Resolve the effective model and provider for an agent using the
/// precedence chain: `agent record → linked persona → global defaults → None`.
///
/// This is the single source of truth used by readiness evaluation, spawn,
/// and deploy-payload construction. All three paths must use this function so
/// they agree on what model/provider the agent will actually run with.
/// Return the global provider/model values that are safe for this record.
///
/// # Arguments
/// * `record` — the `ManagedAgentRecord` (may have `None` for model/provider)
/// * `personas` — all current persona records (looked up by `record.persona_id`)
/// * `global` — global agent config defaults
/// A runtime-less definition can be started on a fallback runtime when its
/// saved global preference is hidden or unavailable. In that case the selected
/// command is pinned on the record, and provider/model defaults belonging to a
/// different preferred runtime must not cross the harness boundary. Explicitly
/// configured definitions and standalone agents keep normal global inheritance.
/// Configs written before `preferred_runtime` existed implicitly belong to
/// Buzz Agent.
pub(crate) fn global_model_provider_for_record<'a>(
record: &ManagedAgentRecord,
personas: &[AgentDefinition],
global: &'a GlobalAgentConfig,
) -> (Option<&'a str>, Option<&'a str>) {
let global_values = (global.model.as_deref(), global.provider.as_deref());
if record.persona_id.is_none() {
return global_values;
}

let definition_runtime = record.runtime.as_deref().or_else(|| {
record
.persona_id
.as_deref()
.and_then(|id| personas.iter().find(|persona| persona.id == id))
.and_then(|persona| persona.runtime.as_deref())
});
if definition_runtime.is_some_and(|runtime| !runtime.trim().is_empty()) {
return global_values;
}

let preferred_runtime = global
.preferred_runtime
.as_deref()
.and_then(crate::managed_agents::known_acp_runtime)
.or_else(|| crate::managed_agents::known_acp_runtime("buzz-agent"));
let selected_command = crate::managed_agents::record_agent_command(record, personas);
let selected_runtime = crate::managed_agents::known_acp_runtime(&selected_command);

if selected_runtime.is_some_and(|selected| {
preferred_runtime.is_some_and(|preferred| preferred.id == selected.id)
}) {
Comment on lines +245 to +247

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve defaults for explicit harness overrides

When a runtime-less persona is provisioned with a deliberate harness override that differs from the global preferred runtime—for example, a channel template explicitly assigns Goose while Buzz Agent is preferred—both record.runtime and the persona runtime remain unset, so this comparison classifies the explicit pin as an implicit fallback and discards global model/provider defaults. useApplyTemplate does not persist the global provider and may omit the model, leaving the created agent unconfigured even though masking is intended only for implicit fallbacks; persist the selection's intent or another durable fallback marker instead of inferring it solely from a command mismatch.

AGENTS.md reference: desktop/src/features/agents/AGENTS.md:L93-L99

Useful? React with 👍 / 👎.

global_values
} else {
(None, None)
}
}

/// Resolve the effective model and provider for an agent using the
/// precedence chain: `agent record → linked persona → applicable global
/// defaults → None`.
///
/// # Returns
/// `(effective_model, effective_provider)` — both `Option<&str>`.
/// This is the single source of truth used by readiness evaluation and spawn.
/// Deploy uses the same global applicability helper with its live-persona-first
/// precedence.
pub(crate) fn resolve_effective_model_provider<'a>(
record: &'a ManagedAgentRecord,
personas: &'a [AgentDefinition],
Expand All @@ -229,17 +269,15 @@ pub(crate) fn resolve_effective_model_provider<'a>(
.and_then(|pid| personas.iter().find(|p| p.id == pid))
.map(|p| (p.model.as_deref(), p.provider.as_deref()))
.unwrap_or((None, None));
let (global_model, global_provider) =
global_model_provider_for_record(record, personas, global);

let effective_model = record
.model
.as_deref()
.or(persona_model)
.or(global.model.as_deref());
let effective_model = record.model.as_deref().or(persona_model).or(global_model);
let effective_provider = record
.provider
.as_deref()
.or(persona_provider)
.or(global.provider.as_deref());
.or(global_provider);

(effective_model, effective_provider)
}
Expand Down
96 changes: 96 additions & 0 deletions desktop/src-tauri/src/managed_agents/global_config/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,102 @@ fn resolve_global_fallback_when_record_and_persona_have_none() {
);
}

#[test]
fn runtime_fallback_does_not_inherit_defaults_from_a_different_preferred_runtime() {
let mut record = bare_record();
record.persona_id = Some("p1".to_string());
record.agent_command_override = Some("goose".to_string());
let personas = vec![persona("p1", None, None)];
let global = GlobalAgentConfig {
model: Some("auto".to_string()),
provider: Some("relay-mesh".to_string()),
preferred_runtime: Some("buzz-agent".to_string()),
..Default::default()
};

assert_eq!(
resolve_effective_model_provider(&record, &personas, &global),
(None, None)
);
}

#[test]
fn legacy_runtime_fallback_does_not_inherit_implicit_buzz_agent_defaults() {
let mut record = bare_record();
record.persona_id = Some("p1".to_string());
record.agent_command_override = Some("goose".to_string());
let personas = vec![persona("p1", None, None)];
let global = GlobalAgentConfig {
model: Some("auto".to_string()),
provider: Some("relay-mesh".to_string()),
preferred_runtime: None,
..Default::default()
};

assert_eq!(
resolve_effective_model_provider(&record, &personas, &global),
(None, None)
);
}

#[test]
fn default_command_fallback_does_not_inherit_defaults_from_another_preferred_runtime() {
let mut record = bare_record();
record.persona_id = Some("p1".to_string());
record.agent_command = "buzz-agent".to_string();
record.agent_command_override = None;
let personas = vec![persona("p1", None, None)];
let global = GlobalAgentConfig {
model: Some("global-model".to_string()),
provider: Some("global-provider".to_string()),
preferred_runtime: Some("goose".to_string()),
..Default::default()
};

assert_eq!(
resolve_effective_model_provider(&record, &personas, &global),
(None, None)
);
}

#[test]
fn runtime_fallback_inherits_defaults_when_it_matches_the_preferred_runtime() {
let mut record = bare_record();
record.persona_id = Some("p1".to_string());
record.agent_command_override = Some("goose".to_string());
let personas = vec![persona("p1", None, None)];
let global = GlobalAgentConfig {
model: Some("global-model".to_string()),
provider: Some("global-provider".to_string()),
preferred_runtime: Some("goose".to_string()),
..Default::default()
};

assert_eq!(
resolve_effective_model_provider(&record, &personas, &global),
(Some("global-model"), Some("global-provider"))
);
}

#[test]
fn explicit_runtime_keeps_global_defaults_when_another_runtime_is_preferred() {
let mut record = bare_record();
record.persona_id = Some("p1".to_string());
record.runtime = Some("goose".to_string());
let personas = vec![persona("p1", None, None)];
let global = GlobalAgentConfig {
model: Some("global-model".to_string()),
provider: Some("global-provider".to_string()),
preferred_runtime: Some("buzz-agent".to_string()),
..Default::default()
};

assert_eq!(
resolve_effective_model_provider(&record, &personas, &global),
(Some("global-model"), Some("global-provider"))
);
}

/// Tier 4 — no persona linked: record.persona_id is None, record has no
/// model/provider; global defaults must still fill in (persona lookup skipped).
#[cfg(feature = "mesh-llm")]
Expand Down
4 changes: 2 additions & 2 deletions desktop/src-tauri/src/managed_agents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ pub use env_vars::*;
pub(crate) use git_bash::git_bash_available;
pub(crate) use git_bash::{discover_git_bash, GitBashPrerequisite};
pub(crate) use global_config::{
load_global_agent_config, resolve_effective_model_provider, save_global_agent_config,
validate_global_config, GlobalAgentConfig,
global_model_provider_for_record, load_global_agent_config, resolve_effective_model_provider,
save_global_agent_config, validate_global_config, GlobalAgentConfig,
};
pub(crate) use managed_node_paths::*;
pub use nest::*;
Expand Down
7 changes: 3 additions & 4 deletions desktop/src-tauri/src/managed_agents/types/requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,9 @@ pub struct CreateManagedAgentRequest {
pub relay_url: Option<String>,
pub acp_command: Option<String>,
pub agent_command: Option<String>,
/// True when `agent_command` is a runtime command the user deliberately
/// picked for a linked persona. Distinguishes a real selection, including an
/// installed alias, from a missing-runtime fallback so a persona-backed
/// create only stores an `agent_command_override` for the former.
/// True when `agent_command` must survive linked-persona inheritance.
/// Includes explicit selections, installed aliases, and the visible
/// implicit fallback for a runtime-less persona.
#[serde(default)]
pub harness_override: bool,
#[serde(default)]
Expand Down
32 changes: 31 additions & 1 deletion desktop/src/features/agents/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,37 @@ with a TypeScript lookup table or an id comparison in a component.
harnesses always keep the field. Gate: `defaults hides model when optional
harness has empty discovery` (and the failed-discovery counterpart) in
`onboarding-agent-defaults.spec.ts`.
9. **The defaults modal is progressively disclosed.** An unset global config
9. **Runtime enablement is a device-local menu preference, not a capability or
lifecycle fact.** Settings persists disabled runtime IDs through
`lib/runtimeVisibilityPreference.ts`; harness dropdowns filter those IDs
from new choices while the raw Rust catalog remains authoritative for
installation, capabilities, and existing agent configuration. Turning a
runtime off must not uninstall it, stop it, or invalidate an existing
agent that already uses it. If the disabled runtime was the saved global
default, consumers immediately ignore that preference and the defaults
editor persists its visible fallback on the next save. Its dependent
provider/model defaults are also ignored for new implicit fallback agents,
without changing the persisted configuration used by existing agents.
Legacy global defaults with no saved preferred runtime are treated as
Buzz Agent-owned for this masking boundary, so hiding Buzz Agent cannot
leak its provider/model into another implicit fallback.
Create-mode dialogs use the implicit masked config; existing definition and
instance edit dialogs use the raw persisted config.
`resolvePersonaRuntime` is the shared visibility boundary for every
runtime-less deployment or provisioning path. Definition-to-instance starts
use the stricter `resolveStartRuntimeForDefinition` wrapper; call one of
these shared resolvers instead of duplicating default selection in a start
surface. Persona-backed provisioning uses
`resolveProvisioningRuntimeForDefinition` so the resolved runtime and
backend pinning bit cannot drift apart. Team deploys use the same
visible-runtime set only for members that need an implicit fallback, so a
fully pinned team remains deployable even when every installed runtime is
hidden. Definitions already pinned to a hidden runtime remain runnable.
Welcome Team reconciliation skips runtime updates for existing agents on a
hidden runtime. Provider pickers use `getPersonaHiddenProviderIds` as the
shared relay-mesh visibility policy, deriving support from each runtime's
catalog-provided `providerEnvVar` rather than its ID.
10. **The defaults modal is progressively disclosed.** An unset global config
starts on the Buzz Agent-first deployment fallback and carries that visible
harness into the next saved edit. The `progressive-defaults` disclosure
preset therefore begins at Provider for Buzz Agent, then reveals Model,
Expand Down
Loading
Loading