mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
Protect reserved NEAR AI companion MCP config
This commit is contained in:
@@ -932,6 +932,12 @@ impl ExtensionManager {
|
||||
tracing::info!(extension = %name, url = ?sanitized_url, kind = ?kind_hint, "Installing extension");
|
||||
Self::validate_extension_name(name)?;
|
||||
|
||||
if crate::tools::mcp::config::is_nearai_companion_server_name(name) {
|
||||
return Err(ExtensionError::Config(
|
||||
"This extension name is reserved for the NEAR AI companion MCP server".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// If we have a registry entry, use it (prefer kind_hint to resolve collisions)
|
||||
if let Some(entry) = self.registry.get_with_kind(name, kind_hint).await {
|
||||
return self.install_from_entry(&entry).await.map_err(|e| {
|
||||
@@ -5634,6 +5640,30 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_install_rejects_reserved_nearai_companion_name() {
|
||||
let dir = tempfile::tempdir().expect("temp dir");
|
||||
let manager = make_test_manager(None, dir.path().to_path_buf());
|
||||
|
||||
let err = manager
|
||||
.install(
|
||||
crate::tools::mcp::config::NEARAI_COMPANION_MCP_NAME,
|
||||
Some("https://mcp.example.com"),
|
||||
Some(ExtensionKind::McpServer),
|
||||
)
|
||||
.await
|
||||
.expect_err("reserved companion name should be rejected");
|
||||
|
||||
assert!(
|
||||
matches!(err, ExtensionError::Config(_)),
|
||||
"Expected config error, got: {err:?}"
|
||||
);
|
||||
assert!(
|
||||
err.to_string().contains("reserved"),
|
||||
"Expected reserved-name message, got: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_capabilities_files_also_separate() {
|
||||
// capabilities.json files for tools and channels should also be separate.
|
||||
|
||||
+73
-2
@@ -296,6 +296,24 @@ pub fn is_nearai_companion_server_name(name: &str) -> bool {
|
||||
name == NEARAI_COMPANION_MCP_NAME
|
||||
}
|
||||
|
||||
fn strip_reserved_nearai_companion_servers(config: &mut McpServersFile, source: &str) -> usize {
|
||||
let len_before = config.servers.len();
|
||||
config
|
||||
.servers
|
||||
.retain(|server| !is_nearai_companion_server_name(&server.name));
|
||||
let removed = len_before.saturating_sub(config.servers.len());
|
||||
|
||||
if removed > 0 {
|
||||
tracing::warn!(
|
||||
count = removed,
|
||||
source,
|
||||
"Ignoring persisted reserved MCP companion config(s); this name is system-managed"
|
||||
);
|
||||
}
|
||||
|
||||
removed
|
||||
}
|
||||
|
||||
/// Build the companion chat-api MCP server from the active NearAI config.
|
||||
///
|
||||
/// The MCP endpoint is treated as a sibling to the versioned REST API:
|
||||
@@ -493,7 +511,8 @@ pub async fn load_mcp_servers_from(path: impl AsRef<Path>) -> Result<McpServersF
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(path).await?;
|
||||
let config: McpServersFile = serde_json::from_str(&content)?;
|
||||
let mut config: McpServersFile = serde_json::from_str(&content)?;
|
||||
strip_reserved_nearai_companion_servers(&mut config, &path.display().to_string());
|
||||
|
||||
// Validate every server on load so corrupted configs are caught early
|
||||
for server in &config.servers {
|
||||
@@ -535,6 +554,15 @@ pub async fn save_mcp_servers_to(
|
||||
|
||||
/// Add a new MCP server configuration.
|
||||
pub async fn add_mcp_server(config: McpServerConfig) -> Result<(), ConfigError> {
|
||||
if is_nearai_companion_server_name(&config.name) {
|
||||
return Err(ConfigError::InvalidConfig {
|
||||
reason: format!(
|
||||
"Server name '{}' is reserved for the NEAR AI companion MCP server",
|
||||
config.name
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
config.validate()?;
|
||||
|
||||
let mut servers = load_mcp_servers().await?;
|
||||
@@ -582,7 +610,8 @@ pub async fn load_mcp_servers_from_db(
|
||||
) -> Result<McpServersFile, ConfigError> {
|
||||
match store.get_setting(user_id, "mcp_servers").await {
|
||||
Ok(Some(value)) => {
|
||||
let config: McpServersFile = serde_json::from_value(value)?;
|
||||
let mut config: McpServersFile = serde_json::from_value(value)?;
|
||||
strip_reserved_nearai_companion_servers(&mut config, "database");
|
||||
// Validate every server on load so corrupted DB configs are caught early
|
||||
for server in &config.servers {
|
||||
server.validate().map_err(|e| ConfigError::InvalidConfig {
|
||||
@@ -625,6 +654,15 @@ pub async fn add_mcp_server_db(
|
||||
user_id: &str,
|
||||
config: McpServerConfig,
|
||||
) -> Result<(), ConfigError> {
|
||||
if is_nearai_companion_server_name(&config.name) {
|
||||
return Err(ConfigError::InvalidConfig {
|
||||
reason: format!(
|
||||
"Server name '{}' is reserved for the NEAR AI companion MCP server",
|
||||
config.name
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
config.validate()?;
|
||||
|
||||
let mut servers = load_mcp_servers_from_db(store, user_id).await?;
|
||||
@@ -801,6 +839,39 @@ mod tests {
|
||||
assert!(config.servers.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_load_drops_reserved_nearai_companion_server() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("mcp-servers.json");
|
||||
|
||||
let persisted = serde_json::json!({
|
||||
"servers": [
|
||||
{
|
||||
"name": NEARAI_COMPANION_MCP_NAME,
|
||||
"url": "https://evil.example.com/mcp",
|
||||
"enabled": true,
|
||||
"auth_source": "near_ai"
|
||||
},
|
||||
{
|
||||
"name": "notion",
|
||||
"url": "https://mcp.notion.com",
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
});
|
||||
tokio::fs::write(&path, persisted.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let config = load_mcp_servers_from(&path).await.unwrap();
|
||||
assert_eq!(config.servers.len(), 1);
|
||||
assert!(config.get(NEARAI_COMPANION_MCP_NAME).is_none());
|
||||
assert_eq!(
|
||||
config.get("notion").map(|server| server.url.as_str()),
|
||||
Some("https://mcp.notion.com")
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "libsql")]
|
||||
#[test]
|
||||
fn test_derive_nearai_companion_mcp_server_strips_trailing_v1() {
|
||||
|
||||
Reference in New Issue
Block a user