feat(managed-agents): Claude Code agent config isolation (B1–B8 + A1/A7 + Thufir invariants) - #4557
Open
wpfleger96 wants to merge 6 commits into
Open
feat(managed-agents): Claude Code agent config isolation (B1–B8 + A1/A7 + Thufir invariants)#4557wpfleger96 wants to merge 6 commits into
wpfleger96 wants to merge 6 commits into
Conversation
Implements the claude-config-gaps plan of record (B1/B2/B3/B6/B7): B1/B2 — Paired atom injection at spawn: CLAUDE_CONFIG_DIR=<managed-root>/claude/<pubkey> and CLAUDE_SECURESTORAGE_CONFIG_DIR="" so each Claude agent has an isolated config root while sharing the owner's default Keychain credential namespace. Generated as a typed ClaudeLaunchPolicy at spawn time, never persisted. Remote agents are excluded. B7 — Layered settings.json projection at spawn: owner ~/.claude/settings.json is read as a base layer; Buzz canonical fields (effortLevel) are applied as an overlay; protected env keys (model authority, credential roots, isolation flags, provider/auth routing, BUZZ_* and NOSTR_* prefixes) are stripped from the base env object before merging. Missing/unreadable owner base degrades to overlay-only with a logged warning. Spawn fails if the projection write fails (B7.5 — never launch on wrong config). B3 — Config root cleanup on agent deletion: cleanup_claude_config_root removes the per-agent directory after the record is gone; validation rejects non-hex pubkeys and traversal attempts; filesystem errors are swallowed so cleanup never blocks deletion. B6 — effort_level added to spawn_config_hash so effort changes raise the restart badge. New module: managed_agents/claude_config — ClaudeLaunchPolicy, is_launch_policy_protected_key predicate (single source of truth for projection and launch assembly), project_settings_json, write_projected_settings, cleanup_claude_config_root. 28 tests covering all contract obligations. Closes #2692, #2884, #3371, #3493. Does not close #2265, #4004 (tracked as follow-up in PR). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…missionMode Auto, effort persistence, bridge path fix A1 — single startup model authority for Claude Code (local agents). Add apply_claude_spawn_policy() to claude_config/mod.rs and call it from spawn_agent_child() for local Claude agents. Injects ANTHROPIC_MODEL from the Buzz-resolved model, writes the B7 projected settings.json, and injects the B1 paired atom (CLAUDE_CONFIG_DIR + CLAUDE_SECURESTORAGE_CONFIG_DIR='') in one call. The protected-key predicate covers ANTHROPIC_MODEL so the owner settings.json env block cannot fight it. Collapses the prior 57-line inline block in runtime.rs; corrects a false comment that claimed descriptor.env was pre-filtered by the predicate (it is not — policy values win by being written after descriptor.env). Also adds owner_mcp_config_path() to claude_config/mod.rs for use by the config_bridge paths fixed in #3493 below. A7 — PermissionMode::Auto variant (wire string 'auto', alias 'auto'). buzz-acp/src/config.rs gets the Auto variant with tests for wire string, is_default() == false, and documented adapter-side graceful downgrade. B4 — canonical effort tier in build_thinking_field. record.effort_level is now the BuzzExplicit tier immediately above the record env tier in the tiers_list, making the B5-persisted effort the 'configured' value in the B4 status contract (configured == running on positive ack, unknown with both values shown on divergence). B5 — persist_agent_effort_level Tauri command and TypeScript binding. Adds persist_agent_effort_level(pubkey, effortLevel) in agent_config.rs, registers it in lib.rs, and exports persistAgentEffortLevel in tauri.ts. Called by the TS observer on positive ACP session/set_config_option ack. The persisted value seeds effortLevel in the projected settings.json overlay at the next spawn (B7). #3493 — config_bridge no longer hardcodes ~/.claude.json. config_bridge/claude.rs::read_config_file() and config_bridge/reader.rs::mcp_config_file_path_for_runtime() now call owner_mcp_config_path() and owner_settings_path() from claude_config::mod, removing the three independent home-dir concatenations. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
… + ratchet fixes Implements B8 (owner user-scope MCP server inheritance) per the frozen contract and Thufir's two hard invariants: 1. Lifecycle serialization: merge executes inside apply_claude_spawn_policy, which is called from spawn_agent_child while managed_agent_runtime_transition mutex is held. Every Buzz path that can spawn an agent (start_pair, restart, restore sweep) holds this mutex; stop acquires it and calls terminate_process + child.wait() before releasing — no live Claude process writes to the agent root when B8 runs. 2. Collision invariant: proactive case-insensitive filter at merge time. Inherited entries whose name matches the ACP server name are omitted + warned before write. Buzz wins by construction; no reliance on SDK object-merge order. Three distinct failure-state warnings (Thufir normative): - Owner unreadable: prior inherited set preserved (warn + return early) - Invalid agent file: state replaced (warn, proceed with empty base) - Write failure: servers may be stale (warn + continue; spawn never blocked) Per-entry provenance: ExtensionEntry gains an optional source field. Entries read from an isolated agent root (agent_mcp_path) are tagged source=owner_user_scope and rendered as inherited from owner user scope in McpServersSection. Panel re-anchor (#3493): read_config_surface accepts agent_mcp_path; for isolated local claude agents the panel reads MCP from the agent-root .claude.json. B8 tests (10 new in claude_config/tests.rs): merge writes servers, preserves non-mcpServers keys, owner-read-failure preserves prior set, empty owner clears inherited set, invalid agent replaced, collision filter (exact + case variant), empty ACP name disables filter, missing owner writes empty set, concurrent writes produce valid JSON, path location contract. Ratchet fixes: agent_discovery.rs, runtime.rs, AgentDefinitionDialog.tsx, AgentInstanceEditDialog.tsx, types.ts — all five ratchet checks pass. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…gaps * origin/main: feat(desktop): make onboarding model defaults skippable (#3968) ci: add guarded desktop release cache prewarm (#4575) fix(mobile): recover stale relay sessions (#4372) chore(release): release Buzz Desktop version 0.5.4 (#4562) test(mobile): assert follow boundary semantics (#4559) docs(release): align desktop handoff instructions (#3988) fix: report agent usage per provider round, not once per turn (#4545) fix(desktop): harden Windows installs against Defender block and orphaned Node (#4382) feat(desktop): improve channel template discovery (#4549) fix(desktop): save key backups to authorized path (#4022) Add channel activity hover menu (#3935) feat(desktop): show saved Run on settings when editing an agent (#4539) Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…A1/B2, panel warnings, tests
B5 (CRITICAL): set_idle_agent_effort now calls agent.state.invalidate_all()
after queuing desired_effort so the next turn creates a fresh session and
applies the effort via session_set_config_option immediately, mirroring
the idle-path model switch. Without the invalidation, effort changes
accumulated silently but never propagated to the running adapter.
Adds harness tests for handle_set_config_option_control
(control_result_tests module in lib.rs): Queued->ok emits ack + session
invalidated; no thought_level_config_id in pool falls back to synthetic ok;
unknown configId emits synthetic ok. Pool test
test_set_idle_agent_effort_queues_and_invalidates_session pins the
Queued+invalidation invariant with a real OwnedAgent.
Remote A1/B2 (IMPORTANT): agents_deploy.rs already emits ANTHROPIC_MODEL
for claude runtimes and BUZZ_ACP_MODEL for all others (was in working tree,
not yet committed). Test launch_block_claude_runtime_uses_anthropic_model_not_buzz_acp_model
asserts the split.
Panel-visible warnings B7+B8 (IMPORTANT): warning infrastructure
(write_spawn_warnings / read_spawn_warnings / config_warnings on
RuntimeConfigSurface + panel render) was in working tree; this commit
brings it to HEAD. Panel uses key={warning} not key={index} per biome lint.
Missing tests (IMPORTANT): apply_claude_spawn_policy spawn-env tests
(B1 paired atom present-and-empty, BUZZ_ACP_MODEL absent) and
test_b8_agent_write_failure were already in claude_config/tests.rs;
adds bridge-level provenance test
b8_mcp_path_override_tags_entries_owner_user_scope to claude.rs verifying
owner_user_scope tag when reading from agent root path.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…5 persistence category gate Fix three CI failures introduced in round 3: 1. AgentDefinitionDialog.tsx + AgentInstanceEditDialog.tsx: inline number[] literal (from ratchet fix commit a64110e) is not assignable to Easing (TS2322). Revert both to use ADVANCED_FIELDS_MOTION_TRANSITION constant as on main. 2. test_b8_agent_write_failure_does_not_block_spawn uses std::os::unix + Permissions::from_mode, which do not exist on windows-msvc. Add #[cfg(unix)] to gate the test to unix targets. 3. B5 persistence gate had two holes: (a) gated on literal configId "effort" — breaks if adapter renames the thought_level configId; (b) synthetic ok from the fallback branch reachable from the UI when the pool has no thought_level_config_id — Desktop would persist a value nothing forwarded. Fix: harness emits category: "thought_level" ONLY on the real-forward branch (when thought_level_id matches). Observer gates persistence on category === "thought_level" instead of a literal configId. Synthetic acks carry no category so Desktop cannot persist them. Add tests: test_b5_real_forward_ack_includes_thought_level_category (renamed configId still persists), test_b5_synthetic_ok_ack_has_no_ category (pool empty -> no category). Update existing B5 tests to assert category presence/absence. 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.
Summary
Implements the Claude Code agent config plan of record: typed launch policy (B2), protected-key predicate (B7), settings.json projection (B7), session config status (B4), resource lifecycle (B3), AGENTS.md coverage (B6), paired isolation atoms (B1), single startup model authority (A1/B2), session-level effort control (B5), owner MCP inheritance (B8), and permission mode (A7).
Changes
A1 — Single startup model authority (local + remote)
apply_claude_spawn_policyinjectsANTHROPIC_MODELfrom Buzz's resolved model and removesBUZZ_ACP_MODEL— eliminating the dual-startup-authority defect for local agentsagents_deploy.rsemitsANTHROPIC_MODELfor remote claude deployments andBUZZ_ACP_MODELfor all other runtimesA7 —
PermissionMode::Auto"auto"inbuzz-acp/src/config.rs;is_default() == false; adapter-side graceful downgrade documentedB1 — Paired isolation atoms
apply_claude_spawn_policysetsCLAUDE_CONFIG_DIR(per-agent root) andCLAUDE_SECURESTORAGE_CONFIG_DIR(empty string) on every local claude spawnB2 — Typed launch policy
ClaudeLaunchPolicy::buildconstructs per-agent config/secure-storage paths from the managed root; used as the authoritative source for B7/B8/B1B3 — Resource lifecycle
try_cleanup_claude_config_rootcalled on agent removalB4 — Session config status
persist_agent_effort_levelTauri command +persistAgentEffortLevelTS bindingrecord.effort_levelis the canonicalBuzzExplicittier inbuild_thinking_field's tier listB5 — End-to-end effort control
thought_level_config_idextracted fromsession/newresponse without hardcoding — stored onAgentModelCapabilitiesset_idle_agent_effortqueuesdesired_effort+ invalidates all sessions so the next turn creates a fresh session and applies effort viasession_set_config_optionimmediately (mirrors idle-path model switch)handle_set_config_option_controlemits real status acks ("ok"/"pending_session"/"no_idle_agent") based on pool outcomedispatchControlResultobserver in TS persists effort only onstatus === "ok"EffortPickerUI component inAgentConfigPanel— visible only wheneffortConfigIdis set (post-first-session for claude agents)B6 — AGENTS.md coverage
B7 — Protected-key predicate + settings.json projection
is_launch_policy_protected_keypredicate covers projection and spawn-env filterproject_settings_json+write_projected_settingswrite the owner overlay to the per-agent settings.jsoncollect_stripped_env_keys+stripped_owner_env_keysinRuntimeConfigSurface+ panel renderlast_spawn_warnings.jsonand surfaced inconfig_warningspanel fieldB8 — Owner MCP inheritance
merge_agent_mcp_servers_with_warningsperforms key-level merge at spawn: reads owner user-scopemcpServers, filters ACP-name collisions, atomically replaces agent-root.claude.json'smcpServerskey.claude.jsonfor isolated agents with per-entry"owner_user_scope"provenancemanaged_agent_runtime_transitionmutex + process reap before B8 writeBUZZ_ACP_MCP_COMMAND, never.claude.json)#3493
Closes
#2692 #2884 #3371 #3493
Gate results (commit
60058c2e6)just desktop-tauri-test: 2192 passed; 0 failed; 14 ignoredjust desktop-test: 4013 passed; 0 failedjust desktop-check: biome clean, file-size ratchet green, px-text clean, pubkey-truncation cleanjust mobile-test: passes on pinned flutter 3.41.7Live gate
Pending — requires Will to run a local Desktop spawn against this branch. Verifies: (1) auth without login prompt, (2) per-agent config root written, (3) first turn succeeds on fresh root.