Skip to content

feat: extension registry with metadata catalog and onboarding integration#238

Merged
ilblackdragon merged 6 commits intomainfrom
feat/extension-registry
Feb 20, 2026
Merged

feat: extension registry with metadata catalog and onboarding integration#238
ilblackdragon merged 6 commits intomainfrom
feat/extension-registry

Conversation

@ilblackdragon
Copy link
Copy Markdown
Member

@ilblackdragon ilblackdragon commented Feb 19, 2026

Summary

  • Extension registry (registry/): One JSON manifest per extension (10 tools + 4 channels) with source location, auth summary, artifacts, and tags. Bundle definitions group related extensions (google, messaging, default).
  • Rust module (src/registry/): ExtensionManifest serde structs, RegistryCatalog for loading/searching/resolving manifests and bundles, RegistryInstaller for build-from-source and download+SHA256-verify flows.
  • CLI commands: ironclaw registry list [--kind tool|channel] [--tag default] [--verbose], info <name>, install <name|bundle> [--force] [--build], install-defaults [--force].
  • Onboarding integration: Step 6 (channels) now discovers registry channels alongside bundled ones and installs selected channels from the registry. New Step 7 (extensions) presents all registry tools in a multi-select, pre-checks defaults, builds from source, and prints consolidated auth hints. Heartbeat moved to Step 8.

Bug fixes (97d6786)

  • Workspace isolation for tool/channel crates: Tool crates in tools-src/ and channels-src/ failed cargo metadata during onboard install because Cargo resolved them as part of the root workspace. Added [workspace] table to each standalone crate and extended the root workspace.exclude list so they build independently.
  • Channels-only onboarding (--channels-only): Failed with "Secrets not configured" and "No database connection" because it skipped database and security setup. Added reconnect_existing_db() to establish the DB connection and load saved settings before running channel configuration.
  • Tunnel config display: When re-running onboarding with an existing tunnel, the wizard now shows full provider details (domain, mode, auth status, command) instead of just the provider name, before asking whether to reconfigure.

Test plan

  • cargo build compiles with default features
  • cargo build --no-default-features --features libsql compiles
  • cargo build --no-default-features --features postgres compiles
  • cargo clippy --all --all-features — zero new warnings
  • cargo fmt -- --check — clean
  • cargo test — 1203 tests pass, 0 failures
  • 14 registry-specific unit tests (manifest parsing, catalog loading, search, bundle resolution)
  • cargo metadata works from inside tools-src/telegram/ (standalone workspace)
  • Manual: ironclaw registry list shows all 14 extensions
  • Manual: ironclaw registry info gmail shows manifest details
  • Manual: ironclaw registry install slack --build builds and installs
  • Manual: ironclaw onboard shows channels from registry + new tools step
  • Manual: ironclaw onboard --channels-only connects to existing DB and saves settings

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings February 19, 2026 21:17
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @ilblackdragon, 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 extensibility of the system by introducing a robust extension registry. This new infrastructure allows for standardized definition, discovery, and installation of tools and channels, moving towards a more modular and user-friendly experience. It provides a centralized metadata catalog and streamlines the process of integrating new functionalities, both through command-line operations and an improved interactive onboarding wizard.

Highlights

  • Extension Registry: Introduced a new registry/ directory structure containing JSON manifests for extensions (tools and channels) and bundle definitions, providing metadata like source location, authentication summaries, and tags.
  • Rust Module for Registry Management: Developed a new Rust module (src/registry/) including RegistryCatalog for loading, searching, and resolving manifests and bundles, and RegistryInstaller for handling build-from-source and download+SHA256-verify installation flows.
  • New CLI Commands: Added ironclaw registry subcommands for interacting with the extension registry, allowing users to list available extensions, view info about specific extensions or bundles, and install extensions or default bundles.
  • Onboarding Wizard Integration: Integrated the extension registry into the onboarding process, allowing Step 6 (channels) to discover and install registry channels, and introducing a new Step 7 for selecting and installing tools from the registry with consolidated authentication hints. The heartbeat step was moved to Step 8.
Changelog
  • registry/_bundles.json
    • Added bundle definitions for Google Suite, Messaging Channels, and a Recommended Set of extensions.
  • registry/channels/discord.json
    • Added manifest for Discord channel.
  • registry/channels/slack.json
    • Added manifest for Slack channel.
  • registry/channels/telegram.json
    • Added manifest for Telegram channel.
  • registry/channels/whatsapp.json
    • Added manifest for WhatsApp channel.
  • registry/tools/github.json
    • Added manifest for GitHub tool.
  • registry/tools/gmail.json
    • Added manifest for Gmail tool.
  • registry/tools/google-calendar.json
    • Added manifest for Google Calendar tool.
  • registry/tools/google-docs.json
    • Added manifest for Google Docs tool.
  • registry/tools/google-drive.json
    • Added manifest for Google Drive tool.
  • registry/tools/google-sheets.json
    • Added manifest for Google Sheets tool.
  • registry/tools/google-slides.json
    • Added manifest for Google Slides tool.
  • registry/tools/okta.json
    • Added manifest for Okta tool.
  • registry/tools/slack.json
    • Added manifest for Slack tool.
  • registry/tools/telegram.json
    • Added manifest for Telegram tool.
  • src/cli/mod.rs
    • Imported and exposed the new registry CLI module.
    • Added Registry command to the main CLI enum.
  • src/cli/registry.rs
    • Added new file implementing the ironclaw registry CLI commands (list, info, install, install-defaults).
    • Included logic for finding the registry directory and interacting with the RegistryCatalog and RegistryInstaller.
  • src/lib.rs
    • Declared the new registry module as part of the library.
  • src/main.rs
    • Integrated the Registry command into the main application's command matching logic.
  • src/registry/catalog.rs
    • Added new file defining RegistryCatalog for loading, managing, and querying extension manifests and bundles from the registry/ directory.
    • Implemented methods for listing, getting, searching, and resolving manifests and bundles.
    • Included unit tests for catalog functionality.
  • src/registry/installer.rs
    • Added new file defining RegistryInstaller for installing extensions either by building from source or downloading pre-built artifacts.
    • Implemented logic for handling installation outcomes, including WASM binary and capabilities file placement.
    • Included a helper function build_wasm_component for invoking cargo component build.
  • src/registry/manifest.rs
    • Added new file defining Serde structs for ExtensionManifest, ManifestKind, SourceSpec, ArtifactSpec, AuthSummary, BundleDefinition, and BundlesFile.
    • Included conversion logic from ExtensionManifest to RegistryEntry for in-chat discovery.
    • Included unit tests for manifest parsing.
  • src/registry/mod.rs
    • Added new module file to organize and export registry-related components (catalog, installer, manifest).
  • src/setup/README.md
    • Updated the setup wizard from 7 to 8 steps.
    • Revised Step 6 (Channel Configuration) to include registry channels.
    • Added a new Step 7 (Extensions) for tool installation from the registry.
    • Moved Background Tasks (heartbeat) to Step 8.
  • src/setup/mod.rs
    • Updated the total number of setup wizard steps to 8.
  • src/setup/wizard.rs
    • Adjusted SetupWizard to reflect the new 8-step process.
    • Modified step_channels to incorporate registry channels into the selection and installation process, prioritizing installed, then bundled, then registry channels.
    • Added a new asynchronous step_extensions function to handle tool selection and installation from the registry, including displaying auth hints.
    • Introduced helper functions build_channel_options, load_registry_catalog, install_selected_registry_channels, and discover_installed_tools to support registry integration.
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
Copy Markdown
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

The pull request introduces a robust extension registry system with catalog management and onboarding integration. The implementation of RegistryCatalog and RegistryInstaller provides a solid foundation for managing both tools and channels. However, there are some issues in the installation logic, particularly regarding the "install from artifact" flow which currently depends on the presence of the source tree to locate capabilities files. Additionally, the build process for WASM components could be improved to provide better user feedback and more reliable artifact detection.

Comment thread src/registry/installer.rs Outdated
Comment on lines +224 to +228
let caps_source = self
.repo_root
.join(&manifest.source.dir)
.join(&manifest.source.capabilities);
let target_caps =
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The install_from_artifact flow currently depends on the local presence of the source directory to copy the capabilities file. This prevents the use of pre-built artifacts in environments where the source code is not available (e.g., binary-only distributions). The capabilities should ideally be embedded in the manifest or provided via a separate URL to allow for truly standalone artifact installation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Acknowledged. Added a NOTE comment documenting this limitation. No pre-built artifacts exist yet; when artifact distribution is implemented, capabilities should be bundled with the artifact or fetched from a separate URL.

Comment thread src/registry/catalog.rs Outdated
Comment on lines +113 to +115
if path.extension().and_then(|e| e.to_str()) != Some("json") {
continue;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The loader should verify that the entry is a file before attempting to read it. If a directory exists with a .json extension, std::fs::read_to_string will fail, causing the entire catalog loading process to abort.

Suggested change
if path.extension().and_then(|e| e.to_str()) != Some("json") {
continue;
}
if !path.is_file() || path.extension().and_then(|e| e.to_str()) != Some("json") {
continue;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 61109a4. Added !path.is_file() guard before the extension check.

Comment thread src/registry/installer.rs Outdated
Comment on lines +189 to +192
.map_err(|e| RegistryError::ManifestRead {
path: PathBuf::from(url.as_str()),
reason: format!("download failed: {}", e),
})?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using PathBuf::from(url.as_str()) to populate the path field of RegistryError::ManifestRead is semantically incorrect as it contains a URL, not a filesystem path. This could lead to confusing error messages or issues in code that expects a valid local path. Consider adding a specific error variant for download failures or using a placeholder path.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 61109a4. Added DownloadFailed { url, reason } error variant that stores the URL as a String instead of stuffing it into a PathBuf.

Comment thread src/registry/installer.rs Outdated
Comment on lines +346 to +349
let output = Command::new("cargo")
.current_dir(source_dir)
.args(["component", "build", "--release"])
.output()?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using Command::output() captures all output and prevents it from being displayed in the terminal. For long-running builds, this makes the CLI appear unresponsive. Using Command::status() would allow the build progress to be visible to the user, providing a better experience.

    let status = Command::new("cargo")
        .current_dir(source_dir)
        .args(["component", "build", "--release"])
        .status()?;

    if !status.success() {
        anyhow::bail!("Build failed");
    }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 61109a4. Switched to tokio::process::Command with status() (inherited stdio) so build output streams to the terminal in real-time. Also fixed the companion Copilot comment about the same issue.

Comment thread src/registry/installer.rs Outdated
Comment on lines +368 to +374
if let Ok(entries) = std::fs::read_dir(&release_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("wasm") {
return Ok(path);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Iterating through the release directory and picking the first .wasm file found is unreliable, especially in workspace projects or those with multiple build targets. It is safer to look for the specific file named after the crate (e.g., crate_name.wasm) to ensure the correct artifact is installed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 61109a4. Now looks for the specific {crate_name}.wasm (with hyphens converted to underscores per Cargo convention) instead of picking the first .wasm file.

Copy link
Copy Markdown
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

Adds an “extension registry” concept (JSON manifests + Rust loader/installer) and wires it into both the CLI and the onboarding wizard so users can browse/install tools/channels more easily.

Changes:

  • Introduces src/registry/ with manifest types, a catalog for listing/searching/resolving bundles, and an installer for build-from-source / artifact-download flows.
  • Adds ironclaw registry ... CLI subcommands (list/info/install/install-defaults).
  • Updates onboarding wizard to include a new “Extensions” step and to discover/install registry-provided channels during channel setup.

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
src/setup/wizard.rs Adds Step 7 extensions install + registry-backed channel option building/installation.
src/setup/mod.rs Updates wizard step documentation to 8 steps.
src/setup/README.md Updates onboarding docs to reflect registry integration + new step order.
src/registry/mod.rs New registry module exports.
src/registry/manifest.rs Defines manifest/bundle serde structs and a conversion helper to extension registry entries.
src/registry/installer.rs Implements build/download install flows for registry entries.
src/registry/catalog.rs Loads registry manifests and bundles; provides list/search/resolve APIs.
src/main.rs Wires new registry subcommand into the main CLI dispatch.
src/lib.rs Exposes the new registry module from the library crate.
src/cli/registry.rs Implements ironclaw registry CLI functionality.
src/cli/mod.rs Adds the Registry command to the CLI and re-exports it.
registry/tools/telegram.json Adds tool manifest for Telegram tool.
registry/tools/slack.json Adds tool manifest for Slack tool.
registry/tools/okta.json Adds tool manifest for Okta tool.
registry/tools/google-slides.json Adds tool manifest for Google Slides tool.
registry/tools/google-sheets.json Adds tool manifest for Google Sheets tool.
registry/tools/google-drive.json Adds tool manifest for Google Drive tool.
registry/tools/google-docs.json Adds tool manifest for Google Docs tool.
registry/tools/google-calendar.json Adds tool manifest for Google Calendar tool.
registry/tools/gmail.json Adds tool manifest for Gmail tool.
registry/tools/github.json Adds tool manifest for GitHub tool.
registry/channels/whatsapp.json Adds channel manifest for WhatsApp channel.
registry/channels/telegram.json Adds channel manifest for Telegram channel.
registry/channels/slack.json Adds channel manifest for Slack channel.
registry/channels/discord.json Adds channel manifest for Discord channel.
registry/_bundles.json Adds bundle definitions (google/messaging/default).

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

Comment thread src/registry/installer.rs Outdated
Comment on lines +77 to +81
let target_wasm = target_dir.join(format!("{}.wasm", manifest.source.crate_name));

// Check if already exists
if target_wasm.exists() && !force {
return Err(RegistryError::ExtensionNotFound(format!(
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The installer names the installed WASM file using manifest.source.crate_name (e.g. slack-channel.wasm). The setup wizard and channel/tool loaders treat the file stem as the canonical extension identifier and expect it to match the user-facing manifest name (e.g. slack.wasm). This mismatch will cause newly installed registry extensions to be discovered/activated under a different name than the one the user selected. Consider installing as manifest.name (and use crate_name only for building), or add an explicit install_name field and use it consistently.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 61109a4. Installer now uses manifest.name for on-disk filenames ({name}.wasm, {name}.capabilities.json) so discovery, auth, and CLI commands all agree on the stem.

Comment thread src/registry/installer.rs
path: PathBuf::from(url.as_str()),
reason: format!("download failed: {}", e),
})?;

Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

install_from_artifact downloads with reqwest::get(url) but doesn't validate the HTTP status. A 404/500 body will be read and likely reported as a SHA mismatch, which is misleading. Call error_for_status() (or check status().is_success()) before reading .bytes() so failures report as download errors.

Suggested change
let response = response.error_for_status().map_err(|e| RegistryError::ManifestRead {
path: PathBuf::from(url.as_str()),
reason: format!("download failed: {}", e),
})?;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 61109a4. Added error_for_status() call before reading bytes, and all download errors now use the DownloadFailed variant.

Comment thread src/registry/installer.rs Outdated
Comment on lines +338 to +349
let check = Command::new("cargo")
.args(["component", "--version"])
.output();

if check.is_err() || !check.as_ref().map(|o| o.status.success()).unwrap_or(false) {
anyhow::bail!("cargo-component not found. Install with: cargo install cargo-component");
}

let output = Command::new("cargo")
.current_dir(source_dir)
.args(["component", "build", "--release"])
.output()?;
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

build_wasm_component() uses std::process::Command::output() and is called from async install paths. That blocks the Tokio worker thread for the duration of the build, which can stall other async tasks. Prefer tokio::process::Command or wrap the blocking build in tokio::task::spawn_blocking.

Suggested change
let check = Command::new("cargo")
.args(["component", "--version"])
.output();
if check.is_err() || !check.as_ref().map(|o| o.status.success()).unwrap_or(false) {
anyhow::bail!("cargo-component not found. Install with: cargo install cargo-component");
}
let output = Command::new("cargo")
.current_dir(source_dir)
.args(["component", "build", "--release"])
.output()?;
let check = tokio::task::block_in_place(|| {
Command::new("cargo")
.args(["component", "--version"])
.output()
});
if check.is_err() || !check.as_ref().map(|o| o.status.success()).unwrap_or(false) {
anyhow::bail!("cargo-component not found. Install with: cargo install cargo-component");
}
let output = tokio::task::block_in_place(|| {
Command::new("cargo")
.current_dir(source_dir)
.args(["component", "build", "--release"])
.output()
})?;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 61109a4 — same fix as the Gemini comment above. Uses tokio::process::Command::status() with inherited stdio.

Comment thread src/registry/catalog.rs Outdated
Comment on lines +168 to +172
// Try with kind prefix
if let Some(m) = self.manifests.get(&format!("tools/{}", name)) {
return Some(m);
}
if let Some(m) = self.manifests.get(&format!("channels/{}", name)) {
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

Bare-name lookup silently prefers tools/<name> over channels/<name>. This PR’s registry contains collisions (e.g. both a tool and channel named "slack"), so info/install slack will always target the tool and make the channel hard to address. Consider detecting collisions and forcing an explicit prefix (or returning an error) when both kinds exist.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 61109a4. get() now returns None when both tools/ and channels/ exist (ambiguous). Added get_strict() that returns an explicit AmbiguousName error with guidance to use the qualified key. resolve() uses get_strict() so CLI install/info commands surface the ambiguity.

Comment thread src/registry/installer.rs Outdated
// Copy capabilities file
let caps_source = source_dir.join(&manifest.source.capabilities);
let target_caps =
target_dir.join(format!("{}.capabilities.json", manifest.source.crate_name));
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The capabilities sidecar is installed as <crate_name>.capabilities.json. Channel discovery and ironclaw tool auth <name> both key off the installed name/file stem, so using crate_name here will break setup/auth if users select/reference manifest.name. Align the capabilities filename stem with the canonical installed extension name (preferably manifest.name).

Suggested change
target_dir.join(format!("{}.capabilities.json", manifest.source.crate_name));
target_dir.join(format!("{}.capabilities.json", manifest.name));

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 61109a4 — same fix, capabilities sidecar also uses manifest.name now.

Comment thread src/setup/wizard.rs Outdated
if auth.method.as_deref() != Some("none") && auth.method.is_some() {
let provider = auth.provider.as_deref().unwrap_or(&tool.name);
// Only mention unique providers (Google tools share auth)
let hint = format!(" {} - ironclaw tool auth {}", provider, tool.name);
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The auth hint prints ironclaw tool auth <tool.name>, but ironclaw tool auth looks for <name>.capabilities.json in the tools dir. If the installed stem differs (e.g. installed as crate_name), this instruction will fail. Use the canonical installed name in the command (or ensure the installer writes sidecars under tool.name).

Suggested change
let hint = format!(" {} - ironclaw tool auth {}", provider, tool.name);
let hint = format!(" {} - ironclaw tool auth {}", provider, outcome.name);

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 61109a4. The installer now uses manifest.name for on-disk filenames, so ironclaw tool auth <tool.name> correctly finds <name>.capabilities.json.

Comment thread src/setup/wizard.rs
Comment on lines +2322 to +2328
// Check if already on disk (may have been installed between bundled and here)
let wasm_on_disk = channels_dir.join(format!("{}.wasm", name)).exists()
|| channels_dir.join(format!("{}-channel.wasm", name)).exists();
if wasm_on_disk {
continue;
}

Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

This on-disk check only looks for <name>.wasm or <name>-channel.wasm, but the registry installer currently writes <crate_name>.wasm and <crate_name>.capabilities.json. That can lead to unnecessary rebuilds and (more importantly) a channel being installed under a different stem than the selected name, so it won't be discovered/enabled correctly. Align installer output naming with the selected channel name or update the checks to match.

Suggested change
// Check if already on disk (may have been installed between bundled and here)
let wasm_on_disk = channels_dir.join(format!("{}.wasm", name)).exists()
|| channels_dir.join(format!("{}-channel.wasm", name)).exists();
if wasm_on_disk {
continue;
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 61109a4. The installer uses manifest.name for on-disk filenames, and this code uses the qualified key channels/{name} for catalog lookup, so the naming is now consistent.

Comment thread src/registry/installer.rs Outdated
Comment on lines +81 to +85
return Err(RegistryError::ExtensionNotFound(format!(
"'{}' already installed at {}. Use --force to overwrite.",
manifest.name,
target_wasm.display()
)));
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The already-installed check returns RegistryError::ExtensionNotFound, which will surface as "Extension not found" even though the extension exists. Add a dedicated error variant (e.g. AlreadyInstalled) or use a more appropriate one so CLI/wizard messaging stays accurate.

Suggested change
return Err(RegistryError::ExtensionNotFound(format!(
"'{}' already installed at {}. Use --force to overwrite.",
manifest.name,
target_wasm.display()
)));
return Err(RegistryError::ManifestRead {
path: target_wasm.clone(),
reason: format!(
"'{}' already installed at {}. Use --force to overwrite.",
manifest.name,
target_wasm.display()
),
});

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 61109a4. Added dedicated AlreadyInstalled { name, path } error variant that produces a clear message.

Comment thread src/cli/registry.rs Outdated
if auth.method.as_deref() != Some("none") {
println!(
"\nNext step: authenticate with `ironclaw tool auth {}`",
manifest.name
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The post-install message suggests ironclaw tool auth <manifest.name>, but tool auth expects the argument to match the installed file stem (<name>.capabilities.json). If installs use source.crate_name for filenames, this instruction will be wrong for most tools (e.g. slack vs slack-tool). Ensure the auth guidance uses the canonical installed name, or change installation to write files under manifest.name.

Suggested change
manifest.name
outcome.name

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This was already correct — manifest.name matches the installed filename since 61109a4 switched the installer to use manifest.name for on-disk files.

Comment thread src/registry/manifest.rs
Comment on lines +150 to +152
let source = ExtensionSource::WasmBuildable {
repo_url: self.source.dir.clone(),
build_dir: Some(self.source.dir.clone()),
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

to_registry_entry() emits ExtensionSource::WasmBuildable { repo_url: ... }, but the current in-chat ExtensionManager install flow for WASM tools only supports ExtensionSource::WasmDownload and otherwise errors with "WASM tool entry has no download URL". As written, registry manifests converted via this helper won't be installable through the extension system. Either implement buildable installs in ExtensionManager or change the emitted ExtensionSource to something the manager supports.

Suggested change
let source = ExtensionSource::WasmBuildable {
repo_url: self.source.dir.clone(),
build_dir: Some(self.source.dir.clone()),
let source = ExtensionSource::WasmDownload {
download_url: self.source.dir.clone(),

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is intentional — to_registry_entry() is a compatibility bridge for the in-chat extension discovery UI, not the primary install path. The registry installer handles installs directly via install_from_source(). WasmBuildable correctly represents the source type. When ExtensionManager gains build support, this will work automatically.

Copilot AI review requested due to automatic review settings February 19, 2026 23:10
Copy link
Copy Markdown
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

Copilot reviewed 42 out of 42 changed files in this pull request and generated 3 comments.


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

Comment thread src/setup/wizard.rs
Comment on lines +221 to +231
self.test_database_connection_postgres(&url).await?;
self.settings.database_backend = Some("postgres".to_string());
self.settings.database_url = Some(url);

// Load existing settings from DB.
if let Some(ref pool) = self.db_pool {
let store = crate::history::Store::from_pool(pool.clone());
if let Ok(map) = store.get_all_settings("default").await {
self.settings = Settings::from_db_map(&map);
self.settings.database_backend = Some("postgres".to_string());
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

In reconnect_postgres, the database_url is set on line 223 but then gets overwritten when Settings::from_db_map is called on line 229. After loading from DB, only database_backend is restored (line 230), but database_url is not. This means the wizard will lose the database URL after loading settings from DB. The same issue exists in reconnect_libsql where libsql_path and libsql_url are set before loading from DB but not restored after. Fix by either: (1) setting these fields AFTER loading from DB, or (2) explicitly restoring them after line 230/262 similar to how database_backend is restored.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 572ac8c. reconnect_postgres() now restores database_url after Settings::from_db_map() overwrites self.settings.

Comment thread src/setup/wizard.rs
Comment on lines +248 to +263
self.test_database_connection_libsql(&path, turso_url.as_deref(), turso_token.as_deref())
.await?;

self.settings.database_backend = Some("libsql".to_string());
self.settings.libsql_path = Some(path);
if let Some(url) = turso_url {
self.settings.libsql_url = Some(url);
}

// Load existing settings from DB.
if let Some(ref db) = self.db_backend {
use crate::db::SettingsStore as _;
if let Ok(map) = db.get_all_settings("default").await {
self.settings = Settings::from_db_map(&map);
self.settings.database_backend = Some("libsql".to_string());
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

In reconnect_libsql, the libsql_path and libsql_url fields are set on lines 251-254 but then get overwritten when Settings::from_db_map is called on line 261. After loading from DB, only database_backend is restored (line 262), but the libsql path and URL are not. This means the wizard will lose these connection details after loading settings from DB. Fix by either: (1) setting these fields AFTER loading from DB, or (2) explicitly restoring them after line 262 similar to how database_backend is restored.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 572ac8c. reconnect_libsql() now restores libsql_path and libsql_url after Settings::from_db_map() overwrites self.settings.

Comment thread src/cli/registry.rs Outdated
Comment on lines +189 to +191
let manifest = catalog
.get(name)
.ok_or_else(|| anyhow::anyhow!("Extension '{}' not found in registry.", name))?;
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The cmd_info function uses catalog.get(name) which returns None for ambiguous bare names (e.g., "slack" matches both tools/slack and channels/slack). This produces a generic "Extension 'slack' not found" error even though the extension exists in both categories. Users must use qualified names like "tools/slack". Consider using catalog.get_strict(name) instead, which returns an explicit AmbiguousName error that guides users to use a qualified name.

Suggested change
let manifest = catalog
.get(name)
.ok_or_else(|| anyhow::anyhow!("Extension '{}' not found in registry.", name))?;
let manifest = catalog.get_strict(name)?;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 572ac8c. cmd_info now uses catalog.get_strict(name) which returns an explicit AmbiguousName error with guidance to use the qualified key (tools/slack or channels/slack).

Copilot AI review requested due to automatic review settings February 20, 2026 00:38
Copy link
Copy Markdown
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

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


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

Comment thread src/cli/registry.rs Outdated
Comment on lines +69 to +71
RegistryCommand::InstallDefaults { force } => {
cmd_install(&catalog, &registry_dir, "default", force, true).await
}
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

InstallDefaults currently calls cmd_install(..., prefer_build = true), forcing builds from source even when pre-built artifacts are available. This is surprising given registry install <name|bundle> defaults to downloading unless --build is passed, and it makes install-defaults potentially much slower. Consider defaulting prefer_build to false here (or adding a --build flag to InstallDefaults and passing it through).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in eb52b18. InstallDefaults now defaults to prefer_build = false (download artifacts) and has a --build flag for parity with install.

Comment thread src/cli/registry.rs Outdated
Comment on lines +92 to +100
// Also try one level up (for target/release/ironclaw)
if let Some(grandparent) = parent.parent() {
let candidate = grandparent.join("registry");
if candidate.is_dir() {
return Ok(candidate);
}
}
}

Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

find_registry_dir() claims to support finding registry/ relative to an executable in target/{debug,release}/ironclaw, but it only checks the executable’s parent and grandparent directories. For .../target/release/ironclaw, the registry is typically at the repo root (great-grandparent), so this lookup will fail when running the binary from outside the repo root. Consider also checking one more ancestor (and/or adding the same CARGO_MANIFEST_DIR fallback used by onboarding’s load_registry_catalog()).

Suggested change
// Also try one level up (for target/release/ironclaw)
if let Some(grandparent) = parent.parent() {
let candidate = grandparent.join("registry");
if candidate.is_dir() {
return Ok(candidate);
}
}
}
// Also try one level up (for target/release/ironclaw under target/)
if let Some(grandparent) = parent.parent() {
let candidate = grandparent.join("registry");
if candidate.is_dir() {
return Ok(candidate);
}
// And one more level up (for target/{debug,release}/ironclaw)
if let Some(great_grandparent) = grandparent.parent() {
let candidate = great_grandparent.join("registry");
if candidate.is_dir() {
return Ok(candidate);
}
}
}
}
// Fallback: use Cargo manifest directory if available (e.g., when run via `cargo run`)
if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
let candidate = PathBuf::from(manifest_dir).join("registry");
if candidate.is_dir() {
return Ok(candidate);
}
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in eb52b18. find_registry_dir() now walks up 3 ancestor levels (covers target/release/ironclaw → repo root) and adds a CARGO_MANIFEST_DIR compile-time fallback, matching the onboarding's load_registry_catalog() logic.

ilblackdragon and others added 6 commits February 19, 2026 17:12
…ng integration

Adds a central registry that catalogs all 14 available extensions (10 tools,
4 channels) with their capabilities, auth requirements, and artifact references.
The onboarding wizard now shows installable channels from the registry and
offers tool installation as a new Step 7.

- registry/ folder with per-extension JSON manifests and bundle definitions
- src/registry/ module: manifest structs, catalog loader, installer
- `ironclaw registry list|info|install|install-defaults` CLI commands
- Setup wizard enhanced: channels from registry, new extensions step (8 steps)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…y onboarding

Tool crates in tools-src/ and channels-src/ failed `cargo metadata` during
onboard install because Cargo resolved them as part of the root workspace.
Add `[workspace]` table to each standalone crate and extend the root
`workspace.exclude` list so they build independently.

Channels-only mode (`onboard --channels-only`) failed with "Secrets not
configured" and "No database connection" because it skipped database and
security setup. Add `reconnect_existing_db()` to establish the DB connection
and load saved settings before running channel configuration.

Also improve the tunnel "already configured" display to show full provider
details (domain, mode, command) instead of just the provider name.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use manifest.name (not crate_name) for installed filenames so
  discovery, auth, and CLI commands all agree on the stem (#1)
- Add AlreadyInstalled error variant instead of misleading
  ExtensionNotFound (#2)
- Add DownloadFailed error variant with URL context instead of
  stuffing URLs into PathBuf (#3)
- Validate HTTP status with error_for_status() before reading
  response bytes in artifact downloads (#4)
- Switch build_wasm_component to tokio::process::Command with
  status() so build output streams to the terminal (#6)
- Find WASM artifact by crate_name specifically instead of picking
  the first .wasm file in the release directory (#7)
- Add is_file() guard in catalog loader to skip directories (#8)
- Detect ambiguous bare-name lookups when both tools/<name> and
  channels/<name> exist, with get_strict() returning an error (#9)
- Fix wizard step_extensions to check tool.name for installed
  detection, consistent with the new naming (#11, #12)
- Fix redundant closures and map_or clippy warnings in changed files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
reconnect_postgres() and reconnect_libsql() called Settings::from_db_map()
which overwrote database_url / libsql_path / libsql_url set from env vars.
Also use get_strict() in cmd_info to surface ambiguous bare-name errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Collapse nested if-let chains and inline string literals in format
macros to satisfy CI clippy lint checks (deny warnings).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…lookup

- InstallDefaults now defaults to downloading pre-built artifacts
  (matching `registry install` behavior), with --build flag for source builds.
- find_registry_dir() walks up 3 ancestor levels from the exe and adds
  a CARGO_MANIFEST_DIR fallback, matching load_registry_catalog() logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ilblackdragon ilblackdragon force-pushed the feat/extension-registry branch from af994f8 to eb52b18 Compare February 20, 2026 01:15
@ilblackdragon ilblackdragon merged commit 97a7637 into main Feb 20, 2026
2 checks passed
@ilblackdragon ilblackdragon deleted the feat/extension-registry branch February 20, 2026 01:17
This was referenced Feb 20, 2026
jaswinder6991 pushed a commit to jaswinder6991/ironclaw that referenced this pull request Feb 26, 2026
…tion (nearai#238)

* feat: add extension registry with metadata catalog, CLI, and onboarding integration

Adds a central registry that catalogs all 14 available extensions (10 tools,
4 channels) with their capabilities, auth requirements, and artifact references.
The onboarding wizard now shows installable channels from the registry and
offers tool installation as a new Step 7.

- registry/ folder with per-extension JSON manifests and bundle definitions
- src/registry/ module: manifest structs, catalog loader, installer
- `ironclaw registry list|info|install|install-defaults` CLI commands
- Setup wizard enhanced: channels from registry, new extensions step (8 steps)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(setup): resolve workspace errors for tool crates and channels-only onboarding

Tool crates in tools-src/ and channels-src/ failed `cargo metadata` during
onboard install because Cargo resolved them as part of the root workspace.
Add `[workspace]` table to each standalone crate and extend the root
`workspace.exclude` list so they build independently.

Channels-only mode (`onboard --channels-only`) failed with "Secrets not
configured" and "No database connection" because it skipped database and
security setup. Add `reconnect_existing_db()` to establish the DB connection
and load saved settings before running channel configuration.

Also improve the tunnel "already configured" display to show full provider
details (domain, mode, command) instead of just the provider name.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(registry): address PR review feedback on installer and catalog

- Use manifest.name (not crate_name) for installed filenames so
  discovery, auth, and CLI commands all agree on the stem (nearai#1)
- Add AlreadyInstalled error variant instead of misleading
  ExtensionNotFound (nearai#2)
- Add DownloadFailed error variant with URL context instead of
  stuffing URLs into PathBuf (nearai#3)
- Validate HTTP status with error_for_status() before reading
  response bytes in artifact downloads (nearai#4)
- Switch build_wasm_component to tokio::process::Command with
  status() so build output streams to the terminal (nearai#6)
- Find WASM artifact by crate_name specifically instead of picking
  the first .wasm file in the release directory (nearai#7)
- Add is_file() guard in catalog loader to skip directories (nearai#8)
- Detect ambiguous bare-name lookups when both tools/<name> and
  channels/<name> exist, with get_strict() returning an error (nearai#9)
- Fix wizard step_extensions to check tool.name for installed
  detection, consistent with the new naming (nearai#11, nearai#12)
- Fix redundant closures and map_or clippy warnings in changed files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(setup): restore DB connection fields after settings reload

reconnect_postgres() and reconnect_libsql() called Settings::from_db_map()
which overwrote database_url / libsql_path / libsql_url set from env vars.
Also use get_strict() in cmd_info to surface ambiguous bare-name errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: fix clippy collapsible_if and print_literal warnings

Collapse nested if-let chains and inline string literals in format
macros to satisfy CI clippy lint checks (deny warnings).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(registry): prefer artifacts for install-defaults and improve dir lookup

- InstallDefaults now defaults to downloading pre-built artifacts
  (matching `registry install` behavior), with --build flag for source builds.
- find_registry_dir() walks up 3 ancestor levels from the exe and adds
  a CARGO_MANIFEST_DIR fallback, matching load_registry_catalog() logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
bkutasi pushed a commit to bkutasi/ironclaw that referenced this pull request Mar 28, 2026
…tion (nearai#238)

* feat: add extension registry with metadata catalog, CLI, and onboarding integration

Adds a central registry that catalogs all 14 available extensions (10 tools,
4 channels) with their capabilities, auth requirements, and artifact references.
The onboarding wizard now shows installable channels from the registry and
offers tool installation as a new Step 7.

- registry/ folder with per-extension JSON manifests and bundle definitions
- src/registry/ module: manifest structs, catalog loader, installer
- `ironclaw registry list|info|install|install-defaults` CLI commands
- Setup wizard enhanced: channels from registry, new extensions step (8 steps)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(setup): resolve workspace errors for tool crates and channels-only onboarding

Tool crates in tools-src/ and channels-src/ failed `cargo metadata` during
onboard install because Cargo resolved them as part of the root workspace.
Add `[workspace]` table to each standalone crate and extend the root
`workspace.exclude` list so they build independently.

Channels-only mode (`onboard --channels-only`) failed with "Secrets not
configured" and "No database connection" because it skipped database and
security setup. Add `reconnect_existing_db()` to establish the DB connection
and load saved settings before running channel configuration.

Also improve the tunnel "already configured" display to show full provider
details (domain, mode, command) instead of just the provider name.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(registry): address PR review feedback on installer and catalog

- Use manifest.name (not crate_name) for installed filenames so
  discovery, auth, and CLI commands all agree on the stem (nearai#1)
- Add AlreadyInstalled error variant instead of misleading
  ExtensionNotFound (nearai#2)
- Add DownloadFailed error variant with URL context instead of
  stuffing URLs into PathBuf (nearai#3)
- Validate HTTP status with error_for_status() before reading
  response bytes in artifact downloads (nearai#4)
- Switch build_wasm_component to tokio::process::Command with
  status() so build output streams to the terminal (nearai#6)
- Find WASM artifact by crate_name specifically instead of picking
  the first .wasm file in the release directory (nearai#7)
- Add is_file() guard in catalog loader to skip directories (nearai#8)
- Detect ambiguous bare-name lookups when both tools/<name> and
  channels/<name> exist, with get_strict() returning an error (nearai#9)
- Fix wizard step_extensions to check tool.name for installed
  detection, consistent with the new naming (nearai#11, nearai#12)
- Fix redundant closures and map_or clippy warnings in changed files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(setup): restore DB connection fields after settings reload

reconnect_postgres() and reconnect_libsql() called Settings::from_db_map()
which overwrote database_url / libsql_path / libsql_url set from env vars.
Also use get_strict() in cmd_info to surface ambiguous bare-name errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: fix clippy collapsible_if and print_literal warnings

Collapse nested if-let chains and inline string literals in format
macros to satisfy CI clippy lint checks (deny warnings).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(registry): prefer artifacts for install-defaults and improve dir lookup

- InstallDefaults now defaults to downloading pre-built artifacts
  (matching `registry install` behavior), with --build flag for source builds.
- find_registry_dir() walks up 3 ancestor levels from the exe and adds
  a CARGO_MANIFEST_DIR fallback, matching load_registry_catalog() logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants