mirror of
https://github.com/nearai/ironclaw.git
synced 2026-09-23 10:01:28 +08:00
Address PR #2050 review comments
Three follow-up fixes from automated reviewers (Copilot, gemini-code-assist): - Gate IRONCLAW_TEST_HTTP_REMAP behind cfg(test, debug_assertions) so a stray env var on a release deployment cannot silently redirect outbound HTTP traffic from production to a test endpoint. - Bound the OAuth token-refresh response body at 64 KiB. A misbehaving or hostile token endpoint could otherwise stream an unbounded body and OOM the process via response.json(). - Cache mcp_supports_auth() metadata-discovery results per server URL on the ExtensionManager. The previous code re-issued a network probe for every unauthenticated MCP server on every list() call, slowing the extensions list endpoint when multiple MCP servers were configured. Cache is invalidated alongside the latent-actions cache on add/update/ remove of MCP servers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -337,7 +337,14 @@ impl AppBuilder {
|
||||
tools_builder =
|
||||
tools_builder.with_credentials(Arc::clone(&credential_registry), Arc::clone(ss));
|
||||
}
|
||||
let http_interceptor = crate::http_intercept::remap_from_env();
|
||||
// Test-only HTTP host remapping. Gated to debug/test builds so a stray
|
||||
// `IRONCLAW_TEST_HTTP_REMAP` env var on a release deployment cannot
|
||||
// silently redirect outbound HTTP from production to a test endpoint.
|
||||
let http_interceptor = if cfg!(any(test, debug_assertions)) {
|
||||
crate::http_intercept::remap_from_env()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(ref interceptor) = http_interceptor {
|
||||
tools_builder = tools_builder.with_http_interceptor(Arc::clone(interceptor));
|
||||
}
|
||||
|
||||
@@ -517,9 +517,19 @@ pub async fn refresh_oauth_access_token(
|
||||
}
|
||||
};
|
||||
|
||||
// Cap the response body at 64 KiB. Legitimate OAuth token responses are
|
||||
// a few hundred bytes; a misbehaving or hostile token endpoint must not
|
||||
// be able to OOM the process by streaming an unbounded body.
|
||||
const MAX_TOKEN_BODY_BYTES: usize = 64 * 1024;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
let body_bytes = response
|
||||
.bytes()
|
||||
.await
|
||||
.map(|b| b.slice(..b.len().min(MAX_TOKEN_BODY_BYTES)))
|
||||
.unwrap_or_default();
|
||||
let body = String::from_utf8_lossy(&body_bytes);
|
||||
tracing::warn!(
|
||||
status = %status,
|
||||
body = %body,
|
||||
@@ -528,7 +538,22 @@ pub async fn refresh_oauth_access_token(
|
||||
return false;
|
||||
}
|
||||
|
||||
let token_data: serde_json::Value = match response.json().await {
|
||||
let body_bytes = match response.bytes().await {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "Failed to read token refresh response body");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
if body_bytes.len() > MAX_TOKEN_BODY_BYTES {
|
||||
tracing::warn!(
|
||||
len = body_bytes.len(),
|
||||
limit = MAX_TOKEN_BODY_BYTES,
|
||||
"OAuth token refresh response exceeds size limit"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
let token_data: serde_json::Value = match serde_json::from_slice(&body_bytes) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "Failed to parse token refresh response");
|
||||
|
||||
@@ -468,6 +468,9 @@ pub struct ExtensionManager {
|
||||
wasm_tools_dir: PathBuf,
|
||||
wasm_channels_dir: PathBuf,
|
||||
latent_wasm_provider_actions: RwLock<HashMap<String, Vec<LatentProviderAction>>>,
|
||||
/// Per-server URL cache for `mcp_supports_auth` metadata discovery.
|
||||
/// Avoids re-issuing a network probe on every `list()` call.
|
||||
mcp_auth_support_cache: RwLock<HashMap<String, bool>>,
|
||||
|
||||
// WASM channel hot-activation infrastructure (set post-construction)
|
||||
channel_runtime: RwLock<Option<ChannelRuntimeState>>,
|
||||
@@ -642,6 +645,7 @@ impl ExtensionManager {
|
||||
wasm_tools_dir,
|
||||
wasm_channels_dir,
|
||||
latent_wasm_provider_actions: RwLock::new(HashMap::new()),
|
||||
mcp_auth_support_cache: RwLock::new(HashMap::new()),
|
||||
channel_runtime: RwLock::new(None),
|
||||
relay_channel_manager: RwLock::new(None),
|
||||
secrets,
|
||||
@@ -2721,6 +2725,7 @@ impl ExtensionManager {
|
||||
// action. Drop the cache so the next listing reflects its
|
||||
// installed/active status.
|
||||
self.invalidate_latent_wasm_provider_actions_cache().await;
|
||||
self.mcp_auth_support_cache.write().await.clear();
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -2767,6 +2772,7 @@ impl ExtensionManager {
|
||||
};
|
||||
if result.is_ok() {
|
||||
self.invalidate_latent_wasm_provider_actions_cache().await;
|
||||
self.mcp_auth_support_cache.write().await.clear();
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -2786,6 +2792,7 @@ impl ExtensionManager {
|
||||
// state; drop the cache so the registry-discovery path can
|
||||
// resurface it as a latent provider action.
|
||||
self.invalidate_latent_wasm_provider_actions_cache().await;
|
||||
self.mcp_auth_support_cache.write().await.clear();
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -3948,10 +3955,16 @@ impl ExtensionManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cache hit: avoid the network probe on every list() call. Cache is
|
||||
// keyed by server URL and invalidated when MCP server config changes.
|
||||
if let Some(&cached) = self.mcp_auth_support_cache.read().await.get(&server.url) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Metadata discovery uses the bounded MCP OAuth client timeouts in
|
||||
// `discover_full_oauth_metadata()`, so this list-path probe cannot hang
|
||||
// indefinitely on a hostile or slow server URL.
|
||||
match discover_full_oauth_metadata(&server.url).await {
|
||||
let supports = match discover_full_oauth_metadata(&server.url).await {
|
||||
Ok(_) => true,
|
||||
Err(crate::tools::mcp::auth::AuthError::NotSupported) => false,
|
||||
Err(error) => {
|
||||
@@ -3963,7 +3976,12 @@ impl ExtensionManager {
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
self.mcp_auth_support_cache
|
||||
.write()
|
||||
.await
|
||||
.insert(server.url.clone(), supports);
|
||||
supports
|
||||
}
|
||||
|
||||
async fn start_secret_oauth_flow(
|
||||
|
||||
Reference in New Issue
Block a user