Complete hidden harness provisioning policy

Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
kenny lopez
2026-07-27 13:29:51 +01:00
parent 711ef24222
commit 1b91579af6
13 changed files with 125 additions and 91 deletions
@@ -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
@@ -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
@@ -561,21 +561,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`
@@ -752,6 +737,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]
@@ -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())
);
}
@@ -245,7 +245,7 @@ pub(crate) fn global_model_provider_for_record<'a>(
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| std::ptr::eq(preferred_runtime, selected)) {
if selected_runtime.is_some_and(|selected| preferred_runtime.id == selected.id) {
global_values
} else {
(None, None)
@@ -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)]
+8 -4
View File
@@ -102,10 +102,14 @@ with a TypeScript lookup table or an id comparison in a component.
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. 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.
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.
Provider pickers use `getPersonaHiddenProviderIds` as the shared
relay-mesh visibility policy.
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
@@ -4,6 +4,7 @@ import test from "node:test";
import {
availableRuntimesForStart,
buildInstanceInputForDefinition,
resolveProvisioningRuntimeForDefinition,
resolveStartRuntimeForDefinition,
shouldPinSelectedRuntimeForDefinition,
} from "./instanceInputForDefinition.ts";
@@ -101,6 +102,33 @@ test("row 2: harnessOverride follows the backend-aligned formula", async () => {
);
});
test("provisioning pins a visible fallback for a runtime-less definition", () => {
const resolved = resolveProvisioningRuntimeForDefinition(
null,
[buzzAgentRuntime, gooseRuntime],
null,
["buzz-agent"],
);
assert.equal(resolved.runtime, gooseRuntime);
assert.equal(resolved.harnessOverride, true);
});
test("provisioning uses product ordering and leaves unavailable configured fallbacks unpinned", () => {
const ordered = resolveProvisioningRuntimeForDefinition(null, [
gooseRuntime,
claudeRuntime,
buzzAgentRuntime,
]);
assert.equal(ordered.runtime, buzzAgentRuntime);
assert.equal(ordered.harnessOverride, true);
const unavailable = resolveProvisioningRuntimeForDefinition("missing", [
gooseRuntime,
]);
assert.equal(unavailable.runtime, gooseRuntime);
assert.equal(unavailable.harnessOverride, false);
});
test("row 3: plain avatar URLs pass through; base64 data URIs upload via the injectable", async () => {
const plain = await buildInstanceInputForDefinition(persona(), gooseRuntime);
assert.equal(plain.avatarUrl, "https://example.com/a.png");
@@ -111,6 +111,35 @@ export function shouldPinSelectedRuntimeForDefinition(
);
}
/**
* Resolve the runtime and backend pinning bit together so persona-backed
* provisioning cannot discard a visible implicit fallback during handoff.
*/
export function resolveProvisioningRuntimeForDefinition(
definitionRuntimeId: string | null | undefined,
runtimes: readonly AcpRuntime[],
preferredRuntimeId?: string | null,
disabledRuntimeIds: readonly string[] = getDisabledAcpRuntimeIdsSnapshot(),
): ResolvePersonaRuntimeResult & { harnessOverride: boolean } {
const defaultRuntime = getDefaultPersonaRuntime(runtimes, preferredRuntimeId);
const resolved = resolvePersonaRuntime(
definitionRuntimeId,
runtimes,
defaultRuntime,
false,
disabledRuntimeIds,
);
return {
...resolved,
harnessOverride:
resolved.runtime !== null &&
shouldPinSelectedRuntimeForDefinition(
definitionRuntimeId,
resolved.runtime.id,
),
};
}
/**
* The single definition→instance mapping (Phase 1B.3.5 rows 2–4). Every
* surface that creates a running instance from a definition builds its
@@ -31,8 +31,8 @@ import {
} from "@/features/agents/ui/bakedEnvHelpers";
import {
AUTO_PROVIDER_DROPDOWN_VALUE,
BLOCK_BUILD_HIDDEN_PROVIDER_IDS,
CUSTOM_PROVIDER_DROPDOWN_VALUE,
getPersonaHiddenProviderIds,
getPersonaProviderOptions,
getProviderApiKeyEnvVar,
runtimeSupportsLlmProviderSelection,
@@ -585,21 +585,16 @@ export function AgentConfigFields({
onConfigChange({ ...config, env_vars: merged });
}
// On internal Block builds, BUZZ_AGENT_PROVIDER is baked in and a boot
// migration rewrites v1→v2. Hide the legacy v1 option so it is not offered
// for new selections; OSS builds show it.
const hideProviderIds = React.useMemo(() => {
const hidden = new Set<string>();
if (bakedEnvKeys.includes("BUZZ_AGENT_PROVIDER")) {
for (const providerId of BLOCK_BUILD_HIDDEN_PROVIDER_IDS) {
hidden.add(providerId);
}
}
if (selectedRuntimeId !== "buzz-agent") {
hidden.add("relay-mesh");
}
return hidden;
}, [bakedEnvKeys, selectedRuntimeId]);
const hideProviderIds = React.useMemo(
() =>
getPersonaHiddenProviderIds({
bakedEnvKeys,
selectableRuntimes: selectedRuntime ? [selectedRuntime] : [],
currentRuntimeId: selectedRuntimeId,
preserveCurrentRuntime: false,
}),
[bakedEnvKeys, selectedRuntime, selectedRuntimeId],
);
const providerOptions = getPersonaProviderOptions(
providerValue,
credentialRuntimeId,
@@ -9,8 +9,7 @@ import {
usePersonasQuery,
useTeamsQuery,
} from "@/features/agents/hooks";
import { shouldPinSelectedRuntimeForDefinition } from "@/features/agents/lib/instanceInputForDefinition";
import { resolvePersonaRuntime } from "@/features/agents/lib/resolvePersonaRuntime";
import { resolveProvisioningRuntimeForDefinition } from "@/features/agents/lib/instanceInputForDefinition";
import { resolveTeamPersonas } from "@/features/agents/lib/teamPersonas";
import { useLastRuntime } from "@/features/agents/lib/useLastRuntime";
import { useChannelTemplatesQuery } from "@/features/channel-templates/hooks";
@@ -73,11 +72,6 @@ export function useApplyTemplate() {
const runtimes = acpRuntimesQuery.data ?? [];
if (runtimes.length === 0) return; // No runtimes — skip silently
// Resolve default provider: user's last-used preference, or first available
const defaultProvider =
runtimes.find((p) => p.id === lastRuntimeId) ?? runtimes[0] ?? null;
if (!defaultProvider) return;
const seenPersonaIds = new Set<string>();
const inputs: CreateChannelManagedAgentInput[] = [];
@@ -88,20 +82,17 @@ export function useApplyTemplate() {
if (seenPersonaIds.has(persona.id)) continue;
seenPersonaIds.add(persona.id);
const requestedRuntimeId = entry.runtime ?? persona.runtime;
const resolved = resolvePersonaRuntime(
const resolved = resolveProvisioningRuntimeForDefinition(
requestedRuntimeId,
runtimes,
defaultProvider,
lastRuntimeId,
);
if (!resolved.runtime) continue;
inputs.push({
runtime: resolved.runtime,
name: persona.displayName,
personaId: persona.id,
harnessOverride: shouldPinSelectedRuntimeForDefinition(
requestedRuntimeId,
resolved.runtime.id,
),
harnessOverride: resolved.harnessOverride,
systemPrompt: persona.systemPrompt,
avatarUrl: persona.avatarUrl ?? undefined,
model: entry.model ?? persona.model ?? undefined,
@@ -119,20 +110,17 @@ export function useApplyTemplate() {
if (seenPersonaIds.has(persona.id)) continue;
seenPersonaIds.add(persona.id);
const requestedRuntimeId = teamEntry.runtime ?? persona.runtime;
const resolved = resolvePersonaRuntime(
const resolved = resolveProvisioningRuntimeForDefinition(
requestedRuntimeId,
runtimes,
defaultProvider,
lastRuntimeId,
);
if (!resolved.runtime) continue;
inputs.push({
runtime: resolved.runtime,
name: persona.displayName,
personaId: persona.id,
harnessOverride: shouldPinSelectedRuntimeForDefinition(
requestedRuntimeId,
resolved.runtime.id,
),
harnessOverride: resolved.harnessOverride,
systemPrompt: persona.systemPrompt,
avatarUrl: persona.avatarUrl ?? undefined,
model: teamEntry.model ?? persona.model ?? undefined,
@@ -8,11 +8,8 @@ import {
type CreateChannelManagedAgentResult,
} from "@/features/agents/hooks";
import { getActivePersonas } from "@/features/agents/lib/catalog";
import {
canResolveAllPersonaRuntimes,
resolvePersonaRuntime,
} from "@/features/agents/lib/resolvePersonaRuntime";
import { shouldPinSelectedRuntimeForDefinition } from "@/features/agents/lib/instanceInputForDefinition";
import { resolveProvisioningRuntimeForDefinition } from "@/features/agents/lib/instanceInputForDefinition";
import { canResolveAllPersonaRuntimes } from "@/features/agents/lib/resolvePersonaRuntime";
import { getUsableTeams } from "@/features/agents/lib/teamPersonas";
import { AddChannelBotPersonasSection } from "@/features/channels/ui/AddChannelBotPersonasSection";
import { AddChannelBotTeamsSection } from "@/features/channels/ui/AddChannelBotTeamsSection";
@@ -148,11 +145,9 @@ export function AddChannelBotDialog({
}
const inputs = selectedPersonas.flatMap((persona) => {
const resolved = resolvePersonaRuntime(
const resolved = resolveProvisioningRuntimeForDefinition(
persona.runtime,
providers,
providers[0] ?? null,
false,
);
if (!resolved.runtime) return [];
return [
@@ -160,10 +155,7 @@ export function AddChannelBotDialog({
runtime: resolved.runtime,
name: persona.displayName,
personaId: persona.id,
harnessOverride: shouldPinSelectedRuntimeForDefinition(
persona.runtime,
resolved.runtime.id,
),
harnessOverride: resolved.harnessOverride,
systemPrompt: persona.systemPrompt,
avatarUrl: persona.avatarUrl ?? undefined,
model: persona.model ?? undefined,
@@ -4,7 +4,7 @@ import {
useAvailableAcpRuntimes,
useCreateChannelManagedAgentMutation,
} from "@/features/agents/hooks";
import { resolvePersonaRuntime } from "@/features/agents/lib/resolvePersonaRuntime";
import { resolveProvisioningRuntimeForDefinition } from "@/features/agents/lib/instanceInputForDefinition";
import type { AgentPersona } from "@/shared/api/types";
type QuickBotDropState = {
@@ -24,7 +24,6 @@ export function useQuickBotDrop(channelId: string | null) {
});
const providers = providersQuery.data ?? [];
const defaultProvider = providers[0] ?? null;
const addBot = React.useCallback(
async (persona: AgentPersona, instanceName: string) => {
@@ -33,11 +32,8 @@ export function useQuickBotDrop(channelId: string | null) {
setState({ pending: true, error: null });
try {
const { runtime } = resolvePersonaRuntime(
persona.runtime,
providers,
defaultProvider,
);
const { harnessOverride, runtime } =
resolveProvisioningRuntimeForDefinition(persona.runtime, providers);
if (!runtime) {
setState({
@@ -54,6 +50,7 @@ export function useQuickBotDrop(channelId: string | null) {
avatarUrl: persona.avatarUrl ?? undefined,
personaId: persona.id,
model: persona.model ?? undefined,
harnessOverride,
});
setState({ pending: false, error: null });
@@ -64,7 +61,7 @@ export function useQuickBotDrop(channelId: string | null) {
});
}
},
[channelId, createMutation, defaultProvider, providers, state.pending],
[channelId, createMutation, providers, state.pending],
);
return { ...state, addBot };
@@ -10,7 +10,7 @@ import {
useProvisionChannelManagedAgentMutation,
useStartManagedAgentMutation,
} from "@/features/agents/hooks";
import { resolvePersonaRuntime } from "@/features/agents/lib/resolvePersonaRuntime";
import { resolveProvisioningRuntimeForDefinition } from "@/features/agents/lib/instanceInputForDefinition";
import { useAddChannelMembersMutation } from "@/features/channels/hooks";
import { filterEffectiveExplicitAgentPubkeys } from "@/features/messages/lib/effectiveExplicitAgentPubkeys";
import type { UseChannelLinksResult } from "@/features/messages/lib/useChannelLinks";
@@ -313,7 +313,6 @@ export function useMentionSendFlow({
}
const runtimes = await getAvailableRuntimes();
const defaultRuntime = runtimes[0] ?? null;
const errors: string[] = [];
const agents: ManagedAgent[] = [];
const pubkeys: string[] = [];
@@ -327,11 +326,8 @@ export function useMentionSendFlow({
}
seenPersonaIds.add(persona.id);
const { runtime } = resolvePersonaRuntime(
persona.runtime,
runtimes,
defaultRuntime,
);
const { harnessOverride, runtime } =
resolveProvisioningRuntimeForDefinition(persona.runtime, runtimes);
if (!runtime) {
errors.push(`${displayName}: No agent runtime available.`);
continue;
@@ -345,6 +341,7 @@ export function useMentionSendFlow({
runtime,
name: persona.displayName,
personaId: persona.id,
harnessOverride,
systemPrompt: persona.systemPrompt,
avatarUrl: persona.avatarUrl ?? undefined,
model: persona.model ?? undefined,