Skip to content

feat(extensions): unify chat and gateway auth paths, clean up secrets on remove#677

Open
henrypark133 wants to merge 1 commit intomainfrom
unify-extension-lifecycle
Open

feat(extensions): unify chat and gateway auth paths, clean up secrets on remove#677
henrypark133 wants to merge 1 commit intomainfrom
unify-extension-lifecycle

Conversation

@henrypark133
Copy link
Collaborator

Summary

  • Secret cleanup on remove: remove() now deletes all associated secrets (setup secrets, webhook secrets, OAuth tokens) using capabilities-file enumeration + provider-tag fallback. Fixes stale secrets surviving uninstall/reinstall cycles.
  • Unified auth path: auth_wasm_channel() delegates to save_setup_secrets() when a token is provided, giving chat the same behavior as the Extensions tab: token validation, webhook secret auto-generation, auto-activation.
  • Generic validation_endpoint: Replaced hardcoded if name == "telegram" token validation with capabilities-driven validation_endpoint URL template. Any channel can declare validation in its capabilities.json.
  • Error separation: Token validation errors re-prompt the user; infrastructure errors propagate to the caller.

Problem

When setting up Telegram via chat (tool_auth), old invalid bot tokens survived tool_remove and caused auth() to report "authenticated" while activation failed. The Extensions tab worked because it used a completely different code path (save_setup_secrets) with validation and auto-generation.

Test plan

  • test_remove_channel_cleans_up_secrets — bot token + webhook secret deleted on remove
  • test_remove_cleans_secrets_by_provider_fallback — provider-tagged secrets caught by fallback
  • test_auth_after_reinstall_detects_missing_secrets — stale secrets don't survive remove+reinstall
  • cargo clippy --all --all-features — zero warnings
  • Manual E2E: install telegram via chat, enter token, verify activation, remove, reinstall, verify clean prompt

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings March 7, 2026 19:49
@github-actions github-actions bot added scope: extensions Extension management size: L 200-499 changed lines risk: medium Business logic, config, or moderate-risk modules contributor: core 20+ merged PRs labels Mar 7, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the robustness and consistency of extension management, particularly for chat channels. It addresses issues with lingering secrets after extension removal and unifies the authentication process, making it more reliable and extensible. The changes ensure that authentication flows are standardized and that sensitive data is properly cleaned up, improving the overall security and user experience of extensions.

Highlights

  • Secret Cleanup on Removal: The remove() function now proactively deletes all associated secrets (setup secrets, webhook secrets, OAuth tokens) for an extension. This prevents stale secrets from persisting across uninstall/reinstall cycles by using capabilities-file enumeration and a provider-tag fallback.
  • Unified Authentication Path: The auth_wasm_channel() method now delegates to save_setup_secrets() when a token is provided. This unifies the authentication flow for chat channels with the Extensions tab, enabling consistent token validation, webhook secret auto-generation, and auto-activation.
  • Generic Token Validation: Hardcoded Telegram-specific token validation has been replaced with a generic, capabilities-driven validation_endpoint URL template. Any channel can now declare its validation endpoint in its capabilities.json file, allowing for flexible and extensible token validation.
  • Improved Error Handling: Authentication errors are now separated: token validation failures (e.g., invalid token, API rejection) will re-prompt the user, while infrastructure errors (e.g., database issues, file read failures) are propagated to the caller for appropriate handling.
Changelog
  • channels-src/telegram/telegram.capabilities.json
    • Added a validation_endpoint to the Telegram capabilities file, enabling generic token validation.
  • src/extensions/manager.rs
    • Modified the remove method to call cleanup_extension_secrets before deleting capabilities files, ensuring secrets are removed.
    • Refactored auth_wasm_channel to utilize save_setup_secrets for token handling, unifying the authentication logic.
    • Implemented specific error handling in auth_wasm_channel to differentiate between token validation errors (re-prompt) and infrastructure errors (propagate).
    • Replaced hardcoded Telegram token validation in save_setup_secrets with a generic mechanism using validation_endpoint from capabilities files.
    • Added Box::pin to an auth call within activate_wasm_channel to resolve potential async recursion issues.
    • Introduced a new cleanup_extension_secrets asynchronous function to systematically delete secrets based on extension kind and a provider-tag fallback.
    • Added new test cases to verify secret cleanup on removal, fallback secret deletion, and correct authentication behavior after reinstallation.
Activity
  • The pull request includes a detailed summary of changes and the problem being addressed.
  • A comprehensive test plan is provided, including unit tests for secret cleanup and reinstallation scenarios.
  • The author confirmed that cargo clippy --all --all-features passed with zero warnings.
  • A manual E2E test plan is outlined for verifying Telegram installation via chat, activation, removal, and reinstallation.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces significant improvements to the extension authentication and removal process, unifying the auth paths for chat and the Extensions tab for greater consistency. However, it introduces a High-severity SSRF vulnerability in the new secret validation logic and misses path traversal validation in the save_setup_secrets method. Additionally, there are suggestions to improve the robustness of the new error handling, enhance performance by using a more efficient data structure, and address code duplication for better maintainability.

Comment on lines +2941 to +2948
let resp = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|e| ExtensionError::Other(e.to_string()))?
.get(&url)
.send()
.await
.map_err(|e| {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The save_setup_secrets method uses a validation_endpoint URL template from the extension's capabilities file to perform a request from the host process. This URL is not validated against an allowlist, allowing a malicious extension to trigger SSRF requests to internal services or leak secrets to external servers. Implement a strict allowlist for validation_endpoint URLs or ensure they match the extension's declared HTTP allowlist. At a minimum, block requests to loopback and private IP ranges.

References
  1. This rule emphasizes the importance of resolving hostnames to IP addresses and validating all resolved IPs against restricted ranges to prevent DNS-based SSRF vulnerabilities, aligning with the need to block requests to loopback and private IP ranges.

Comment on lines +2177 to +2181
let msg = e.to_string();
if msg.contains("Invalid token")
|| msg.contains("API returned")
|| msg.contains("validate token")
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Relying on string matching (.contains()) for error handling is brittle and can lead to bugs if the error messages change. It would be more robust to introduce a specific error variant for token validation failures.

For example, you could add a TokenValidationFailed(String) variant to your ExtensionError enum. Then, save_setup_secrets can return Err(ExtensionError::TokenValidationFailed(...)) and this code can handle it explicitly with if let ExtensionError::TokenValidationFailed(msg) = e or a match statement. This would make the error handling type-safe.

References
  1. This rule advocates for creating specific error variants for different failure modes to provide semantically correct and clear error messages, which aligns with the suggestion to use a specific error variant for token validation failures instead of brittle string matching.

if kind == ExtensionKind::WasmChannel {
let cap_path = self
.wasm_channels_dir
.join(format!("{}.capabilities.json", name));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The save_setup_secrets method uses the name parameter to construct file paths without calling validate_extension_name. This allows an attacker to provide a name with path traversal sequences (e.g., ..) to interact with files outside the intended extensions directory. Although the impact is limited by the forced .capabilities.json extension, it is recommended to call Self::validate_extension_name(name)?; at the beginning of the method to ensure the name is safe.

References
  1. This rule requires validation of path traversal characters in extension names at the public API boundary to prevent security vulnerabilities, directly supporting the recommendation to validate the name parameter.

Comment on lines +2146 to +2159
let mut still_missing = Vec::new();
for s in required_secrets {
if s.optional {
continue;
}
if !self
.secrets
.exists(&self.user_id, &s.name)
.await
.unwrap_or(false)
{
still_missing.push(s);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block of code to find missing secrets is a duplicate of the logic at lines 2115-2127. To improve maintainability and reduce duplication, consider extracting this logic into a private helper function, for example:

async fn get_missing_secrets<'a>(
    &self,
    required_secrets: &'a [crate::channels::wasm::SecretSetupSchema],
) -> Vec<&'a crate::channels::wasm::SecretSetupSchema> {
    // ... implementation ...
}

You could then call self.get_missing_secrets(required_secrets).await in both places.

/// 1. Read secret names from the capabilities file (while it still exists)
/// 2. Fallback: list all secrets where `provider == extension_name`
async fn cleanup_extension_secrets(&self, name: &str, kind: ExtensionKind) {
let mut deleted_names: Vec<String> = Vec::new();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better performance, consider using a HashSet<String> for deleted_names instead of a Vec<String>. The contains() check on line 3247 has O(n) time complexity for a Vec, but would be O(1) on average for a HashSet. This is more efficient, especially if an extension has many secrets.

To make this change, you'll also need to replace all calls to .push() on deleted_names with .insert().

Suggested change
let mut deleted_names: Vec<String> = Vec::new();
let mut deleted_names: std::collections::HashSet<String> = std::collections::HashSet::new();

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses inconsistent authentication behavior between chat-driven setup and the Extensions UI, and fixes stale secrets persisting across uninstall/reinstall cycles for extensions.

Changes:

  • Add secret cleanup during ExtensionManager::remove() using capabilities-based secret enumeration plus a provider-tag fallback.
  • Unify chat auth for WASM channels by routing token entry through save_setup_secrets() (enabling validation, auto-generation, and activation).
  • Replace Telegram-specific token validation with a generic validation_endpoint declared in channel capabilities (Telegram updated accordingly).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/extensions/manager.rs Implements secret cleanup on remove, unifies WASM channel auth path via save_setup_secrets, and adds capabilities-driven token validation.
channels-src/telegram/telegram.capabilities.json Adds validation_endpoint for Telegram bot token validation via the Telegram API.
Comments suppressed due to low confidence (1)

src/extensions/manager.rs:2988

  • save_setup_secrets reads and parses the channel capabilities file multiple times (allowed-secret collection, validation_endpoint, and auto-generate blocks). This adds redundant I/O/JSON parsing and makes the logic harder to keep consistent. Consider loading cap_file once for the channel path and reusing it for allowed names, validation, and auto-generation.
        // Load allowed secret names from the extension's capabilities file
        let allowed: std::collections::HashSet<String> = 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 secrets against the validation_endpoint if declared in capabilities.
        // The endpoint URL template uses {secret_name} placeholders that are
        // substituted with the provided secret value before making the request.
        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)
                && let Some(ref endpoint_template) = cap_file.setup.validation_endpoint
                && let Some(secret_def) = cap_file
                    .setup
                    .required_secrets
                    .iter()
                    .find(|s| !s.optional && secrets.contains_key(&s.name))
                && let Some(token_value) = secrets.get(&secret_def.name)
            {
                let token = token_value.trim();
                if !token.is_empty() {
                    let encoded =
                        url::form_urlencoded::byte_serialize(token.as_bytes()).collect::<String>();
                    let url =
                        endpoint_template.replace(&format!("{{{}}}", secret_def.name), &encoded);
                    let resp = reqwest::Client::builder()
                        .timeout(std::time::Duration::from_secs(10))
                        .build()
                        .map_err(|e| ExtensionError::Other(e.to_string()))?
                        .get(&url)
                        .send()
                        .await
                        .map_err(|e| {
                            ExtensionError::Other(format!("Failed to validate token: {}", e))
                        })?;
                    if !resp.status().is_success() {
                        return Err(ExtensionError::Other(format!(
                            "Invalid token (API returned {})",
                            resp.status()
                        )));
                    }
                }
            }
        }

        // Validate and store each submitted secret
        for (secret_name, secret_value) in secrets {
            if !allowed.contains(secret_name.as_str()) {
                return Err(ExtensionError::Other(format!(
                    "Unknown secret '{}' for extension '{}'",
                    secret_name, name
                )));
            }
            if secret_value.trim().is_empty() {
                continue;
            }
            let params =
                CreateSecretParams::new(secret_name, secret_value).with_provider(name.to_string());
            self.secrets
                .create(&self.user_id, params)
                .await
                .map_err(|e| ExtensionError::AuthFailed(e.to_string()))?;
        }

        // 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)
            {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +3233 to +3251
ExtensionKind::McpServer => {
for suffix in ["access_token", "refresh_token"] {
let secret_name = format!("mcp_{}_{}", name, suffix);
if self.secrets.delete(&self.user_id, &secret_name).await.ok() == Some(true) {
deleted_names.push(secret_name);
}
}
}
}

// Fallback: delete any secrets tagged with this provider
if let Ok(all_refs) = self.secrets.list(&self.user_id).await {
for secret_ref in all_refs {
if secret_ref.provider.as_deref() == Some(name)
&& !deleted_names.contains(&secret_ref.name)
{
let _ = self.secrets.delete(&self.user_id, &secret_ref.name).await;
deleted_names.push(secret_ref.name);
}
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cleanup_extension_secrets doesn’t fully clean up MCP server secrets: MCP auth stores a DCR client id under mcp_{name}_client_id (see McpServerConfig::client_id_secret_name() usage), but the removal path only deletes access/refresh tokens. Also, MCP secrets are tagged with provider mcp:{name}, so the provider-tag fallback (provider == name) won’t catch any remaining MCP-tagged secrets. Consider deleting the mcp_{name}_client_id secret explicitly and extending the provider fallback to include mcp:{name} (or a mcp: prefix match).

Copilot uses AI. Check for mistakes.
Comment on lines +2173 to +2193
Err(e) => {
// Token validation errors (bad token, API rejected) should
// re-prompt the user. Infrastructure errors (DB down, file
// read failure) should propagate to the caller.
let msg = e.to_string();
if msg.contains("Invalid token")
|| msg.contains("API returned")
|| msg.contains("validate token")
{
return Ok(AuthResult::awaiting_token(
name,
ExtensionKind::WasmChannel,
format!(
"Authentication failed: {}. Please provide a valid token.",
e
),
cap_file.setup.setup_url.clone(),
));
}
return Err(e);
}
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling here classifies failures by substring matching on e.to_string(), and will treat infrastructure failures as “token validation” in some cases (e.g., save_setup_secrets returns Other("Failed to validate token: ...") on network/timeout, which matches "validate token" and triggers a user re-prompt). Also, the subsequent exists(...).unwrap_or(false) re-check will silently convert SecretsStore errors into “missing secret”, again re-prompting instead of surfacing an infra problem. Consider adding a structured error variant for validation failures (vs infra) and propagating SecretsStore read errors rather than swallowing them.

Copilot uses AI. Check for mistakes.
Comment on lines +2924 to +2955
if let Ok(cap_bytes) = tokio::fs::read(&cap_path).await
&& let Ok(cap_file) =
crate::channels::wasm::ChannelCapabilitiesFile::from_bytes(&cap_bytes)
&& let Some(ref endpoint_template) = cap_file.setup.validation_endpoint
&& let Some(secret_def) = cap_file
.setup
.required_secrets
.iter()
.find(|s| !s.optional && secrets.contains_key(&s.name))
&& let Some(token_value) = secrets.get(&secret_def.name)
{
let token = token_value.trim();
if !token.is_empty() {
let encoded =
url::form_urlencoded::byte_serialize(token.as_bytes()).collect::<String>();
let url =
endpoint_template.replace(&format!("{{{}}}", secret_def.name), &encoded);
let resp = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|e| ExtensionError::Other(e.to_string()))?
.get(&url)
.send()
.await
.map_err(|e| {
ExtensionError::Other(format!("Failed to validate token: {}", e))
})?;
if !resp.status().is_success() {
return Err(ExtensionError::Other(format!(
"Invalid token (API returned {})",
resp.status()
)));
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validation_endpoint substitution/selection logic is too narrow for the stated “capabilities-driven” behavior: it picks the first non-optional secret present in the submitted map and only replaces that one {secret_name} placeholder. If the template references a different secret (or multiple secrets), placeholders can remain unexpanded and the request may hit an invalid URL, potentially blocking unrelated secret updates. Consider (1) replacing placeholders for all provided secrets (and/or validating that every placeholder in the template is satisfied) and (2) failing fast with a clear error when the template can’t be fully rendered.

Suggested change
if let Ok(cap_bytes) = tokio::fs::read(&cap_path).await
&& let Ok(cap_file) =
crate::channels::wasm::ChannelCapabilitiesFile::from_bytes(&cap_bytes)
&& let Some(ref endpoint_template) = cap_file.setup.validation_endpoint
&& let Some(secret_def) = cap_file
.setup
.required_secrets
.iter()
.find(|s| !s.optional && secrets.contains_key(&s.name))
&& let Some(token_value) = secrets.get(&secret_def.name)
{
let token = token_value.trim();
if !token.is_empty() {
let encoded =
url::form_urlencoded::byte_serialize(token.as_bytes()).collect::<String>();
let url =
endpoint_template.replace(&format!("{{{}}}", secret_def.name), &encoded);
let resp = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|e| ExtensionError::Other(e.to_string()))?
.get(&url)
.send()
.await
.map_err(|e| {
ExtensionError::Other(format!("Failed to validate token: {}", e))
})?;
if !resp.status().is_success() {
return Err(ExtensionError::Other(format!(
"Invalid token (API returned {})",
resp.status()
)));
if let Ok(cap_bytes) = tokio::fs::read(&cap_path).await {
if let Ok(cap_file) =
crate::channels::wasm::ChannelCapabilitiesFile::from_bytes(&cap_bytes)
{
if let Some(ref endpoint_template) = cap_file.setup.validation_endpoint {
// Render the validation endpoint URL by substituting all relevant
// non-optional required secrets, and ensure no placeholders remain.
let mut url = endpoint_template.clone();
let mut any_replaced = false;
for secret_def in &cap_file.setup.required_secrets {
if secret_def.optional {
continue;
}
if let Some(raw_value) = secrets.get(&secret_def.name) {
let token = raw_value.trim();
if token.is_empty() {
continue;
}
let encoded = url::form_urlencoded::byte_serialize(
token.as_bytes(),
)
.collect::<String>();
let placeholder = format!("{{{}}}", secret_def.name);
if url.contains(&placeholder) {
url = url.replace(&placeholder, &encoded);
any_replaced = true;
}
}
}
// If at least one substitution occurred, verify that no placeholders remain.
if any_replaced {
if url.contains('{') && url.contains('}') {
return Err(ExtensionError::Other(
"Validation endpoint template contains unresolved placeholders; \
ensure all required secrets are provided."
.to_string(),
));
}
let resp = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|e| ExtensionError::Other(e.to_string()))?
.get(&url)
.send()
.await
.map_err(|e| {
ExtensionError::Other(format!(
"Failed to validate token: {}",
e
))
})?;
if !resp.status().is_success() {
return Err(ExtensionError::Other(format!(
"Invalid token (API returned {})",
resp.status()
)));
}
}

Copilot uses AI. Check for mistakes.
let kind = self.determine_installed_kind(name).await?;

// Clean up secrets before deleting capabilities files
self.cleanup_extension_secrets(name, kind).await;
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove() calls cleanup_extension_secrets() but discards any failures inside that cleanup (most operations are ok()/ignored). If the secrets backend is unavailable, remove will still succeed while leaving stale secrets behind—the exact class of issue this PR is trying to prevent. Consider returning Result<()> from cleanup_extension_secrets and either failing remove() on cleanup errors or at least logging warnings when deletions/listing fail.

Suggested change
self.cleanup_extension_secrets(name, kind).await;
if let Err(err) = self.cleanup_extension_secrets(name, kind).await {
eprintln!(
"Warning: failed to clean up secrets for extension '{}': {}",
name, err
);
}

Copilot uses AI. Check for mistakes.
Refactors the extension lifecycle to eliminate the divergence between
chat and gateway paths that caused Telegram setup via chat to fail
(missing webhook secret auto-generation, no token validation).

Key changes:
- Rename save_setup_secrets() → configure(): single entrypoint for
  providing secrets to any extension (WasmChannel, WasmTool, MCP).
  Validates, stores, auto-generates, and activates.
- Add configure_token(): convenience wrapper for single-token callers
  (chat auth card, WebSocket, agent auth mode).
- Refactor auth() to pure status check: remove token parameter,
  delete token-storing branches from auth_mcp/auth_wasm_tool,
  rename auth_wasm_channel → auth_wasm_channel_status.
- Add ConfigureResult/MissingSecret types for structured responses.
- Replace hardcoded Telegram token validation with generic
  validation_endpoint from capabilities.json.
- Update all callers (9 files) to use the new interface.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@henrypark133 henrypark133 force-pushed the unify-extension-lifecycle branch from 64a72a0 to 974d916 Compare March 7, 2026 21:17
@github-actions github-actions bot added size: L 200-499 changed lines scope: agent Agent core (agent loop, router, scheduler) scope: channel/web Web gateway channel scope: tool/builtin Built-in tools size: XL 500+ changed lines and removed size: L 200-499 changed lines labels Mar 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: medium Business logic, config, or moderate-risk modules scope: agent Agent core (agent loop, router, scheduler) scope: channel/web Web gateway channel scope: extensions Extension management scope: tool/builtin Built-in tools size: L 200-499 changed lines size: XL 500+ changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants