Skip to content

feat(ironhub): install tools and skills from IronHub (CLI, agent tools, gateway, live catalog)#3737

Open
neo-sky wants to merge 7 commits into
nearai:stagingfrom
neo-sky:feat/tool-registry-installer
Open

feat(ironhub): install tools and skills from IronHub (CLI, agent tools, gateway, live catalog)#3737
neo-sky wants to merge 7 commits into
nearai:stagingfrom
neo-sky:feat/tool-registry-installer

Conversation

@neo-sky
Copy link
Copy Markdown

@neo-sky neo-sky commented May 17, 2026

Summary

  • Adds the ability to install tools and skills from IronHub by name, both from the CLI (ironclaw ironhub install/search/list/info) and as agent-callable tools, so a running agent can install at runtime instead of only at build time.
  • Adds gateway HTTP endpoints and an HMAC-verified deep-link receiver so a hosted agent (e.g. agent.near.ai) can install from a hub "Install to Agent" link, with per-user signing keys managed in agent Settings.
  • Points the default catalog at a unified manifest (the existing GitHub-release tools.json plus the live moderated Iliad catalog) with a per-entry provenance tier; the pinned --release-tag path is preserved as a GitHub escape hatch.
  • Every artifact still passes the existing SHA-256 verify-before-write and a closed host allowlist; community content is labelled and gated, never silently trusted.

Change Type

  • New feature
  • Security

Linked Issue

None. This is net-new work brought to the team directly; happy to file a tracking issue if the team prefers one on record.

Validation

  • cargo fmt --all -- --check
  • cargo clippy --all --benches --tests --examples --all-features -- -D warnings (zero warnings)
  • cargo build / cargo check --no-default-features --features libsql
  • Relevant tests pass: full cargo test --lib (5710 passing), including the IronHub installer, provenance, host-allowlist, dispatch, and gateway handler suites
  • cargo test --features integration — N/A; no database-backed or integration behaviour changed on this side
  • Manual testing: full local end-to-end smoke test of the pipeline against live Iliad — manifest transform, provenance mapping, artifact funnel, and a real ironclaw binary fetching, SHA-256 verifying, and installing through the genuine host allowlist and TLS path

Security Impact

Yes. New outbound fetches (catalog manifest + artifacts) are constrained to a closed host allowlist (hub.ironclaw.com plus GitHub release infrastructure), HTTPS-only, IP literals rejected. Every artifact is SHA-256-verified before write. Agent-callable install tools use strict parameter schemas (regex + additionalProperties:false), per-tool rate limiting, and an approval gate; unverified community content requires an explicit acknowledgement. The deep-link path is HMAC-verified with a short replay window; signing keys are per-user in the secrets store. No credential is exposed to tool processes.

Database Impact

None. No migrations or schema changes on the ironclaw side.

Blast Radius

Registry installer, tool registry/dispatch, and the gateway web handlers + embedded SPA. The feature is additive (new tools/endpoints/surface). The one behavioural change is the default IronHub manifest URL; the pinned-release path is unchanged.

Rollback Plan

Revert the branch. The change is additive with no migrations and no persisted state, so reverting removes the new tools, endpoints, and the default-URL change cleanly with no data cleanup.

Review Follow-Through

The HMAC deep-link receiver is complete and tested on this side; the matching IronHub-side "Install to Agent" sender and profile key field is a separate follow-up in the IronHub repo and not part of this PR. Iliad-sourced entries are intentionally surfaced as verified (not official); official is reserved for GitHub-release artifacts. Happy to adjust scope or split commits if that helps review.


Review track: C (security/runtime)

neo-sky added 6 commits May 14, 2026 13:18
Adds a name-based install path that resolves a tool or skill against the
IronHub release manifest at github.com/nearai/ironhub/releases, downloads
the wasm + capabilities or SKILL.md, verifies the published SHA-256, and
writes into ~/.ironclaw/{tools,skills}/. Local-path install keeps working
for dev workflows. Manifest entry names, release tags, and post-write
artifact sizes are all validated before trusting any value used as a
filesystem path or URL component, so a poisoned manifest cannot escape
the install tree.
Renames the IronHub install surface from `ironclaw tool install <name>` (with path/hub autodetection) to a dedicated `ironclaw ironhub *` subcommand tree, mirroring the platform's brand identity. Adds four agent-callable builtin tools (ironhub_install/search/list/info) sharing the same HubInstaller code so the hosted agent at agent.near.ai can drive installs via chat. Installed tools hot-load into the live ToolRegistry via ExtensionManager::ensure_extension_ready, skills via SkillRegistry::reload, both visible on the next agent turn without restart.
Tightens the four tool schemas with regex patterns, additionalProperties: false, and length caps so the dispatcher rejects LLM injection before execute. Adds rate_limit_config (6/min, 30/hour) to ironhub_install. Pins five dispatch-level injection tests plus a regression test asserting IronHub skills land in the Installed bucket, not the Trusted user_dir.
Wires the four IronHub agent tools to gateway HTTP routes so a web UI can call install/search/list/info via REST. Install requires AdminUser (matches AdminUser docstring guidance for extension/skill installation); read-only routes accept any AuthenticatedUser. All routes dispatch through ToolDispatcher so the schema regex, additionalProperties:false, rate limit, and audit ActionRecord all fire identically to chat-driven installs. DTOs use serde(deny_unknown_fields) so the axum extractor rejects unknown params before they bypass the dispatcher gate. Nine handler tests cover auth-required, admin-required, schema injection (path traversal, unknown field), and happy-path dispatch via a stub tool.
Per-user HMAC signing keys make hub install links verifiable by the agent. Adds signing-key CRUD + verify-intent endpoints, a #/install/<tool> deep-link confirm flow, and a Settings card to manage the key. Hub-side button is a separate nearai/ironhub PR.
Points the default manifest at the hub's merged catalog endpoint and tags every entry with a provenance tier, gating unverified community installs behind an explicit ack. Adds ironhub_remove and keeps the pinned release-tag path as a GitHub escape hatch. SHA-256 verify and the closed host allowlist are unchanged, with hub.ironclaw.com the only added host.
@github-actions github-actions Bot added scope: channel/cli TUI / CLI channel scope: channel/web Web gateway channel scope: tool Tool infrastructure scope: tool/builtin Built-in tools size: XL 500+ changed lines risk: medium Business logic, config, or moderate-risk modules contributor: new First-time contributor labels May 17, 2026
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

This pull request introduces the IronHub integration, enabling users to browse and install WASM tools and skills directly from the IronHub catalog via both the CLI and a new web-based interface. The implementation includes new API handlers for installation, search, and signing key management, alongside a dedicated installer registry and frontend components. Feedback highlights several critical areas for improvement: a potential Denial of Service vulnerability in artifact downloading due to missing size limits, the need for atomic file operations during tool installation to prevent inconsistent states, and logic inconsistencies in name validation across different modules. Additionally, the review identifies issues with error handling during skill registry reloads and misleading error mapping in the extension manager.

Comment thread src/registry/installer.rs Outdated

/// Download an artifact from a URL.
async fn download_artifact(url: &str) -> Result<bytes::Bytes, RegistryError> {
pub(crate) async fn download_artifact(url: &str) -> Result<bytes::Bytes, RegistryError> {
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.

security-high high

The download_artifact function downloads the entire response body into memory without a size limit. This can be exploited as a Denial of Service (DoS) vector. Consider adding a max_size parameter (using u64 to ensure infallible conversions from usize on all platforms) and using response.bytes_stream() with a limit to prevent unbounded memory consumption.

References
  1. Prefer using u64 for length prefixes and size limits to ensure that conversions from usize are infallible, avoiding panics and potential DoS vectors.

Comment thread src/registry/hub_installer.rs Outdated
Comment on lines +248 to +255
fs::write(&target_wasm, wasm_bytes)
.await
.map_err(RegistryError::Io)?;
confirm_written_size(&target_wasm, wasm_bytes.len()).await?;
fs::write(&target_caps, caps_bytes)
.await
.map_err(RegistryError::Io)?;
confirm_written_size(&target_caps, caps_bytes.len()).await?;
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 installation of WASM tools is not atomic. If writing the capabilities file fails after the WASM file has been written, the system is left in an inconsistent state. Consider writing to temporary files and using tokio::fs::rename to ensure both files are updated atomically, adhering to async I/O practices. If implementing a cleanup mechanism on failure, ensure any secondary errors are logged as warnings so the primary error is preserved and returned.

References
  1. In async functions, use asynchronous I/O operations like tokio::fs to avoid blocking the runtime.
  2. When a primary operation fails and a subsequent cleanup fails, log the secondary failure as a warning and return the original primary error.

Comment thread src/cli/hub_install.rs Outdated
if !(first.is_ascii_lowercase() || first.is_ascii_digit()) {
return false;
}
chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
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

There is an inconsistency between this validation and validate_hub_artifact_name in src/registry/hub_installer.rs. This function only allows hyphens, while the registry validation also allows underscores. Aligning these prevents unexpected rejections. Additionally, validation at this public API boundary must strictly check for path traversal characters (/, , .., \0) to prevent security vulnerabilities.

References
  1. Path traversal characters in extension names must be validated at the public API boundary for all operations to prevent security vulnerabilities.

);
poison.into_inner()
});
handle.block_on(guard.reload());
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 result of guard.reload() is ignored. If the skill registry fails to reload after a new skill is installed, the tool will still report success, but the new skill won't be available to the agent. The return value of block_on should be captured and handled.

Comment thread src/tools/builtin/ironhub.rs Outdated
.extension_manager
.list(Some(ExtensionKind::WasmTool), false, &ctx.user_id)
.await
.map_err(|_| catalog_unavailable())?
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 error mapping catalog_unavailable() is misleading here. extension_manager.list is a local operation on the installed extensions; its failure indicates an internal system error or a problem with the local storage, not an issue with the remote IronHub catalog.

Comment thread src/channels/web/handlers/ironhub.rs Outdated
Comment on lines +196 to +205
if store
.exists(&user.user_id, IRONHUB_SIGNING_KEY_NAME)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
{
store
.delete(&user.user_id, IRONHUB_SIGNING_KEY_NAME)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
}
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 signing key regeneration process is not atomic. If the delete operation succeeds but the subsequent create operation fails, the user will be left without any signing key. While not critical, it forces the user to manually retry. Consider if the SecretsStore supports an atomic replacement or ensure the old key is preserved until the new one is successfully created.

…dator parity)

Stream artifact downloads with a hard per-call size cap so an oversized response can't exhaust memory, clean up the wasm if the capabilities write fails, align the by-name validator with the registry's underscore charset, preserve the previous signing key if regeneration fails, and use a local error class when the remove read-back can't reach the extension store. Adds regression tests for the size cap, partial-artifact cleanup, and validator parity.
@neo-sky
Copy link
Copy Markdown
Author

neo-sky commented May 18, 2026

Thanks for the review. Addressed in 9d6ca674:

  • download_artifact now streams with a hard per-call size cap (content-length early-reject plus per-chunk enforcement); every caller passes its existing limit. (security-high)
  • Tool install cleans up the wasm if the capabilities write fails; a secondary cleanup failure is logged as a warning and the original error is returned.
  • looks_like_hub_name now accepts _ so the by-name path matches validate_hub_artifact_name.
  • Signing-key regeneration captures and best-effort restores the previous key if create fails.
  • The remove read-back uses a local error class instead of the catalog-unavailable message.

On the reload() comment: SkillRegistry::reload(&mut self) -> Vec<String> returns the rediscovered skill names, not a Result, so there is no error path to capture. The reload runs after the artifact is written and SHA-256-verified. Left as-is; happy to add a debug log of the reloaded count if that is preferred.

fmt and clippy (all features) are clean and the targeted tests pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: new First-time contributor risk: medium Business logic, config, or moderate-risk modules scope: channel/cli TUI / CLI channel scope: channel/web Web gateway channel scope: tool/builtin Built-in tools scope: tool Tool infrastructure size: XL 500+ changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant