fix: fall back to build-from-source when extension download fails (#312)

* fix: fall back to build-from-source when extension download fails

Extension manifests hardcode GitHub release URLs for WASM artifacts,
but these artifacts are not yet published to any release. This causes
all WASM extension installs to fail with HTTP 404.

Add a fallback_source field to RegistryEntry so that when the primary
WasmDownload source fails (e.g., 404), the installer automatically
falls back to WasmBuildable (build from source). The manifest
conversion now populates this fallback whenever a download URL is set.

Fixes nearai/ironclaw#298

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Address Copilot/Gemini review feedback

- Skip fallback for AlreadyInstalled errors (Gemini)
- Include both primary and fallback errors in combined message (Copilot)
- Fix comment to match broader behavior (any error, not just download) (Copilot)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Address serrrfirat review feedback

- Forward AlreadyInstalled from fallback directly instead of wrapping
  in ExtensionError::Other (defensive, prevents misleading error message)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Add unit tests for fallback install logic

Extract fallback_decision() and combine_install_errors() from
install_from_entry() to enable direct unit testing without requiring
a full ExtensionManager setup.

Tests cover:
- Primary success returns directly (no fallback attempted)
- AlreadyInstalled short-circuits (no fallback attempted)
- Download failure with fallback available triggers fallback
- Error without fallback source returns primary error
- Both-fail produces combined error with both messages
- AlreadyInstalled from fallback is forwarded directly

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: firat.sertgoz <[email protected]>
This commit is contained in:
Bowen Wang
2026-02-23 06:51:43 -08:00
committed by GitHub
co-authored by Claude Opus 4.6 firat.sertgoz
parent ebb4ce95e3
commit f4ba85ffa2
5 changed files with 330 additions and 23 deletions
+20
View File
@@ -208,6 +208,7 @@ fn builtin_entries() -> Vec<RegistryEntry> {
source: ExtensionSource::McpUrl {
url: "https://mcp.notion.com/mcp".to_string(),
},
fallback_source: None,
auth_hint: AuthHint::Dcr,
},
RegistryEntry {
@@ -227,6 +228,7 @@ fn builtin_entries() -> Vec<RegistryEntry> {
source: ExtensionSource::McpUrl {
url: "https://mcp.linear.app".to_string(),
},
fallback_source: None,
auth_hint: AuthHint::Dcr,
},
RegistryEntry {
@@ -245,6 +247,7 @@ fn builtin_entries() -> Vec<RegistryEntry> {
source: ExtensionSource::McpUrl {
url: "https://mcp.google.com/calendar".to_string(),
},
fallback_source: None,
auth_hint: AuthHint::Dcr,
},
RegistryEntry {
@@ -263,6 +266,7 @@ fn builtin_entries() -> Vec<RegistryEntry> {
source: ExtensionSource::McpUrl {
url: "https://mcp.google.com/drive".to_string(),
},
fallback_source: None,
auth_hint: AuthHint::Dcr,
},
RegistryEntry {
@@ -282,6 +286,7 @@ fn builtin_entries() -> Vec<RegistryEntry> {
source: ExtensionSource::McpUrl {
url: "https://mcp.github.com".to_string(),
},
fallback_source: None,
auth_hint: AuthHint::Dcr,
},
RegistryEntry {
@@ -301,6 +306,7 @@ fn builtin_entries() -> Vec<RegistryEntry> {
source: ExtensionSource::McpUrl {
url: "https://mcp.slack.com".to_string(),
},
fallback_source: None,
auth_hint: AuthHint::Dcr,
},
RegistryEntry {
@@ -320,6 +326,7 @@ fn builtin_entries() -> Vec<RegistryEntry> {
source: ExtensionSource::McpUrl {
url: "https://mcp.sentry.dev/sse".to_string(),
},
fallback_source: None,
auth_hint: AuthHint::Dcr,
},
RegistryEntry {
@@ -339,6 +346,7 @@ fn builtin_entries() -> Vec<RegistryEntry> {
source: ExtensionSource::McpUrl {
url: "https://mcp.stripe.com".to_string(),
},
fallback_source: None,
auth_hint: AuthHint::Dcr,
},
RegistryEntry {
@@ -358,6 +366,7 @@ fn builtin_entries() -> Vec<RegistryEntry> {
source: ExtensionSource::McpUrl {
url: "https://mcp.cloudflare.com/sse".to_string(),
},
fallback_source: None,
auth_hint: AuthHint::Dcr,
},
RegistryEntry {
@@ -375,6 +384,7 @@ fn builtin_entries() -> Vec<RegistryEntry> {
source: ExtensionSource::McpUrl {
url: "https://mcp.asana.com".to_string(),
},
fallback_source: None,
auth_hint: AuthHint::Dcr,
},
RegistryEntry {
@@ -393,6 +403,7 @@ fn builtin_entries() -> Vec<RegistryEntry> {
source: ExtensionSource::McpUrl {
url: "https://mcp.intercom.com".to_string(),
},
fallback_source: None,
auth_hint: AuthHint::Dcr,
},
// WASM channels (telegram, slack, discord, whatsapp) come from the embedded
@@ -417,6 +428,7 @@ mod tests {
source: ExtensionSource::McpUrl {
url: "https://example.com".to_string(),
},
fallback_source: None,
auth_hint: AuthHint::Dcr,
};
@@ -439,6 +451,7 @@ mod tests {
source: ExtensionSource::McpUrl {
url: "https://example.com".to_string(),
},
fallback_source: None,
auth_hint: AuthHint::Dcr,
};
@@ -461,6 +474,7 @@ mod tests {
source: ExtensionSource::McpUrl {
url: "https://example.com".to_string(),
},
fallback_source: None,
auth_hint: AuthHint::Dcr,
};
@@ -483,6 +497,7 @@ mod tests {
source: ExtensionSource::McpUrl {
url: "https://example.com".to_string(),
},
fallback_source: None,
auth_hint: AuthHint::Dcr,
};
@@ -546,6 +561,7 @@ mod tests {
source: ExtensionSource::McpUrl {
url: "https://custom.example.com".to_string(),
},
fallback_source: None,
auth_hint: AuthHint::Dcr,
};
@@ -571,6 +587,7 @@ mod tests {
source: ExtensionSource::McpUrl {
url: "https://example.com".to_string(),
},
fallback_source: None,
auth_hint: AuthHint::None,
};
@@ -595,6 +612,7 @@ mod tests {
build_dir: Some("channels-src/telegram".to_string()),
crate_name: Some("telegram-channel".to_string()),
},
fallback_source: None,
auth_hint: AuthHint::CapabilitiesAuth,
},
// This shares a name with the builtin slack-mcp but has a different kind, so both should appear
@@ -609,6 +627,7 @@ mod tests {
build_dir: Some("tools-src/slack".to_string()),
crate_name: Some("slack-tool".to_string()),
},
fallback_source: None,
auth_hint: AuthHint::CapabilitiesAuth,
},
];
@@ -644,6 +663,7 @@ mod tests {
source: ExtensionSource::McpUrl {
url: "https://other.slack.com".to_string(),
},
fallback_source: None,
auth_hint: AuthHint::Dcr,
}];