feat(extensions): add OAuth setup UI for WASM tools + display name labels (#437)

Add setup.required_secrets to tool capabilities.json files so users can
configure OAuth client credentials (Google, Slack, Okta, Telegram) through
the Extensions UI Setup modal instead of environment variables.

- Add ToolSetupSchema/ToolSecretSetupSchema types to capabilities_schema.rs
- Extend get_setup_schema(), save_setup_secrets(), list() to handle WasmTool
- Extract load_tool_capabilities() helper to reduce duplication
- Auto-activate tools after saving setup secrets
- Show display_name labels (Channel/Tool/MCP) in extension cards
- Update button labels: "Setup" when unconfigured, "Reconfigure" when set
- Replace "Set" badge with checkmark in configure modal
- Fix innerHTML XSS pattern in slash autocomplete (use textContent)
- Add tests for ToolSetupSchema parsing and resolve_nested promotion
- Update registry display names (e.g. "Telegram Channel" vs "Telegram Tool")

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
Henry Park
2026-02-28 19:57:55 -08:00
committed by GitHub
co-authored by Claude Sonnet 4.6
parent 9b25e7566c
commit afb49597ac
22 changed files with 418 additions and 75 deletions
+75
View File
@@ -62,6 +62,11 @@ pub struct CapabilitiesFile {
#[serde(default)]
pub auth: Option<AuthCapabilitySchema>,
/// Setup schema: secrets the user must provide before the tool can be used.
/// Mirrors the channel `setup.required_secrets` pattern.
#[serde(default)]
pub setup: Option<ToolSetupSchema>,
/// Nested capabilities wrapper for channel-level JSON compatibility.
///
/// Channel capabilities files nest tool capabilities under a `"capabilities"` key.
@@ -95,6 +100,7 @@ impl CapabilitiesFile {
self.tool_invoke = self.tool_invoke.or(inner.tool_invoke);
self.workspace = self.workspace.or(inner.workspace);
self.auth = self.auth.or(inner.auth);
self.setup = self.setup.or(inner.setup);
}
self
}
@@ -516,6 +522,26 @@ fn default_success_status() -> u16 {
200
}
/// Setup schema for WASM tools: secrets the user must provide via the UI.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ToolSetupSchema {
/// Secrets the user must provide before the tool can be used.
#[serde(default)]
pub required_secrets: Vec<ToolSecretSetupSchema>,
}
/// A single secret required during tool setup.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolSecretSetupSchema {
/// Secret name in the secrets store (e.g. "google_oauth_client_id").
pub name: String,
/// User-facing prompt (e.g. "Google OAuth Client ID").
pub prompt: String,
/// If true, the user may skip this secret.
#[serde(default)]
pub optional: bool,
}
#[cfg(test)]
mod tests {
use crate::tools::wasm::capabilities_schema::{CapabilitiesFile, CredentialLocationSchema};
@@ -976,6 +1002,55 @@ mod tests {
assert_eq!(caps.auth.unwrap().secret_name, "my_auth_token");
}
#[test]
fn test_parse_tool_setup_schema() {
let json = r#"{
"setup": {
"required_secrets": [
{
"name": "google_oauth_client_id",
"prompt": "Google OAuth Client ID"
},
{
"name": "google_oauth_client_secret",
"prompt": "Google OAuth Client Secret",
"optional": true
}
]
}
}"#;
let caps = CapabilitiesFile::from_json(json).unwrap();
let setup = caps.setup.unwrap();
assert_eq!(setup.required_secrets.len(), 2);
assert_eq!(setup.required_secrets[0].name, "google_oauth_client_id");
assert_eq!(setup.required_secrets[0].prompt, "Google OAuth Client ID");
assert!(!setup.required_secrets[0].optional);
assert_eq!(setup.required_secrets[1].name, "google_oauth_client_secret");
assert!(setup.required_secrets[1].optional);
}
#[test]
fn test_resolve_nested_setup_promoted() {
// setup inside capabilities wrapper should be promoted to top level
let json = r#"{
"capabilities": {
"setup": {
"required_secrets": [
{ "name": "my_secret", "prompt": "Enter secret" }
]
}
}
}"#;
let caps = CapabilitiesFile::from_json(json).unwrap();
assert!(
caps.setup.is_some(),
"setup should be promoted from inner capabilities"
);
assert_eq!(caps.setup.unwrap().required_secrets[0].name, "my_secret");
}
#[test]
fn test_resolve_nested_empty_capabilities_noop() {
// Empty inner capabilities should not clobber outer http