feat(desktop): thread baked build env into global agent config UI (#1722)

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Will Pfleger
2026-07-10 15:45:42 -04:00
committed by GitHub
co-authored by npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
parent febbb06909
commit 2f2ad409a7
6 changed files with 348 additions and 50 deletions
+3 -1
View File
@@ -303,7 +303,9 @@ const overrides = new Map([
// global-agent-config: get_agent_config_surface / write_agent_config_field /
// put_agent_session_config commands + GlobalAgentConfig serde types. New file
// in this PR; queued to split with the command module refactor.
["src-tauri/src/commands/agent_config.rs", 1002],
// +17: baked-env-global-unify: BUZZ_AGENT_THINKING_EFFORT added to
// is_safe_to_reveal allowlist + baked_env_thinking_effort_is_unmasked test.
["src-tauri/src/commands/agent_config.rs", 1019],
// draft-persistence predicate: submit-time `loadDraft` check + inline comment
// + deps-array entry in submitMessage closes the never-persisted-boundary
// defect (Thufir Pass-3 finding). Load-bearing correctness fix; queued to
@@ -275,11 +275,13 @@ pub struct BakedEnvEntry {
///
/// Allowlist (case-insensitive):
/// - `BUZZ_AGENT_PROVIDER`, `BUZZ_AGENT_MODEL` — agent runtime selection
/// - `BUZZ_AGENT_THINKING_EFFORT` — non-secret enum (none/minimal/low/medium/high/xhigh/max)
/// - `DATABRICKS_HOST`, `DATABRICKS_MODEL` — Block non-secret defaults
fn is_safe_to_reveal(key: &str) -> bool {
const SAFE_KEYS: &[&str] = &[
"BUZZ_AGENT_PROVIDER",
"BUZZ_AGENT_MODEL",
"BUZZ_AGENT_THINKING_EFFORT",
"DATABRICKS_HOST",
"DATABRICKS_MODEL",
];
@@ -973,6 +975,19 @@ mod tests {
assert!(token.masked);
}
#[test]
fn baked_env_thinking_effort_is_unmasked() {
// BUZZ_AGENT_THINKING_EFFORT is a non-secret enum — must not be masked.
let entries = baked_env_from_map(&[("BUZZ_AGENT_THINKING_EFFORT", "medium")]);
assert_eq!(entries.len(), 1);
let effort = entries
.iter()
.find(|e| e.key == "BUZZ_AGENT_THINKING_EFFORT")
.unwrap();
assert_eq!(effort.value, "medium");
assert!(!effort.masked);
}
#[test]
fn baked_env_allowlist_is_case_insensitive() {
// Known-safe keys — case-insensitive match must allow them.
@@ -980,6 +995,8 @@ mod tests {
assert!(super::is_safe_to_reveal("BUZZ_AGENT_PROVIDER"));
assert!(super::is_safe_to_reveal("buzz_agent_model"));
assert!(super::is_safe_to_reveal("BUZZ_AGENT_MODEL"));
assert!(super::is_safe_to_reveal("buzz_agent_thinking_effort"));
assert!(super::is_safe_to_reveal("BUZZ_AGENT_THINKING_EFFORT"));
assert!(super::is_safe_to_reveal("databricks_host"));
assert!(super::is_safe_to_reveal("DATABRICKS_HOST"));
assert!(super::is_safe_to_reveal("databricks_model"));
@@ -8,6 +8,8 @@
* the emitted record (buildRecord merges required keys from value).
* 3. Provider/runtime switch (skipKeys change) triggers a row reprojection
* — the guard fires when skipKeys changes, even if value is unchanged.
* 4. inheritedRows: render/exclusion/override/no-serialize invariants.
* 5. getBakedProviderInheritLabel: label helper correctness.
*
* These are pure-logic tests — no React renderer needed. The transition tests
* (Invariant 3) exercise the real exported `skipKeysEqual` guard that controls
@@ -23,6 +25,7 @@ import {
skipKeysEqual,
isRequiredKeyMissing,
} from "./EnvVarsEditor.tsx";
import { getBakedProviderInheritLabel } from "./bakedEnvHelpers.ts";
// ── Invariant 1: toRows excludes skip keys ─────────────────────────────────
@@ -352,3 +355,160 @@ test("isRequiredKeyMissing_keyExplicitlyEmpty_noInherited_missing", () => {
"explicit empty local value with no inherited must be missing",
);
});
// ── Invariant 4: inheritedRows — display/exclusion/override/no-serialize ───
//
// These tests exercise the pure-logic invariants that must hold for the
// inherited build-defaults feature in EnvVarsEditor:
//
// (a) An inherited row with no matching local row IS visible (would render).
// (b) An inherited row whose key appears in `rows` is HIDDEN (local wins).
// (c) Serialization (buildRecord) NEVER includes inherited-only rows.
// (d) A local override row for a masked secret shows the masked value.
//
// Tests (a)(d) operate on the same helpers used by the component render
// (toRows, toRecord, buildRecord) plus a simulated filter that mirrors the
// JSX `.filter((irow) => !rows.some((r) => r.key === irow.key))`.
function simulateInheritedFilter(inheritedRows, rows) {
return inheritedRows.filter((irow) => !rows.some((r) => r.key === irow.key));
}
test("inheritedRows_no_local_row_row_is_visible", () => {
// DATABRICKS_HOST baked, no local row → inherited row shows.
const inherited = [
{
key: "DATABRICKS_HOST",
value: "https://example.databricks.com/",
masked: false,
},
];
const rows = toRows({}, new Set()); // no local env vars
const visible = simulateInheritedFilter(inherited, rows);
assert.equal(
visible.length,
1,
"inherited row must be visible when no local override",
);
assert.equal(visible[0].key, "DATABRICKS_HOST");
});
test("inheritedRows_local_row_same_key_inherited_hidden", () => {
// User adds a local DATABRICKS_HOST row → inherited row must be hidden.
const inherited = [
{
key: "DATABRICKS_HOST",
value: "https://baked.databricks.com/",
masked: false,
},
];
const value = { DATABRICKS_HOST: "https://user.databricks.com/" };
const rows = toRows(value, new Set());
const visible = simulateInheritedFilter(inherited, rows);
assert.equal(
visible.length,
0,
"inherited row must be hidden when local row has same key",
);
});
test("inheritedRows_not_serialized_in_buildRecord", () => {
// buildRecord must never include keys that come only from inherited rows.
// Simulate: inherited has SECRET_KEY, local value does not.
const requiredKeys = [];
const value = { MY_VAR: "foo" };
const rows = toRows(value, new Set(requiredKeys));
// buildRecord reimplemented inline to match EnvVarsEditor's buildRecord:
const base = {};
for (const key of requiredKeys) {
if (key in value) base[key] = value[key];
}
const record = { ...base, ...toRecord(rows) };
assert.equal(
"SECRET_KEY" in record,
false,
"inherited-only key must NOT appear in serialized record",
);
assert.equal(record.MY_VAR, "foo", "non-inherited key preserved");
});
test("inheritedRows_masked_secret_local_override_shows_masked_build_value", () => {
// Edge case: baked key is a masked secret (e.g. API_KEY → "••••••"),
// user types a local override → the hint would show the masked "••••••" value.
const inherited = [{ key: "API_KEY", value: "••••••", masked: true }];
// The component finds override by: inheritedRows.find(irow => irow.key === row.key)
const row = { id: "r1", key: "API_KEY", value: "my-real-key" };
const override = inherited.find((irow) => irow.key === row.key);
assert.ok(override, "override entry must be found for masked key");
assert.equal(
override.value,
"••••••",
"masked baked value shown in override hint",
);
assert.equal(override.masked, true, "masked flag preserved");
});
test("inheritedRows_structured_keys_excluded_from_generic_rows", () => {
// BUZZ_AGENT_PROVIDER, BUZZ_AGENT_MODEL, BUZZ_AGENT_THINKING_EFFORT must
// be excluded from bakedGenericRows (they go to structured fields instead).
// This mirrors the BAKED_STRUCTURED_KEYS filter in GlobalAgentConfigSettingsCard.
const STRUCTURED = new Set([
"BUZZ_AGENT_PROVIDER",
"BUZZ_AGENT_MODEL",
"BUZZ_AGENT_THINKING_EFFORT",
]);
const allBaked = [
{ key: "BUZZ_AGENT_PROVIDER", value: "databricks_v2", masked: false },
{ key: "BUZZ_AGENT_MODEL", value: "goose-claude-opus-4-8", masked: false },
{ key: "BUZZ_AGENT_THINKING_EFFORT", value: "medium", masked: false },
{
key: "DATABRICKS_HOST",
value: "https://example.databricks.com/",
masked: false,
},
{ key: "DATABRICKS_MODEL", value: "goose-claude-opus-4-8", masked: false },
];
const generic = allBaked.filter((e) => !STRUCTURED.has(e.key));
assert.equal(
generic.length,
2,
"only non-structured keys go to generic rows",
);
const genericKeys = generic.map((e) => e.key).sort();
assert.deepEqual(genericKeys, ["DATABRICKS_HOST", "DATABRICKS_MODEL"]);
});
// ── Invariant 5: getBakedProviderInheritLabel — label helper ───────────────
test("getBakedProviderInheritLabel_known_provider_returns_friendly_name", () => {
const options = [
{ id: "anthropic", label: "Anthropic" },
{ id: "databricks_v2", label: "Databricks v2 (AI Gateway)" },
{ id: "openai", label: "OpenAI" },
];
const label = getBakedProviderInheritLabel("databricks_v2", options);
assert.equal(
label,
"Databricks v2 (AI Gateway) (inherited from build)",
"known provider id must resolve to friendly label",
);
});
test("getBakedProviderInheritLabel_unknown_provider_falls_back_to_raw_id", () => {
const options = [{ id: "anthropic", label: "Anthropic" }];
const label = getBakedProviderInheritLabel("my-custom-provider", options);
assert.equal(
label,
"my-custom-provider (inherited from build)",
"unknown provider id must fall back to raw id",
);
});
test("getBakedProviderInheritLabel_empty_options_falls_back_to_raw_id", () => {
const label = getBakedProviderInheritLabel("databricks_v2", []);
assert.equal(
label,
"databricks_v2 (inherited from build)",
"empty options table must fall back to raw id",
);
});
@@ -11,6 +11,18 @@ import {
export type EnvVarsValue = Record<string, string>;
/**
* A single baked / build-time env row passed as an inherited default.
* The value is already masked server-side for secrets (`masked === true`).
*/
export type InheritedEnvRow = {
key: string;
/** Display value — real value or `••••••` for masked keys. */
value: string;
/** True when Rust replaced the real value with the mask placeholder. */
masked: boolean;
};
/**
* Build a rows array from a value record, optionally skipping a set of keys.
* Exported for unit tests.
@@ -133,6 +145,21 @@ type EnvVarsEditorProps = {
* it is set. Only acts on keys that appear in `requiredKeys`.
*/
focusKey?: string;
/**
* Read-only rows for baked / build-time inherited defaults. Displayed after
* required rows and before user-managed rows. A local row whose key matches
* an inherited row takes precedence — the inherited row is hidden and an
* "Overrides build default" hint is shown beneath the local row instead.
*
* **Invariant:** these rows are purely display; they never enter `rows` state
* and are never included in `buildRecord` / `onChange` output. Nothing baked
* is ever written into `global-agent-config.json`.
*
* Opt-in — agent create/edit dialogs do not pass this prop and are untouched.
*/
inheritedRows?: readonly InheritedEnvRow[];
/** Label for the inherited-row tag (e.g. "build"). Defaults to "build". */
inheritedRowsLabel?: string;
};
type Row = { id: string; key: string; value: string };
@@ -156,6 +183,8 @@ export function EnvVarsEditor({
requiredKeys = [],
fileSatisfiedKeys = [],
focusKey,
inheritedRows = [],
inheritedRowsLabel = "build",
}: EnvVarsEditorProps) {
// Keys that render as their own special rows (required amber rows or
// file-satisfied read-only rows). These must NEVER enter `rows` state —
@@ -417,10 +446,64 @@ export function EnvVarsEditor({
</div>
))}
{/* Inherited baked-build rows — read-only, visible value (pre-masked by Rust).
Hidden when a local user row has the same key (local wins). */}
{inheritedRows
.filter((irow) => !rows.some((r) => r.key === irow.key))
.map((irow) => (
<div key={irow.key} className="space-y-1">
<div className="flex items-center gap-2">
<div
className={cn(
"flex min-h-11 flex-1 items-center gap-1.5 px-3",
PERSONA_FIELD_SHELL_CLASS,
"border-muted-foreground/20 bg-muted/20",
)}
>
<Lock
className="h-3 w-3 shrink-0 text-muted-foreground/40"
aria-hidden
/>
<span
className="font-mono text-sm leading-6 text-foreground/60"
data-testid="env-vars-inherited-key"
>
{irow.key}
</span>
<span className="ml-1 rounded-sm bg-muted px-1 py-0.5 text-2xs font-medium text-muted-foreground">
Inherited from {inheritedRowsLabel}
</span>
</div>
<div
className={cn(
"flex min-h-11 flex-[2] items-center px-3",
PERSONA_FIELD_SHELL_CLASS,
"opacity-60",
)}
>
<span
className={cn(
"font-mono text-sm",
irow.masked
? "text-muted-foreground/50"
: "text-foreground/70",
)}
data-testid="env-vars-inherited-value"
>
{irow.value}
</span>
</div>
{/* Spacer to align with the remove-button column */}
<div className="h-9 w-9 shrink-0" aria-hidden />
</div>
</div>
))}
{/* User-managed rows */}
{rows.length === 0 &&
requiredKeys.length === 0 &&
fileSatisfiedKeys.length === 0 ? (
fileSatisfiedKeys.length === 0 &&
inheritedRows.length === 0 ? (
<p className="text-xs italic text-muted-foreground">
No variables set.
</p>
@@ -494,6 +577,19 @@ export function EnvVarsEditor({
</span>
</p>
) : null}
{(() => {
if (row.key.length === 0) return null;
const override = inheritedRows.find(
(irow) => irow.key === row.key,
);
if (!override) return null;
return (
<p className="ml-1 text-xs text-muted-foreground">
Overrides {inheritedRowsLabel} default{" "}
<span className="font-mono">{override.value}</span>
</p>
);
})()}
</div>
);
})}
@@ -0,0 +1,22 @@
/**
* Pure helper functions for displaying baked build env values in the global
* agent config card. Extracted into their own module so unit tests can import
* them without pulling in React, Tauri IPC, or TanStack Query.
*/
/**
* Return the provider option label for the zero-value (inherit) option when a
* baked provider is present. Falls back to the raw provider id when the id
* doesn't appear in the options table.
*
* Used in GlobalAgentConfigSettingsCard to relabel the provider dropdown's
* empty-selection option when a baked build provider is set.
*/
export function getBakedProviderInheritLabel(
bakedProviderId: string,
options: readonly { id: string; label: string }[],
): string {
const match = options.find((o) => o.id === bakedProviderId);
const friendlyName = match ? match.label : bakedProviderId;
return `${friendlyName} (inherited from build)`;
}
@@ -21,6 +21,8 @@ import { getBakedBuildEnv, type BakedEnvEntry } from "@/shared/api/tauri";
import { globalAgentConfigQueryKey } from "@/features/agents/useGlobalAgentConfig";
import { useAcpRuntimesQuery } from "@/features/agents/hooks";
import { EnvVarsEditor } from "@/features/agents/ui/EnvVarsEditor";
import type { InheritedEnvRow } from "@/features/agents/ui/EnvVarsEditor";
import { getBakedProviderInheritLabel } from "@/features/agents/ui/bakedEnvHelpers";
import {
AUTO_PROVIDER_DROPDOWN_VALUE,
CUSTOM_PROVIDER_DROPDOWN_VALUE,
@@ -47,6 +49,17 @@ const EMPTY_CONFIG: GlobalAgentConfig = {
model: null,
};
/**
* Baked env keys that map to structured fields (provider/model/effort dropdowns).
* These are routed to their own UI controls and must NOT appear as generic
* inherited rows in the env editor.
*/
const BAKED_STRUCTURED_KEYS = new Set([
"BUZZ_AGENT_PROVIDER",
"BUZZ_AGENT_MODEL",
BUZZ_AGENT_THINKING_EFFORT,
]);
type SaveState = "idle" | "saving" | "saved" | "error";
export function GlobalAgentConfigSettingsCard() {
@@ -100,6 +113,28 @@ export function GlobalAgentConfigSettingsCard() {
});
}, []);
// Derive structured-field values and generic env rows from bakedEnv.
// Structured keys (BUZZ_AGENT_PROVIDER / BUZZ_AGENT_MODEL / BUZZ_AGENT_THINKING_EFFORT)
// route to their respective dropdown controls and are excluded from the
// generic inherited-rows list passed to EnvVarsEditor.
const bakedProvider = React.useMemo(
() => bakedEnv.find((e) => e.key === "BUZZ_AGENT_PROVIDER")?.value ?? null,
[bakedEnv],
);
const bakedModel = React.useMemo(
() => bakedEnv.find((e) => e.key === "BUZZ_AGENT_MODEL")?.value ?? null,
[bakedEnv],
);
const bakedEffort = React.useMemo(
() =>
bakedEnv.find((e) => e.key === BUZZ_AGENT_THINKING_EFFORT)?.value ?? null,
[bakedEnv],
);
const bakedGenericRows = React.useMemo<readonly InheritedEnvRow[]>(
() => bakedEnv.filter((e) => !BAKED_STRUCTURED_KEYS.has(e.key)),
[bakedEnv],
);
// Resolve the buzz-agent runtime catalog entry for model discovery.
// The card is always visible (open=true), so the query is always enabled.
const runtimesQuery = useAcpRuntimesQuery();
@@ -213,6 +248,15 @@ export function GlobalAgentConfigSettingsCard() {
? CUSTOM_PROVIDER_DROPDOWN_VALUE
: providerValue || AUTO_PROVIDER_DROPDOWN_VALUE;
// When a baked provider is present and no explicit global provider is set,
// relabel the zero-value option to surface the inherited-from-build value.
// When an explicit provider IS set, the zero-value option is still shown in
// the list but never selected — its label doesn't matter.
const providerZeroLabel = React.useMemo(() => {
if (!bakedProvider) return null;
return getBakedProviderInheritLabel(bakedProvider, providerOptions);
}, [bakedProvider, providerOptions]);
return (
<section className="min-w-0" data-testid="settings-global-agent-config">
<SettingsSectionHeader
@@ -251,7 +295,7 @@ export function GlobalAgentConfigSettingsCard() {
key={opt.id}
value={opt.id || AUTO_PROVIDER_DROPDOWN_VALUE}
>
{opt.label}
{opt.id === "" ? (providerZeroLabel ?? opt.label) : opt.label}
</option>
))}
<option value={CUSTOM_PROVIDER_DROPDOWN_VALUE}>
@@ -277,6 +321,7 @@ export function GlobalAgentConfigSettingsCard() {
<AgentModelField
disabled={false}
discoveredModelOptions={discoveredModelOptions}
globalModel={bakedModel ?? undefined}
id="global-agent-model"
isCustomModelEditing={isCustomModelEditing}
isRequired={false}
@@ -308,6 +353,7 @@ export function GlobalAgentConfigSettingsCard() {
effortDefault={effortDefault}
effortValid={effortValid}
htmlFor="global-agent-thinking-effort"
inheritedEffort={bakedEffort ?? undefined}
label="Default thinking / effort"
onChange={(value) => {
setConfig((prev) => {
@@ -352,6 +398,8 @@ export function GlobalAgentConfigSettingsCard() {
: next;
handleEnvVarsChange(merged);
}}
inheritedRows={bakedGenericRows}
inheritedRowsLabel="build"
label="Global environment variables"
helperText="Injected into all agents as the lowest-priority layer. Per-agent values override these."
/>
@@ -359,53 +407,6 @@ export function GlobalAgentConfigSettingsCard() {
</SettingsOptionGroup>
)}
{/* Baked build defaults — only visible in internal (Block) builds.
OSS builds return an empty array, so this section is hidden entirely. */}
{bakedEnv.length > 0 && (
<div className="mt-4">
<SettingsOptionGroup>
<div className="p-3">
<p className="mb-1.5 text-xs font-medium text-foreground">
Baked build defaults
</p>
<p className="mb-2 text-xs text-muted-foreground">
Set by your build. Override any of these above.
</p>
<div className="flex flex-col gap-1">
{bakedEnv.map((entry) => {
const friendlyLabel =
entry.key === "BUZZ_AGENT_PROVIDER"
? "Baked provider"
: entry.key === "BUZZ_AGENT_MODEL"
? "Baked model"
: null;
return (
<div
className="flex items-baseline gap-2 font-mono text-xs"
key={entry.key}
>
<code className="shrink-0 text-muted-foreground">
{friendlyLabel ?? entry.key}
</code>
<span className="text-muted-foreground">=</span>
<code
className={
entry.masked
? "text-muted-foreground/50"
: "text-foreground"
}
>
{entry.value}
</code>
</div>
);
})}
</div>
</div>
</SettingsOptionGroup>
</div>
)}
{/* Save bar */}
{!isLoading && !loadError && (
<div className="mt-4 flex items-center gap-3">