Skip to content

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

Merged
wpfleger96 merged 33 commits into
mainfrom
duncan/agent-readiness
Jul 6, 2026
Merged

feat(desktop,buzz-acp): add harness-agnostic config bridge and setup-listener mode#1411
wpfleger96 merged 33 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. It also silences false-required markers for env keys already satisfied in the runtime's config file (e.g. ~/.config/goose/config.yaml), and fixes two dialog labeling bugs in PersonaDialog.

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. Env keys already set in the harness config file were incorrectly shown as required.

  • 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
  • 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; 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
  • Config bridge at config time: refactor goose_requirements() to accept injected Option<&RuntimeFileConfig> (pure fn, deterministic in tests); collect_missing_requirements reads ~/.config/goose/config.yaml once and passes it in. File config silences provider, model, and extra-map credential keys (e.g. DATABRICKS_HOST). Add get_runtime_file_config Tauri command + useRuntimeFileConfigQuery hook; all three dialogs (PersonaDialog, CreateAgentDialog, EditAgentDialog) pass file-satisfied keys to computeLocalModeGate; EnvVarsEditor renders them as muted "Set in goose config" rows rather than amber Required rows
  • Fix PersonaDialog required/Optional flip-flop: providerIsRequired was computed from missingNormalizedFields.includes('provider') — would clear the required asterisk once a value was selected. Changed to runtimeSupportsLlmProviderSelection(runtime) (static predicate)
  • Fix PersonaDialog runtime label: runtime dropdown was labeled "Provider", colliding with the "LLM provider" field below. Relabeled to "Agent runtime" to match CreateAgentDialog and EditAgentDialog

@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 pushed a commit that referenced this pull request Jul 1, 2026
wpfleger96 pushed a commit that referenced this pull request Jul 1, 2026
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 wpfleger96 force-pushed the duncan/agent-readiness branch from 4255b29 to c5c8671 Compare July 1, 2026 05:06
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 wpfleger96 force-pushed the duncan/agent-readiness branch from c5c8671 to e71375a Compare July 1, 2026 18:09
@wpfleger96

Copy link
Copy Markdown
Collaborator Author

Shot 01 — Create: buzz-agent selected, provider empty → required marker shown, save allowed

LLM Provider field shows required marker (*); Create agent button is enabled (save no longer blocked by incomplete config).

01-create-buzzagent-empty-provider-marker

Shot 02 — Create: buzz-agent + anthropic, model empty → required marker shown, save allowed

Provider selected, model field shows required marker; Create agent button still enabled.

02-create-buzzagent-empty-model-marker

Shot 03 — Create: buzz-agent + anthropic + model set, API key missing → amber required row, save allowed

ANTHROPIC_API_KEY row shown with amber "Required" badge; button enabled (nudge will guide user after spawn).

03-create-missing-credential-row

Shot 04 — Create: all required fields satisfied → Create button enabled

Provider, model, and credential all set; button enabled as expected.

04-create-all-required-satisfied-enabled

Shot 05 — Create: claude (CLI-login) runtime → no provider/model required, button enabled

Claude uses out-of-band auth; provider/model fields hidden; button immediately enabled.

05-create-cli-login-runtime-no-provider-required

Shot 07 — Edit: goose runtime, extracted provider + model fields shown

Edit Agent dialog showing the shared AgentProviderField / AgentModelField extraction.

07-edit-dialog-extracted-fields

Shot 08 — Create: goose runtime, provider empty → required marker shown, save allowed

Same required-marker behavior as buzz-agent (both support provider selection).

08-create-goose-empty-provider-marker

wpfleger96 pushed a commit that referenced this pull request Jul 1, 2026
@wpfleger96 wpfleger96 force-pushed the duncan/agent-readiness branch from 9c9065a to f2f7151 Compare July 1, 2026 18:21
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 wpfleger96 force-pushed the duncan/agent-readiness branch from 4ff2ef0 to ef83d5e Compare July 1, 2026 19:10
@wpfleger96 wpfleger96 marked this pull request as ready for review July 1, 2026 19:10
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 wpfleger96 force-pushed the duncan/agent-readiness branch from 7a7ef98 to 94beb72 Compare July 1, 2026 20:27
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 wpfleger96 force-pushed the duncan/agent-readiness branch from 94beb72 to 2dfff07 Compare July 1, 2026 20:42
wpfleger96 added a commit that referenced this pull request Jul 1, 2026
…ention

Two fixes from Will's manual E2E test on #1411.

## Task A — PersonaDialog readiness gate

PersonaDialog ("New agent" menu) predates the shared gate components and
rendered its own bespoke 'Optional' label for LLM provider, so it missed
the #1411 readiness gate entirely. Wire it in:

- Import computeLocalModeGate + requiredCredentialEnvKeys from
  personaDialogPickers (same gate used by CreateAgentDialog).
- Replace the static 'Optional' span with RequiredFieldLabel; shows a red
  asterisk when the provider field is required and unset.
- Thread requiredEnvKeys through PersonaAdvancedFields → EnvVarsEditor so
  missing credential keys render as amber locked rows, matching
  CreateAgentDialog behavior.
- PersonaAdvancedFields gains an optional requiredEnvKeys prop (defaults
  to [], no call-site breakage).

AddChannelBotDialog audited: selects ACP runtimes, no LLM
provider/API-key surface — no gate needed.

## Task B — nudge every mention

setup_mode.rs had a 30s per-channel cooldown (NUDGE_COOLDOWN /
channel_last_nudge) that silently swallowed the 2nd–4th rapid @mentions
in Will's repro. The user's intent is clear on every mention, and a
mention is an intentional act — rate-limiting is not needed here.

Remove the per-channel cooldown entirely; keep the event-id dedup which
guards against reconnect-replay double-nudging the same event.

- Drop NUDGE_COOLDOWN constant, channel_last_nudge HashMap, and the
  Duration/Instant/HashMap imports.
- Simplify should_nudge_for_event signature: remove channel_id,
  channel_last_nudge, nudge_cooldown params.
- Update both call tests to match the new signature.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96 added a commit that referenced this pull request Jul 2, 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 added a commit that referenced this pull request Jul 2, 2026
…ention

Two fixes from Will's manual E2E test on #1411.

PersonaDialog ("New agent" menu) predates the shared gate components and
rendered its own bespoke 'Optional' label for LLM provider, so it missed
the #1411 readiness gate entirely. Wire it in:

- Import computeLocalModeGate + requiredCredentialEnvKeys from
  personaDialogPickers (same gate used by CreateAgentDialog).
- Replace the static 'Optional' span with RequiredFieldLabel; shows a red
  asterisk when the provider field is required and unset.
- Thread requiredEnvKeys through PersonaAdvancedFields → EnvVarsEditor so
  missing credential keys render as amber locked rows, matching
  CreateAgentDialog behavior.
- PersonaAdvancedFields gains an optional requiredEnvKeys prop (defaults
  to [], no call-site breakage).

AddChannelBotDialog audited: selects ACP runtimes, no LLM
provider/API-key surface — no gate needed.

setup_mode.rs had a 30s per-channel cooldown (NUDGE_COOLDOWN /
channel_last_nudge) that silently swallowed the 2nd–4th rapid @mentions
in Will's repro. The user's intent is clear on every mention, and a
mention is an intentional act — rate-limiting is not needed here.

Remove the per-channel cooldown entirely; keep the event-id dedup which
guards against reconnect-replay double-nudging the same event.

- Drop NUDGE_COOLDOWN constant, channel_last_nudge HashMap, and the
  Duration/Instant/HashMap imports.
- Simplify should_nudge_for_event signature: remove channel_id,
  channel_last_nudge, nudge_cooldown params.
- Update both call tests to match the new signature.

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 de99aa5 to c65a9ee Compare July 2, 2026 16:32
wpfleger96 added a commit that referenced this pull request Jul 2, 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 added a commit that referenced this pull request Jul 2, 2026
…ention

Two fixes from Will's manual E2E test on #1411.

PersonaDialog ("New agent" menu) predates the shared gate components and
rendered its own bespoke 'Optional' label for LLM provider, so it missed
the #1411 readiness gate entirely. Wire it in:

- Import computeLocalModeGate + requiredCredentialEnvKeys from
  personaDialogPickers (same gate used by CreateAgentDialog).
- Replace the static 'Optional' span with RequiredFieldLabel; shows a red
  asterisk when the provider field is required and unset.
- Thread requiredEnvKeys through PersonaAdvancedFields → EnvVarsEditor so
  missing credential keys render as amber locked rows, matching
  CreateAgentDialog behavior.
- PersonaAdvancedFields gains an optional requiredEnvKeys prop (defaults
  to [], no call-site breakage).

AddChannelBotDialog audited: selects ACP runtimes, no LLM
provider/API-key surface — no gate needed.

setup_mode.rs had a 30s per-channel cooldown (NUDGE_COOLDOWN /
channel_last_nudge) that silently swallowed the 2nd–4th rapid @mentions
in Will's repro. The user's intent is clear on every mention, and a
mention is an intentional act — rate-limiting is not needed here.

Remove the per-channel cooldown entirely; keep the event-id dedup which
guards against reconnect-replay double-nudging the same event.

- Drop NUDGE_COOLDOWN constant, channel_last_nudge HashMap, and the
  Duration/Instant/HashMap imports.
- Simplify should_nudge_for_event signature: remove channel_id,
  channel_last_nudge, nudge_cooldown params.
- Update both call tests to match the new signature.

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 c65a9ee to 820461d Compare July 2, 2026 17:52
wpfleger96 added a commit that referenced this pull request Jul 2, 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 added a commit that referenced this pull request Jul 2, 2026
…ention

Two fixes from Will's manual E2E test on #1411.

PersonaDialog ("New agent" menu) predates the shared gate components and
rendered its own bespoke 'Optional' label for LLM provider, so it missed
the #1411 readiness gate entirely. Wire it in:

- Import computeLocalModeGate + requiredCredentialEnvKeys from
  personaDialogPickers (same gate used by CreateAgentDialog).
- Replace the static 'Optional' span with RequiredFieldLabel; shows a red
  asterisk when the provider field is required and unset.
- Thread requiredEnvKeys through PersonaAdvancedFields → EnvVarsEditor so
  missing credential keys render as amber locked rows, matching
  CreateAgentDialog behavior.
- PersonaAdvancedFields gains an optional requiredEnvKeys prop (defaults
  to [], no call-site breakage).

AddChannelBotDialog audited: selects ACP runtimes, no LLM
provider/API-key surface — no gate needed.

setup_mode.rs had a 30s per-channel cooldown (NUDGE_COOLDOWN /
channel_last_nudge) that silently swallowed the 2nd–4th rapid @mentions
in Will's repro. The user's intent is clear on every mention, and a
mention is an intentional act — rate-limiting is not needed here.

Remove the per-channel cooldown entirely; keep the event-id dedup which
guards against reconnect-replay double-nudging the same event.

- Drop NUDGE_COOLDOWN constant, channel_last_nudge HashMap, and the
  Duration/Instant/HashMap imports.
- Simplify should_nudge_for_event signature: remove channel_id,
  channel_last_nudge, nudge_cooldown params.
- Update both call tests to match the new signature.

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 820461d to 691fce3 Compare July 2, 2026 23:06
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 25 commits July 6, 2026 18:07
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>
…primitive)

Add a structured fenced sentinel block (buzz:config-nudge) to the setup-mode
nudge message body so the Buzz desktop app can detect and render the payload as
a rich Attachment card instead of plain text. Non-desktop clients see the
existing plain-text fallback unchanged.

Changes:
- buzz-acp setup_mode.rs: add agent_pubkey to SetupPayload; nudge_body() appends
  a fenced json block with agent_name, agent_pubkey, requirements array
- desktop runtime.rs: include agent_pubkey in setup payload env-var JSON
- desktop configNudge.ts: extractConfigNudge(), stripConfigNudgeSentinel(),
  isConfigNudgePayload() type guard, ConfigNudgePayload/ConfigNudgeRequirement types
- desktop config-nudge-attachment.tsx: ConfigNudgeCard built on Attachment primitive
  with state=error tint; per-requirement rows; AttachmentTrigger opens Edit Agent
- desktop openEditAgentEvent.ts: window-event bus for requestOpenEditAgent(pubkey),
  consumePendingOpenEditAgent, subscribeOpenEditAgent; mirrors openCreateAgentEvent
- desktop UserProfilePanel.tsx: subscribes to openEditAgent event and auto-opens
  editAgentOpen when panel mounts for the matching pubkey
- desktop markdown.tsx: detect sentinel in rendered body, strip it, render
  ConfigNudgeCard below prose content
- desktop check-file-sizes.mjs: bump overrides for markdown.tsx, runtime.rs,
  UserProfilePanel.tsx (all load-bearing additions, not debt)

Un-block save: remove local-mode config-completeness gate from canSubmit in both
CreateAgentDialog and EditAgentDialog. Required field markers (isRequired amber
rows) remain as visual cues; users can save incomplete config and the nudge card
guides them to fix it.

Tests: 416 Rust + 1453 TS tests pass; biome clean; tsc clean
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The mock bridge resolves prereqsQuery asynchronously — the dialog's
provider field becoming visible confirms only that providersQuery resolved,
but prereqsQuery resolves in a separate tick. The 5s default Playwright
timeout was insufficient in some environments; bumped to 10s for shots
01/02/03/08 (the ones that assert enabled with incomplete fields).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Three fixes from Thufir's mandatory re-review (pass 1):

Fix 1 (IMPORTANT): authenticate config-nudge card before rendering.
Any message body with a valid buzz:config-nudge fence was previously
rendered as a card regardless of message author, allowing untrusted
content to forge an official-looking destructive Attachment.

- Add configNudgeAuthorPubkey?: string | null to MarkdownProps
- Gate the extractConfigNudge useMemo: only extract when prop is set
  AND normalizePubkey(payload.agent_pubkey) === normalizePubkey(author)
- Import normalizePubkey into markdown.tsx
- In MessageRow, pass the message.pubkey only when the author is a
  known agent (resolvedAgentPubkeys). All other Markdown callsites
  pass nothing, keeping the card path off by default.
- Add memo comparator entry for configNudgeAuthorPubkey.
- Tests: authGuard_mismatchedAuthor_returnsNull,
  authGuard_noAuthorPubkey_returnsNull,
  authGuard_undefinedAuthorPubkey_returnsNull,
  authGuard_matchingAuthor_returnsPayload,
  authGuard_matchingAuthor_caseInsensitive (configNudge.test.mjs)

Fix 2 (IMPORTANT): clear stale pending open-edit request.
subscribeOpenEditAgent's live handler called handler() without clearing
pendingEditAgentPubkey, leaving a stale request that could reopen Edit
Agent on a later panel remount. Set pendingEditAgentPubkey = null inside
the matching branch before handler(), mirroring openCreateAgentEvent.
- New test file openEditAgentEvent.test.mjs with 6 tests; key invariant:
  after subscribeOpenEditAgent handles a live event,
  consumePendingOpenEditAgent returns false.

Fix 3 (MINOR): update stale runtime.rs comment.
The JSON shape comment at line 1764 omitted agent_pubkey; the code
at line 1796 was already correct. Comment updated.

Gates: biome clean, tsc clean, 1497 TS tests pass, 416 Rust tests pass.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Thufir pass-2 finding: the pass-1 fix checked message.pubkey (the
resolved display-author), which resolveEventAuthorPubkey can override
via a caller-controlled actor/p tag before falling back to event.pubkey.
A human-signed event carrying actor=<agent-pubkey> + a matching
buzz:config-nudge payload would still pass the guard and render a forged
card.

Fix: add signerPubkey (raw event.pubkey, normalized) to TimelineMessage
and populate it in formatTimelineMessages. MessageRow now gates
configNudgeAuthorPubkey on signerPubkey — not pubkey — AND additionally
requires message.kind === KIND_STREAM_MESSAGE, restricting the card
upgrade to the setup-listener wire format.

Also update the configNudge.ts wire-format comment to include
agent_pubkey, matching the runtime.rs fix from the pass-1 commit.

New regression test authGuard_signerIsHuman_tagAttributedToAgent_returnsNull
in configNudge.test.mjs: raw signer = human pubkey, payload agent_pubkey =
agent pubkey (as if actor-tagged to agent), configNudgeAuthorPubkey passed
as signer (human) → guard returns null, fence not stripped.

Gates: biome clean, tsc clean, 1498 TS tests pass, 416 Rust tests pass.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Thufir pass-3 finding: the prior regression test passed HUMAN_PUBKEY
directly into the guard shim, so a revert of MessageRow from signerPubkey
back to message.pubkey (the spoofable attributed author) would still pass.

Fix: extract the MessageRow author-selection predicate as a pure exported
helper getConfigNudgeAuthorPubkey() in configNudgeAuthPubkey.ts, wire
MessageRow to call it, and add configNudgeAuthPubkey.test.mjs exercising
three cases via a real TimelineMessage from formatTimelineMessages:

- signerIsHuman_actorTagAttributedToAgent_returnsUndefined: signer=human,
  actor-tag=agent, pubkey resolves to agent; helper returns undefined.
  Directly pins the seam: a revert to message.pubkey would cause this to
  return the agent pubkey and fail.
- signerIsAgent_genuine_returnsAgentPubkey: signer=agent; helper returns
  the agent pubkey (positive case).
- nonKind9_agentSigner_returnsUndefined: kind:9 restriction enforced even
  when the signer is a known agent.

Gates: biome clean, tsc clean, 1501 TS tests pass, 416 Rust tests pass.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…tion

Two render bugs reported on the config-nudge card:

1. Prose + card both rendered simultaneously. The markdown node was
   emitted unconditionally, so the human-readable fallback text
   appeared above the card on desktop. Gate markdownNode behind
   `configNudge === null` so the card fully replaces the prose on
   desktop; the wire body is unchanged so CLI/non-card clients still
   see the plaintext fallback.

2. Card text truncated unreadably. The Attachment had a fixed w-80
   (320px) width, and AttachmentTitle's built-in truncate plus truncate
   on each RequirementRow clipped content to a single line. Drop the
   fixed width in favour of max-w-lg, override the title to
   whitespace-normal line-clamp-2, and let requirement rows wrap with
   overflow-wrap:anywhere so long env key names don't overflow.

Also adds two render-suppression tests to configNudge.test.mjs that
pin the prose-suppression contract at the parse level.

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

Address Thufir pass 1 findings on the render-only fix commit:

IMPORTANT: The parser-level tests in configNudge.test.mjs did not pin the
render guard in markdown.tsx (`configNudge === null ? markdownNode : null`).
They would have passed even if the guard reverted to always rendering prose.

Fix: extract the configNudge computation into a pure module
(computeConfigNudge.ts) so it can be imported without the full markdown.tsx
dependency chain (emoji-mart crashes the node:test loader). Add three
render-level tests in markdown.test.mjs using a GuardStub component that
imports the same computeConfigNudge function MarkdownInner calls:
- nudgeGuard_sentinelPresentMatchingAuthor_cardRenderedProseAbsent (fails on
  revert to always-render-prose, verified manually)
- nudgeGuard_sentinelPresentWrongAuthor_proseRenderedCardAbsent
- nudgeGuard_noSentinel_proseRenderedCardAbsent

Wire MarkdownInner to call computeConfigNudge (removing the inline useMemo
body), and remove the now-redundant normalizePubkey and extractConfigNudge
direct imports from markdown.tsx.

MINOR: max-w-[min(100%,32rem)] replaces max-w-lg shrink-0 on the card root,
capping both readability width and container width to prevent overflow in
narrow timeline columns.

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

The regression guard in markdown.test.mjs previously re-implemented the
prose/card selection ternary in a local GuardStub, so a revert of
markdown.tsx's guard would leave tests green. This commit extracts the
branch into a named export — selectProseOrNudge — in computeConfigNudge.ts.
MarkdownInner calls the helper instead of an inline ternary; the tests import
and call the same function, so any change to the helper is directly observable
at unit-test time.

No behavior change: the helper is a one-line extraction of the existing
configNudge === null ? markdownNode : null branch.

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

Two fixes from Will's manual E2E test on #1411.

PersonaDialog ("New agent" menu) predates the shared gate components and
rendered its own bespoke 'Optional' label for LLM provider, so it missed
the #1411 readiness gate entirely. Wire it in:

- Import computeLocalModeGate + requiredCredentialEnvKeys from
  personaDialogPickers (same gate used by CreateAgentDialog).
- Replace the static 'Optional' span with RequiredFieldLabel; shows a red
  asterisk when the provider field is required and unset.
- Thread requiredEnvKeys through PersonaAdvancedFields → EnvVarsEditor so
  missing credential keys render as amber locked rows, matching
  CreateAgentDialog behavior.
- PersonaAdvancedFields gains an optional requiredEnvKeys prop (defaults
  to [], no call-site breakage).

AddChannelBotDialog audited: selects ACP runtimes, no LLM
provider/API-key surface — no gate needed.

setup_mode.rs had a 30s per-channel cooldown (NUDGE_COOLDOWN /
channel_last_nudge) that silently swallowed the 2nd–4th rapid @mentions
in Will's repro. The user's intent is clear on every mention, and a
mention is an intentional act — rate-limiting is not needed here.

Remove the per-channel cooldown entirely; keep the event-id dedup which
guards against reconnect-replay double-nudging the same event.

- Drop NUDGE_COOLDOWN constant, channel_last_nudge HashMap, and the
  Duration/Instant/HashMap imports.
- Simplify should_nudge_for_event signature: remove channel_id,
  channel_last_nudge, nudge_cooldown params.
- Update both call tests to match the new signature.

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

The config bridge already reads ~/.config/goose/config.yaml at runtime
to show provenance notes in the agent side panel. But neither the Rust
readiness gate nor the dialog gate consulted it at configuration time,
so env keys satisfied in the goose config file (e.g. DATABRICKS_HOST)
were incorrectly shown as required.

Changes:
- Rust: refactor goose_requirements() to accept Option<&RuntimeFileConfig>
  via injection rather than reading disk internally; collect_missing_requirements
  reads the file once and passes it in, keeping the fn pure and testable.
  File config silences provider, model, and extra-map credential keys.
- Rust: add read_goose_file_config() helper to config_bridge/mod.rs.
- Rust: add RuntimeFileConfigSubset struct and get_runtime_file_config Tauri
  command (sync) in agent_config.rs; register in lib.rs invoke handler.
- Frontend: add RuntimeFileConfigSubset type + getRuntimeFileConfig() to tauri.ts;
  add useRuntimeFileConfigQuery(runtimeId, {enabled}) to hooks.ts.
- Frontend: extend computeLocalModeGate() in personaDialogPickers.tsx to accept
  optional runtimeFileConfig; produce fileSatisfiedEnvKeys alongside existing
  missingNormalizedFields / missingEnvKeys.
- Frontend: add fileSatisfiedKeys prop to EnvVarsEditor — renders as muted
  read-only rows labeled 'Set in goose config' with a lock icon.
- Frontend: wire useRuntimeFileConfigQuery + fileSatisfiedEnvKeys through all
  three dialogs (PersonaDialog, CreateAgentDialog, EditAgentDialog).
- Fix PersonaDialog.tsx: providerIsRequired was computed from
  missingNormalizedFields.includes('provider') — would flip the asterisk off
  once a provider was selected. Changed to runtimeSupportsLlmProviderSelection
  (static predicate), which is the correct semantics.
- Fix PersonaDialog.tsx: runtime dropdown was labeled 'Provider', colliding
  with the 'LLM provider' field below. Relabeled to 'Agent runtime' to match
  CreateAgentDialog and EditAgentDialog.
- Tests: add goose_file_config_tests module with 4 deterministic unit tests
  (silences-databricks, env-empty-file-absent-not-ready,
  silences-provider-model-but-not-anthropic-key, env-provider-wins-over-file).
  Fix 3 existing goose tests to call goose_requirements() directly with None
  file config so they are deterministic on developer machines with real goose
  config files.
- File-size gate: add readiness.rs exception; bump PersonaDialog.tsx (1016→1032)
  and tauri.ts (1235→1250) with scoped justifications.

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

The goose config parser stored a flat top-level DATABRICKS_HOST value
under the key 'provider.host' when active_provider was absent, instead
of the canonical 'DATABRICKS_HOST'. This meant file_key_present() in
readiness.rs and satisfiedEnvKeys in the Tauri subset both missed the
value — so DATABRICKS_HOST was incorrectly marked required even when
it was already set in the goose config file.

Fix: separate flat-key resolution from nested-provider resolution in
parse_goose_config. A flat DATABRICKS_HOST always maps to
extra["DATABRICKS_HOST"] regardless of active_provider. The nested
providers.<name>.host path retains its active_provider-based key
selection (DATABRICKS_HOST for databricks variants, <p>.host otherwise).

Tests added:
- goose.rs: extend databricks_host_without_explicit_provider_infers_databricks
  to also assert extra["DATABRICKS_HOST"] is canonical; add
  goose_provider_databricks_flat_host_no_active_provider for the
  GOOSE_PROVIDER=databricks + flat host case
- readiness.rs (goose_file_config_tests): two new integration tests
  (goose_flat_databricks_host_in_file_config_silences_requirement,
  goose_goose_provider_databricks_flat_host_silences_databricks_host)
  confirming the parser fix propagates through to the gate

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
readiness.rs grew by 1 line (cargo fmt reformatted closures inline
after rebase). tauri.ts grew by 26 lines from PRs that landed on main
between the prior rebase base and this rebase tip (346efe0).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Three fixes from Wes's review:

1. readiness.rs — DMG PATH bug (blocking): cli_login_requirements probed
   probe_args[0] with a bare std::process::Command::new, so in a packaged
   macOS DMG (GUI PATH lacks npm/homebrew) claude/codex wouldn't be found
   even when logged in. Now resolves probe_args[0] through resolve_command()
   first; if None (genuinely absent) → NotReady. Adds two unit tests covering
   the missing-binary and resolvable-binary cases.

2. EnvVarsEditor.tsx — duplicate-row bug (blocking): updateRequiredValue
   called onChange without updating lastEmitted.current, causing the resync
   effect (recordsEqual check) to re-run setRows(toRows(value)) on every
   keystroke in an amber required-key row — surfacing the required key as a
   second normal editable row. Fix: update lastEmitted.current = next before
   calling onChange, same bookkeeping as emit(). Verified live: typing in the
   amber row updates the value in place with no duplicate row appearing.

3. markdown.tsx — dead stripConfigNudgeSentinel call (nit): when
   configNudge !== null, selectProseOrNudge returns null so processedContent
   is never rendered; the strip call was dead work. Removed the call and
   its now-unused import; added a comment explaining why.

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

Thufir pass-2 found two IMPORTANT correctness issues in Range A:

EnvVarsEditor — state invariant fix (Defect 1):
The previous lastEmitted patch closed the keystroke-dup-row path but left
two broken paths:

1. Pre-saved required key rendered twice: toRows(value) included required
   keys in rows, so a dialog opened with ANTHROPIC_API_KEY in value rendered
   the amber locked row (from requiredKeys.map) AND a second normal editable
   row (from rows). The comment at :103 already claimed required rows are not
   part of rows state — this makes that true.

2. User-typed required secret dropped on next normal-row edit: required keys
   were never in rows, so emit() called toRecord(rows) → {} → onChange({}),
   wiping any secret the user had just typed into the amber row.

Fix: add a skipKeys set (useMemo over requiredKeys + fileSatisfiedKeys) and
pass it to toRows at both the init and the resync sites. Export toRows and
toRecord as named exports for unit testing. Add buildRecord() inside the
component that merges required-key values from value into toRecord(rows)
before calling onChange, so required secrets survive subsequent normal-row
edits. lastEmitted.current is initialised to value (full record shape) so
recordsEqual compares on the same projection without special-casing.

Adds 10 unit tests in EnvVarsEditor.test.mjs covering both invariants:
toRows exclusion (pre-saved key → 0 normal rows for that key) and
buildRecord round-trip (type required value → add normal row → required
value survives in emitted record).

readiness.rs — load-bearing test assertion (Defect 2):
cli_login_requirements_resolvable_binary_runs_probe_at_resolved_path
previously iterated over reqs asserting only that each element is CliLogin,
meaning the loop ran zero times (empty → pass) when the probe succeeded.
A regression returning CliLogin for a resolvable success command would have
passed silently. Replace the zero-assertion loop with assert!(reqs.is_empty())
using true as the probe binary — universally resolvable on POSIX/macOS/Linux
CI, always exits 0.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Thufir pass-3 found that the row-resync effect guard checked only
recordsEqual(lastEmitted.current, value). When requiredKeys or
fileSatisfiedKeys changed (provider/runtime switch) while value stayed
equal to lastEmitted, the guard was false and rows were NOT rebuilt.
Two bad paths:

  - normal→required: the key was already a normal row; the amber row
    rendered from requiredKeys.map but the stale normal row stayed in
    rows. Duplicate row returns.

  - required→normal: the key was excluded from rows; the required row
    disappeared; rows were never rebuilt to include it; and buildRecord
    no longer merged it (not in requiredKeys). Key dropped on next edit.

Fix: export skipKeysEqual(a, b) and track the previous skipKeys in a
prevSkipKeys ref. The effect now triggers when EITHER value changed OR
the skip-key set changed. On a skip-key-only transition, rows are
rebuilt via setRows(toRows(value, skipKeys)) and lastEmitted.current is
refreshed to value.

Adds 5 tests for Invariant 3 (skipKeys transition detection):
  - skipKeysEqual_detects_normal_to_required_transition: asserts
    skipKeysEqual returns false for the transition AND that toRows with
    the new skip set excludes the now-required key.
  - skipKeysEqual_detects_required_to_normal_transition: asserts
    skipKeysEqual returns false AND that toRows with empty skip set
    restores the key as a normal row with its value intact.
  - skipKeysEqual_no_rebuild_when_keys_unchanged: same membership but
    different Set reference → true (no spurious rebuild).
  - skipKeysEqual_empty_sets_are_equal
  - skipKeysEqual_different_sizes_are_not_equal

Load-bearing proof: patching skipKeysEqual to always return true causes
the two transition tests and the size-mismatch test to fail; restoring
the real implementation makes all 15 pass.

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 f623802 to dbf60e1 Compare July 6, 2026 22:10
@wpfleger96 wpfleger96 merged commit d8355d3 into main Jul 6, 2026
28 checks passed
@wpfleger96 wpfleger96 deleted the duncan/agent-readiness branch July 6, 2026 22:23
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