diff --git a/registry/channels/discord.json b/registry/channels/discord.json index 403c8802..4f4f590d 100644 --- a/registry/channels/discord.json +++ b/registry/channels/discord.json @@ -1,9 +1,9 @@ { "name": "discord", - "display_name": "Discord", + "display_name": "Discord Channel", "kind": "channel", "version": "0.1.0", - "description": "Discord Gateway/Webhook channel for slash commands, buttons, and messages", + "description": "Talk to your agent in Discord", "keywords": ["messaging", "chat", "discord", "bot"], "source": { diff --git a/registry/channels/slack.json b/registry/channels/slack.json index 319cf07e..b23ab17e 100644 --- a/registry/channels/slack.json +++ b/registry/channels/slack.json @@ -1,9 +1,9 @@ { "name": "slack", - "display_name": "Slack", + "display_name": "Slack Channel", "kind": "channel", "version": "0.1.0", - "description": "Slack Events API channel for receiving and responding to Slack messages", + "description": "Talk to your agent in Slack", "keywords": ["messaging", "chat", "workspace", "slack"], "source": { diff --git a/registry/channels/telegram.json b/registry/channels/telegram.json index 5c318f01..785d2abd 100644 --- a/registry/channels/telegram.json +++ b/registry/channels/telegram.json @@ -1,9 +1,9 @@ { "name": "telegram", - "display_name": "Telegram", + "display_name": "Telegram Channel", "kind": "channel", "version": "0.1.0", - "description": "Telegram Bot API channel for receiving and responding to messages", + "description": "Talk to your agent through a Telegram bot", "keywords": ["messaging", "bot", "chat", "telegram"], "source": { diff --git a/registry/channels/whatsapp.json b/registry/channels/whatsapp.json index 56aacced..9eda5093 100644 --- a/registry/channels/whatsapp.json +++ b/registry/channels/whatsapp.json @@ -1,9 +1,9 @@ { "name": "whatsapp", - "display_name": "WhatsApp", + "display_name": "WhatsApp Channel", "kind": "channel", "version": "0.1.0", - "description": "WhatsApp Cloud API channel for receiving and responding to messages", + "description": "Talk to your agent through WhatsApp", "keywords": ["messaging", "chat", "whatsapp", "meta"], "source": { diff --git a/registry/tools/slack.json b/registry/tools/slack.json index 8e33cba5..60416f65 100644 --- a/registry/tools/slack.json +++ b/registry/tools/slack.json @@ -1,9 +1,9 @@ { "name": "slack-tool", - "display_name": "Slack", + "display_name": "Slack Tool", "kind": "tool", "version": "0.1.0", - "description": "Post messages, read channels, and manage conversations via Slack API", + "description": "Your agent uses Slack to post and read messages in your workspace", "keywords": ["messaging", "chat", "workspace"], "source": { diff --git a/registry/tools/telegram.json b/registry/tools/telegram.json index 07e51f66..4e5d426c 100644 --- a/registry/tools/telegram.json +++ b/registry/tools/telegram.json @@ -1,9 +1,9 @@ { "name": "telegram-mtproto", - "display_name": "Telegram", + "display_name": "Telegram Tool", "kind": "tool", "version": "0.1.0", - "description": "Telegram user-mode integration via MTProto for messages and contacts", + "description": "Your agent uses your Telegram account to read and send messages", "keywords": ["messaging", "chat", "telegram", "mtproto"], "source": { diff --git a/src/channels/web/handlers/extensions.rs b/src/channels/web/handlers/extensions.rs index 58eeffaf..71a3db12 100644 --- a/src/channels/web/handlers/extensions.rs +++ b/src/channels/web/handlers/extensions.rs @@ -51,6 +51,7 @@ pub async fn extensions_list_handler( }; ExtensionInfo { name: ext.name, + display_name: ext.display_name, kind: ext.kind.to_string(), description: ext.description, url: ext.url, diff --git a/src/channels/web/server.rs b/src/channels/web/server.rs index f5bd9bfe..0964a01b 100644 --- a/src/channels/web/server.rs +++ b/src/channels/web/server.rs @@ -1740,6 +1740,7 @@ async fn extensions_list_handler( }; ExtensionInfo { name: ext.name, + display_name: ext.display_name, kind: ext.kind.to_string(), description: ext.description, url: ext.url, diff --git a/src/channels/web/static/app.js b/src/channels/web/static/app.js index 18481253..2bd7a641 100644 --- a/src/channels/web/static/app.js +++ b/src/channels/web/static/app.js @@ -324,8 +324,14 @@ function showSlashAutocomplete(matches) { const row = document.createElement('div'); row.className = 'slash-ac-item'; row.dataset.index = i; - row.innerHTML = '' + escapeHtml(item.cmd) + '' - + '' + escapeHtml(item.desc) + ''; + var cmdSpan = document.createElement('span'); + cmdSpan.className = 'slash-ac-cmd'; + cmdSpan.textContent = item.cmd; + var descSpan = document.createElement('span'); + descSpan.className = 'slash-ac-desc'; + descSpan.textContent = item.desc; + row.appendChild(cmdSpan); + row.appendChild(descSpan); row.addEventListener('mousedown', (e) => { e.preventDefault(); // prevent blur selectSlashItem(item.cmd); @@ -1643,6 +1649,8 @@ function loadServerLogLevel() { // --- Extensions --- +var kindLabels = { 'wasm_channel': 'Channel', 'wasm_tool': 'Tool', 'mcp_server': 'MCP' }; + function loadExtensions() { const extList = document.getElementById('extensions-list'); const wasmList = document.getElementById('available-wasm-list'); @@ -1718,7 +1726,7 @@ function renderAvailableExtensionCard(entry) { const kind = document.createElement('span'); kind.className = 'ext-kind kind-' + entry.kind; - kind.textContent = entry.kind; + kind.textContent = kindLabels[entry.kind] || entry.kind; header.appendChild(kind); card.appendChild(header); @@ -1784,7 +1792,7 @@ function renderMcpServerCard(entry, installedExt) { var kind = document.createElement('span'); kind.className = 'ext-kind kind-mcp_server'; - kind.textContent = 'mcp_server'; + kind.textContent = kindLabels['mcp_server'] || 'mcp_server'; header.appendChild(kind); if (installedExt) { @@ -1868,12 +1876,12 @@ function renderExtensionCard(ext) { const name = document.createElement('span'); name.className = 'ext-name'; - name.textContent = ext.name; + name.textContent = ext.display_name || ext.name; header.appendChild(name); const kind = document.createElement('span'); kind.className = 'ext-kind kind-' + ext.kind; - kind.textContent = ext.kind; + kind.textContent = kindLabels[ext.kind] || ext.kind; header.appendChild(kind); // Auth dot only for non-WASM-channel extensions (channels use the stepper instead) @@ -1981,7 +1989,7 @@ function renderExtensionCard(ext) { if (ext.needs_setup) { const configBtn = document.createElement('button'); configBtn.className = 'btn-ext configure'; - configBtn.textContent = ext.authenticated ? 'Reconfigure' : 'Configure'; + configBtn.textContent = ext.authenticated ? 'Reconfigure' : 'Setup'; configBtn.addEventListener('click', () => showConfigureModal(ext.name)); actions.appendChild(configBtn); } @@ -2102,7 +2110,8 @@ function renderConfigureModal(name, secrets) { if (secret.provided) { const badge = document.createElement('span'); badge.className = 'field-provided'; - badge.textContent = 'Set'; + badge.textContent = '\u2713'; + badge.title = 'Already configured'; inputRow.appendChild(badge); } if (secret.auto_generate && !secret.provided) { diff --git a/src/channels/web/types.rs b/src/channels/web/types.rs index f1daaf95..c96e3d4b 100644 --- a/src/channels/web/types.rs +++ b/src/channels/web/types.rs @@ -367,6 +367,8 @@ pub struct TransitionInfo { #[derive(Debug, Serialize)] pub struct ExtensionInfo { pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, pub kind: String, pub description: Option, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src/extensions/manager.rs b/src/extensions/manager.rs index d4bfbb92..d35f4a6a 100644 --- a/src/extensions/manager.rs +++ b/src/extensions/manager.rs @@ -323,9 +323,15 @@ impl ExtensionManager { Vec::new() }; + let display_name = self + .registry + .get_with_kind(&server.name, Some(ExtensionKind::McpServer)) + .await + .map(|e| e.display_name); extensions.push(InstalledExtension { name: server.name.clone(), kind: ExtensionKind::McpServer, + display_name, description: server.description.clone(), url: Some(server.url.clone()), authenticated, @@ -352,15 +358,22 @@ impl ExtensionManager { for (name, _discovered) in tools { let active = self.tool_registry.has(&name).await; + let display_name = self + .registry + .get_with_kind(&name, Some(ExtensionKind::WasmTool)) + .await + .map(|e| e.display_name); + let (authenticated, needs_setup) = self.check_tool_auth_status(&name).await; extensions.push(InstalledExtension { name: name.clone(), kind: ExtensionKind::WasmTool, + display_name, description: None, url: None, - authenticated: true, // WASM tools don't always need auth + authenticated, active, tools: if active { vec![name] } else { Vec::new() }, - needs_setup: false, + needs_setup, installed: true, activation_error: None, }); @@ -385,9 +398,15 @@ impl ExtensionManager { let (authenticated, needs_setup) = self.check_channel_auth_status(&name).await; let activation_error = errors.get(&name).cloned(); + let display_name = self + .registry + .get_with_kind(&name, Some(ExtensionKind::WasmChannel)) + .await + .map(|e| e.display_name); extensions.push(InstalledExtension { name, kind: ExtensionKind::WasmChannel, + display_name, description: None, url: None, authenticated, @@ -424,6 +443,7 @@ impl ExtensionManager { extensions.push(InstalledExtension { name: entry.name, kind: entry.kind, + display_name: Some(entry.display_name), description: Some(entry.description), url: None, authenticated: false, @@ -1442,6 +1462,51 @@ impl ExtensionManager { (all_provided, true) } + /// Load and parse a WASM tool's capabilities file. + /// + /// Returns `None` if the file doesn't exist or can't be parsed. + async fn load_tool_capabilities( + &self, + name: &str, + ) -> Option { + let cap_path = self + .wasm_tools_dir + .join(format!("{}.capabilities.json", name)); + let cap_bytes = tokio::fs::read(&cap_path).await.ok()?; + crate::tools::wasm::CapabilitiesFile::from_bytes(&cap_bytes).ok() + } + + /// Check whether a WASM tool's required setup secrets are provided. + /// + /// Returns `(authenticated, needs_setup)` — same semantics as `check_channel_auth_status`. + async fn check_tool_auth_status(&self, name: &str) -> (bool, bool) { + let Some(cap_file) = self.load_tool_capabilities(name).await else { + return (true, false); + }; + let Some(setup) = &cap_file.setup else { + return (true, false); + }; + if setup.required_secrets.is_empty() { + return (true, false); + } + let mut all_provided = true; + for secret in &setup.required_secrets { + if secret.optional { + continue; + } + if !self + .secrets + .exists(&self.user_id, &secret.name) + .await + .unwrap_or(false) + { + all_provided = false; + break; + } + } + (all_provided, true) + } + async fn auth_wasm_channel( &self, name: &str, @@ -2179,6 +2244,30 @@ impl ExtensionManager { } Ok(fields) } + ExtensionKind::WasmTool => { + let Some(cap_file) = self.load_tool_capabilities(name).await else { + return Ok(Vec::new()); + }; + + let mut fields = Vec::new(); + if let Some(setup) = &cap_file.setup { + for secret in &setup.required_secrets { + let provided = self + .secrets + .exists(&self.user_id, &secret.name) + .await + .unwrap_or(false); + fields.push(crate::channels::web::types::SecretFieldInfo { + name: secret.name.clone(), + prompt: secret.prompt.clone(), + optional: secret.optional, + provided, + auto_generate: false, + }); + } + } + Ok(fields) + } _ => Ok(Vec::new()), } } @@ -2193,34 +2282,52 @@ impl ExtensionManager { secrets: &std::collections::HashMap, ) -> Result { let kind = self.determine_installed_kind(name).await?; - if kind != ExtensionKind::WasmChannel { - return Err(ExtensionError::Other( - "Setup is only supported for WASM channels".to_string(), - )); - } - let cap_path = self - .wasm_channels_dir - .join(format!("{}.capabilities.json", name)); - if !cap_path.exists() { - return Err(ExtensionError::Other(format!( - "Capabilities file not found for '{}'", - name - ))); - } - let cap_bytes = tokio::fs::read(&cap_path) - .await - .map_err(|e| ExtensionError::Other(e.to_string()))?; - let cap_file = crate::channels::wasm::ChannelCapabilitiesFile::from_bytes(&cap_bytes) - .map_err(|e| ExtensionError::Other(e.to_string()))?; - - // Build allowed secret names from capabilities - let allowed: std::collections::HashSet = cap_file - .setup - .required_secrets - .iter() - .map(|s| s.name.clone()) - .collect(); + // Load allowed secret names from the extension's capabilities file + let allowed: std::collections::HashSet = match kind { + ExtensionKind::WasmChannel => { + let cap_path = self + .wasm_channels_dir + .join(format!("{}.capabilities.json", name)); + if !cap_path.exists() { + return Err(ExtensionError::Other(format!( + "Capabilities file not found for '{}'", + name + ))); + } + let cap_bytes = tokio::fs::read(&cap_path) + .await + .map_err(|e| ExtensionError::Other(e.to_string()))?; + let cap_file = + crate::channels::wasm::ChannelCapabilitiesFile::from_bytes(&cap_bytes) + .map_err(|e| ExtensionError::Other(e.to_string()))?; + cap_file + .setup + .required_secrets + .iter() + .map(|s| s.name.clone()) + .collect() + } + ExtensionKind::WasmTool => { + let cap_file = self.load_tool_capabilities(name).await.ok_or_else(|| { + ExtensionError::Other(format!("Capabilities file not found for '{}'", name)) + })?; + match cap_file.setup { + Some(s) => s.required_secrets.iter().map(|s| s.name.clone()).collect(), + None => { + return Err(ExtensionError::Other(format!( + "Tool '{}' has no setup schema — no secrets to configure", + name + ))); + } + } + } + _ => { + return Err(ExtensionError::Other( + "Setup is only supported for WASM channels and tools".to_string(), + )); + } + }; // Validate and store each submitted secret for (secret_name, secret_value) in secrets { @@ -2241,33 +2348,70 @@ impl ExtensionManager { .map_err(|e| ExtensionError::AuthFailed(e.to_string()))?; } - // Auto-generate any missing secrets that have auto_generate set - for secret_def in &cap_file.setup.required_secrets { - if let Some(ref auto_gen) = secret_def.auto_generate { - let already_provided = secrets - .get(&secret_def.name) - .is_some_and(|v| !v.trim().is_empty()); - let already_stored = self - .secrets - .exists(&self.user_id, &secret_def.name) - .await - .unwrap_or(false); - if !already_provided && !already_stored { - use rand::RngCore; - let mut bytes = vec![0u8; auto_gen.length]; - rand::thread_rng().fill_bytes(&mut bytes); - let hex_value: String = bytes.iter().map(|b| format!("{b:02x}")).collect(); - let params = CreateSecretParams::new(&secret_def.name, &hex_value) - .with_provider(name.to_string()); - self.secrets - .create(&self.user_id, params) - .await - .map_err(|e| ExtensionError::AuthFailed(e.to_string()))?; - tracing::info!( - "Auto-generated secret '{}' for channel '{}'", - secret_def.name, - name + // Auto-generate any missing secrets (channel-only feature) + if kind == ExtensionKind::WasmChannel { + let cap_path = self + .wasm_channels_dir + .join(format!("{}.capabilities.json", name)); + if let Ok(cap_bytes) = tokio::fs::read(&cap_path).await + && let Ok(cap_file) = + crate::channels::wasm::ChannelCapabilitiesFile::from_bytes(&cap_bytes) + { + for secret_def in &cap_file.setup.required_secrets { + if let Some(ref auto_gen) = secret_def.auto_generate { + let already_provided = secrets + .get(&secret_def.name) + .is_some_and(|v| !v.trim().is_empty()); + let already_stored = self + .secrets + .exists(&self.user_id, &secret_def.name) + .await + .unwrap_or(false); + if !already_provided && !already_stored { + use rand::RngCore; + let mut bytes = vec![0u8; auto_gen.length]; + rand::thread_rng().fill_bytes(&mut bytes); + let hex_value: String = + bytes.iter().map(|b| format!("{b:02x}")).collect(); + let params = CreateSecretParams::new(&secret_def.name, &hex_value) + .with_provider(name.to_string()); + self.secrets + .create(&self.user_id, params) + .await + .map_err(|e| ExtensionError::AuthFailed(e.to_string()))?; + tracing::info!( + "Auto-generated secret '{}' for channel '{}'", + secret_def.name, + name + ); + } + } + } + } + } + + // For tools, save and attempt auto-activation + if kind == ExtensionKind::WasmTool { + match self.activate_wasm_tool(name).await { + Ok(result) => { + return Ok(SetupResult { + message: format!( + "Configuration saved and tool '{}' activated. {}", + name, result.message + ), + activated: true, + }); + } + Err(e) => { + tracing::debug!( + "Auto-activation of tool '{}' after setup failed: {}", + name, + e ); + return Ok(SetupResult { + message: format!("Configuration saved for '{}'.", name), + activated: false, + }); } } } diff --git a/src/extensions/mod.rs b/src/extensions/mod.rs index 0d7828e3..3b166b46 100644 --- a/src/extensions/mod.rs +++ b/src/extensions/mod.rs @@ -187,6 +187,9 @@ fn default_true() -> bool { pub struct InstalledExtension { pub name: String, pub kind: ExtensionKind, + /// Human-readable display name (e.g. "Telegram Channel" vs "Telegram Tool"). + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, /// Server or source URL (e.g. MCP server endpoint). diff --git a/src/tools/wasm/capabilities_schema.rs b/src/tools/wasm/capabilities_schema.rs index f03f6f2c..50d8f338 100644 --- a/src/tools/wasm/capabilities_schema.rs +++ b/src/tools/wasm/capabilities_schema.rs @@ -62,6 +62,11 @@ pub struct CapabilitiesFile { #[serde(default)] pub auth: Option, + /// 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, + /// 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, +} + +/// 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 diff --git a/tools-src/gmail/gmail-tool.capabilities.json b/tools-src/gmail/gmail-tool.capabilities.json index 013cd690..e3f8a79b 100644 --- a/tools-src/gmail/gmail-tool.capabilities.json +++ b/tools-src/gmail/gmail-tool.capabilities.json @@ -42,5 +42,17 @@ } }, "env_var": "GOOGLE_OAUTH_TOKEN" + }, + "setup": { + "required_secrets": [ + { + "name": "google_oauth_client_id", + "prompt": "Google OAuth Client ID (from console.cloud.google.com/apis/credentials)" + }, + { + "name": "google_oauth_client_secret", + "prompt": "Google OAuth Client Secret" + } + ] } } diff --git a/tools-src/google-calendar/google-calendar-tool.capabilities.json b/tools-src/google-calendar/google-calendar-tool.capabilities.json index 7ea74499..0a37772b 100644 --- a/tools-src/google-calendar/google-calendar-tool.capabilities.json +++ b/tools-src/google-calendar/google-calendar-tool.capabilities.json @@ -41,5 +41,17 @@ } }, "env_var": "GOOGLE_OAUTH_TOKEN" + }, + "setup": { + "required_secrets": [ + { + "name": "google_oauth_client_id", + "prompt": "Google OAuth Client ID (from console.cloud.google.com/apis/credentials)" + }, + { + "name": "google_oauth_client_secret", + "prompt": "Google OAuth Client Secret" + } + ] } } diff --git a/tools-src/google-docs/google-docs-tool.capabilities.json b/tools-src/google-docs/google-docs-tool.capabilities.json index 9beee15d..2bab1abb 100644 --- a/tools-src/google-docs/google-docs-tool.capabilities.json +++ b/tools-src/google-docs/google-docs-tool.capabilities.json @@ -41,5 +41,17 @@ } }, "env_var": "GOOGLE_OAUTH_TOKEN" + }, + "setup": { + "required_secrets": [ + { + "name": "google_oauth_client_id", + "prompt": "Google OAuth Client ID (from console.cloud.google.com/apis/credentials)" + }, + { + "name": "google_oauth_client_secret", + "prompt": "Google OAuth Client Secret" + } + ] } } diff --git a/tools-src/google-drive/google-drive-tool.capabilities.json b/tools-src/google-drive/google-drive-tool.capabilities.json index 54c1735c..cc49db8c 100644 --- a/tools-src/google-drive/google-drive-tool.capabilities.json +++ b/tools-src/google-drive/google-drive-tool.capabilities.json @@ -46,5 +46,17 @@ } }, "env_var": "GOOGLE_OAUTH_TOKEN" + }, + "setup": { + "required_secrets": [ + { + "name": "google_oauth_client_id", + "prompt": "Google OAuth Client ID (from console.cloud.google.com/apis/credentials)" + }, + { + "name": "google_oauth_client_secret", + "prompt": "Google OAuth Client Secret" + } + ] } } diff --git a/tools-src/google-sheets/google-sheets-tool.capabilities.json b/tools-src/google-sheets/google-sheets-tool.capabilities.json index 0e64fb1e..23f7f46b 100644 --- a/tools-src/google-sheets/google-sheets-tool.capabilities.json +++ b/tools-src/google-sheets/google-sheets-tool.capabilities.json @@ -41,5 +41,17 @@ } }, "env_var": "GOOGLE_OAUTH_TOKEN" + }, + "setup": { + "required_secrets": [ + { + "name": "google_oauth_client_id", + "prompt": "Google OAuth Client ID (from console.cloud.google.com/apis/credentials)" + }, + { + "name": "google_oauth_client_secret", + "prompt": "Google OAuth Client Secret" + } + ] } } diff --git a/tools-src/google-slides/google-slides-tool.capabilities.json b/tools-src/google-slides/google-slides-tool.capabilities.json index ce99d7a3..e5920c71 100644 --- a/tools-src/google-slides/google-slides-tool.capabilities.json +++ b/tools-src/google-slides/google-slides-tool.capabilities.json @@ -41,5 +41,17 @@ } }, "env_var": "GOOGLE_OAUTH_TOKEN" + }, + "setup": { + "required_secrets": [ + { + "name": "google_oauth_client_id", + "prompt": "Google OAuth Client ID (from console.cloud.google.com/apis/credentials)" + }, + { + "name": "google_oauth_client_secret", + "prompt": "Google OAuth Client Secret" + } + ] } } diff --git a/tools-src/okta/okta-tool.capabilities.json b/tools-src/okta/okta-tool.capabilities.json index 14a0879d..1badf9d3 100644 --- a/tools-src/okta/okta-tool.capabilities.json +++ b/tools-src/okta/okta-tool.capabilities.json @@ -89,5 +89,17 @@ "setup_url": "https://developer.okta.com/docs/guides/implement-oauth-for-okta/main/", "token_hint": "OAuth2 access token (JWT)", "env_var": "OKTA_OAUTH_TOKEN" + }, + "setup": { + "required_secrets": [ + { + "name": "okta_oauth_client_id", + "prompt": "Okta OAuth Client ID" + }, + { + "name": "okta_oauth_client_secret", + "prompt": "Okta OAuth Client Secret" + } + ] } } diff --git a/tools-src/slack/slack-tool.capabilities.json b/tools-src/slack/slack-tool.capabilities.json index 753cffc6..d6119e45 100644 --- a/tools-src/slack/slack-tool.capabilities.json +++ b/tools-src/slack/slack-tool.capabilities.json @@ -46,5 +46,17 @@ "setup_url": "https://api.slack.com/apps", "token_hint": "Starts with 'xoxb-'", "env_var": "SLACK_BOT_TOKEN" + }, + "setup": { + "required_secrets": [ + { + "name": "slack_oauth_client_id", + "prompt": "Slack OAuth Client ID (from api.slack.com/apps)" + }, + { + "name": "slack_oauth_client_secret", + "prompt": "Slack OAuth Client Secret" + } + ] } } diff --git a/tools-src/telegram/telegram-tool.capabilities.json b/tools-src/telegram/telegram-tool.capabilities.json index 03736e06..869081e9 100644 --- a/tools-src/telegram/telegram-tool.capabilities.json +++ b/tools-src/telegram/telegram-tool.capabilities.json @@ -24,5 +24,17 @@ "display_name": "Telegram", "instructions": "1. Go to https://my.telegram.org/apps and create an app\n2. Store your API ID and hash in the workspace:\n - Write your numeric API ID to telegram/api_id\n - Write your API hash string to telegram/api_hash\n3. Use the 'login' action with your phone number\n4. Use 'submit_auth_code' with the code you receive\n5. Use 'submit_2fa_password' if you have 2FA enabled\n6. Save the returned session JSON to telegram/session.json", "setup_url": "https://my.telegram.org/apps" + }, + "setup": { + "required_secrets": [ + { + "name": "telegram_api_id", + "prompt": "Telegram API ID (from my.telegram.org/apps)" + }, + { + "name": "telegram_api_hash", + "prompt": "Telegram API Hash" + } + ] } }