Skip to content

feat(desktop,buzz-acp): add harness-agnostic config bridge and setup-listener mode#1411

Draft
wpfleger96 wants to merge 17 commits into
mainfrom
duncan/agent-readiness
Draft

feat(desktop,buzz-acp): add harness-agnostic config bridge and setup-listener mode#1411
wpfleger96 wants to merge 17 commits into
mainfrom
duncan/agent-readiness

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

This PR adds a harness-agnostic config bridge that detects unconfigured managed agents and spawns buzz-acp in a minimal setup-listener mode instead of the normal agent pool.

Before this, a managed agent missing its provider, model, or credential keys would fail silently or crash-loop on spawn. Users had no indication what was missing or where to configure it.

  • Add readiness.rs to desktop/src-tauri/src/managed_agents: EffectiveAgentEnv resolver, Requirement enum (NormalizedField / EnvKey / CliLogin), and agent_readiness() predicate; covers buzz-agent, goose, claude, codex runtimes with 11 unit tests
  • Add requiredCredentialEnvKeys() to personaDialogPickers.tsx mapping runtime+provider to required env keys; EnvVarsEditor gains a requiredKeys prop rendering locked amber rows at top with a Required badge when empty
  • Add isMissingRequiredDropdownField() in personaDialogPickers.tsx wired to useAgentConfigSurface().data?.normalized.{model,provider}.isRequired; EditAgentDialog shows required * labels on model/provider dropdowns sourced from the config-bridge normalized surface (single source of truth — flows from KnownAcpRuntime.required_normalized_fieldsread_config_surface()NormalizedField.isRequired)
  • Add setup_mode.rs to buzz-acp: SetupPayload deserialization, run_setup_listener() event loop — connects to relay, subscribes channels (mentions-only), applies author gate, and replies with a surface-correct nudge naming each missing requirement; 30s per-channel cooldown and per-event-id dedup; 6 unit tests
  • Wire early branch in buzz-acp tokio_main: if BUZZ_ACP_SETUP_PAYLOAD is set, enter setup mode and skip the agent pool entirely
  • Wire readiness check in spawn_agent_child (runtime.rs): calls resolve_effective_agent_env() + agent_readiness() after resolving runtime_meta; if NotReady, serializes requirements as BUZZ_ACP_SETUP_PAYLOAD JSON

@wpfleger96 wpfleger96 marked this pull request as ready for review June 30, 2026 23:10
@wpfleger96 wpfleger96 marked this pull request as draft June 30, 2026 23:11
wpfleger96 added a commit that referenced this pull request Jul 1, 2026
Seven Playwright-captured screenshots walking the CreateAgentDialog
gate firing and clearing, plus the extracted Edit dialog fields.
Added agent-readiness-screenshots.spec.ts to the smoke project in
playwright.config.ts (alongside config-bridge-screenshots.spec.ts).

Shot 06 (provider-mode bypass) is not captured: the mock bridge's
discover_backend_providers always returns [] so the Run-on selector
never renders in the test fixture.

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

Copy link
Copy Markdown
Collaborator Author

PR #1411 — CreateAgentDialog local-mode readiness gate

This PR brings Create up to Edit's block-save guarantee: a local-mode buzz-agent/goose can no longer be created with a missing dialog-fixable credential that would crash-loop at spawn. Screenshots below walk the gate firing and clearing.

Create — buzz-agent, empty provider → blocked

Provider marked required; Create button disabled until a provider is chosen.
{{01-create-buzzagent-empty-provider-blocked}}

Create — buzz-agent + anthropic, empty model → blocked

Model marked required; Create stays disabled until a model is set.
{{02-create-buzzagent-empty-model-blocked}}

Create — missing required credential → amber row + blocked

ANTHROPIC_API_KEY surfaced as a required row so the user can see exactly what to add.
03-create-missing-credential-row

Create — all required satisfied → enabled

Provider, model, and credential present; Create button enabled.
{{04-create-all-required-satisfied-enabled}}

Create — CLI-login runtime → provider/model not required

claude/codex authenticate via CLI login, so provider/model aren't required and Create stays enabled.
{{05-create-cli-login-runtime-no-provider-required}}

Edit — shared extracted provider/model fields

Edit's provider/model fields now come from the shared personaProviderModelFields component; appearance is unchanged.
{{07-edit-dialog-extracted-fields}}

Create — goose, empty provider → blocked

The second provider-selection runtime is gated identically to buzz-agent.
{{08-create-goose-empty-provider-blocked}}

wpfleger96 pushed a commit that referenced this pull request Jul 1, 2026
@wpfleger96

Copy link
Copy Markdown
Collaborator Author

PR #1411 — CreateAgentDialog local-mode readiness gate

This PR brings Create up to Edit's block-save guarantee: a local-mode buzz-agent/goose can no longer be created with a missing dialog-fixable credential that would crash-loop at spawn. Screenshots below walk the gate firing and clearing.

Create — buzz-agent, empty provider → blocked

Provider marked required; Create button disabled until a provider is chosen.
01-create-buzzagent-empty-provider-blocked

Create — buzz-agent + anthropic, empty model → blocked

Model marked required; Create stays disabled until a model is set.
02-create-buzzagent-empty-model-blocked

Create — missing required credential → amber row + blocked

ANTHROPIC_API_KEY surfaced as a required row so the user can see exactly what to add.
03-create-missing-credential-row

Create — all required satisfied → enabled

Provider, model, and credential present; Create button enabled.
04-create-all-required-satisfied-enabled

Create — CLI-login runtime → provider/model not required

claude/codex authenticate via CLI login, so provider/model aren't required and Create stays enabled.
05-create-cli-login-runtime-no-provider-required

Edit — shared extracted provider/model fields

Edit's provider/model fields now come from the shared personaProviderModelFields component; appearance is unchanged.
07-edit-dialog-extracted-fields

Create — goose, empty provider → blocked

The second provider-selection runtime is gated identically to buzz-agent.
08-create-goose-empty-provider-blocked

wpfleger96 pushed a commit that referenced this pull request Jul 1, 2026
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 17 commits July 1, 2026 01:02
…dicate

Introduces managed_agents/readiness.rs with:
- EffectiveAgentEnv: resolved process env a spawn would receive
  (baked floor -> runtime metadata -> user env_vars, last-wins)
- resolve_effective_agent_env(): assembles EffectiveAgentEnv from
  record + personas + KnownAcpRuntime; no AppHandle dependency
- Requirement enum with surface discriminator: NormalizedField (provider/
  model dropdowns), EnvKey (credential env rows), CliLogin (claude/codex)
- AgentReadiness: Ready | NotReady(Vec<Requirement>)
- agent_readiness(): evaluates effective env against runtime requirements
  (buzz-agent/goose: provider+model+creds; claude/codex: CLI login probe;
   unknown command: always Ready)
- Databricks token is NOT required (OAuth PKCE is the normal path)
- 17 unit tests covering all providers and surface variants

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

Adds provider-aware required credential rows to EnvVarsEditor:
- requiredCredentialEnvKeys() in personaDialogPickers.tsx: pure function
  mapping runtime+provider to required env keys (mirrors Rust readiness.rs)
- EnvVarsEditor gains requiredKeys prop: locked rows at top with amber
  highlight, read-only key name, editable masked value, Required badge when
  empty, inherited-value hint when persona has the key set
- EditAgentDialog wires requiredEnvKeys memo (selectedRuntime + provider)
  into EnvVarsEditor so the required set updates live as provider changes
- Databricks shows DATABRICKS_HOST only (DATABRICKS_TOKEN not required)
- claude/codex show no required env rows (handled via CLI login surface)
- 10 new tests covering all provider+runtime combinations

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

When a managed agent is missing required credentials, provider, or model,
the desktop now spawns buzz-acp in setup-listener mode rather than the
normal agent pool.  Agents in setup mode respond to @mentions with a
surface-correct nudge message that names exactly what to configure and
where.

Desktop side (runtime.rs):
- After resolving runtime_meta, calls resolve_effective_agent_env() +
  agent_readiness() to detect missing requirements
- If NotReady, serializes requirements as BUZZ_ACP_SETUP_PAYLOAD JSON
  (format mirrors SetupPayload serde tags in buzz-acp)
- Normal pool env vars are still set; buzz-acp detects the payload and
  branches before starting agents

buzz-acp side (setup_mode.rs + lib.rs):
- New setup_mode module: SetupPayload / RequirementPayload deserialization,
  run_setup_listener() event loop
- setup_mode is entered via early branch in tokio_main when
  BUZZ_ACP_SETUP_PAYLOAD is present; normal pool path unchanged
- Listener: connects to relay, subscribes to channels (mentions-only),
  applies author gate + event_mentions_agent filter, emits a nudge
  reply naming each missing requirement and the UI surface to fix it
- Per-channel 30s nudge cooldown; per-event-id dedup guards replay
- Membership add/remove events handled so newly-joined channels get
  subscriptions without a restart
- 6 unit tests covering payload parse, nudge body, codex CLI copy, etc.

Also extends the frontend config-surface path:
- isMissingRequiredDropdownField() helper in personaDialogPickers.tsx
- EditAgentDialog shows required (*) labels on model/provider dropdowns
  when the normalized config surface reports them as missing
- reader.rs: unwrap_or fallback on resolve_with_override to tolerate
  agents with no provider configured (avoids panic on unset agent)

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

Replace the async useAgentConfigSurface() query with a pure static check
based on runtime ID.  Only buzz-agent and goose require normalized model
and provider fields; the set is known at load time and does not change.

- Add runtimeRequiresNormalizedField(runtimeId, field): pure fn that
  returns true for buzz-agent/goose + model/provider combinations
- Simplify isMissingRequiredDropdownField signature: takes a boolean
  isRequired flag instead of a field descriptor object
- Remove useAgentConfigSurface call from EditAgentDialog: no longer
  needed; required-mark computation is now synchronous
- Update test to call runtimeRequiresNormalizedField in the unknown-field
  case so the test stays accurate under the new signature

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Restore useAgentConfigSurface() as the source for model/provider
required-mark state, replacing the static runtimeRequiresNormalizedField()
predicate introduced in the previous commit.

The static helper duplicated backend runtime knowledge in TypeScript.
A new runtime or changed required-field set on the Rust side would be
correct in KnownAcpRuntime.required_normalized_fields and the
config-bridge reader, but silently unbadged in EditAgentDialog because
the TS predicate was not updated.

The config-surface path is already used by AgentConfigPanel and
ModelPicker; NormalizedField.isRequired flows from:
  KnownAcpRuntime.required_normalized_fields
  → read_config_surface() required_fields.contains()
  → build_provider_field(is_required) / build_model_field(is_required)
  → NormalizedField { is_required }
  → useAgentConfigSurface().data?.normalized.{model,provider}.isRequired

Restore isMissingRequiredDropdownField(field: { isRequired: boolean } | null | undefined, value) signature and remove runtimeRequiresNormalizedField.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Three findings from Thufir's Pass-1 seam review:

[CRITICAL] BUZZ_ACP_SETUP_PAYLOAD was not in RESERVED_ENV_KEYS and
the Ready path did not remove it, so a saved agent env var or ambient
parent-process value could forge setup mode on a Ready agent or
suppress it on a NotReady one.  Fix: add the key to RESERVED_ENV_KEYS;
compute the optional payload first, then unconditionally
env_remove("BUZZ_ACP_SETUP_PAYLOAD") after user env is written, and
set it only when desktop computed NotReady.

[IMPORTANT] run_setup_listener() broke on relay close instead of
reconnecting, making the advertised nudged_event_ids replay-dedup
guard unreachable.  Fix: mirror the normal-mode reconnect branch
(relay.reconnect().await, exit only if background task is gone).

[IMPORTANT] The six existing setup-mode tests covered payload parsing
and nudge copy only — not the loop-wiring for the two safety-critical
guards.  Fix: extract should_nudge_for_event() as a pure helper that
captures the author-gate verdict, event-id dedup, and per-channel
cooldown; refactor the loop to call it; add two targeted tests:
test_non_allowlisted_author_returns_no_nudge (author_allowed=false →
no nudge, dedup set stays empty) and test_same_event_id_twice_nudges_
exactly_once (replay dedup via should_nudge_for_event).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Two setup-payload tests raced on BUZZ_ACP_SETUP_PAYLOAD under Rust's
default parallel test runner: setup_payload_from_env_returns_none_when_
unset read the global env while setup_payload_from_env_returns_err_on_
malformed_json was mutating it with set_var/remove_var, causing
non-deterministic failures on filtered runs.

Fix: extract SetupPayload::from_raw_env_value(raw: Option<String>) as
the pure parser core; refactor from_env() to delegate to it (no
behavior change). Rewrite the two flaky tests to call from_raw_env_value
directly with None / Some("not-valid-json{{{"): no global env mutation,
safe to run concurrently. Add a third test for the empty-string case.
Delete the misleading safety comment that claimed same-process test
serialization (wrong: cargo test is multi-threaded by default).

Also fix a stale comment at the env_remove call site in runtime.rs that
said the key is removed "after user env has been written (above)" —
merged_user_env() actually writes below. Rewrote it to name the two
real guards: RESERVED_ENV_KEYS strip (guard 1, handles user/persona env)
and env_remove (guard 2, clears ambient parent-process env), with a note
that ordering relative to merged_user_env() is NOT what makes this safe.

Also drop unused mut on ids vec in handle_setup_membership().

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Run cargo fmt and biome check --write to satisfy CI formatter gates.
No logic changes — formatting only.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Run cargo fmt for the desktop/src-tauri manifest (separate from workspace).
Bump runtime.rs override 2150 → 2207 (env-boundary CRITICAL fix growth).
Add reader.rs override at 1016 (config-bridge reader growth, queued to split).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The px-text gate rejects raw pixel sizes that don't scale with zoom.
Use the established rem-based text-2xs token (0.6875rem) instead.

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

Fold modelRequired, providerRequired, and hasRequiredEnvKeyMissing into
EditAgentDialog's canSubmit guard. The dialog already computed all three
values for the required-mark UI; this wires them to the submit button.

CLI-login runtimes (claude, codex) return [] from requiredCredentialEnvKeys
and are never blocked — their requirement is an out-of-band CLI step with
no in-dialog remedy. The runtime setup-listener nudge stays as the backstop
for out-of-band degradation after save.

CreateAgentDialog is not changed: in local mode it has no provider/model
dropdown state to key the env-key gate off of, and the existing
providerConfigComplete already handles the backend-provider path correctly.

Adds 11 tests covering: missing key blocked, provided key allowed, empty
string blocked, claude/codex not blocked, databricks host cases, and the
isMissingRequiredDropdownField predicate for required/optional/null fields.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…g semantics; gate on prospective runtime

Two correctness fixes closing the block-save/nudge contract:

1. Rust readiness.rs: credential checks (ANTHROPIC_API_KEY, OPENAI_COMPAT_API_KEY,
   DATABRICKS_HOST) used contains_key, so an empty-string value bypassed the
   requirement. Provider/model likewise treated empty-string as present. Changed
   all six credential checks to map_or(true, |v| v.is_empty()) and added
   .filter(|v| !v.is_empty()) on provider/model extraction in both
   buzz_agent_requirements and goose_requirements. Empty-string now triggers the
   runtime nudge, closing the drift with the dialog gate.

2. EditAgentDialog.tsx: requiredEnvKeys keyed off the current dropdown runtime,
   not the post-submit runtime. On the inherit-runtime transition (e.g. claude pin
   -> inherit buzz-agent persona), the gate validated the old pin's requirements
   instead of the prospective runtime's. Hoisted effectiveRuntimeIdForSubmit to
   component scope as prospectiveRuntimeId (useMemo over the same dual-match
   derivation), then wired both requiredEnvKeys and the submit path to consume it.
   Single source of truth — gate and write always agree on which runtime is saved.

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

The block-save gate for required credential env keys was calling
requiredCredentialEnvKeys(prospectiveRuntimeId, providerForDiscovery).
providerForDiscovery is suppressed to "" when the CURRENT selected runtime
is provider-locked (claude/codex) — so on the claude-pin → inherit-buzz-agent
transition, the gate computed requiredCredentialEnvKeys("buzz-agent", "")
= [] and falsely allowed saving a config missing ANTHROPIC_API_KEY.

Add providerForRequiredKeys = runtimeSupportsLlmProviderSelection(
prospectiveRuntimeId) ? provider : "", keyed off the PROSPECTIVE runtime.
This is intentionally separate from providerForDiscovery, which remains
keyed off the current visible runtime for live model discovery.

Update transition tests to mirror the component's providerForRequiredKeys
computation via runtimeSupportsLlmProviderSelection(prospectiveRuntimeId),
so the test exercises the same path the component uses rather than hardcoding
the provider directly. Add file-size override for EditAgentDialog.tsx at 1004.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Extract AgentProviderField/AgentModelField from EditAgentDialog into
personaProviderModelFields.tsx and import into both dialogs — no duplication.
This also removes the 1004-line EditAgentDialog.tsx file-size override (now
791 lines, 793 per gate counter).

CreateAgentDialog local mode (buzz-agent/goose) now has:
- Structured provider + model fields with live model discovery via
  usePersonaModelDiscovery — rendered when the runtime supports provider
  selection.
- localCredsSatisfied gate on canSubmit: requiredCredentialEnvKeys(selectedRuntimeId,
  providerForRequiredKeys).every(key => envVars[key].length > 0). Create
  has no inherit checkbox so selectedRuntimeId IS the prospective runtime;
  no prospectiveRuntimeId hoist needed.
- provider/model from structured state included in local-mode submit payload.

Thread provider through the Rust create path:
- Add provider: Option<String> to CreateManagedAgentRequest (types.rs:329).
- In the create handler, provider field on record falls back to input.provider
  (after snapshot_provider) mirroring how model falls back to input.model.
- Add provider?: string to CreateManagedAgentInput (types.ts:383).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…env rows on Create

Two correctness gaps closed (Thufir Pass 1):

1. Provider/model normalized fields now required in Create's canSubmit.
   readiness.rs buzz_agent_requirements and goose_requirements both require
   non-empty BUZZ_AGENT_PROVIDER / BUZZ_AGENT_MODEL; empty string = NotReady.
   Introduce computeLocalModeGate() in personaDialogPickers.tsx — a pure helper
   that returns missingNormalizedFields + missingEnvKeys + satisfied so canSubmit,
   field isRequired, and EnvVarsEditor.requiredKeys all share the same predicate.
   AgentProviderField and AgentModelField now render isRequired={true} when
   llmProviderFieldVisible (i.e. when the runtime requires provider selection).

2. Pass requiredKeys={requiredEnvKeys} to Create's EnvVarsEditor, matching Edit.
   Previously the button could disable for a missing ANTHROPIC_API_KEY with no
   amber locked row naming the key — user had to know it manually.

Tests rewritten to exercise computeLocalModeGate directly (not a re-derived copy
of the predicate): missing provider blocked, missing model blocked, all required
present allowed, claude CLI-login unblocked, provider/mesh bypass unchanged,
requiredEnvKeys ⊆ full required key list (EnvVarsEditor parity).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The local-mode gate (computeLocalModeGate) now blocks create if provider,
model, or required credential is empty for buzz-agent/goose runtimes. The
smoke test 'create agent supports parallelism and system prompt overrides'
defaulted to buzz-agent with no provider/model, so the submit button was
disabled and the test timed out.

Update the test to select provider=anthropic, a custom model, and fill the
ANTHROPIC_API_KEY required row before opening Advanced setup. Parallelism
and system-prompt assertions are unchanged — the mock bridge writes both
fields to the agent log regardless of provider/model.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Seven Playwright-captured screenshots walking the CreateAgentDialog
gate firing and clearing, plus the extracted Edit dialog fields.
Added agent-readiness-screenshots.spec.ts to the smoke project in
playwright.config.ts (alongside config-bridge-screenshots.spec.ts).

Shot 06 (provider-mode bypass) is not captured: the mock bridge's
discover_backend_providers always returns [] so the Run-on selector
never renders in the test fixture.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96 wpfleger96 force-pushed the duncan/agent-readiness branch from 4255b29 to c5c8671 Compare July 1, 2026 05:06
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.

1 participant