mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(buzz-acp): support multiple MCP servers
Signed-off-by: Luke Tornquist <tornquist@squareup.com>
This commit is contained in:
@@ -159,6 +159,10 @@ RUST_LOG=buzz_relay=debug,buzz_datastore=info,buzz_db=debug,buzz_auth=debug,buzz
|
||||
# Binary for an optional MCP server sidecar (e.g. buzz-dev-mcp for buzz-agent).
|
||||
# BUZZ_ACP_MCP_COMMAND=
|
||||
|
||||
# JSON array of additional ACP stdio MCP server definitions. These servers do
|
||||
# not inherit Buzz credentials; declare any required env values in the file.
|
||||
# BUZZ_ACP_MCP_SERVERS_FILE=
|
||||
|
||||
# Number of parallel agent subprocesses (1–32).
|
||||
# BUZZ_ACP_AGENTS=1
|
||||
|
||||
|
||||
@@ -110,13 +110,35 @@ All configuration is via environment variables (or CLI flags — every env var h
|
||||
| `BUZZ_RELAY_URL` | no | `ws://localhost:3000` | Relay WebSocket URL. |
|
||||
| `BUZZ_ACP_AGENT_COMMAND` | no | `goose` | Agent binary to spawn. |
|
||||
| `BUZZ_ACP_AGENT_ARGS` | no | `acp` | Agent arguments (comma-separated). |
|
||||
| `BUZZ_ACP_MCP_COMMAND` | no | `""` (empty) | Path to an optional MCP server binary to provide to the agent subprocess. |
|
||||
| `BUZZ_ACP_MCP_COMMAND` | no | `""` (empty) | Path to the Buzz MCP server binary. Buzz relay credentials are injected into this server. |
|
||||
| `BUZZ_ACP_MCP_SERVERS_FILE` | no | — | Path to a JSON array of additional ACP stdio MCP server definitions. |
|
||||
| `BUZZ_ACP_IDLE_TIMEOUT` | no | `620` | Idle timeout: max seconds of silence before cancelling a turn. Resets on any agent stdout activity. |
|
||||
| `BUZZ_ACP_MAX_TURN_DURATION` | no | `7200` | Absolute wall-clock cap per turn (safety valve). |
|
||||
| `BUZZ_API_TOKEN` | no | — | API token (required if relay enforces token auth). |
|
||||
|
||||
**Note:** `BUZZ_ACP_AGENT_ARGS` splits on commas. For args with values, use: `-c,key="value"`.
|
||||
|
||||
Additional MCP servers use the ACP stdio schema. Keep `BUZZ_ACP_MCP_COMMAND` set for the Buzz messaging/development MCP, then point `BUZZ_ACP_MCP_SERVERS_FILE` at a JSON file for any other servers:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "filesystem",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
|
||||
"env": [{ "name": "LOG_LEVEL", "value": "info" }]
|
||||
},
|
||||
{
|
||||
"name": "search",
|
||||
"command": "/usr/local/bin/search-mcp",
|
||||
"args": [],
|
||||
"env": []
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
File-defined servers receive only the environment entries declared in the file. Buzz credentials are injected only into the legacy `BUZZ_ACP_MCP_COMMAND` server.
|
||||
|
||||
**Legacy env vars:** `BUZZ_ACP_PRIVATE_KEY`, `BUZZ_ACP_API_TOKEN`, and `BUZZ_ACP_TURN_TIMEOUT` (replaced by `BUZZ_ACP_IDLE_TIMEOUT`) are still accepted as fallbacks.
|
||||
|
||||
### Parallel Agents & Heartbeat
|
||||
|
||||
@@ -26,7 +26,7 @@ const MAX_LINE_SIZE: usize = 10_000_000; // 10 MB
|
||||
///
|
||||
/// Corresponds to the `McpServerStdio` variant in the ACP schema.
|
||||
/// All four fields are **required** by the schema (`args` and `env` may be empty arrays).
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct McpServer {
|
||||
pub name: String,
|
||||
pub command: String,
|
||||
@@ -35,7 +35,7 @@ pub struct McpServer {
|
||||
}
|
||||
|
||||
/// A single environment variable for an MCP server.
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct EnvVar {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
|
||||
@@ -13,6 +13,7 @@ use thiserror::Error;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::acp::McpServer;
|
||||
use crate::filter::SubscriptionRule;
|
||||
|
||||
/// Default idle timeout (seconds) when neither `--idle-timeout` nor the
|
||||
@@ -261,6 +262,10 @@ pub struct CliArgs {
|
||||
#[arg(long, env = "BUZZ_ACP_MCP_COMMAND", default_value = "")]
|
||||
pub mcp_command: String,
|
||||
|
||||
/// JSON file containing additional ACP stdio MCP server definitions.
|
||||
#[arg(long, env = "BUZZ_ACP_MCP_SERVERS_FILE")]
|
||||
pub mcp_servers_file: Option<PathBuf>,
|
||||
|
||||
/// Idle timeout: max seconds of silence before killing a turn.
|
||||
/// Resets on any agent stdout activity.
|
||||
#[arg(long, env = "BUZZ_ACP_IDLE_TIMEOUT")]
|
||||
@@ -507,6 +512,8 @@ pub struct Config {
|
||||
pub agent_command: String,
|
||||
pub agent_args: Vec<String>,
|
||||
pub mcp_command: String,
|
||||
/// Additional ACP stdio MCP servers loaded from `mcp_servers_file`.
|
||||
pub mcp_servers: Vec<McpServer>,
|
||||
pub idle_timeout_secs: u64,
|
||||
pub max_turn_duration_secs: u64,
|
||||
pub agents: u32,
|
||||
@@ -867,6 +874,11 @@ impl Config {
|
||||
None
|
||||
};
|
||||
|
||||
let mcp_servers = match &args.mcp_servers_file {
|
||||
Some(path) => load_mcp_servers_file(path)?,
|
||||
None => Vec::new(),
|
||||
};
|
||||
|
||||
if args.heartbeat_interval > 0 && args.heartbeat_interval < 10 {
|
||||
return Err(ConfigError::ConfigFile(
|
||||
"heartbeat interval must be 0 (disabled) or ≥10 seconds".into(),
|
||||
@@ -1077,6 +1089,7 @@ impl Config {
|
||||
agent_command,
|
||||
agent_args,
|
||||
mcp_command: args.mcp_command,
|
||||
mcp_servers,
|
||||
idle_timeout_secs,
|
||||
max_turn_duration_secs,
|
||||
agents: args.agents,
|
||||
@@ -1143,12 +1156,13 @@ impl Config {
|
||||
format!(" allowed_respond_to=[{}]", modes.join(","))
|
||||
};
|
||||
format!(
|
||||
"relay={} pubkey={} agent_cmd={} {} mcp_cmd={} idle_timeout={}s max_turn={}s agents={} heartbeat={}s subscribe={:?} dedup={:?} meh={:?} ignore_self={} context_limit={} max_turns_per_session={} presence={} typing={} memory={} model={} permission_mode={} {}{}",
|
||||
"relay={} pubkey={} agent_cmd={} {} mcp_cmd={} additional_mcp_servers={} idle_timeout={}s max_turn={}s agents={} heartbeat={}s subscribe={:?} dedup={:?} meh={:?} ignore_self={} context_limit={} max_turns_per_session={} presence={} typing={} memory={} model={} permission_mode={} {}{}",
|
||||
self.relay_url,
|
||||
self.keys.public_key().to_hex(),
|
||||
self.agent_command,
|
||||
self.agent_args.join(" "),
|
||||
self.mcp_command,
|
||||
self.mcp_servers.len(),
|
||||
self.idle_timeout_secs,
|
||||
self.max_turn_duration_secs,
|
||||
self.agents,
|
||||
@@ -1170,6 +1184,21 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
fn load_mcp_servers_file(path: &std::path::Path) -> Result<Vec<McpServer>, ConfigError> {
|
||||
let content = std::fs::read_to_string(path).map_err(|error| {
|
||||
ConfigError::ConfigFile(format!(
|
||||
"failed to read MCP servers file {}: {error}",
|
||||
path.display()
|
||||
))
|
||||
})?;
|
||||
serde_json::from_str(&content).map_err(|error| {
|
||||
ConfigError::ConfigFile(format!(
|
||||
"invalid MCP servers file {}: {error}",
|
||||
path.display()
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct TomlConfig {
|
||||
#[serde(default)]
|
||||
@@ -1446,6 +1475,7 @@ fn rule_applies_to_channel(rule: &SubscriptionRule, channel_id: Uuid) -> bool {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::acp::{EnvVar, McpServer};
|
||||
use crate::filter::{ChannelScope, SubscriptionRule};
|
||||
use clap::{Parser, ValueEnum};
|
||||
|
||||
@@ -1457,6 +1487,7 @@ mod tests {
|
||||
agent_command: "goose".into(),
|
||||
agent_args: vec!["acp".into()],
|
||||
mcp_command: "".into(),
|
||||
mcp_servers: vec![],
|
||||
idle_timeout_secs: DEFAULT_IDLE_TIMEOUT_SECS,
|
||||
max_turn_duration_secs: DEFAULT_MAX_TURN_DURATION_SECS,
|
||||
agents: 1,
|
||||
@@ -2771,6 +2802,89 @@ channels = "ALL"
|
||||
const TEST_PRIVATE_KEY: &str =
|
||||
"0000000000000000000000000000000000000000000000000000000000000001";
|
||||
|
||||
#[test]
|
||||
fn mcp_servers_file_loads_acp_stdio_definitions() {
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"buzz-acp-mcp-servers-{}.json",
|
||||
uuid::Uuid::new_v4()
|
||||
));
|
||||
std::fs::write(
|
||||
&path,
|
||||
r#"[
|
||||
{
|
||||
"name": "filesystem",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
|
||||
"env": [{"name": "LOG_LEVEL", "value": "debug"}]
|
||||
},
|
||||
{
|
||||
"name": "search",
|
||||
"command": "/usr/local/bin/search-mcp",
|
||||
"args": [],
|
||||
"env": []
|
||||
}
|
||||
]"#,
|
||||
)
|
||||
.expect("write MCP servers fixture");
|
||||
|
||||
let path_arg = path.display().to_string();
|
||||
let args = CliArgs::try_parse_from([
|
||||
"buzz-acp",
|
||||
"--private-key",
|
||||
TEST_PRIVATE_KEY,
|
||||
"--mcp-servers-file",
|
||||
&path_arg,
|
||||
])
|
||||
.expect("parse MCP servers file option");
|
||||
let config = Config::from_args(args).expect("load MCP servers file through Config");
|
||||
std::fs::remove_file(&path).expect("remove MCP servers fixture");
|
||||
|
||||
let servers = config.mcp_servers;
|
||||
assert_eq!(servers.len(), 2);
|
||||
assert_eq!(servers[0].name, "filesystem");
|
||||
assert_eq!(servers[0].command, "npx");
|
||||
assert_eq!(
|
||||
servers[0].args,
|
||||
[
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-filesystem",
|
||||
"/workspace"
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
servers[0].env,
|
||||
[EnvVar {
|
||||
name: "LOG_LEVEL".into(),
|
||||
value: "debug".into(),
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
servers[1],
|
||||
McpServer {
|
||||
name: "search".into(),
|
||||
command: "/usr/local/bin/search-mcp".into(),
|
||||
args: vec![],
|
||||
env: vec![],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_servers_file_reports_invalid_json_with_its_path() {
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"buzz-acp-invalid-mcp-servers-{}.json",
|
||||
uuid::Uuid::new_v4()
|
||||
));
|
||||
std::fs::write(&path, "not json").expect("write invalid MCP servers fixture");
|
||||
|
||||
let error = load_mcp_servers_file(&path).expect_err("invalid JSON must fail");
|
||||
std::fs::remove_file(&path).expect("remove invalid MCP servers fixture");
|
||||
|
||||
let message = error.to_string();
|
||||
assert!(message.contains("invalid MCP servers file"), "{message}");
|
||||
assert!(message.contains(&path.display().to_string()), "{message}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allowed_respond_to_full_path_rejects_disallowed_mode() {
|
||||
// --allowed-respond-to=owner-only,allowlist + --respond-to=anyone → ConfigError
|
||||
|
||||
+106
-49
@@ -4999,60 +4999,65 @@ async fn run_models(args: ModelsArgs) -> Result<()> {
|
||||
}
|
||||
|
||||
fn build_mcp_servers(config: &Config) -> Vec<McpServer> {
|
||||
let mut servers = config.mcp_servers.clone();
|
||||
if config.mcp_command.is_empty() {
|
||||
return vec![];
|
||||
return servers;
|
||||
}
|
||||
vec![McpServer {
|
||||
name: std::path::Path::new(&config.mcp_command)
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("mcp")
|
||||
.to_string(),
|
||||
command: config.mcp_command.clone(),
|
||||
args: vec![],
|
||||
env: {
|
||||
let mut env = vec![
|
||||
EnvVar {
|
||||
name: "BUZZ_RELAY_URL".into(),
|
||||
value: config.relay_url.clone(),
|
||||
},
|
||||
EnvVar {
|
||||
name: "BUZZ_PRIVATE_KEY".into(),
|
||||
// bech32 encoding of a valid secret key is infallible.
|
||||
// Panic here is correct: injecting a bogus secret would cause
|
||||
// delayed, hard-to-diagnose agent failures downstream.
|
||||
value: config
|
||||
.keys
|
||||
.secret_key()
|
||||
.to_bech32()
|
||||
.expect("secret key bech32 encoding should never fail"),
|
||||
},
|
||||
];
|
||||
// Forward BUZZ_AUTH_TAG (NIP-OA owner attestation credential)
|
||||
// so the MCP server can attach it to every signed event.
|
||||
if let Ok(auth_tag) = std::env::var("BUZZ_AUTH_TAG") {
|
||||
if !auth_tag.is_empty() {
|
||||
env.push(EnvVar {
|
||||
name: "BUZZ_AUTH_TAG".into(),
|
||||
value: auth_tag,
|
||||
});
|
||||
servers.insert(
|
||||
0,
|
||||
McpServer {
|
||||
name: std::path::Path::new(&config.mcp_command)
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("mcp")
|
||||
.to_string(),
|
||||
command: config.mcp_command.clone(),
|
||||
args: vec![],
|
||||
env: {
|
||||
let mut env = vec![
|
||||
EnvVar {
|
||||
name: "BUZZ_RELAY_URL".into(),
|
||||
value: config.relay_url.clone(),
|
||||
},
|
||||
EnvVar {
|
||||
name: "BUZZ_PRIVATE_KEY".into(),
|
||||
// bech32 encoding of a valid secret key is infallible.
|
||||
// Panic here is correct: injecting a bogus secret would cause
|
||||
// delayed, hard-to-diagnose agent failures downstream.
|
||||
value: config
|
||||
.keys
|
||||
.secret_key()
|
||||
.to_bech32()
|
||||
.expect("secret key bech32 encoding should never fail"),
|
||||
},
|
||||
];
|
||||
// Forward BUZZ_AUTH_TAG (NIP-OA owner attestation credential)
|
||||
// so the MCP server can attach it to every signed event.
|
||||
if let Ok(auth_tag) = std::env::var("BUZZ_AUTH_TAG") {
|
||||
if !auth_tag.is_empty() {
|
||||
env.push(EnvVar {
|
||||
name: "BUZZ_AUTH_TAG".into(),
|
||||
value: auth_tag,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Forward the agent's display name so dev-mcp can use it as the git
|
||||
// author name instead of the raw npub. Read from the process env
|
||||
// rather than Config: this is a pass-through of a contract owned
|
||||
// upstream, and absent simply means dev-mcp falls back to the npub.
|
||||
if let Ok(display_name) = std::env::var("BUZZ_ACP_DISPLAY_NAME") {
|
||||
if !display_name.is_empty() {
|
||||
env.push(EnvVar {
|
||||
name: "BUZZ_ACP_DISPLAY_NAME".into(),
|
||||
value: display_name,
|
||||
});
|
||||
// Forward the agent's display name so dev-mcp can use it as the git
|
||||
// author name instead of the raw npub. Read from the process env
|
||||
// rather than Config: this is a pass-through of a contract owned
|
||||
// upstream, and absent simply means dev-mcp falls back to the npub.
|
||||
if let Ok(display_name) = std::env::var("BUZZ_ACP_DISPLAY_NAME") {
|
||||
if !display_name.is_empty() {
|
||||
env.push(EnvVar {
|
||||
name: "BUZZ_ACP_DISPLAY_NAME".into(),
|
||||
value: display_name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
env
|
||||
env
|
||||
},
|
||||
},
|
||||
}]
|
||||
);
|
||||
servers
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -6728,6 +6733,7 @@ mod build_mcp_servers_tests {
|
||||
agent_command: "goose".into(),
|
||||
agent_args: vec!["acp".into()],
|
||||
mcp_command: "test-mcp-server".into(),
|
||||
mcp_servers: vec![],
|
||||
idle_timeout_secs: config::DEFAULT_IDLE_TIMEOUT_SECS,
|
||||
max_turn_duration_secs: config::DEFAULT_MAX_TURN_DURATION_SECS,
|
||||
agents: 1,
|
||||
@@ -6787,6 +6793,56 @@ mod build_mcp_servers_tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_mcp_servers_are_appended_without_buzz_credentials() {
|
||||
let mut config = test_config();
|
||||
config.mcp_servers = vec![McpServer {
|
||||
name: "filesystem".into(),
|
||||
command: "npx".into(),
|
||||
args: vec![
|
||||
"-y".into(),
|
||||
"@modelcontextprotocol/server-filesystem".into(),
|
||||
],
|
||||
env: vec![EnvVar {
|
||||
name: "MCP_ROOT".into(),
|
||||
value: "/workspace".into(),
|
||||
}],
|
||||
}];
|
||||
|
||||
let servers = build_mcp_servers(&config);
|
||||
|
||||
assert_eq!(servers.len(), 2);
|
||||
assert_eq!(servers[0].name, "test-mcp-server");
|
||||
assert!(servers[0]
|
||||
.env
|
||||
.iter()
|
||||
.any(|entry| entry.name == "BUZZ_PRIVATE_KEY"));
|
||||
assert_eq!(servers[1], config.mcp_servers[0]);
|
||||
assert!(
|
||||
!servers[1]
|
||||
.env
|
||||
.iter()
|
||||
.any(|entry| entry.name == "BUZZ_PRIVATE_KEY"),
|
||||
"file-defined MCP servers must not inherit the agent's Buzz secret"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_mcp_servers_work_without_legacy_mcp_command() {
|
||||
let mut config = test_config();
|
||||
config.mcp_command.clear();
|
||||
config.mcp_servers = vec![McpServer {
|
||||
name: "search".into(),
|
||||
command: "search-mcp".into(),
|
||||
args: vec![],
|
||||
env: vec![],
|
||||
}];
|
||||
|
||||
let servers = build_mcp_servers(&config);
|
||||
|
||||
assert_eq!(servers, config.mcp_servers);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_new_mcp_server_forwards_buzz_auth_tag() {
|
||||
let _guard = ENV_LOCK.lock().unwrap();
|
||||
@@ -6951,6 +7007,7 @@ mod error_outcome_emission_tests {
|
||||
agent_command: "true".into(),
|
||||
agent_args: vec![],
|
||||
mcp_command: "test-mcp-server".into(),
|
||||
mcp_servers: vec![],
|
||||
idle_timeout_secs: config::DEFAULT_IDLE_TIMEOUT_SECS,
|
||||
max_turn_duration_secs: config::DEFAULT_MAX_TURN_DURATION_SECS,
|
||||
agents: 1,
|
||||
|
||||
@@ -31,6 +31,7 @@ const AUTHORITATIVE_KEYS: &[&str] = &[
|
||||
"BUZZ_ACP_RESPOND_TO",
|
||||
"BUZZ_ACP_RESPOND_TO_ALLOWLIST",
|
||||
"BUZZ_ACP_MCP_COMMAND",
|
||||
"BUZZ_ACP_MCP_SERVERS_FILE",
|
||||
"BUZZ_ACP_EXIT_AFTER_INACTIVITY",
|
||||
START_NONCE_KEY,
|
||||
];
|
||||
@@ -355,6 +356,7 @@ mod tests {
|
||||
"BUZZ_ACP_AGENT_OWNER": "cafe",
|
||||
"BUZZ_ACP_AGENT_COMMAND": "/bin/sh",
|
||||
"BUZZ_ACP_MCP_COMMAND": "/bin/sh",
|
||||
"BUZZ_ACP_MCP_SERVERS_FILE": "/tmp/untrusted.json",
|
||||
"BUZZ_ACP_EXIT_AFTER_INACTIVITY": "0",
|
||||
},
|
||||
"owner_pubkey": "beef"
|
||||
@@ -368,6 +370,7 @@ mod tests {
|
||||
assert_eq!(env["BUZZ_ACP_AGENT_OWNER"], "beef");
|
||||
assert_eq!(env["BUZZ_ACP_AGENT_COMMAND"], "goose");
|
||||
assert_eq!(env["BUZZ_ACP_MCP_COMMAND"], "buzz-dev-mcp");
|
||||
assert!(!env.contains_key("BUZZ_ACP_MCP_SERVERS_FILE"));
|
||||
assert_eq!(env["BUZZ_ACP_EXIT_AFTER_INACTIVITY"], "7200");
|
||||
assert_eq!(env["BUZZ_MANAGED_AGENT_START_NONCE"], "gen0001");
|
||||
}
|
||||
|
||||
@@ -183,6 +183,7 @@ fn reserved_keys_include_code_execution_surface() {
|
||||
"BUZZ_ACP_AGENT_COMMAND",
|
||||
"BUZZ_ACP_AGENT_ARGS",
|
||||
"BUZZ_ACP_MCP_COMMAND",
|
||||
"BUZZ_ACP_MCP_SERVERS_FILE",
|
||||
] {
|
||||
assert!(is_reserved_env_key(key), "{key} should be reserved");
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ pub(crate) const RESERVED_ENV_KEYS: &[&str] = &[
|
||||
"BUZZ_ACP_AGENT_COMMAND",
|
||||
"BUZZ_ACP_AGENT_ARGS",
|
||||
"BUZZ_ACP_MCP_COMMAND",
|
||||
"BUZZ_ACP_MCP_SERVERS_FILE",
|
||||
// Control-plane parallelism: the Desktop resolves the effective
|
||||
// worker-pool size (applying any per-harness cap) and writes it into
|
||||
// launch.policy_env. A user-supplied BUZZ_ACP_AGENTS would bypass the
|
||||
|
||||
Reference in New Issue
Block a user