fix(managed-agents): apply Claude Code agent config via ACP/env; respect CLAUDE_CONFIG_DIR - #4557
Open
wpfleger96 wants to merge 8 commits into
Open
fix(managed-agents): apply Claude Code agent config via ACP/env; respect CLAUDE_CONFIG_DIR#4557wpfleger96 wants to merge 8 commits into
wpfleger96 wants to merge 8 commits into
Conversation
wpfleger96
force-pushed
the
duncan/claude-config-gaps
branch
from
August 4, 2026 23:38
d3662b8 to
952e5c4
Compare
…ect CLAUDE_CONFIG_DIR Fix five gaps in Claude Code agent configuration in Buzz Desktop. Buzz sets config via env vars at spawn and ACP messages at runtime; file layout on disk stays the owner's. Model (fixes #2692): ANTHROPIC_MODEL is injected at local claude spawn as the single startup model authority. BUZZ_ACP_MODEL is removed from the spawned env to prevent two simultaneous model authorities. Remote claude deploys receive ANTHROPIC_MODEL in policy_env, never BUZZ_ACP_MODEL. PermissionMode Auto (fixes #2884): adds the Auto variant to PermissionMode with wire string "auto" and tests. The adapter handles graceful downgrade when the active model does not support it. Effort end-to-end via ACP (B5): EffortPicker in the config panel discovers the thought_level configId from the session cache (never hardcoded) and calls set_config_option. The harness verifies the configId, forwards to the adapter, and emits an ack carrying category: "thought_level" only on a real forward. The observer persists the canonical value only on ok+category. At next session creation, desired_effort is applied via session_set_config_option so the persisted default takes effect on first turn after a restart. Honest acks: no fabricated ok anywhere. Synthetic acks (unknown configIds) carry no category so the observer cannot persist them. the agent's effective CLAUDE_CONFIG_DIR env var (record > persona > global), falling back to ~/.claude/ when unset. MCP config stays at ~/.claude.json regardless (CLAUDE_CONFIG_DIR does not remap the global MCP config file). The panel shows a Keychain caveat note when a custom dir is active: Claude keys its login to the config-dir path, so a custom dir creates a fresh Keychain namespace and the agent needs re-authentication unless the user also manages CLAUDE_SECURESTORAGE_CONFIG_DIR. Closes #2692, #2884, #3493 Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96
force-pushed
the
duncan/claude-config-gaps
branch
from
August 4, 2026 23:39
952e5c4 to
c1397bf
Compare
…aude.json Finding 1 — effort startup-default glue: - Add BUZZ_ACP_EFFORT_LEVEL CLI arg + env var to buzz-acp config - Add startup_effort to PoolStartup and OwnedAgent structs - Extract OwnedAgent::resolve_startup_effort() method that arms desired_effort from startup_effort + capabilities-derived configId - Call resolve_startup_effort() at first session creation after capabilities are populated (replaces inline block) - Inject BUZZ_ACP_EFFORT_LEVEL at spawn in runtime.rs from record.effort_level - Fix stale doc comment in types.rs (was settings.json seeding language) Finding 2 — .claude.json path honors CLAUDE_CONFIG_DIR: - Fix claude.rs read_config_file: resolve .claude.json relative to config_dir when set, same as settings.json (binary does the same) - Fix reader.rs mcp_config_file_path_for_runtime to accept and use claude_config_dir for the claude case - Fix agent_config.rs CLAUDE_CONFIG_DIR lookup to use resolve_effective_agent_env instead of hand-rolled record chain that skipped definition-env tier and baked floor Tests added: - resolve_startup_effort arms desired_effort from startup_effort + configId - resolve_startup_effort does not override live pick - resolve_startup_effort is no-op when startup_effort absent - resolve_startup_effort is no-op when model lacks thought_level Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Three test-mod Config{} literals in buzz-acp were missing the new
effort_level field, causing compilation failures under --all-targets
(Windows CI caught this; desktop gates run a separate workspace).
Add effort_level: None to all three.
Run cargo fmt --all and cargo fmt --manifest-path desktop/src-tauri/Cargo.toml
to fix the rustfmt diffs caught by CI Rust Lint and desktop-tauri-fmt-check.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…emote parity, option validation
Replace per-worker desired_effort/startup_effort with ONE pool-level
desired_effort (AgentPool::desired_effort). Closes I-2 (queued-as-ok),
I-3 (first-idle-only), and I-5 (spawn-drift) together.
Rust (crates/buzz-acp):
- AgentPool gains desired_effort field; set_idle_agent_effort replaced
by set_pool_effort (stores pool-level, invalidates all idle sessions,
returns Stored { invalidated }) and clear_pool_effort (sets None,
invalidates all idle).
- try_claim always copies pool.desired_effort onto the claimed agent.
- return_agent propagates startup-resolved effort back to pool level.
- create_session_and_apply_model emits honest final control_result ack
(ok/failure) after the real ACP call; no pre-persist on queued state.
- handle_set_config_option_control: clear path emits "cleared"; non-empty
path emits "pending_session"; I-7 validates value against adapter-
advertised options and emits "invalid_value" on mismatch.
- M-1: restore damaged PoolStartup doc comment.
- Tests: set_pool_effort_stores_and_invalidates, multi-worker convergence,
clear_pool_effort, test_b5_empty_value_emits_cleared_ack,
test_b5_invalid_value_emits_invalid_value_ack_and_does_not_update_pool.
TypeScript (desktop/src):
- effortOutcome.ts: awaitEffortOutcome helper — subscribes before send,
awaits correlated final result (ok/failure/invalid_value/cleared), falls
back to pending_session on timeout.
- effortOutcome.test.mjs: 13 tests covering all statuses, correlation,
cleanup, and deferred-path (pending_session → final ok).
- EffortPicker: uses awaitEffortOutcome; empty value = clear (I-1);
effortOptions from adapter (I-7); surfaces pending_session / failure /
invalid_value status messages; invalidates queries on ok/cleared.
- observerRelayStore: persist on ok+thought_level (final applied ack) OR
cleared+thought_level (Auto clear); skip all other statuses.
- types.ts: SetConfigOptionResult named type; effortOptions field on
RuntimeConfigSurface.
agents_deploy.rs (I-4): project record.effort_level → BUZZ_ACP_EFFORT_LEVEL
into remote policy_env, mirroring local spawn; positive/negative tests.
config_bridge (M-2/M-3): direct test for mcp_config_file_path_for_runtime
with custom CLAUDE_CONFIG_DIR; treat empty/blank CLAUDE_CONFIG_DIR as unset
in agent_config.rs (matches Claude's || homedir() semantics).
reader.rs: effort_options populated from session cache for claude runtime.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
V-1 (clear resurrection prevention): add effort_ever_picked flag to AgentPool. return_agent propagates a startup-resolved desired_effort to pool level ONLY when no live pick/clear has ever been made. A user clear sets effort_ever_picked=true; subsequent worker returns can no longer resurrect the cleared value. V-2 (all-busy capability loss): add PoolEffortCapabilities cache and capabilities_ever_discovered flag to AgentPool. Pool-level cache is written at return_agent (refreshed from the returning worker's capabilities) and via notify_capabilities_discovered. The handle_set_config_option_control path reads from the cache instead of scanning idle agent slots, so picks and clears are never silently dropped when all workers are checked out. V-3 (busy-worker session convergence): return_agent compares the worker's checkout snapshot (desired_effort) against the current pool value. If they differ (a pick or clear arrived while the worker was busy), the worker's sessions are invalidated so the next try_claim creates a fresh session under the current pool value. Tests added (pool.rs effort_tests): - test_v1_clear_while_busy_return_does_not_resurrect_cleared_effort - test_v2_pick_while_all_busy_is_stored_not_dropped - test_v3_busy_worker_sessions_invalidated_on_return_after_pick - test_startup_effort_propagates_to_pool_on_first_return_when_no_live_pick Existing tests updated (lib.rs control_result_tests, pool.rs effort_tests): four tests that construct AgentPool::from_slots with agents carrying model_capabilities now also call notify_capabilities_discovered to populate the pool-level cache, matching production behavior where the cache is written at return_agent time. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…scovery window Before this fix, a pick or clear sent during the first turn (before any worker returns and populates the pool capability cache) fell through to the synthetic-ok branch. `capabilities_ever_discovered` was false, `effort_capabilities.config_id` was None, and `is_thought_level` was false: the harness emitted a fabricated ok with no category, the value was never stored, and the EffortPicker reported success with nothing applied or persisted. Fix: - Desktop sends `category: "thought_level"` on all effort frames (sendSetConfigOption gains an optional category param; EffortPicker passes it). The harness uses this as the trust signal in the pre-discovery window (case D). - Harness (lib.rs): adds case D — `!capabilities_ever_discovered && frame_category == "thought_level" && configId != "unknown"` — to the `is_thought_level` check. Picks and clears in this window are stored and acked `pending_session`/`cleared` rather than synthetic ok. - pool.rs: removes the `NoCatalog` guard from `set_pool_effort` and the `NoCatalog` variant entirely. The caller already gates on `is_thought_level`; `set_pool_effort` always stores. Removes the unreachable `NoCatalog => pending_session` match arm from the handler. - `notify_capabilities_discovered` moved to `#[cfg(test)]` with an honest doc. In production the cache is written only at `return_agent`. All false doc claims (pool.rs:264, 306; lib.rs:1020-21) corrected. - Tests: two new case-D tests in lib.rs (`test_b5_pre_discovery_pick_with_category_stores_and_emits_pending_session`, `test_b5_pre_discovery_clear_with_category_emits_cleared_not_synthetic_ok`); existing NoCatalog tests rewritten to match new semantics (always stores). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…ation (IMPORTANT 1/2/3) Addresses all three IMPORTANT findings from Thufir Pass 2 plus the extraction MINOR that feeds harness-side invalid_value validation in production. IMPORTANT 1 — provisional pick + rollback: - Added committed_effort field to AgentPool as the last adapter-confirmed baseline. Pending picks are provisional until the ACP call resolves. - return_agent: generation-matched Applied → commit; Failed → rollback to committed_effort so the failed candidate is never recopied by try_claim. - Stale-generation results (superseded by newer pick/clear) are discarded entirely — no commit, no rollback, pool unchanged. IMPORTANT 2 — per-request nonce correlation: - AgentPool.effort_generation incremented on every pick/clear; carried on checked-out agents as desired_effort_gen. Pool echoes it as a nonce field (from pending_effort_nonce, set from the Desktop's crypto.randomUUID() nonce) in all immediate and final acks. - Desktop: sendSetConfigOption gains optional nonce param; AgentConfigPanel generates crypto.randomUUID() per request and registers it before awaiting. - awaitEffortOutcome: nonce is the primary correlation key; rejects acks where frame.nonce !== nonce even if configId and value match (stale same-value picks). - observerRelayStore: persistence gate checks ackNonce === registered before persisting ok/cleared; backwards-compat — acks without nonce always pass. - Four new nonce correlation tests in effortOutcome.test.mjs. IMPORTANT 3 — two-phase clear: - handle_set_config_option_control: empty value now emits pending_session (non-terminal) instead of terminal cleared. Pool is cleared immediately so future sessions run without effort, but confirmation waits for adapter. - create_session_and_apply_model: new else-if branch — desired_effort=None but desired_effort_gen set → pending clear; emits final cleared ack with nonce+ category after the session creates without effort override. Observer persists null only on this final cleared. - Tests updated: test_b5_empty_value_emits_pending_session_ack and test_b5_pre_discovery_clear_with_category_emits_pending_session_not_synthetic_ok. MINOR — extract_agent_config_options (feeds harness invalid_value production): - New function in acp.rs that retains both category=="model" and category=="thought_level" entries from session/new configOptions. - AgentModelCapabilities.config_options_raw now populated via this function so the pool-level capability cache includes thought_level valid_values in production (not just tests). The harness invalid_value guard now runs for real adapter picks. MINOR — picker query invalidation: - AgentConfigPanel: on ok/cleared outcome, invalidates agentConfigSurfaceQueryKey (source of currentEffort) in addition to managedAgentsQueryKey so the panel reflects the new committed effort immediately. MINOR — restore damaged doc comment: - lib.rs test_b5_real_forward_ack_includes_thought_level_category: restored the middle line of the three-line doc comment that was dropped in a prior commit. Pool tests: 4 new (test_failure_rolls_back_desired_effort_to_committed, test_applied_commits_desired_effort_to_committed, test_stale_gen_failure_does_not_rollback_pending_pick, test_cleared_commits_none_to_committed_effort). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
F-1: gate resolve_startup_effort on desired_effort_gen.is_none() resolve_startup_effort re-armed desired_effort from startup_effort whenever it was None, which could not distinguish 'never picked' from 'user just cleared'. A post-clear checkout carries desired_effort=None and desired_effort_gen=Some(N); the old guard would re-arm the startup value, fire the apply branch instead of the cleared branch, and emit an ok ack carrying the clear's nonce — causing the observer to persist the old value over the user's explicit clear, with perpetual V-3 churn. Fix: add && self.desired_effort_gen.is_none() so the startup-seeding path is only taken when no live pick/clear has ever been stored (gen never set). Startup seeding (gen None) still works; post-clear (gen Some) falls through to the cleared branch as intended. New test: test_resolve_startup_effort_noop_after_live_clear_gen_is_some F-2: emit real configId in final cleared ack (pool.rs:1465) The cleared ack hardcoded "effort" as the configId. awaitEffortOutcome checks frame.configId !== configId before the nonce, so any adapter whose thought_level configId differs would leave the clear promise unsettled and fall to the 8s timeout. Fix: read the configId from agent.model_capabilities (populated just above at line 1336), falling back to "effort" when capabilities are not yet populated (pre-discovery clear path). F-3: box PoolEvent::Wake large variant (lib.rs:1910) AgentPool grew past clippy's large_enum_variant threshold after the committed_effort, nonce, and capability-cache fields were added. CI Rust Lint and Windows Rust both failed on this branch at 333362a. Fix: Box<Result<AgentPool, String>> in the Wake variant; box on construction, unbox on match. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes five agent-config gaps for Claude Code managed agents in Buzz Desktop.
What this changes
A1 — Model authority for local and remote Claude spawns
apply_claude_model_envinruntime.rssetsANTHROPIC_MODELfrom the agent's effective model and removesBUZZ_ACP_MODELfrom the child env (Desktop owns model selection; ACP is the live-switch channel only). Remote Claude deploys addANTHROPIC_MODELtopolicy_envviaagents_deploy.rs;BUZZ_ACP_MODELis never injected remotely. Contract tests cover both the local inject-and-remove path and the remote Claude/non-Claude cases.B5 — Effort end-to-end via ACP
Pool-level desired effort. A single
AgentPool::desired_effortis the sole authority.set_pool_effortstores the value and invalidates all idle sessions;clear_pool_effortresets toNoneand also invalidates. Everytry_claimcopies the pool's current value onto the checked-out agent so all workers converge immediately — busy workers apply it at their next session.Provisional picks with commit/rollback.
AgentPool::committed_efforttracks the last adapter-confirmed value. A pick is provisional untilcreate_session_and_apply_modelresolves: onok→ commitcommitted_effort; onfailure→ roll backdesired_efforttocommitted_effortso the failed candidate is never recopied bytry_claimor retried. A monotoniceffort_generationcounter letsreturn_agentdistinguish the current pick from superseded ones — stale-generation results are discarded without touching committed state.Startup default.
record.effort_levelis injected asBUZZ_ACP_EFFORT_LEVELat local spawn (runtime.rs) and intopolicy_envat remote deploy (agents_deploy.rs). The harness reads it intoPoolStartup.startup_effort;resolve_startup_effort()armsdesired_effortafter capabilities arrive at the first session creation, using the capabilities-derivedthought_level_config_id.return_agentpropagates a startup-resolved effort back to pool level only when no live pick or clear has ever been made (effort_ever_picked = false), preventing resurrection of a cleared value.Two-phase acks for both pick and clear.
handle_set_config_option_control:pending_sessionimmediate ack (pool stored); finalok/failurearrives fromcreate_session_and_apply_modelafter the real ACP call.pending_sessionimmediate ack (pool cleared toNone); finalclearedack arrives fromcreate_session_and_apply_modelwhen the session runs without an effort override.invalid_valueack; pool unchanged.Per-request nonce correlation. The Desktop generates a
crypto.randomUUID()nonce per pick/clear and passes it in the control frame. The harness echoes it in all acks (immediate and final).awaitEffortOutcomeuses the nonce as the primary correlation key, rejecting acks whereframe.nonce !== nonce; this prevents a stale ack from a same-value pick arriving late or from a superseded request settling the wrong promise. The observer persistence gate also checks the registered nonce, so late acks (e.g. after the 8s timeout) cannot overwrite a newer persisted value.Observer persistence.
dispatchControlResultpersists onok + thought_level(final applied) orcleared + thought_level(final clear confirmed); all other statuses (includingpending_sessionandfailure) do not persist. The nonce gate adds a second layer: mismatched nonces are silently dropped.EffortPicker. Subscribes to
control_resultbefore sending (no dropped acks), awaits the correlated final result viaawaitEffortOutcomewith an 8-second timeout, surfacespending_session/failure/invalid_valuestatus messages, and invalidates bothmanagedAgentsQueryKeyandagentConfigSurfaceQueryKeyonok/cleared. Options come from the adapter-advertisedeffortOptions(exported throughRuntimeConfigSurface;extract_agent_config_optionsretains both"model"and"thought_level"entries so the pool-level capability cache and the harnessinvalid_valueguard work in production); falls back tolow/medium/highfor older adapters.PermissionMode
Autoconfig.rsaddsAutoto thePermissionModeenum and tests coverage incrates/buzz-acp.#3493 — Respect user-set
CLAUDE_CONFIG_DIRconfig_bridgeresolves bothsettings.jsonand.claude.jsonpanel paths from the agent's effective env viaresolve_effective_agent_env(full tier chain: baked floor → definition env → global → persona → record).mcp_config_file_path_for_runtimehonors the resolved custom dir; empty/blank values are treated as unset (matching Claude'sCLAUDE_CONFIG_DIR || homedir()semantics).agent_config.rsroutes through the same effective-env path so the panel and the process cannot diverge on which dir is active.AgentConfigPanelshows a Keychain-logout caveat when a custom dir is in effect (Claude behavior, not Buzz's).Scope explicitly excluded
Per-agent config dir provisioning,
CLAUDE_SECURESTORAGE_CONFIG_DIRsentinel injection, settings.json projection, protected-key stripping, B8 MCP inheritance, spawn serialization, andlast_spawn_warningssurface are absent from this diff. Silent-fallback machinery for non-Claude runtimes (#2265/#4004) is a tracked follow-up.Closes #2692, #2884, #3493