Skip to content

feat(buzz-acp): wire persona-pack MCP servers and skills into spawn - #7359

Open
mraad wants to merge 6 commits into
block:mainfrom
mraad:feat/acp-persona-pack-spawn
Open

feat(buzz-acp): wire persona-pack MCP servers and skills into spawn#7359
mraad wants to merge 6 commits into
block:mainfrom
mraad:feat/acp-persona-pack-spawn

Conversation

@mraad

@mraad mraad commented Sep 4, 2026

Copy link
Copy Markdown

Summary

Persona packs already resolved per-persona MCP servers and scoped skills, but
nothing consumed the result: resolve_pack was reachable only from
buzz pack validate/show, and ResolvedPersona.skills carried the comment
"reserved for future use, not yet wired". Every agent behind a harness host
therefore saw the same tools, which is the opposite of
VISION_AGENT.md's "ten agents in parallel behind Buzz, each with their own MCP
configuration".

This wires the pack into buzz-acp spawn.

--persona-pack <DIR> / --persona <NAME> (mutually required;
BUZZ_ACP_PERSONA_PACK / BUZZ_ACP_PERSONA_NAME) resolve a pack at startup:

  • MCP — the persona's merged servers (pack .mcp.json + frontmatter
    mcp_servers:) are passed to every session/new alongside the Buzz-managed
    server. The Buzz server keeps its name, so a colliding pack entry is dropped
    rather than shadowing the agent's relay credentials.
  • Skillspack::resolve_skills scoping (claimed by a persona → that
    persona only; unclaimed → all) is materialized into
    <workdir>/.agents/skills/<name> and linked into the per-runtime discovery
    directories (.claude/skills, .goose/skills, .codex/skills). An existing
    skill is never overwritten but is still linked, so an operator-pinned version
    survives and stays discoverable.

--workdir <DIR> (BUZZ_ACP_WORKDIR) is applied before anything reads the
working directory, so the ACP session cwd, the agent subprocess, and skill
discovery agree. This is the part that makes one agent's capabilities actually
unshared with another's — agents sharing a directory share their skills.

Pack failures are terminal: an unloadable pack, an unknown persona, or a
declared-but-missing skill directory aborts startup rather than running an agent
with silently fewer capabilities than its persona declares (AGENTS.md rule 1).

Desktop spawn plumbing is untouched — it stays detached per the team migration
in desktop/src-tauri/src/migration/detach.rs.

Related issue

None found — searched open issues/PRs for persona-pack MCP and skill wiring.

Testing

cargo test -p buzz-acp (926 pass), cargo test -p buzz-persona --lib
(127 pass), cargo fmt --all --check, cargo clippy -p buzz-acp -p buzz-persona --all-targets -D warnings, and just ci.

Live run of the built binary against a two-persona fixture pack — alpha
claims the search skill, shared is unclaimed:

INFO buzz_acp: agent working directory set workdir=…/agent-alpha
INFO buzz_acp::skills: materialized persona skill skill=search …
INFO buzz_acp::skills: materialized persona skill skill=shared …
INFO buzz_acp: agent working directory set workdir=…/agent-beta
INFO buzz_acp::skills: materialized persona skill skill=shared …

agent-alpha received search + shared; agent-beta received shared only,
each under its own workdir with the three runtime directories symlinked at its
own .agents/skills. (Both runs then exited on a deliberately nonexistent agent
command — no ACP binary was needed to exercise this path.)

MCP is verified on the wire rather than only in config: a new acp.rs test
drives a scripted fake agent and asserts the serialized session/new request
carries mcpServers[0] with its command, args, and env.

Guards were mutation-checked — removing the MCP name-collision check, the
missing-skill error, or the skill no-clobber check each fails exactly its own
test, so none of them is decorative (TESTING.md "Review-Proven Test Standards").

Out of scope, called out explicitly: desktop spawn plumbing, persona hooks
(still parsed-but-unwired), persona runtime_env_vars, and ${VAR}
interpolation in MCP env.

🤖 Generated with Claude Code

https://claude.ai/code/session_017VkcJb4EkTx6pps1hPW9Xy

Persona packs resolved MCP servers and scoped skills, but nothing consumed
them: `resolve_pack` was only reachable from `buzz pack validate/show`, and
`ResolvedPersona.skills` was marked "reserved for future use, not yet wired".
Every agent behind one harness host therefore saw the same tools.

Add `--persona-pack <DIR>` / `--persona <NAME>` (mutually required) so the
harness resolves a pack at startup and applies it:

- MCP: the persona's merged servers (pack `.mcp.json` + frontmatter
  `mcp_servers:`) are passed to every `session/new` alongside the
  Buzz-managed server. The Buzz server keeps its name, so a colliding pack
  entry is dropped rather than shadowing the agent's relay credentials.
- Skills: `pack::resolve_skills` scoping (claimed by a persona -> that
  persona only; unclaimed -> all) is materialized into
  `<workdir>/.agents/skills/<name>` and linked into the per-runtime
  discovery directories. An existing skill is never overwritten, but is
  still linked, so an operator-pinned version survives and stays reachable.

Add `--workdir <DIR>`, applied before anything reads the working directory,
so the ACP session cwd, the agent subprocess, and skill discovery agree.
That is what makes one agent's capabilities unshared with another's.

Pack failures are terminal: an unloadable pack, an unknown persona, or a
declared-but-missing skill directory aborts startup rather than running an
agent with silently fewer capabilities than its persona declares.

Desktop spawn plumbing is untouched; it stays detached per the team
migration in `migration/detach.rs`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VkcJb4EkTx6pps1hPW9Xy
Signed-off-by: mraad <mraad23@gmail.com>
@mraad
mraad requested a review from a team as a code owner September 4, 2026 23:41
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

🔐 Codex Security Review

Status: review required for the current range.

The current range is 3c7f288c60d67df78577b237e27c3dfc8831aaa1...087b5e34b169d05a30b89ff6ceefa85fd7a49fda.
A new review must complete for this exact range. When manual authorization
is required, a Block organization member must comment exactly
@buzz-security-review 087b5e34b169d05a30b89ff6ceefa85fd7a49fda to authorize a new review.
Any previous review applies only to its recorded range.

mraad and others added 5 commits September 5, 2026 08:31
A pack could only declare stdio MCP servers, and the restriction failed
silently: `McpServerConfig` required `command` at the frontmatter layer, and
`parse_mcp_server_config` returned `None` for anything without one, so a
url-backed entry was dropped during the merge. The agent then started with
fewer tools than its persona declared and nothing said so.

Model the transport explicitly. `ResolvedMcpServer` now carries
`McpTransport::Stdio { command, args, env }` or `McpTransport::Http { url,
headers }`, chosen by which field the entry declares. `ResolvedMcpServer` is
the pack-side type; `acp::McpServer` gains the matching wire variants.

The stdio wire form deliberately carries **no** `type` field. Adapters route
stdio on its absence (`else if (!("type" in server))`), so emitting
`"type":"stdio"` would match neither branch and the server would never start —
a silent tool loss of exactly the kind this commit removes. HTTP serializes as
`{"type":"http", name, url, headers}`.

Failures are now terminal and named:
- an entry with neither `command` nor `url` fails resolution,
- `type: "sse"` is rejected during resolution rather than at session start,
  where the runtime would refuse it with a message that names no pack entry.

`mcp_servers_with_git_origin` injects `BUZZ_GIT_ORIGIN_*` into stdio servers
only; an HTTP server runs elsewhere and has no subprocess environment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VkcJb4EkTx6pps1hPW9Xy
Signed-off-by: mraad <mraad23@gmail.com>
…-spawn

Signed-off-by: mraad <mraad23@gmail.com>
…-spawn

Signed-off-by: mraad <mraad23@gmail.com>
`returns_when_escaped_descendant_retains_pipe` flaked with
`ParseIntError { kind: Empty }` when read under a loaded lane.

The 300ms probe timeout was not a tuning knob, it was the setup's race
budget: the backgrounded descendant has to fork, exec perl, load POSIX,
`setsid()` out of the leader's process group, and record its PID before
the deadline fires the `killpg`. Lose that race and the descendant dies
inside the group, leaving the PID file empty, and the test asserts
against a descendant that never escaped.

Measured spawn->PID-recorded latency on a 16-core machine:

  idle                        med  30ms  p95  33ms  max  47ms  0/40 >300ms
  2x concurrent suite         med  48ms  p95 115ms  max 177ms  0/40 >300ms
  4x suite + 24 spinners      med  68ms  p95 233ms  max 498ms  1/40 >300ms

Raise the timeout to 3s (~6x the observed max) and the outer watchdog to
15s, and record those numbers at the call site so the budget is not
trimmed back toward the escape latency later. CI runners have fewer
cores than the machine these were measured on.

Also replace the bare `.expect("descendant PID must be numeric")` with a
message that names the setup race, so a future recurrence reports why the
run proved nothing instead of surfacing an opaque parse error.

The test now costs its full 3s timeout by construction (the leader loops
forever to force the timeout path); that is the price of that path being
deterministic.

No production change. Verified: 12/12 under the load that reproduced the
flake; still falsifiable (deleting the production `stop.store(true)` fails
it with "output_with_timeout did not return within 15s"); full tauri lib
suite 3165 passed / 0 failed, fmt and clippy clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GecRn4WhoM35gz9fRY7Y1A
Signed-off-by: mraad <mraad23@gmail.com>
Add a second rule to Review-Proven Test Standards: a wall-clock timeout
that gates test setup is a race budget, not a tuning knob. Size it
against measured worst-case latency on a loaded machine rather than the
idle case, record the measurement at the call site so it is not trimmed
back toward the latency it exists to clear, and make a lost setup race
say so rather than surfacing as an opaque parse error.

Generalizes the flake fixed in a41fcb3, where the escaped-writer probe
in bounded_command.rs budgeted 300ms for a descendant whose spawn-to-ready
latency measured ~30ms idle but p95 233ms / max 498ms under a loaded lane.

Also correct the section intro, which read "this is the test-quality rule"
in the singular and attributed the section wholesale to the 25-PR review
mining. This rule came from a fixed flake, not a review thread, so the
intro now separates the mined rule from later additions instead of
extending that provenance to them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GecRn4WhoM35gz9fRY7Y1A
Signed-off-by: mraad <mraad23@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