Skip to content

feat(desktop): harness-agnostic config bridge#887

Open
wpfleger96 wants to merge 25 commits into
mainfrom
wpfleger/phase4-config-bridge
Open

feat(desktop): harness-agnostic config bridge#887
wpfleger96 wants to merge 25 commits into
mainfrom
wpfleger/phase4-config-bridge

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Adds a harness-agnostic config provenance surface that reads agent configuration from all available sources and exposes it to the frontend with full provenance — where each value came from.

Agent config was fragmented across four uncoordinated mechanisms with no single surface to see what a running agent would actually use. The silent failure mode was concrete: Wes's goose personas failed because active_provider in ~/.config/goose/config.yaml was invisible to Buzz and overrode our injected model. The bridge surfaces that ambient config, establishes a clear precedence order, and exposes write-back metadata so the UI can route writes to the cheapest live mechanism.

This PR ships a read-only config surface. No write capability ships here — ConfigWriteMechanism is retained as read-only display metadata used in provenanceSentence() for origin badge display, not as a write mechanism. Write dispatch lands in a follow-on PR.

  • New config_bridge module with per-runtime config file readers: YAML for goose (~/.config/goose/config.yaml), JSON for Claude Code (~/.claude/settings.json + ~/.claude.json), TOML for Codex (~/.codex/config.toml). 25+ unit tests covering all formats, malformed files, and missing files.
  • Four-tier precedence: Buzz-explicit > ACP configOptions from session/new > env vars > config file on disk. Pre-spawn surfaces tier 2 only; post-spawn adds ACP tiers.
  • session_config_captured observer event emitted after session/new (and after apply_model_switch so the desktop caches the post-switch state); put_agent_session_config Tauri command populates SessionConfigCache in AppState. parse_models handles both the object shape ({currentModelId, availableModels}) and legacy array shape from session/new. Stable agents that report model via configOptions (category=model) are also surfaced.
  • get_agent_config_surface Tauri command. Config file "extra" fields are extracted by walking the user's config directly (config-driven iteration), not by consulting a vendored schema — all keys the user has set are surfaced automatically regardless of whether the schema defines them (intentional: supports arbitrary keys like env vars). Note: this currently applies to Claude and Codex; Goose extras are still hardcoded (tracked in goose.rs TODO).
  • AgentConfigPanel component with origin badges (Buzz/ACP/ConfigFile/Env), collapsible advanced section, and sources footer showing all four tier statuses. Override/strikethrough rendering shows superseded config values inline. Read-only fields display an info icon with tooltip.
  • ModelPicker shows provenance label when config surface data is available post-spawn (best-effort, non-blocking).
  • Playwright E2E screenshot spec covering 7 scenarios (goose, claude-code, codex, pre-spawn, overrides, advanced section, sources footer) for visual review.
  • is_required: bool on NormalizedField (Rust) / isRequired: boolean on NormalizedField (TypeScript), populated from a new required_normalized_fields slice on KnownAcpRuntime. The UI does not yet act on isRequired; that wiring lands in a follow-on PR.
  • spawn_key_refusal guard restored at the top of build_deploy_payload — provider deploy now fails closed when the private key is unavailable, matching local spawn behavior.
  • Session config cache cleared for exited agents in sync_managed_agent_processes callers — crashed/restarted agents no longer keep stale ACP tiers.
  • Persona-snapshotted model re-tagged as PersonaDefault when record.model == persona_model and no live override is active, so persona-created agents show the correct origin.
  • record.provider checked as fallback in build_provider_field so records with only the structured provider field (no env var) surface the correct provider.
  • ModelPicker invalidates the managed agents query after updateManagedAgent; session_config_captured triggers immediate config-surface query invalidation via a callback slot in useManagedAgentObserverBridge.

Stack: #794 → this PR

@wpfleger96 wpfleger96 force-pushed the wpfleger/phase3b-normalized-config branch 2 times, most recently from 66de98f to 60f95a2 Compare June 6, 2026 01:02
@wpfleger96 wpfleger96 changed the title feat(desktop): phase 4 — harness-agnostic config bridge feat(desktop): harness-agnostic config bridge Jun 6, 2026
@wpfleger96 wpfleger96 force-pushed the wpfleger/phase3b-normalized-config branch 6 times, most recently from de81ed1 to 0c44d4e Compare June 9, 2026 17:57
@wpfleger96 wpfleger96 force-pushed the wpfleger/phase4-config-bridge branch from 44cc07f to 678b871 Compare June 9, 2026 18:02
@wpfleger96 wpfleger96 force-pushed the wpfleger/phase3b-normalized-config branch from 0c44d4e to ba28ea0 Compare June 9, 2026 20:31
Base automatically changed from wpfleger/phase3b-normalized-config to main June 9, 2026 20:42
@wpfleger96 wpfleger96 force-pushed the wpfleger/phase4-config-bridge branch 2 times, most recently from c392c34 to 9939a10 Compare June 11, 2026 19:24
wpfleger96 pushed a commit that referenced this pull request Jun 11, 2026
wpfleger96 pushed a commit that referenced this pull request Jun 11, 2026
@wpfleger96 wpfleger96 force-pushed the wpfleger/phase4-config-bridge branch from 3228e7a to 139ada2 Compare June 11, 2026 20:16
wpfleger96 pushed a commit that referenced this pull request Jun 11, 2026
wpfleger96 pushed a commit that referenced this pull request Jun 11, 2026
wpfleger96 pushed a commit that referenced this pull request Jun 11, 2026
@wpfleger96 wpfleger96 force-pushed the wpfleger/phase4-config-bridge branch 5 times, most recently from 69e6bc8 to fba912f Compare June 16, 2026 21:33
@wpfleger96 wpfleger96 force-pushed the wpfleger/phase4-config-bridge branch 3 times, most recently from ac3a45e to 69d2c4e Compare June 17, 2026 04:19
@wpfleger96 wpfleger96 force-pushed the wpfleger/phase4-config-bridge branch from 29e02e4 to b4a830c Compare June 25, 2026 19:48
@wpfleger96 wpfleger96 marked this pull request as ready for review June 26, 2026 17:40

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖 Simplification pass — 6 comments on dead/unused code and deduplication opportunities. None of these touch the core semantic functionality (reader precedence, per-runtime extraction, ACP tier merge).

Comment thread desktop/src-tauri/src/managed_agents/config_bridge/writer.rs Outdated
Comment thread desktop/src-tauri/src/managed_agents/config_bridge/reader.rs
Comment thread desktop/src/features/agents/ui/ModelPicker.tsx Outdated
Comment thread desktop/src-tauri/src/managed_agents/config_bridge/claude.rs Outdated
Comment thread desktop/src/features/agents/ui/AgentConfigPanel.tsx
Comment thread desktop/src/testing/e2eBridge.ts

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review from the Brain + Pinky crew (posted via Wes's account). Two independent passes that converged.

Verdict: Request changes — one real correctness fix, then this is a strong merge. The architecture is solid and conservative: harness-agnostic, read-only config provenance; no relay/DB schema changes; config-file contents stay local to the desktop process and never cross the relay; the hard precedence and runtime-override cases are well-tested (non-vacuous tests that name the mutant each kills).

Blocking

  1. (inline) crates/buzz-acp/src/pool.rssession_config_captured is emitted before the model switch is applied. See the inline comment.

Required before merge (not code review comments — author tasks)

  1. Rewrite the PR description. The "cut write path" commit removed the entire write path, but the body still describes write_agent_config_field, session/set_config_option writes, a "single lock scope closes a TOCTOU race," and a serde fix for "silently broken writes." None of that is in the diff anymore — write_agent_config_field/WriteConfigTarget return zero matches; ConfigWriteMechanism is now read-only display metadata. This is a read-only provenance surface (cleaner, safer scope — good). Please update the description so a merger isn't misled into thinking write capability ships.
  2. Rebase / resolve conflicts — GitHub reports the branch conflicts with main.

Non-blocking

  • Goose extra fields are still hardcoded (its own TODO in goose.rs), so the "all keys surfaced automatically, no schema" claim only holds for Claude/Codex today. Tighten the wording.
  • Cosmetic: build_model_field has a dead EnvVar-origin branch that tags an empty field EnvVar when no value resolves.

Validation

  • cargo test -p buzz-acp — all relevant tests green (model_in_catalog, pool requeue/switch_model invalidation).
  • Desktop node tests green (incl. liveSwitchOutcome).
  • config_bridge Rust unit tests couldn't run locally (missing desktop/src-tauri/binaries/buzz-acp-* sidecar) — logic is pure and covered by the in-tree unit tests.

Comment thread crates/buzz-acp/src/pool.rs Outdated
wpfleger96 and others added 24 commits June 26, 2026 15:18
Four-tier config bridge that reads agent configuration from config
files (goose YAML, claude JSON, codex TOML), ACP session data, env
vars, and Sprout-explicit overrides — surfacing a unified normalized
config surface to the desktop UI regardless of runtime.

Key changes:
- Config bridge module with per-runtime file readers
- ACP session config caching for post-spawn config visibility
- AgentConfigPanel component with origin badges and tier provenance
- Serde internally-tagged enums matching TypeScript discriminated unions
- TOCTOU-safe write path with single lock scope
- E2E mock handler for get_agent_config_surface with per-runtime
  fixtures and a Playwright screenshot spec covering 7 scenarios
- Info icon + tooltip on read-only fields; override/strikethrough
  rendering for superseded config values

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…place badges with provenance sentences

Persona-linked agents had their inherited model/prompt/provider invisible
in the config panel, and the source indicators (checkmark/hourglass icons,
colored badges, footer) were undecipherable.

Phase 1: Resolve all three persona fields (prompt, model, provider) in a
single resolve_config_surface helper called by both get_agent_config_surface
(read) and write_agent_config_field (write). The helper injects resolved
values into the record where absent, calls the reader, then re-tags the
injected fields from BuzzExplicit to PersonaDefault. The re-tag is triple-
gated so a value the user set explicitly in Buzz is never re-tagged. Sharing
the helper keeps the read and write surfaces identical, so plan_config_write
never returns "field not available" for a persona-sourced field. Reader stays
untouched (pure tier-merge function).

Phase 2: Add AgentConfigPanel to ProfileSummaryView in the profile pop-out,
gated on isBot && isOwner && managedAgent defined.

Phase 3: Remove SourcesFooter and colored OriginBadge pills. Replace with
gray inline provenance sentences below each value ("Set in Buzz", "Inherited
from persona", "From environment variable", etc). No action clauses.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Picking a model on a persona-linked running agent now applies to the live
instance instead of forcing a restart. The switch rides the existing
`desired_model` lever plus the Interrupt requeue/invalidate machinery: a busy
turn cancel-switch-requeues onto a fresh session under the new model; an idle
session invalidates and re-applies on its next turn. The override is
runtime-only — never persisted to `record.model`, gone on restart/respawn, so
spawn resolution stays persona-wins.

`ControlSignal` drops `Copy` to carry the owned model id; the post-match
re-read is replaced by a `requeues()` predicate. A model absent from the
agent's catalog surfaces an `unsupported_model` control_result (idle path
guards pre-cancel; busy path validates on the re-created session post-cancel)
so the picker rejects rather than silently no-opping. The desktop keys the
override-active display off the ACP `current_model` diverging from the persona
model (the harness-only `desired_model` is unreadable by the reader), shows the
persona as a non-struck secondary, and folds the standalone Configuration block
into the metadata card.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
… flag

The runtime-override display keyed off `acp_model != persona_model`, which
cannot distinguish a live ModelPicker pick from a session left stale after a
mid-life persona edit. Editing a running persona-linked agent's model A->B
false-positived as a live override — re-introducing the display-versus-reality
bug this surface exists to kill. The harness now stamps a `model_overridden`
flag into session_config_captured (true only when a SwitchModel control signal
set desired_model, reset on spawn); the reader gates the override on that flag.

Also fix multi-channel sendLiveSwitch: it resolved on the first control_result
of any status, so a fast `sent` from one channel masked a later
`unsupported_model` from another. Now any single rejection fails the pick
immediately (fail-fast); success requires every channel to acknowledge.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The config-bridge provenance rows and ModelPicker origin/restart labels
used hardcoded text-[11px]/text-[10px] literals. The px-text guard (added
to main via the rem-token migration) forbids arbitrary font-size literals
so text scales with Cmd +/- zoom. Swap all four to the text-2xs meta-text
token (0.6875rem), the documented sibling for these decorative sub-labels.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The any-unsupported-fails-fast counting for a live model switch was locked
inside the sendLiveSwitch useCallback in ModelPicker, verified by read but
not unit-pinned. Extract the counting (remaining decrement, immediate
unsupported reject, resolve-once, unsubscribe-once, timeout fallback) into a
pure awaitLiveSwitchOutcome helper with the relay subscription, per-channel
sends, and timeout scheduler injected. The component wires the real
subscribeControlResults / window timer / dispatch; behavior is unchanged.

The helper is node:test-drivable with synthetic frames and a manual clock.
The masking-guard test fails against a first-ack-resolves variant and passes
against the shipped fail-fast logic.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The interim settled===false checks used a single await Promise.resolve()
drain, which the outcome.then callback outruns by one microtask tick, so
the guard passed even against a first-ack-resolves variant. Drain five
ticks before each interim check so it deterministically regresses an early
resolve.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
A genuine-explicit agent (own record model, no persona) that live-switched
mid-session rendered nowhere: resolve_config_surface passed no override
baseline for an explicit record, so apply_runtime_override early-returned
and the panel showed the stale record model as primary with the live model
struck through — display contradicting reality.

Carry the override baseline as a typed (value, origin) pair end-to-end so
the secondary is tagged by its true source (BuzzExplicit for the record
case, PersonaDefault for personas) instead of a hardcoded PersonaDefault.
Build the record-model baseline only when model_overridden is true, so a
persona edited mid-life still does not false-positive. On a live pick equal
to the baseline, yield a clean single-value field rather than passing the
pre-polluted base through (build_model_field independently sets an
AcpConfigOption secondary for the record-plus-live-session case).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The live-acp-vs-record override is now exclusively apply_runtime_override's
job, gated on model_overridden. build_model_field's acp-derived secondary
predated that gate: with record_model=Some(X) and acp_model=Some(Y) it
populated overridden_value=Some(Y) unconditionally, and that row passed
straight through apply_runtime_override's !model_overridden early-return —
surfacing a live override before any switch was applied.

Collapse the secondary to express only the static record-vs-file precedence
(a Buzz-explicit model shadowing a config-file model); drop all acp_model
references from it.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The v4 screenshots captured the old origin-badge UI. The shipped surface
now renders inline provenance sentences ("Set in Buzz", "Inherited from
persona", "Live override (this session only)", etc.), a folded config panel,
and a non-struck persona baseline for runtime overrides.

Re-grounds the screenshot scenarios to the sentence-based render and adds a
multiOriginSurface fixture (one distinct origin per row) so the provenance-
sentences shot witnesses multiple distinct sentences in one frame instead of
duplicating the folded-panel capture.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…tooltips

The agent side-panel Configuration section rendered with AgentConfigPanel's
own dense header (small inline icon, muted heading) inside the ProfileFieldGroup
card, so it read as bolted on rather than part of the card. Re-skin the section
header to the panel's ProfileFieldRow convention (circular icon badge, px-4 row
rhythm, foreground label) so it reads as one more section; the divider stays.

Long config values truncate correctly but had no way to reveal the full text.
Add a native title attribute carrying the untruncated value on the truncated
NormalizedRow/AdvancedRow values, guarded so null placeholders get no tooltip —
matching the existing ProfileFieldRow title convention. Covers both the side
panel and the Agents nav menu since both embed the same AgentConfigPanel.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The truncated NormalizedRow override span (config-file system prompt
in the dual-prompt case) rendered inside the truncate container with no
title, so its full text was unreadable on hover — the same gap the
primary value tooltip already closes.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Main's d256935 added agent_command_override, persona_source_version,
and provider to ManagedAgentRecord. Two test fixture initializers on
this branch were missing them, causing E0063 compile errors in CI.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Opens the user-profile-panel for a managed agent by clicking its avatar
in the #agents channel, waits for the Configuration section to render,
and captures the full panel. Covers the isBot && isOwner path that
renders the Configuration footer inside ProfileSummaryView.

Co-authored-by: Will Pfleger <wpfleger@block.xyz>
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
… extraction

codex.rs and claude.rs previously enumerated a fixed set of config keys
in their extra blocks, silently dropping any field not in the list as
new options were added upstream. This meant settings like
service_tier, plan_mode_reasoning_effort, and alwaysThinkingEnabled
were invisible in the config surface.

Check in JSON Schema snapshots for both runtimes and introduce
schema_walker::extract_schema_fields, which walks the schema's
top-level properties and surfaces every key the user has actually set.
Scalars stringify directly; arrays become '[N items]'; objects flatten
one level as key.subkey = value. Normalized fields (model, provider,
mode, thinking_effort, etc.) are passed via a skip list to avoid
double-counting.

Also adds schema_version (fetched_at from versions.json) to
RuntimeFileConfig and threads it through to ConfigSourceReport as
config_schema_version so the frontend can surface which schema snapshot
was used.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…d, add SHA-change warnings, consolidate VERSIONS_JSON

schema_walker.rs: expand doc comment to explain that object subkeys are
iterated from the config value (not filtered against schema properties),
making arbitrary keys like env vars visible. Also move VERSIONS_JSON
embed and schema_version() lookup here so codex.rs and claude.rs share
one embed instead of two identical constants.

claude.rs: add inline comment on provider_locked insert to make clear
it is a Buzz-synthesized annotation, not a field from the user's file.

codex.rs: remove duplicate VERSIONS_JSON constant and parse_codex_schema_version();
delegate to schema_walker::schema_version("codex").

refresh-harness-schemas.sh: read old SHAs from versions.json before
overwriting, then print a warning line for any harness whose SHA changed.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…ld extraction

The schema-driven approach (extract_schema_fields + vendored JSON Schema
snapshots) had a fundamental flaw: a drifted snapshot is functionally
equivalent to a hardcoded field list. Any new config key added upstream
would be silently invisible until someone ran refresh-harness-schemas.sh.
The committed schema files also triggered Biome linting failures on CI.

Replace with config-driven iteration: walk the user's own config object
directly, surface every key they have set, skip only the normalized fields
extracted into typed struct fields (model, provider, mode, etc.). No schema
required, no vendored JSON, no drift, no Biome issue.

extract_schema_fields(schema_json, config, skip) → extract_config_fields(config, skip)

Remove: CODEX_SCHEMA, CLAUDE_SCHEMA, VERSIONS_JSON constants; schemas/
directory (codex.config.schema.json, claude-code-settings.schema.json,
versions.json); schema_version() function; schema_version field on
RuntimeFileConfig; config_schema_version field on ConfigSourceReport;
refresh-harness-schemas.sh script.

The skip lists in codex.rs and claude.rs are the only harness-specific
coupling that remains — they map harness config keys to normalized struct
fields. That coupling is intentional and load-bearing.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
… config bridge

Two bugs in the agent config panel:

1. agent_config.rs resolved runtime_meta via record.agent_command (the
   create-time snapshot, never updated). When a user changes an agent's
   harness in the UI the change goes to the persona's runtime field, not
   agent_command. Both get_agent_config_surface and write_agent_config_field
   now call effective_agent_command (the same resolution the spawn path uses)
   so the config bridge dispatches to the correct harness.

2. reader.rs built the advanced section exclusively from file_config.extra
   (tier 2b). Any env vars in record.env_vars beyond the four normalized
   fields (model, provider, thinking effort, system prompt) were invisible.
   read_config_surface now appends remaining record.env_vars to advanced as
   BuzzExplicit / RespawnWithEnvVar fields, skipping keys already covered by
   normalized fields and keys already present in file_config.extra.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…flattening

Four display correctness fixes in the config bridge:

1. types.rs: add HarnessConstraint variant to ConfigOrigin. The locked
   provider branch in reader.rs was using EnvVar which caused the UI to
   show "From environment variable" for Alia's Anthropic provider — it is
   a harness constraint, not a user-set env var.

2. reader.rs: use ConfigOrigin::HarnessConstraint in the provider_locked
   branch so Claude Code's locked Anthropic provider displays correctly.

3. goose.rs: infer provider="databricks" when DATABRICKS_HOST is present
   but no explicit GOOSE_PROVIDER or active_provider is set. This surfaces
   the effective provider for agents on the Databricks OAuth path (Paul,
   Duncan) who previously showed no provider in the panel.

4. codex.rs: default provider="openai" when model_provider is absent.
   Codex uses OpenAI implicitly when no provider is configured; Thufir
   previously showed no provider.

5. schema_walker.rs: recurse two levels deep instead of one. Objects at
   depth 2 are now flattened as "key.subkey.subsubkey" rather than "{...}".
   This correctly surfaces codex [projects."<path>"] trust_level entries
   and [tui.model_availability_nux] model keys. Depth 3+ still shows "{...}".

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…e types

Add "harnessConstraint" to the ConfigOrigin union in types.ts to match
the new Rust variant added in 786bbe5. Add a provenanceSentence case
in AgentConfigPanel.tsx so the UI shows "Locked by harness" instead of
falling through to an unhandled case.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
… values

Three fixes:

1. goose.rs: remove dead nested.host branch from Databricks inference arm.
   The nested struct is Some only when active_provider is Some, but arm 2
   already returns Some(active_provider) in that case — arm 3 never executes
   when active_provider is set. The nested.host condition was always false
   when reached. Only the flat DATABRICKS_HOST check is needed.

2. schema_walker.rs: emit "{...}" for empty inner objects instead of
   silently dropping the key. The previous two-level walker iterated over
   an empty object's entries and produced zero output, making the key
   disappear entirely. Reverts the claude.rs test workaround (pre-commit: {}
   now correctly asserts hooks.pre-commit = "{...}").

3. schema_walker.rs: join scalar arrays comma-separated instead of showing
   "[N items]". Adds format_array() helper: when all elements are scalars
   (string, bool, number, null), joins them with ", "; when any element is
   a nested object or array, falls back to "[N items]". This surfaces
   tui.status_line as its actual values rather than "[6 items]".

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Mark normalized config fields as required per harness so the UI can
eventually prompt users to set the minimum viable config before an
agent will work. Required-ness is harness-specific: buzz-agent and
goose require model + provider; claude code and codex have no
user-configurable required fields.

Adds required_normalized_fields to KnownAcpRuntime, threads is_required
through the Rust reader/writer, and mirrors isRequired in the TypeScript
NormalizedField type and e2e bridge fixtures.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…e, fix ModelPicker double-fetch

Remove the entire write path (writer.rs, write_agent_config_field command,
WriteConfigTarget/WriteConfigFieldRequest/WriteConfigResult types) — no
frontend consumer exists yet. is_writable removed from NormalizedField and
ConfigField; write_via/ConfigWriteMechanism kept for provenanceSentence.

Extract resolve_with_override() in reader.rs to deduplicate the tier-picking
and override-tracking pattern shared by build_provider_field, build_mode_field,
and build_thinking_field.

Replace imperative getAgentConfigSurface call in ModelPicker with
useAgentConfigSurface hook so React Query deduplicates when panel and picker
are both rendered for the same agent.

Drop provider_locked extra insert from claude.rs — build_provider_field
already handles the locked state via runtime_meta.provider_locked.

Remove isRunning prop from AgentConfigPanel (unused) and its two call sites.

Replace 4 hardcoded pubkey constants in buildMockConfigSurface with the
ALICE_PUBKEY/BOB_PUBKEY/CHARLIE_PUBKEY/OUTSIDER_PUBKEY constants already
defined in the same file.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
High-1: restore spawn_key_refusal guard at top of build_deploy_payload.
The doc comment said 'fails closed' but there was no actual guard — a
keyring outage would serialize private_key_nsec: "" into the payload.

High-3: move session_config_captured emit to after apply_model_switch.
The emit now fires post-switch so the desktop caches the applied model,
not the pre-switch desired state. modelOverridden is false on the
unsupported arm so the panel doesn't show a stale override badge.

High-4: surface acp_model for stable agents. Stable agents report model
via configOptions (category=model, current_value) rather than the models
field. acp_model now falls back to find_config_option_value(c, "model")
so their current model is surfaced in the panel.

High-5: clear SessionConfigCache for exited agents. sync_managed_agent_processes
now returns (bool, Vec<String>) — the exited pubkeys — so callers can
clear the session cache for each agent that exited. Covers both the
process-exit loop and the stale-pid cleanup loop.

High-6: re-tag persona-snapshotted model as PersonaDefault. Persona-created
agents have record.model set at create time from the persona snapshot, so
had_model is true even though the model came from the persona. Re-tag
when record.model == persona_model, no live override, and a persona is
linked (non-persona agents with an explicit model keep BuzzExplicit).

Medium-1/2: update stale comments in build_deploy_payload. The doc comment
said env/model/provider were pinned and never read live; the inline comment
said 'same precedence as local spawn'. Both are now accurate.

Medium-3: add record.provider as fallback in build_provider_field. The
reader only checked record.env_vars for the provider env var key; records
with only the structured record.provider field (no env var) now surface
the correct provider in the panel.

Medium-8: invalidate managed agents query after updateManagedAgent in
ModelPicker. Persisted model changes were stale until the next poll.

Medium-9: invalidate config-surface query after session_config_captured.
Added a callback slot (setSessionConfigCapturedCallback) wired up by
useManagedAgentObserverBridge so React Query invalidates immediately
instead of waiting for the 30s poll.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96 wpfleger96 force-pushed the wpfleger/phase4-config-bridge branch from dbe505a to 241728d Compare June 26, 2026 19:37
agent_config.rs (get_agent_config_surface) and agent_models.rs
(get_agent_models, update_managed_agent) were calling
sync_managed_agent_processes but not clearing the session cache for
agents that exited. The panel could serve stale config/model state for
a stopped agent until one of the other covered paths ran. Destructure
the returned (bool, Vec<String>) and call state.clear_session_cache for
each exited pubkey at all three sites.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants