Skip to content

feat(managed-agents): Claude Code agent config isolation (B1–B8 + A1/A7 + Thufir invariants) - #4557

Open
wpfleger96 wants to merge 6 commits into
mainfrom
duncan/claude-config-gaps
Open

feat(managed-agents): Claude Code agent config isolation (B1–B8 + A1/A7 + Thufir invariants)#4557
wpfleger96 wants to merge 6 commits into
mainfrom
duncan/claude-config-gaps

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Aug 3, 2026

Copy link
Copy Markdown
Member

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_policy injects ANTHROPIC_MODEL from Buzz's resolved model and removes BUZZ_ACP_MODEL — eliminating the dual-startup-authority defect for local agents
  • agents_deploy.rs emits ANTHROPIC_MODEL for remote claude deployments and BUZZ_ACP_MODEL for all other runtimes

A7 — PermissionMode::Auto

  • Wire string "auto" in buzz-acp/src/config.rs; is_default() == false; adapter-side graceful downgrade documented

B1 — Paired isolation atoms

  • apply_claude_spawn_policy sets CLAUDE_CONFIG_DIR (per-agent root) and CLAUDE_SECURESTORAGE_CONFIG_DIR (empty string) on every local claude spawn

B2 — Typed launch policy

  • ClaudeLaunchPolicy::build constructs per-agent config/secure-storage paths from the managed root; used as the authoritative source for B7/B8/B1

B3 — Resource lifecycle

  • try_cleanup_claude_config_root called on agent removal

B4 — Session config status

  • persist_agent_effort_level Tauri command + persistAgentEffortLevel TS binding
  • record.effort_level is the canonical BuzzExplicit tier in build_thinking_field's tier list

B5 — End-to-end effort control

  • thought_level_config_id extracted from session/new response without hardcoding — stored on AgentModelCapabilities
  • set_idle_agent_effort queues desired_effort + invalidates all sessions so the next turn creates a fresh session and applies effort via session_set_config_option immediately (mirrors idle-path model switch)
  • handle_set_config_option_control emits real status acks ("ok" / "pending_session" / "no_idle_agent") based on pool outcome
  • dispatchControlResult observer in TS persists effort only on status === "ok"
  • EffortPicker UI component in AgentConfigPanel — visible only when effortConfigId is set (post-first-session for claude agents)

B6 — AGENTS.md coverage

  • Spawn policy, isolation atoms, and effort control documented in repo AGENTS.md

B7 — Protected-key predicate + settings.json projection

  • Single is_launch_policy_protected_key predicate covers projection and spawn-env filter
  • project_settings_json + write_projected_settings write the owner overlay to the per-agent settings.json
  • B7.7c: collect_stripped_env_keys + stripped_owner_env_keys in RuntimeConfigSurface + panel render
  • Spawn warnings (B7 base unreadable + B8 failure states) written to last_spawn_warnings.json and surfaced in config_warnings panel field

B8 — Owner MCP inheritance

  • merge_agent_mcp_servers_with_warnings performs key-level merge at spawn: reads owner user-scope mcpServers, filters ACP-name collisions, atomically replaces agent-root .claude.json's mcpServers key
  • Three distinct failure warnings: owner unreadable (prior set preserved), agent file unparsable (state replaced), write failure (spawn continues)
  • Panel reads MCP servers from agent-root .claude.json for isolated agents with per-entry "owner_user_scope" provenance
  • Lifecycle serialization: managed_agent_runtime_transition mutex + process reap before B8 write
  • Collision invariant: file-level filter + registration-layer isolation (ACP server comes via BUZZ_ACP_MCP_COMMAND, never .claude.json)

#3493

  • Config bridge now reads settings from per-agent root for isolated agents and owner file otherwise; panel reflects the actual config the spawned process reads

Closes

#2692 #2884 #3371 #3493

Gate results (commit 60058c2e6)

  • just desktop-tauri-test: 2192 passed; 0 failed; 14 ignored
  • just desktop-test: 4013 passed; 0 failed
  • just desktop-check: biome clean, file-size ratchet green, px-text clean, pubkey-truncation clean
  • just mobile-test: passes on pinned flutter 3.41.7

Live 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.

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>
@wpfleger96
wpfleger96 requested a review from a team as a code owner August 3, 2026 16:25
…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>
@wpfleger96 wpfleger96 changed the title feat(managed-agents): isolate Claude Code agent config per-agent root feat(managed-agents): claude config gaps — isolation, model authority, PermissionMode Auto, effort persistence, bridge path fix Aug 3, 2026
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 2 commits August 3, 2026 16:12
… + 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>
@wpfleger96 wpfleger96 changed the title feat(managed-agents): claude config gaps — isolation, model authority, PermissionMode Auto, effort persistence, bridge path fix feat(managed-agents): Claude Code agent config isolation (B1–B8 + A1/A7 + Thufir invariants) Aug 3, 2026
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 2 commits August 3, 2026 18:12
…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>
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