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"
+ }
+ ]
}
}