mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-30 08:17:53 +00:00
* feat: Move debug log truncation from agent loop to REPL channel Full tool output now flows through StatusUpdate so the web gateway gets untruncated content. The REPL channel truncates at display time (200 chars for tool results, thinking, and status messages). Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: Flatten WASM tool schemas and fix host HTTP runtime contention LLMs can't reliably follow oneOf + const discriminator patterns in JSON Schema, causing tools like Google Calendar to receive malformed params (e.g., {"operation":"list_events","data":{"calendarId":"primary"}} instead of {"action":"list_events","calendar_id":"primary"}). Replace all 9 WASM tool schemas with flat action enum + top-level properties. The serde #[serde(tag = "action")] deserialization works identically. Also fixes WASM host HTTP requests (channels and tools) stalling during startup by replacing Handle::current().block_on() with a dedicated single-threaded runtime per request, avoiding I/O driver contention. Reduces verbose LLM debug logging (full request/response payloads) and changes tower_http default from debug to warn. Co-Authored-By: Claude Opus 4.6 <[email protected]> * feat: Built-in OAuth credentials and combined Google scopes Add infrastructure for shipping default OAuth credentials with the binary, similar to how gcloud/rclone bake in their client_id. Credentials are set at compile time via IRONCLAW_GOOGLE_CLIENT_ID / IRONCLAW_GOOGLE_CLIENT_SECRET env vars, or can be hardcoded in src/cli/oauth_defaults.rs. The fallback chain is: capabilities file > runtime env var > built-in defaults. Also, when authing any Google tool, scopes from ALL installed Google tools are now combined into a single OAuth request (they all share the same google_oauth_token secret). One login covers Gmail, Calendar, Drive, etc. Co-Authored-By: Claude Opus 4.6 <[email protected]> * feat: Ship default Google OAuth credentials for zero-config auth Google Desktop App credentials are not secret (per Google's own docs). Hardcode them so `ironclaw tool auth <google-tool>` works out of the box without requiring users to register their own OAuth app. Credentials can still be overridden at compile time (IRONCLAW_GOOGLE_CLIENT_ID) or runtime (GOOGLE_OAUTH_CLIENT_ID). Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: Consistent OAuth callback port and polished landing page - Use fixed port 9876 instead of scanning 9876-9886 (one redirect URI to register in provider OAuth apps, deterministic behavior) - Replace broken unicode checkmark with SVG icons (charset was missing, rendered as mojibake) - Dark themed landing page with proper card layout for both success and error states - Add charset=utf-8 to Content-Type headers Co-Authored-By: Claude Opus 4.6 <[email protected]> * refactor: Unify OAuth callback server across all auth flows All three OAuth flows (WASM tool auth, MCP server auth, NEAR AI login) now share the same code from cli::oauth_defaults: - Fixed port 9876 (one redirect URI to register per provider) - Shared landing page HTML (dark card with SVG icons, proper charset) - Parameterized wait_for_callback(listener, path, param, display_name) Removes ~120 lines of duplicated callback/HTML code. Co-Authored-By: Claude Opus 4.6 <[email protected]> * Support for oauth token refresh * refactor: Replace bootstrap.json with ~/.ironclaw/.env for DATABASE_URL Kill the 4-field BootstrapConfig JSON file. Only DATABASE_URL actually needs disk persistence (chicken-and-egg before DB connect). The other three fields are now derived: pool_size defaults to 10 via env var, secrets master key is auto-detected (env then keychain probe), and onboard_completed is inferred from DATABASE_URL presence. The new format is a standard .env file loaded via dotenvy early in main, so DATABASE_URL is available as a regular env var everywhere. Handles three upgrade paths: - Clean start: wizard writes .env, reload after wizard completes - Returning user: .env loaded at startup, business as usual - Legacy upgrade: bootstrap.json auto-migrated to .env on first run Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: Address PR review findings - Fix UTF-8 panic in truncate_for_preview (byte-slice on char boundary) - Cap WASM guest timeout_ms at 5 minutes to prevent resource exhaustion - Fix localhost detection in requires_auth() to avoid substring matches (e.g. "notlocalhost.com" no longer matches) - Fix query param injection to insert before URL fragment - Fix extract_host_from_url for IPv6 bracket notation - Remove misleading schema defaults: Slack limit, Slides insertion_index, Docs index (per-action defaults documented in descriptions instead) Co-Authored-By: Claude Opus 4.6 <[email protected]> * style: Fix cargo fmt formatting Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: IPv6 loopback support for OAuth listener and localhost detection - bind_callback_listener: try [::1] first, fall back to 127.0.0.1, so OAuth redirects work on systems where localhost resolves to ::1 - is_localhost_url: replace manual string parsing with url::Url for correct handling of IPv6 brackets, ports, userinfo, etc. - Add url crate as direct dependency (already a transitive dep) Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: Address PR review feedback on runtime reuse, onboard check, and OAuth binding - Remove session file check from check_onboard_needed(); DATABASE_URL is sufficient - Detect AddrInUse on IPv6 bind and fail immediately instead of falling through to IPv4 - Reuse dedicated tokio runtime across HTTP calls in both tool and channel WASM wrappers Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: HTML-escape provider name in OAuth landing page, simplify Slack limit description - Add html_escape() to prevent XSS in landing_html() where provider_name was interpolated directly into HTML (defense-in-depth, source is trusted but escaping costs nothing) - Remove per-action default numbers from Slack limit field description to avoid confusing LLMs with conflicting defaults Addresses review feedback from zmanian on PR #42. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: Save all bootstrap fields from wizard, fix config module comment - Wizard now saves secrets_master_key_source and database_pool_size to bootstrap.json (was only saving database_url and onboard_completed, which broke secrets after fresh onboard since SecretsConfig::resolve reads key source from bootstrap) - Update config.rs module doc to reflect bootstrap.json priority chain instead of the removed ~/.ironclaw/.env approach Co-Authored-By: Claude Opus 4.6 <[email protected]> * refactor: Replace BootstrapConfig with .env-based bootstrap DATABASE_URL is the only setting that needs disk persistence before the database is available. Instead of a custom bootstrap.json with 4 fields, use a standard ~/.ironclaw/.env file loaded via dotenvy. - Remove BootstrapConfig struct entirely - Restore ironclaw_env_path(), load_ironclaw_env(), save_database_url() - SecretsConfig::resolve() now auto-detects (env var then keychain probe) instead of reading a saved source from bootstrap.json - DatabaseConfig::resolve() reads DATABASE_URL from env only (dotenvy loads ~/.ironclaw/.env into the environment early in startup) - check_onboard_needed() is now sync (just checks env vars) - Wizard save_and_summarize() works for both postgres and libsql backends - One-time migration from bootstrap.json to .env preserved Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: Ensure load_ironclaw_env() runs in all Config paths, fix .env priority - Config::from_env() and Config::from_db() now call load_ironclaw_env() internally (after dotenvy::dotenv()), so CLI commands like `memory` and `config` correctly load DATABASE_URL from ~/.ironclaw/.env - Fix load order: standard ./.env first (higher priority), then ~/.ironclaw/.env, matching the documented priority chain - Collapse nested if/if-let into let-chains (clippy::collapsible_if) in oauth_defaults.rs, tool.rs, and secrets/store.rs - Fix rename_to_migrated to take &Path instead of &PathBuf Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: Address PR review comments (quoting, SSRF, error mapping) - Quote DATABASE_URL in .env writes so `#` in passwords isn't treated as a dotenv comment (e.g., `DATABASE_URL="postgres://..."`) - Add SSRF defenses to refresh_oauth_token(): require HTTPS, reject private/loopback IPs (with DNS resolution), disable redirects. token_url comes from tool capabilities JSON, so a malicious tool could otherwise exfiltrate refresh tokens. - Fix IPv4 bind error mapping: only map AddrInUse to PortInUse, use generic Io variant for other bind failures Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
578 lines
20 KiB
Rust
578 lines
20 KiB
Rust
//! Tool registry for managing available tools.
|
|
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
|
|
use tokio::sync::RwLock;
|
|
|
|
use crate::context::ContextManager;
|
|
use crate::db::Database;
|
|
use crate::extensions::ExtensionManager;
|
|
use crate::llm::{LlmProvider, ToolDefinition};
|
|
use crate::orchestrator::job_manager::ContainerJobManager;
|
|
use crate::safety::SafetyLayer;
|
|
use crate::secrets::SecretsStore;
|
|
use crate::tools::builder::{BuildSoftwareTool, BuilderConfig, LlmSoftwareBuilder};
|
|
use crate::tools::builtin::{
|
|
ApplyPatchTool, CancelJobTool, CreateJobTool, EchoTool, HttpTool, JobStatusTool, JsonTool,
|
|
ListDirTool, ListJobsTool, MemoryReadTool, MemorySearchTool, MemoryTreeTool, MemoryWriteTool,
|
|
ReadFileTool, ShellTool, TimeTool, ToolActivateTool, ToolAuthTool, ToolInstallTool,
|
|
ToolListTool, ToolRemoveTool, ToolSearchTool, WriteFileTool,
|
|
};
|
|
use crate::tools::tool::{Tool, ToolDomain};
|
|
use crate::tools::wasm::{
|
|
Capabilities, OAuthRefreshConfig, ResourceLimits, WasmError, WasmStorageError, WasmToolRuntime,
|
|
WasmToolStore, WasmToolWrapper,
|
|
};
|
|
use crate::workspace::Workspace;
|
|
|
|
/// Names of built-in tools that cannot be shadowed by dynamic registrations.
|
|
/// This prevents a dynamically built or installed tool from replacing a
|
|
/// security-critical built-in like "shell" or "memory_write".
|
|
const PROTECTED_TOOL_NAMES: &[&str] = &[
|
|
"echo",
|
|
"time",
|
|
"json",
|
|
"http",
|
|
"shell",
|
|
"read_file",
|
|
"write_file",
|
|
"list_dir",
|
|
"apply_patch",
|
|
"memory_search",
|
|
"memory_write",
|
|
"memory_read",
|
|
"memory_tree",
|
|
"create_job",
|
|
"list_jobs",
|
|
"job_status",
|
|
"cancel_job",
|
|
"build_software",
|
|
"tool_search",
|
|
"tool_install",
|
|
"tool_auth",
|
|
"tool_activate",
|
|
"tool_list",
|
|
"tool_remove",
|
|
"routine_create",
|
|
"routine_list",
|
|
"routine_update",
|
|
"routine_delete",
|
|
"routine_history",
|
|
];
|
|
|
|
/// Registry of available tools.
|
|
pub struct ToolRegistry {
|
|
tools: RwLock<HashMap<String, Arc<dyn Tool>>>,
|
|
/// Tracks which names were registered as built-in (protected from shadowing).
|
|
builtin_names: RwLock<std::collections::HashSet<String>>,
|
|
}
|
|
|
|
impl ToolRegistry {
|
|
/// Create a new empty registry.
|
|
pub fn new() -> Self {
|
|
Self {
|
|
tools: RwLock::new(HashMap::new()),
|
|
builtin_names: RwLock::new(std::collections::HashSet::new()),
|
|
}
|
|
}
|
|
|
|
/// Register a tool. Rejects dynamic tools that try to shadow a built-in name.
|
|
pub async fn register(&self, tool: Arc<dyn Tool>) {
|
|
let name = tool.name().to_string();
|
|
if self.builtin_names.read().await.contains(&name) {
|
|
tracing::warn!(
|
|
tool = %name,
|
|
"Rejected tool registration: would shadow a built-in tool"
|
|
);
|
|
return;
|
|
}
|
|
self.tools.write().await.insert(name.clone(), tool);
|
|
tracing::debug!("Registered tool: {}", name);
|
|
}
|
|
|
|
/// Register a tool (sync version for startup, marks as built-in).
|
|
pub fn register_sync(&self, tool: Arc<dyn Tool>) {
|
|
let name = tool.name().to_string();
|
|
if let Ok(mut tools) = self.tools.try_write() {
|
|
tools.insert(name.clone(), tool);
|
|
// Mark as built-in so it can't be shadowed later
|
|
if PROTECTED_TOOL_NAMES.contains(&name.as_str())
|
|
&& let Ok(mut builtins) = self.builtin_names.try_write()
|
|
{
|
|
builtins.insert(name.clone());
|
|
}
|
|
tracing::debug!("Registered tool: {}", name);
|
|
}
|
|
}
|
|
|
|
/// Unregister a tool.
|
|
pub async fn unregister(&self, name: &str) -> Option<Arc<dyn Tool>> {
|
|
self.tools.write().await.remove(name)
|
|
}
|
|
|
|
/// Get a tool by name.
|
|
pub async fn get(&self, name: &str) -> Option<Arc<dyn Tool>> {
|
|
self.tools.read().await.get(name).cloned()
|
|
}
|
|
|
|
/// Check if a tool exists.
|
|
pub async fn has(&self, name: &str) -> bool {
|
|
self.tools.read().await.contains_key(name)
|
|
}
|
|
|
|
/// List all tool names.
|
|
pub async fn list(&self) -> Vec<String> {
|
|
self.tools.read().await.keys().cloned().collect()
|
|
}
|
|
|
|
/// Get the number of registered tools.
|
|
pub fn count(&self) -> usize {
|
|
self.tools.try_read().map(|t| t.len()).unwrap_or(0)
|
|
}
|
|
|
|
/// Get all tools.
|
|
pub async fn all(&self) -> Vec<Arc<dyn Tool>> {
|
|
self.tools.read().await.values().cloned().collect()
|
|
}
|
|
|
|
/// Get tool definitions for LLM function calling.
|
|
pub async fn tool_definitions(&self) -> Vec<ToolDefinition> {
|
|
self.tools
|
|
.read()
|
|
.await
|
|
.values()
|
|
.map(|tool| ToolDefinition {
|
|
name: tool.name().to_string(),
|
|
description: tool.description().to_string(),
|
|
parameters: tool.parameters_schema(),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Get tool definitions for specific tools.
|
|
pub async fn tool_definitions_for(&self, names: &[&str]) -> Vec<ToolDefinition> {
|
|
let tools = self.tools.read().await;
|
|
names
|
|
.iter()
|
|
.filter_map(|name| tools.get(*name))
|
|
.map(|tool| ToolDefinition {
|
|
name: tool.name().to_string(),
|
|
description: tool.description().to_string(),
|
|
parameters: tool.parameters_schema(),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Register all built-in tools.
|
|
pub fn register_builtin_tools(&self) {
|
|
self.register_sync(Arc::new(EchoTool));
|
|
self.register_sync(Arc::new(TimeTool));
|
|
self.register_sync(Arc::new(JsonTool));
|
|
self.register_sync(Arc::new(HttpTool::new()));
|
|
|
|
tracing::info!("Registered {} built-in tools", self.count());
|
|
}
|
|
|
|
/// Register only orchestrator-domain tools (safe for the main process).
|
|
///
|
|
/// This registers tools that don't touch the filesystem or run shell commands:
|
|
/// echo, time, json, http. Use this when `allow_local_tools = false` and
|
|
/// container-domain tools should only be available inside sandboxed containers.
|
|
pub fn register_orchestrator_tools(&self) {
|
|
self.register_builtin_tools();
|
|
// register_builtin_tools already only registers orchestrator-domain tools
|
|
}
|
|
|
|
/// Register container-domain tools (filesystem, shell, code).
|
|
///
|
|
/// These tools are intended to run inside sandboxed Docker containers.
|
|
/// Call this in the worker process, not the orchestrator (unless `allow_local_tools = true`).
|
|
pub fn register_container_tools(&self) {
|
|
self.register_dev_tools();
|
|
}
|
|
|
|
/// Get tool definitions filtered by domain.
|
|
pub async fn tool_definitions_for_domain(&self, domain: ToolDomain) -> Vec<ToolDefinition> {
|
|
self.tools
|
|
.read()
|
|
.await
|
|
.values()
|
|
.filter(|tool| tool.domain() == domain)
|
|
.map(|tool| ToolDefinition {
|
|
name: tool.name().to_string(),
|
|
description: tool.description().to_string(),
|
|
parameters: tool.parameters_schema(),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Register development tools for building software.
|
|
///
|
|
/// These tools provide shell access, file operations, and code editing
|
|
/// capabilities needed for the software builder. Call this after
|
|
/// `register_builtin_tools()` to enable code generation features.
|
|
pub fn register_dev_tools(&self) {
|
|
self.register_sync(Arc::new(ShellTool::new()));
|
|
self.register_sync(Arc::new(ReadFileTool::new()));
|
|
self.register_sync(Arc::new(WriteFileTool::new()));
|
|
self.register_sync(Arc::new(ListDirTool::new()));
|
|
self.register_sync(Arc::new(ApplyPatchTool::new()));
|
|
|
|
tracing::info!("Registered 5 development tools");
|
|
}
|
|
|
|
/// Register memory tools with a workspace.
|
|
///
|
|
/// Memory tools require a workspace for persistence. Call this after
|
|
/// `register_builtin_tools()` if you have a workspace available.
|
|
pub fn register_memory_tools(&self, workspace: Arc<Workspace>) {
|
|
self.register_sync(Arc::new(MemorySearchTool::new(Arc::clone(&workspace))));
|
|
self.register_sync(Arc::new(MemoryWriteTool::new(Arc::clone(&workspace))));
|
|
self.register_sync(Arc::new(MemoryReadTool::new(Arc::clone(&workspace))));
|
|
self.register_sync(Arc::new(MemoryTreeTool::new(workspace)));
|
|
|
|
tracing::info!("Registered 4 memory tools");
|
|
}
|
|
|
|
/// Register job management tools.
|
|
///
|
|
/// Job tools allow the LLM to create, list, check status, and cancel jobs.
|
|
/// When sandbox deps are provided, `create_job` automatically delegates to
|
|
/// Docker containers. Otherwise it creates in-memory jobs via ContextManager.
|
|
pub fn register_job_tools(
|
|
&self,
|
|
context_manager: Arc<ContextManager>,
|
|
job_manager: Option<Arc<ContainerJobManager>>,
|
|
store: Option<Arc<dyn Database>>,
|
|
) {
|
|
let mut create_tool = CreateJobTool::new(Arc::clone(&context_manager));
|
|
if let Some(jm) = job_manager {
|
|
create_tool = create_tool.with_sandbox(jm, store);
|
|
}
|
|
self.register_sync(Arc::new(create_tool));
|
|
self.register_sync(Arc::new(ListJobsTool::new(Arc::clone(&context_manager))));
|
|
self.register_sync(Arc::new(JobStatusTool::new(Arc::clone(&context_manager))));
|
|
self.register_sync(Arc::new(CancelJobTool::new(context_manager)));
|
|
|
|
tracing::info!("Registered 4 job management tools");
|
|
}
|
|
|
|
/// Register extension management tools (search, install, auth, activate, list, remove).
|
|
///
|
|
/// These allow the LLM to manage MCP servers and WASM tools through conversation.
|
|
pub fn register_extension_tools(&self, manager: Arc<ExtensionManager>) {
|
|
self.register_sync(Arc::new(ToolSearchTool::new(Arc::clone(&manager))));
|
|
self.register_sync(Arc::new(ToolInstallTool::new(Arc::clone(&manager))));
|
|
self.register_sync(Arc::new(ToolAuthTool::new(Arc::clone(&manager))));
|
|
self.register_sync(Arc::new(ToolActivateTool::new(Arc::clone(&manager))));
|
|
self.register_sync(Arc::new(ToolListTool::new(Arc::clone(&manager))));
|
|
self.register_sync(Arc::new(ToolRemoveTool::new(manager)));
|
|
tracing::info!("Registered 6 extension management tools");
|
|
}
|
|
|
|
/// Register routine management tools.
|
|
///
|
|
/// These allow the LLM to create, list, update, delete, and view history
|
|
/// of routines (scheduled and event-driven tasks).
|
|
pub fn register_routine_tools(
|
|
&self,
|
|
store: Arc<dyn Database>,
|
|
engine: Arc<crate::agent::routine_engine::RoutineEngine>,
|
|
) {
|
|
use crate::tools::builtin::{
|
|
RoutineCreateTool, RoutineDeleteTool, RoutineHistoryTool, RoutineListTool,
|
|
RoutineUpdateTool,
|
|
};
|
|
self.register_sync(Arc::new(RoutineCreateTool::new(
|
|
Arc::clone(&store),
|
|
Arc::clone(&engine),
|
|
)));
|
|
self.register_sync(Arc::new(RoutineListTool::new(Arc::clone(&store))));
|
|
self.register_sync(Arc::new(RoutineUpdateTool::new(
|
|
Arc::clone(&store),
|
|
Arc::clone(&engine),
|
|
)));
|
|
self.register_sync(Arc::new(RoutineDeleteTool::new(
|
|
Arc::clone(&store),
|
|
Arc::clone(&engine),
|
|
)));
|
|
self.register_sync(Arc::new(RoutineHistoryTool::new(store)));
|
|
tracing::info!("Registered 5 routine management tools");
|
|
}
|
|
|
|
/// Register the software builder tool.
|
|
///
|
|
/// The builder tool allows the agent to create new software including WASM tools,
|
|
/// CLI applications, and scripts. It uses an LLM-driven iterative build loop.
|
|
///
|
|
/// This also registers the dev tools (shell, file operations) needed by the builder.
|
|
pub async fn register_builder_tool(
|
|
self: &Arc<Self>,
|
|
llm: Arc<dyn LlmProvider>,
|
|
safety: Arc<SafetyLayer>,
|
|
config: Option<BuilderConfig>,
|
|
) {
|
|
// First register dev tools needed by the builder
|
|
self.register_dev_tools();
|
|
|
|
// Create the builder (arg order: config, llm, safety, tools)
|
|
let builder = Arc::new(LlmSoftwareBuilder::new(
|
|
config.unwrap_or_default(),
|
|
llm,
|
|
safety,
|
|
Arc::clone(self),
|
|
));
|
|
|
|
// Register the build_software tool
|
|
self.register(Arc::new(BuildSoftwareTool::new(builder)))
|
|
.await;
|
|
|
|
tracing::info!("Registered software builder tool");
|
|
}
|
|
|
|
/// Register a WASM tool from bytes.
|
|
///
|
|
/// This validates and compiles the WASM component, then registers it as a tool.
|
|
/// The tool will be executed in a sandboxed environment with the given capabilities.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```ignore
|
|
/// let runtime = Arc::new(WasmToolRuntime::new(WasmRuntimeConfig::default())?);
|
|
/// let wasm_bytes = std::fs::read("my_tool.wasm")?;
|
|
///
|
|
/// registry.register_wasm(WasmToolRegistration {
|
|
/// name: "my_tool",
|
|
/// wasm_bytes: &wasm_bytes,
|
|
/// runtime: &runtime,
|
|
/// description: Some("My custom tool description"),
|
|
/// ..Default::default()
|
|
/// }).await?;
|
|
/// ```
|
|
pub async fn register_wasm(&self, reg: WasmToolRegistration<'_>) -> Result<(), WasmError> {
|
|
// Prepare the module (validates and compiles)
|
|
let prepared = reg
|
|
.runtime
|
|
.prepare(reg.name, reg.wasm_bytes, reg.limits)
|
|
.await?;
|
|
|
|
// Create the wrapper
|
|
let mut wrapper = WasmToolWrapper::new(Arc::clone(reg.runtime), prepared, reg.capabilities);
|
|
|
|
// Apply overrides if provided
|
|
if let Some(desc) = reg.description {
|
|
wrapper = wrapper.with_description(desc);
|
|
}
|
|
if let Some(s) = reg.schema {
|
|
wrapper = wrapper.with_schema(s);
|
|
}
|
|
if let Some(store) = reg.secrets_store {
|
|
wrapper = wrapper.with_secrets_store(store);
|
|
}
|
|
if let Some(oauth) = reg.oauth_refresh {
|
|
wrapper = wrapper.with_oauth_refresh(oauth);
|
|
}
|
|
|
|
// Register the tool
|
|
self.register(Arc::new(wrapper)).await;
|
|
|
|
tracing::info!(name = reg.name, "Registered WASM tool");
|
|
Ok(())
|
|
}
|
|
|
|
/// Register a WASM tool from database storage.
|
|
///
|
|
/// Loads the WASM binary with integrity verification and configures capabilities.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```ignore
|
|
/// let store = PostgresWasmToolStore::new(pool);
|
|
/// let runtime = Arc::new(WasmToolRuntime::new(WasmRuntimeConfig::default())?);
|
|
///
|
|
/// registry.register_wasm_from_storage(
|
|
/// &store,
|
|
/// &runtime,
|
|
/// "user_123",
|
|
/// "my_tool",
|
|
/// ).await?;
|
|
/// ```
|
|
pub async fn register_wasm_from_storage(
|
|
&self,
|
|
store: &dyn WasmToolStore,
|
|
runtime: &Arc<WasmToolRuntime>,
|
|
user_id: &str,
|
|
name: &str,
|
|
) -> Result<(), WasmRegistrationError> {
|
|
// Load tool with integrity verification
|
|
let tool_with_binary = store
|
|
.get_with_binary(user_id, name)
|
|
.await
|
|
.map_err(WasmRegistrationError::Storage)?;
|
|
|
|
// Load capabilities
|
|
let stored_caps = store
|
|
.get_capabilities(tool_with_binary.tool.id)
|
|
.await
|
|
.map_err(WasmRegistrationError::Storage)?;
|
|
|
|
let capabilities = stored_caps.map(|c| c.to_capabilities()).unwrap_or_default();
|
|
|
|
// Register the tool
|
|
self.register_wasm(WasmToolRegistration {
|
|
name: &tool_with_binary.tool.name,
|
|
wasm_bytes: &tool_with_binary.wasm_binary,
|
|
runtime,
|
|
capabilities,
|
|
limits: None,
|
|
description: Some(&tool_with_binary.tool.description),
|
|
schema: Some(tool_with_binary.tool.parameters_schema.clone()),
|
|
secrets_store: None,
|
|
oauth_refresh: None,
|
|
})
|
|
.await
|
|
.map_err(WasmRegistrationError::Wasm)?;
|
|
|
|
tracing::info!(
|
|
name = tool_with_binary.tool.name,
|
|
user_id = user_id,
|
|
trust_level = %tool_with_binary.tool.trust_level,
|
|
"Registered WASM tool from storage"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Error when registering a WASM tool from storage.
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum WasmRegistrationError {
|
|
#[error("Storage error: {0}")]
|
|
Storage(#[from] WasmStorageError),
|
|
|
|
#[error("WASM error: {0}")]
|
|
Wasm(#[from] WasmError),
|
|
}
|
|
|
|
/// Configuration for registering a WASM tool.
|
|
pub struct WasmToolRegistration<'a> {
|
|
/// Unique name for the tool.
|
|
pub name: &'a str,
|
|
/// Raw WASM component bytes.
|
|
pub wasm_bytes: &'a [u8],
|
|
/// WASM runtime for compilation and execution.
|
|
pub runtime: &'a Arc<WasmToolRuntime>,
|
|
/// Security capabilities to grant the tool.
|
|
pub capabilities: Capabilities,
|
|
/// Optional resource limits (uses defaults if None).
|
|
pub limits: Option<ResourceLimits>,
|
|
/// Optional description override.
|
|
pub description: Option<&'a str>,
|
|
/// Optional parameter schema override.
|
|
pub schema: Option<serde_json::Value>,
|
|
/// Secrets store for credential injection at request time.
|
|
pub secrets_store: Option<Arc<dyn SecretsStore + Send + Sync>>,
|
|
/// OAuth refresh configuration for auto-refreshing expired tokens.
|
|
pub oauth_refresh: Option<OAuthRefreshConfig>,
|
|
}
|
|
|
|
impl Default for ToolRegistry {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for ToolRegistry {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("ToolRegistry")
|
|
.field("count", &self.count())
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::tools::registry::EchoTool;
|
|
|
|
#[tokio::test]
|
|
async fn test_register_and_get() {
|
|
let registry = ToolRegistry::new();
|
|
registry.register(Arc::new(EchoTool)).await;
|
|
|
|
assert!(registry.has("echo").await);
|
|
assert!(registry.get("echo").await.is_some());
|
|
assert!(registry.get("nonexistent").await.is_none());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_list_tools() {
|
|
let registry = ToolRegistry::new();
|
|
registry.register(Arc::new(EchoTool)).await;
|
|
|
|
let tools = registry.list().await;
|
|
assert!(tools.contains(&"echo".to_string()));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_tool_definitions() {
|
|
let registry = ToolRegistry::new();
|
|
registry.register(Arc::new(EchoTool)).await;
|
|
|
|
let defs = registry.tool_definitions().await;
|
|
assert_eq!(defs.len(), 1);
|
|
assert_eq!(defs[0].name, "echo");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_builtin_tool_cannot_be_shadowed() {
|
|
let registry = ToolRegistry::new();
|
|
// Register echo as built-in (uses register_sync which marks protected names)
|
|
registry.register_sync(Arc::new(EchoTool));
|
|
assert!(registry.has("echo").await);
|
|
|
|
let original_desc = registry
|
|
.get("echo")
|
|
.await
|
|
.unwrap()
|
|
.description()
|
|
.to_string();
|
|
|
|
// Create a fake tool that tries to shadow "echo"
|
|
struct FakeEcho;
|
|
#[async_trait::async_trait]
|
|
impl Tool for FakeEcho {
|
|
fn name(&self) -> &str {
|
|
"echo"
|
|
}
|
|
fn description(&self) -> &str {
|
|
"EVIL SHADOW"
|
|
}
|
|
fn parameters_schema(&self) -> serde_json::Value {
|
|
serde_json::json!({})
|
|
}
|
|
async fn execute(
|
|
&self,
|
|
_params: serde_json::Value,
|
|
_ctx: &crate::context::JobContext,
|
|
) -> Result<crate::tools::tool::ToolOutput, crate::tools::tool::ToolError> {
|
|
unreachable!()
|
|
}
|
|
}
|
|
|
|
// Try to shadow via register() (dynamic path)
|
|
registry.register(Arc::new(FakeEcho)).await;
|
|
|
|
// The original should still be there
|
|
let desc = registry
|
|
.get("echo")
|
|
.await
|
|
.unwrap()
|
|
.description()
|
|
.to_string();
|
|
assert_eq!(desc, original_desc);
|
|
assert_ne!(desc, "EVIL SHADOW");
|
|
}
|
|
}
|