Skip to content

feat(acp): accept extra MCP servers via BUZZ_ACP_EXTRA_MCP_COMMANDS - #6651

Open
BradGroux wants to merge 3 commits into
block:mainfrom
BradGroux:agent/extra-mcp-servers
Open

feat(acp): accept extra MCP servers via BUZZ_ACP_EXTRA_MCP_COMMANDS#6651
BradGroux wants to merge 3 commits into
block:mainfrom
BradGroux:agent/extra-mcp-servers

Conversation

@BradGroux

@BradGroux BradGroux commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Note: This is a replacement for PR #6031, which was accidentally closed when branches were force-pushed after a commit identity rewrite. The changes are identical, rebased onto the latest main.

What users saw

Desktop-managed Buzz agents could receive only the built-in buzz-dev-mcp server. Operators had no process-level way to attach additional MCP servers to the ACP session.

What changed

  • Adds BUZZ_ACP_EXTRA_MCP_COMMANDS for newline-separated extra MCP server commands.
  • Parses each command with shell-aware quoting (shlex) and preserves quoted paths, empty arguments, and commas inside arguments.
  • Supports an optional name=command prefix so operators can pin server names explicitly — reordering entries no longer silently renames a server and strips the agent of its tools.
  • Fails startup before connecting to the relay when quoting is malformed, without echoing the command in the error.
  • Generates deterministic, unique MCP server names that satisfy the agent name validator.
  • Keeps Buzz relay credentials out of extra MCP server environments on the buzz-agent MCP spawn path.
  • Documents the credential isolation scope: the trusted flag covers MCP servers spawned by buzz-agent's McpRegistry. Third-party ACP adapters (claude-agent-acp, codex-acp) that spawn their own MCP children inherit the full parent environment including BUZZ_PRIVATE_KEY; the README now states this explicitly.
  • Reserves the setting from portable persona and per-agent environment overrides because it is a code-execution surface.
  • Documents configuration, quoting, security boundaries, and an example in the ACP README.

Example:

BUZZ_ACP_EXTRA_MCP_COMMANDS='memory=npx -y memory-mcp
npx -y mcp-remote https://mcp.tavily.com/mcp/?tavilyApiKey=...'

Verification

  • cargo test -p buzz-acp --lib (918 tests pass, including 22 build_mcp_servers tests)
  • cargo test -p buzz-agent --lib (525 pass)
  • cargo clippy -p buzz-acp --all-targets -- -D warnings (clean)
  • Focused Desktop reserved-environment-key test

Non-goals

  • No dedicated Desktop configuration UI.
  • No config-file format.
  • No Buzz credential forwarding to extra MCP servers.
  • No per-session cwd for MCP servers (see rsaulo's comment — acknowledged as a follow-up).

Closes #6023

@BradGroux

Copy link
Copy Markdown
Contributor Author

This is a replacement for #6031, which was accidentally closed when branches were force-pushed after a commit identity rewrite. Prior review comments and feedback are preserved on the old PR:

#6031

@BradGroux
BradGroux force-pushed the agent/extra-mcp-servers branch from 3b695eb to 71f6b6f Compare August 25, 2026 19:16
@BradGroux

Copy link
Copy Markdown
Contributor Author

Rebased onto the latest main (was 36 commits behind). All 3 commits applied cleanly. Desktop-managed agents still have no mechanism for extra MCP servers on current main, so this feature is still needed.

No review comments on this PR. CI will verify the build and test gate.

@BradGroux
BradGroux force-pushed the agent/extra-mcp-servers branch from 71f6b6f to 6fde130 Compare August 29, 2026 16:15
@github-actions

github-actions Bot commented Aug 29, 2026

Copy link
Copy Markdown

🔐 Codex Security Review

Status: review required for the current range.

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

@BradGroux

Copy link
Copy Markdown
Contributor Author

Rebased onto the latest main (was 62 commits behind at f249710, now at 00e61ea). The branch applies cleanly with all tests passing.

Still-needed assessment: No upstream commits in the 62-commit window address the same issue. This PR remains needed.

Review requests: buzz-oss-team review request is active from the previous pass.

@BradGroux
BradGroux force-pushed the agent/extra-mcp-servers branch from 6fde130 to 175624a Compare September 2, 2026 17:30
@BradGroux

Copy link
Copy Markdown
Contributor Author

Rebased onto current block:main (2af9773d6) — 38 new commits since the last rebase pass (Aug 29, 00e61eafa). Rebased cleanly with no conflicts.

Still needed: Checked all 38 new upstream commits for overlap with this PR's topic. None of the upstream changes address the issue this PR fixes. The PR remains relevant.

Verification:

  • Branch is 0 commits behind block:main, mergeable on GitHub
  • All commits authored as Brad Groux (3053586+BradGroux noreply)
  • DCO signoff preserved on all commits

@rsaulo

rsaulo commented Sep 3, 2026

Copy link
Copy Markdown

Operator context, for whatever it's worth: we run nine Desktop-managed agents in a fork, each wired to a per-project memory MCP server alongside buzz-dev-mcp — the exact gap in #6023. We've carried an equivalent patch since Aug 10 (~3 weeks in production). Three things we hit that may be useful here.

Comma delimiter vs shlex. value_delimiter = ',' splits the env value before shlex::split runs, so a comma inside a quoted argument can't survive: npx -y srv --filter 'a,b' becomes two entries, the first with an unmatched quote. Startup then fails citing "entry 2", which doesn't correspond to anything the operator typed — and since the error deliberately doesn't echo the command (good call), it's hard to diagnose. Newline separation, or an explicit name=command form, removes the ambiguity. We went with explicit names after derived ones collided the moment two servers were npx-based; your npx/npx-2 fix handles that, but the suffix depends on list order, so reordering silently renames a server and the agent loses those tools with no error.

Credential isolation. The trusted check lives in buzz-agent's spawn_one. Desktop-managed agents on third-party adapters (claude-agent-acp, codex) spawn the adapter in acp.rs:462 with no env_clear, so it inherits BUZZ_PRIVATE_KEY (set at runtime.rs:564) and passes it to MCP children it spawns itself. Is the README's unconditional claim meant to cover those? Our fork has the same hole — not a knock, just where we landed too.

cwd. build_mcp_servers runs once at startup, and session/new gets the process cwd; ACP's stdio schema has no cwd field. One agent, one project: fine. An agent answering channels from different projects resolved the wrong project for us, and there's nowhere in a process-global vec to express that. mcp_servers_with_git_origin already rewrites env per session, so the seam exists. Out of scope here, or worth a follow-up?

@BradGroux
BradGroux force-pushed the agent/extra-mcp-servers branch from 175624a to 4973a38 Compare September 3, 2026 15:28
@BradGroux

Copy link
Copy Markdown
Contributor Author

Thanks @rsaulo — this is exactly the kind of production field testing that catches the gaps a single-operator setup can't expose. All three points are addressed below.

1. Comma delimiter vs shlex — fixed

You're right: value_delimiter = ',' split the env value before shlex::split, so a comma inside a quoted argument like npx -y srv --filter 'a,b' became two entries, the first with an unmatched quote. The error pointed at "entry 2", which didn't correspond to anything the operator typed.

Fix: changed value_delimiter from , to \n (newline). Commas inside arguments now survive shlex splitting because the entry boundary is no longer ambiguous. The error message format is unchanged — it still identifies the entry index and does not echo the raw command.

Added a test (extra_mcp_commands_comma_in_args_preserved) that verifies npx -y srv --filter a,b produces a single server with ["-y", "srv", "--filter", "a,b"] as args.

2. Derived name collisions on reorder — fixed

You identified that npx / npx-2 suffixes depend on list order, so reordering silently renames a server and the agent loses those tools with no error.

Fix: added an optional name=command prefix syntax. An operator can now write memory=npx -y memory-mcp to pin the server name explicitly. The name is validated as ASCII alphanumeric + hyphen (no slashes, no empty), so a URL with = in a query param like https://mcp.tavily.com/mcp/?tavilyApiKey=test-key is not misinterpreted as a name prefix — the = is inside a path segment containing /, so it falls through to the derived-name path.

Without an explicit name, the existing derive-from-stem + numeric suffix behavior is unchanged.

Added two tests:

  • extra_mcp_commands_explicit_name_prefix — verifies memory=npx -y memory-mcp produces a server named memory.
  • extra_mcp_commands_explicit_name_stable_on_reorder — verifies that swapping the order of two explicitly-named entries keeps the names stable.
  • extra_mcp_commands_url_with_equals_not_treated_as_name — verifies a URL with = in query params is not misparsed as a name prefix.

3. Credential isolation scope — README clarified

You're correct that the trusted flag only covers the buzz-agent McpRegistry spawn path. When a third-party ACP adapter (claude-agent-acp, codex-acp) spawns its own MCP children, the adapter process itself inherits the full parent environment including BUZZ_PRIVATE_KEY (set at runtime.rs:532), and there's no env_clear on the adapter spawn in acp.rs:spawn. The adapter's children inherit whatever the adapter passes them.

The README previously stated the isolation unconditionally. It now says:

Extra servers do not receive BUZZ_PRIVATE_KEY, BUZZ_RELAY_URL, or BUZZ_AUTH_TAG on the buzz-agent MCP spawn path — they are third-party tools, not Buzz-native MCP. Note: the credential isolation applies to MCP servers spawned directly by buzz-agent's McpRegistry. When using a third-party ACP adapter (e.g. claude-agent-acp, codex-acp) that spawns its own MCP children, the adapter process inherits the full parent environment including BUZZ_PRIVATE_KEY; operators should assume those children can access Buzz credentials unless the adapter itself isolates them.

This is the same hole you and we have in our forks. Plugging it at the adapter spawn boundary (env_clear + selective passthrough on AcpClient::spawn) is a separate change with broader blast radius — it would affect every adapter, not just extra MCP — so it's out of scope for this PR.

4. cwd — acknowledged, out of scope

You're right that build_mcp_servers runs once at startup with the process cwd, and ACP's stdio schema has no cwd field. One agent answering channels from different projects would resolve the wrong project. The seam you pointed out (mcp_servers_with_git_origin rewriting env per session) is the right place to address it, but per-session MCP server cwd is a design change that needs its own issue and PR. I've noted it as a non-goal in the PR description so it's tracked.


Rebased onto latest main (7a9a5233d). All 918 buzz-acp unit tests pass (including 22 build_mcp_servers tests), 525 buzz-agent tests pass, and clippy is clean. PR is MERGEABLE.

Thanks again for the thorough field report — the comma delimiter bug in particular would have bitten anyone with non-trivial command args.

@BradGroux
BradGroux force-pushed the agent/extra-mcp-servers branch from 4973a38 to 28b0823 Compare September 3, 2026 17:18
@BradGroux

Copy link
Copy Markdown
Contributor Author

Rebase and review — 2026-09-03

Base commit: 752cbfc03 (latest main)
Mergeable: Yes
Conflicts: None — rebased cleanly.

Accuracy review: No extra MCP server support via env var on main.
Continued relevance: Still needed.

@BradGroux
BradGroux force-pushed the agent/extra-mcp-servers branch from 28b0823 to ecf0d7b Compare September 3, 2026 17:45
@BradGroux

Copy link
Copy Markdown
Contributor Author

Rebase and review update — Sep 3, 2026

Rebased onto latest main (88687876f). No conflicts.

Accuracy review: Main does not have BUZZ_ACP_EXTRA_MCP_COMMANDS or the build_mcp_servers extension for user-provided MCP servers. The feature is still absent from main. The PR (including the rsaulo review feedback: newline delimiter, name=command prefix syntax, credential isolation README clarification) is still needed.

Mergeable status: Confirmed MERGEABLE on GitHub after force-push.

wiggdevin added a commit to ZeroSum-Solutions/buzz that referenced this pull request Sep 4, 2026
… credentials (#4)

* feat(acp): accept extra MCP servers via BUZZ_ACP_EXTRA_MCP_COMMANDS

Desktop-managed Buzz agents could only use one MCP server
(buzz-dev-mcp). Users who wanted web search or other third-party MCP
tools alongside the built-in local tools had no way to add them — the
desktop flow hardcodes a single MCP command and build_mcp_servers()
always returned a vec of one entry.

Added BUZZ_ACP_EXTRA_MCP_COMMANDS, a comma-separated env var where each
entry is split on whitespace into command + args. build_mcp_servers()
appends each as a separate McpServer after the primary server. Extra
servers do not receive Buzz relay credentials (BUZZ_RELAY_URL,
BUZZ_PRIVATE_KEY) or auth tags — they are third-party tools, not
Buzz-native MCP servers.

The primary mcp_command short-circuit is preserved: if it is empty, no
servers are returned at all, even when extra commands are configured.

This is option 1 from issue block#6023. It unblocks the web-search use case
(e.g. npx -y mcp-remote https://mcp.tavily.com/mcp/...) without desktop
UI changes.

Closes block#6023

Signed-off-by: dm-builder <f01486f036641ccb52c11bb1e0ff2346ea89a8b4e3b49cd772249948f6fcbae6@digitalmeld.communities.buzz.xyz>
Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
Signed-off-by: Brad Groux <bradgroux@users.noreply.github.com>
Signed-off-by: wiggdevin <202901685+wiggdevin@users.noreply.github.com>

* Fix extra MCP server name collisions and shell-aware argv parsing

Two correctness issues in the extra MCP server path:

1. Server names derived only from the executable stem collided for common
   multi-server configurations. Two wrappers like 'npx -y first-mcp' and
   'npx -y second-mcp' both became 'npx', tripping McpRegistry's duplicate
   check at spawn. Now disambiguate with a numeric suffix (npx, npx-2).

2. split_whitespace() corrupted quoted executable paths and arguments
   containing spaces. Replace with shlex::split, which handles standard
   shell quoting. Malformed entries are skipped with a warning instead of
   being silently reinterpreted.

Also update the config doc comment to document the delimiter/quoting
contract.

Addresses themiguelamador's review feedback.

Co-authored-by: Brad Groux <brad@digitalmeld.com>
Signed-off-by: Brad Groux <brad@digitalmeld.com>
Signed-off-by: wiggdevin <202901685+wiggdevin@users.noreply.github.com>

* Address P0/P1 review feedback: credential isolation, fail-closed, name sanitization

Four issues from wesbillman/Carl's review:

P0 — Extra MCP processes received Buzz identity credentials despite the
PR's isolation claim. build_mcp_servers gave extras an empty per-server
env, but buzz-agent's spawn_one cleared and repopulated every MCP child's
environment from PASSTHROUGH_ENV, which includes BUZZ_PRIVATE_KEY,
BUZZ_RELAY_URL, and BUZZ_AUTH_TAG. Added a trusted flag to McpServer and
McpServerStdio; spawn_one now withholds identity credentials from
untrusted servers. The primary buzz-dev-mcp server is trusted; extras are
not.

P1 — BUZZ_ACP_EXTRA_MCP_COMMANDS was absent from Desktop's reserved env
key list, allowing a portable persona or per-agent env to inject an
arbitrary command. Added to reserved_env_keys.rs and its test.

P1 — Malformed quoting logged the raw command (which may contain an
embedded API key) and silently skipped the entry. Now fails closed with
only the entry index; the raw command is never echoed.

P1 — Generated names used the executable stem verbatim, violating
McpRegistry's ASCII alphanumeric/hyphen and 128-byte contract. Added
sanitize_mcp_name to replace non-conforming characters with hyphens,
strip leading/trailing hyphens, and truncate to 128 bytes.

Co-authored-by: Brad Groux <brad@digitalmeld.com>
Signed-off-by: Brad Groux <brad@digitalmeld.com>
Signed-off-by: wiggdevin <202901685+wiggdevin@users.noreply.github.com>

* style(buzz-acp): rustfmt the imported PR block#6651 extra-MCP parser

The upstream branch predates this repo's rustfmt run, so `just fmt-check`
failed on the cherry-picked `build_mcp_servers` and `sanitize_mcp_name`
bodies. No behavior change.

Signed-off-by: wiggdevin <202901685+wiggdevin@users.noreply.github.com>

* docs(mcp): name NOSTR_PRIVATE_KEY in the untrusted-server credential lists

`is_buzz_identity_env` withholds four variables from an untrusted MCP
server, but the doc comments and the buzz-acp README named only three, so
a reader could conclude NOSTR_PRIVATE_KEY still reaches a third-party
server. Also corrects the McpServer schema note, which said all four
fields are required and now has a fifth, optional one.

Signed-off-by: wiggdevin <202901685+wiggdevin@users.noreply.github.com>

* test(buzz-agent): bind the untrusted MCP spawn boundary end to end

Adds FAKE_MCP_ENV_REPORT to the fake MCP server: tools/call returns the
sorted names (never the values) of the variables the server process was
spawned with. Two integration tests drive a real buzz-agent child through
session/new and a tool call:

- an untrusted server receives none of BUZZ_PRIVATE_KEY,
  NOSTR_PRIVATE_KEY, BUZZ_RELAY_URL or BUZZ_AUTH_TAG, while a trusted one
  in the same session receives all four — so the withholding half cannot
  pass merely because the parent never had them;
- the filter stays narrow: an untrusted server keeps PATH, HOME,
  BUZZ_ACP_DISPLAY_NAME and its wire-declared env.

Removing the `!spec.trusted && is_buzz_identity_env(k)` guard fails the
first test.

Signed-off-by: wiggdevin <202901685+wiggdevin@users.noreply.github.com>

* fix(acp): cut extra MCP server names to the registry's 128-byte limit

`sanitize_mcp_name` truncated with an inclusive range (`sanitized[..=128]`),
which keeps 129 bytes. `buzz-agent`'s `McpRegistry::spawn_all` rejects any
name over `MAX_NAME_LEN` (128) and fails the whole `session/new`, so an
executable path or `name=` prefix longer than 128 characters silently cost
the agent every one of its MCP tools, not just the extra server.

The disambiguation suffix had the same class of bug: `format!("{base_name}-{i}")`
appended to a base name already at the ceiling, pushing it back over.

- Cut on an exclusive bound through a shared `fit_mcp_name` helper, which
  also re-trims a hyphen the cut may expose and takes the cut on a character
  boundary so it cannot panic.
- Re-cut the stem against the suffix width when disambiguating, including
  two-digit suffixes.
- Name the limit `MAX_MCP_NAME_LEN` and point it at the constant it mirrors.

Tests, each verified falsifiable against the pre-fix code:

- `extra_mcp_commands_long_name_truncated_to_registry_limit` — a 130-byte
  explicit name and a 200-byte executable stem both come back at exactly 128
  bytes (fails at 129 with the inclusive slice restored).
- `extra_mcp_commands_disambiguation_suffix_respects_registry_limit` — 11
  entries sharing a name already at the ceiling stay unique and within the
  limit (fails at 130 bytes with the plain `format!` restored).
- `untrusted_mcp_env_extra_command_reaches_tool_list` — a new end-to-end test
  that runs the whole seam in one process: the real
  `BUZZ_ACP_EXTRA_MCP_COMMANDS` variable, `buzz-acp`'s own argument parser,
  `build_mcp_servers`, a real `buzz-agent` child, `session/new` off the wire,
  two real MCP subprocesses, and the tool list the agent offers the model.
  It lives in its own test binary because it writes a process-global
  environment variable. Previously the two halves of this seam were tested
  apart, which is how a 129-byte name reached `session/new` unnoticed.

`mcp_servers_wire_json` and the `CliArgs`/`Config`/`ConfigError` re-exports
make that end-to-end test possible: it drives the harness's real parser
rather than a hand-written copy of the wire shape that could drift from it.

Also documents, in the flag help and the README, that a name near the
128-byte ceiling still fails because `buzz-agent` caps the qualified tool
name `<server>__<tool>` at 64 bytes. That tighter bound is left as upstream
has it — narrowing it would change behaviour the port is carrying.

Signed-off-by: wiggdevin <202901685+wiggdevin@users.noreply.github.com>

* fix(buzz-agent): confine MCP hooks and declared env to trusted servers

Two holes in the trust boundary this port introduces, both reachable once
BUZZ_ACP_EXTRA_MCP_COMMANDS can put a `trusted: false` server in the same
mcpServers array as the built-in one.

McpRegistry::call_hooks picked its targets from the MCP_HOOK_SERVERS
allowlist alone and never read spec.trusted. Desktop launches hook-capable
runtimes with a wildcard allowlist, so an operator-supplied third-party
server advertising _Stop or _PostCompact became an agent-control surface:
_Stop decides whether a turn may end, and _PostCompact text is spliced into
the fresh context after a handoff. call_hooks now requires spec.trusted.

spawn_one dropped the four Buzz identity variables from the ambient
passthrough set for an untrusted server, then applied the wire-declared
spec.env four lines later without the same filter — so a credential named
in mcpServers[].env reached the child regardless of the flag. The declared
env is filtered on the same rule.

Desktop names the built-in server in MCP_HOOK_SERVERS instead of passing
"*", derived from the MCP command's file stem the way build_mcp_servers
derives it. The comment justifying the wildcard claimed the name was
hard-coded to "buzz-mcp", which was never true and is now moot.

Tests: hook_never_invoked_on_untrusted_server pins the hook guard (removing
it makes the agent loop on the objection: 1 LLM call becomes 2), and
untrusted_mcp_env_withheld_when_declared_on_the_wire pins the env guard
against an agent process that has no ambient identity variables at all, so
the wire declaration is the only possible source. The existing hook tests
now declare their fake server trusted, as the harness does for its own.

Signed-off-by: wiggdevin <202901685+wiggdevin@users.noreply.github.com>

* fix(buzz-acp): correct the extra-MCP parser's contracts and bind its wire seam

Four defects in build_mcp_servers, three of them inherited verbatim from
PR block#6651 and reported upstream as parity bugs, not fork regressions.

An extras-only configuration produced zero servers. The function returned
early when config.mcp_command was empty, before the extras loop —
BUZZ_ACP_MCP_COMMAND defaults to "" and Desktop writes "" whenever the
runtime has no MCP command, so setting only BUZZ_ACP_EXTRA_MCP_COMMANDS
started the harness clean and offered the model no tools at all. The
primary server is now appended conditionally and the extras always parsed;
only an entirely unset configuration yields an empty array. The test that
pinned the old behaviour as correct is replaced by one asserting the extra
server survives.

A non-blank entry that shell-split to nothing was dropped at a silent
`continue`: `memory=`, whitespace after the `=`, an empty quoted command,
or a comment-only line each started the harness with that server missing
and nothing logged, while malformed quoting correctly aborted startup. Both
shapes now fail closed with the entry index and no echo of the entry.

The generated-name cap enforced the wrong contract. MAX_MCP_NAME_LEN
mirrored buzz-agent's 128-byte bound on the name alone, but the registry
builds `<server>__<tool>` and fails the whole session above its 64-byte
MAX_QNAME_LEN — so a name cut to exactly 128 bytes was still fatal the
moment the server advertised any tool. The cap is budgeted against the
qualified name instead, and assert_registry_name_contract now covers that
bound rather than describing the looser one.

Buzz origin metadata was injected into untrusted extras.
mcp_servers_with_git_origin pushed BUZZ_GIT_ORIGIN_CHANNEL_ID or
BUZZ_GIT_ORIGIN_AGENT_NAME into every element of the array; before this
port the array held exactly one server, and it now holds third-party ones.
The origin goes only to trusted servers, and the pool tests run a mixed
array so the guard is falsifiable.

The last hop is now bound as well. The e2e test called
mcp_servers_wire_json and hand-sent session/new, so replacing the
PromptContext assignment with vec![] or breaking the pool's env injection
left it green. Both steps live behind one type: McpServerSet::from_config
is the only way to fill PromptContext::mcp_servers (from_servers is
cfg(test)) and for_session is the only way to read it back, so the wire
array cannot be produced any other way and the test drives exactly the code
the harness runs.

Signed-off-by: wiggdevin <202901685+wiggdevin@users.noreply.github.com>

* fix(buzz-agent): contain an over-long third-party tool name; narrow the trust claims

An untrusted MCP server's tool names are outside this repo's control:
`valid_name` accepts 128 bytes, while a server name generated by buzz-acp
can only reserve about 30 for the bare tool. A 32-byte server name plus a
real 32-byte tool (`get_google_search_console_report`) makes a 66-byte
qualified name, and `spawn_all` returned Err for the whole array — so one
operator-supplied server took the built-in one down with it and the agent
started a session with no tools at all. Drop that one tool, with a warning,
when the server is untrusted; a trusted server keeps the hard error, because
there an over-long name is our own packaging bug.

Narrow what the spawn filter is documented to buy. It removes four variable
names from the child's environment. It is not isolation: the child runs under
the same UID with HOME and SSH_AUTH_SOCK, so it can read what the agent's
user can, including a plaintext nsec in managed-agents.json when the keyring
is unreachable. Real per-server isolation belongs to the MCP-registry ticket.

`trusted` is `#[serde(default)]`, so an ACP client that predates the
extension declares every server untrusted — fail-safe for credentials, a
behavior change for hooks. Document it on the field and in the README, and
stop failing silently: an allowlisted-but-untrusted server is now logged once
per registry, and an untrusted spawn says so.

Tests: a real-process test with a 32-byte server name and a 32-byte tool
(the session survives, the fitting tools are still offered, the long one is
not), and a hook test whose declaration carries no `trusted` key at all.

Signed-off-by: wiggdevin <202901685+wiggdevin@users.noreply.github.com>

* fix(buzz-acp): refuse extra MCP servers an adapter cannot honour, and bound them

The credential-withholding guarantee these servers are declared under is
`buzz-agent`'s: it alone reads the `trusted` marker. `BUZZ_ACP_AGENT_COMMAND`
defaults to `goose`, `AcpClient::spawn` never clears the environment, and the
extras went into every `session/new` regardless — so with any other adapter
the entries were spawned by a process holding BUZZ_PRIVATE_KEY and
BUZZ_RELAY_URL, and the marker, the spawn filter and their tests all sat off
that path. Gate the extras on the same normalized command identity
`default_agent_args` keys on, and fail at boot with a message that names the
adapter.

Mirror the registry's 16-server cap. `spawn_all` rejects the whole array past
it, so without a startup bound a 17-server configuration produced a harness
that came online and then failed every session — the same class the
MAX_QNAME_LEN mirror fixed. A re-export from buzz-agent lets a test pin the
two constants together instead of a comment.

Make the `name=` promise true. Two entries claiming one explicit name are now
refused instead of one being silently renamed by position, and a
disambiguation suffix is hashed from the entry itself rather than counted off
in order, so reordering two names that truncate alike no longer swaps which
executable owns each qualified tool name.

Build the server set before presence goes online, so an unusable
configuration stops the process at boot rather than after it advertises
itself. Narrow the doc comments to what the trust marker actually buys, and
replace the CLI/README example that put an API key in argv.

Signed-off-by: wiggdevin <202901685+wiggdevin@users.noreply.github.com>

---------

Signed-off-by: dm-builder <f01486f036641ccb52c11bb1e0ff2346ea89a8b4e3b49cd772249948f6fcbae6@digitalmeld.communities.buzz.xyz>
Signed-off-by: Brad Groux <bradgroux@users.noreply.github.com>
Signed-off-by: wiggdevin <202901685+wiggdevin@users.noreply.github.com>
Signed-off-by: Brad Groux <brad@digitalmeld.com>
Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
Co-authored-by: Brad Groux <brad@digitalmeld.com>
BradGroux and others added 3 commits September 4, 2026 23:58
Desktop-managed Buzz agents could only use one MCP server
(buzz-dev-mcp). Users who wanted web search or other third-party MCP
tools alongside the built-in local tools had no way to add them — the
desktop flow hardcodes a single MCP command and build_mcp_servers()
always returned a vec of one entry.

Added BUZZ_ACP_EXTRA_MCP_COMMANDS, a comma-separated env var where each
entry is split on whitespace into command + args. build_mcp_servers()
appends each as a separate McpServer after the primary server. Extra
servers do not receive Buzz relay credentials (BUZZ_RELAY_URL,
BUZZ_PRIVATE_KEY) or auth tags — they are third-party tools, not
Buzz-native MCP servers.

The primary mcp_command short-circuit is preserved: if it is empty, no
servers are returned at all, even when extra commands are configured.

This is option 1 from issue block#6023. It unblocks the web-search use case
(e.g. npx -y mcp-remote https://mcp.tavily.com/mcp/...) without desktop
UI changes.

Closes block#6023

Signed-off-by: dm-builder <f01486f036641ccb52c11bb1e0ff2346ea89a8b4e3b49cd772249948f6fcbae6@digitalmeld.communities.buzz.xyz>
Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
Signed-off-by: Brad Groux <bradgroux@users.noreply.github.com>
Signed-off-by: Brad Groux <bradgroux@hotmail.com>
Signed-off-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
Two correctness issues in the extra MCP server path:

1. Server names derived only from the executable stem collided for common
   multi-server configurations. Two wrappers like 'npx -y first-mcp' and
   'npx -y second-mcp' both became 'npx', tripping McpRegistry's duplicate
   check at spawn. Now disambiguate with a numeric suffix (npx, npx-2).

2. split_whitespace() corrupted quoted executable paths and arguments
   containing spaces. Replace with shlex::split, which handles standard
   shell quoting. Malformed entries are skipped with a warning instead of
   being silently reinterpreted.

Also update the config doc comment to document the delimiter/quoting
contract.

Addresses themiguelamador's review feedback.

Co-authored-by: Brad Groux <brad@digitalmeld.com>
Signed-off-by: Brad Groux <brad@digitalmeld.com>
Signed-off-by: Brad Groux <bradgroux@hotmail.com>
Signed-off-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
…e sanitization

Four issues from wesbillman/Carl's review:

P0 — Extra MCP processes received Buzz identity credentials despite the
PR's isolation claim. build_mcp_servers gave extras an empty per-server
env, but buzz-agent's spawn_one cleared and repopulated every MCP child's
environment from PASSTHROUGH_ENV, which includes BUZZ_PRIVATE_KEY,
BUZZ_RELAY_URL, and BUZZ_AUTH_TAG. Added a trusted flag to McpServer and
McpServerStdio; spawn_one now withholds identity credentials from
untrusted servers. The primary buzz-dev-mcp server is trusted; extras are
not.

P1 — BUZZ_ACP_EXTRA_MCP_COMMANDS was absent from Desktop's reserved env
key list, allowing a portable persona or per-agent env to inject an
arbitrary command. Added to reserved_env_keys.rs and its test.

P1 — Malformed quoting logged the raw command (which may contain an
embedded API key) and silently skipped the entry. Now fails closed with
only the entry index; the raw command is never echoed.

P1 — Generated names used the executable stem verbatim, violating
McpRegistry's ASCII alphanumeric/hyphen and 128-byte contract. Added
sanitize_mcp_name to replace non-conforming characters with hyphens,
strip leading/trailing hyphens, and truncate to 128 bytes.

Co-authored-by: Brad Groux <brad@digitalmeld.com>
Signed-off-by: Brad Groux <brad@digitalmeld.com>
Signed-off-by: Brad Groux <bradgroux@hotmail.com>
Signed-off-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
@BradGroux

Copy link
Copy Markdown
Contributor Author

Rebase pass — Sep 5, 2025

Rebased onto current upstream main (f038cbbb0). Main moved 62 commits since the Sep 2 rebase pass (2af9773d6).

Rebase result

Conflict resolved in desktop/src-tauri/src/managed_agents/reserved_env_keys.rs (upstream added PI_ACP_PI_COMMAND to reserved keys, PR added BUZZ_ACP_EXTRA_MCP_COMMANDS; kept both).

Still-needed assessment

Checked all 62 new main commits for overlap with this PR's topic. None obsoleted:

Verification

  • PR confirmed MERGEABLE on GitHub
  • All commits authored as Brad Groux (3053586+BradGroux noreply)
  • DCO signoff preserved on all commits
  • Review requests to buzz-oss-team still active from the Aug 24 recovery pass

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.

No way to add web search or other third-party MCP servers to a desktop-managed agent

2 participants