fix: wire secrets store into all WASM runtime activation paths (#479)

WASM tools and channels activated at runtime (via web UI or CLI) were
missing secrets store wiring, causing credential injection to silently
fail. Tools like web-search would get 401s from APIs even though the
user had configured their API key.

Four bugs fixed:
- activate_wasm_tool(): WasmToolLoader created without .with_secrets_store()
- register_wasm_from_storage(): hardcoded secrets_store: None
- WasmChannelLoader: no secrets_store field at all (added field + builder)
- activate_wasm_channel() and startup path: both missed wiring secrets

The startup path in app.rs was correct; all runtime paths now match it.

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
Henry Park
2026-03-02 16:56:24 -08:00
committed by GitHub
co-authored by Claude Sonnet 4.6
parent 8530f44630
commit 6adf95b6d1
4 changed files with 22 additions and 5 deletions
+13 -1
View File
@@ -19,12 +19,14 @@ use crate::channels::wasm::schema::ChannelCapabilitiesFile;
use crate::channels::wasm::wrapper::WasmChannel;
use crate::db::SettingsStore;
use crate::pairing::PairingStore;
use crate::secrets::SecretsStore;
/// Loads WASM channels from the filesystem.
pub struct WasmChannelLoader {
runtime: Arc<WasmChannelRuntime>,
pairing_store: Arc<PairingStore>,
settings_store: Option<Arc<dyn SettingsStore>>,
secrets_store: Option<Arc<dyn SecretsStore + Send + Sync>>,
}
impl WasmChannelLoader {
@@ -38,9 +40,16 @@ impl WasmChannelLoader {
runtime,
pairing_store,
settings_store,
secrets_store: None,
}
}
/// Set the secrets store for host-based credential injection in WASM channels.
pub fn with_secrets_store(mut self, store: Arc<dyn SecretsStore + Send + Sync>) -> Self {
self.secrets_store = Some(store);
self
}
/// Load a single WASM channel from a file pair.
///
/// Expects:
@@ -127,7 +136,7 @@ impl WasmChannelLoader {
.await?;
// Create the channel
let channel = WasmChannel::new(
let mut channel = WasmChannel::new(
self.runtime.clone(),
prepared,
capabilities,
@@ -135,6 +144,9 @@ impl WasmChannelLoader {
self.pairing_store.clone(),
self.settings_store.clone(),
);
if let Some(ref secrets) = self.secrets_store {
channel = channel.with_secrets_store(Arc::clone(secrets));
}
tracing::info!(
name = name,
+4 -2
View File
@@ -1804,7 +1804,8 @@ impl ExtensionManager {
None
};
let loader = WasmToolLoader::new(Arc::clone(runtime), Arc::clone(&self.tool_registry));
let loader = WasmToolLoader::new(Arc::clone(runtime), Arc::clone(&self.tool_registry))
.with_secrets_store(Arc::clone(&self.secrets));
loader
.load_from_files(name, &wasm_path, cap_path_option)
.await
@@ -1915,7 +1916,8 @@ impl ExtensionManager {
Arc::clone(&channel_runtime),
Arc::clone(&pairing_store),
settings_store,
);
)
.with_secrets_store(Arc::clone(&self.secrets));
let loaded = loader
.load_from_files(name, &wasm_path, cap_path_option)
.await
+4 -1
View File
@@ -911,11 +911,14 @@ async fn setup_wasm_channels(
let pairing_store = Arc::new(PairingStore::new());
let settings_store: Option<Arc<dyn ironclaw::db::SettingsStore>> =
database.map(|db| Arc::clone(db) as Arc<dyn ironclaw::db::SettingsStore>);
let loader = WasmChannelLoader::new(
let mut loader = WasmChannelLoader::new(
Arc::clone(&runtime),
Arc::clone(&pairing_store),
settings_store,
);
if let Some(secrets) = secrets_store {
loader = loader.with_secrets_store(Arc::clone(secrets));
}
let results = match loader
.load_from_dir(&config.channels.wasm_channels_dir)
+1 -1
View File
@@ -585,7 +585,7 @@ impl ToolRegistry {
limits: None,
description: Some(&tool_with_binary.tool.description),
schema: Some(tool_with_binary.tool.parameters_schema.clone()),
secrets_store: None,
secrets_store: self.secrets_store.clone(),
oauth_refresh: None,
})
.await