mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Complete hidden harness provisioning policy
Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user