mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 15:40:18 +00:00
* feat(mcp): transport abstraction, stdio/UDS transports, and OAuth fixes Extract McpTransport trait from HTTP-coupled McpClient, enabling pluggable transport backends. Implements stdio and Unix domain socket transports for local MCP server integration, fixes OAuth discovery per RFC 9728, and adds SSRF protection. Transport abstraction (Step 2): - McpTransport trait with send(), shutdown(), supports_http_features() - HttpMcpTransport extracted from McpClient with SSE parsing, session tracking - Shared JSON-RPC framing helpers (write_jsonrpc_line, spawn_jsonrpc_reader) - McpClient refactored to hold Arc<dyn McpTransport> Stdio transport (#652, Step 4): - StdioMcpTransport spawns child process, communicates via stdin/stdout - McpProcessManager for lifecycle management with exponential backoff restart - Background stderr drain task for debug logging Unix domain socket transport (#134, Step 5): - UnixMcpTransport connects to existing Unix sockets - Reuses shared JSON-RPC framing from transport.rs HTML error body sanitization (#263, Step 1): - sanitize_error_body() detects HTML, strips control chars, truncates to 500 Custom headers (#639, Step 3): - headers field on McpServerConfig, merged into every HTTP request - --header CLI arg for `mcp add` Config and CLI updates (Step 6): - McpTransportConfig tagged enum (Http/Stdio/Unix) with serde support - EffectiveTransport for zero-copy config dispatch - CLI: --transport, --command, --arg, --env, --socket flags for `mcp add` - `mcp list` shows transport type OAuth fixes (#299, Step 8): - Multi-strategy discovery (401-based, RFC 9728, direct) - RFC 8707 resource parameter in auth and refresh flows - SSRF protection with IPv4-mapped IPv6 bypass detection - Well-known URI construction per RFC 8414 Closes #652, #134, #639, #263, #299 Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(mcp): address audit findings from crate review - Fix SSRF bypass: make validate_url_safe async with DNS resolution to block hostnames that resolve to private/link-local IPs - Fix UTF-8 truncation: use char-based truncation in sanitize_error_body to avoid panicking on multi-byte characters - Fix SSE parser: process only complete lines to handle chunks split across boundaries, add 10MB buffer size limit - Add debug_assert for transport type mismatch in new_with_config - Propagate custom headers in new_with_transport constructor - Deduplicate effective_transport() calls in CLI list command - Gate test-only accessors with #[cfg(test)] to eliminate dead_code warnings - Document JSON-RPC notification id:0 limitation in protocol.rs - Document total backoff wait time (31s) in process.rs - Add regression test for multi-byte UTF-8 truncation Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(mcp): address PR review findings from Copilot, Gemini, and zmanian Moderate/High fixes: - Plumb custom headers through new_authenticated constructor - Restrict HTTP to localhost only in validate_url_safe (prevent plaintext credential leaks over non-localhost HTTP) - Add mcp_process_manager.shutdown_all() to app shutdown path to prevent orphaning stdio child processes - Validate discovered authorization_url before opening browser (prevent malicious MCP server redirecting to phishing page) Medium fixes: - Upgrade debug_assert to assert in new_with_config (fires in release) - Remove pending map entry on Ok(Err(_)) in stdio/unix send() to avoid stale entries and unnecessary 30s waits - Shut down old transport in try_restart() before spawning replacement - Redact env var values in mcp list --verbose (may contain secrets) - Drain pending requests on shutdown to wake waiters immediately - Add IPv6 link-local, site-local, unique-local, and documentation ranges to is_dangerous_ip SSRF protection Low fixes: - Truncate logged JSON parse error lines to 200 chars (prevent sensitive data in logs) - Remove misleading shutdown comment in unix_transport - Use tempfile::tempdir() instead of hardcoded /tmp/ path in test - Adopt main's improved sanitize_error_body (HTML tag stripping, 200-char truncation with char_indices) Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(mcp): gate unix_transport with #[cfg(unix)] for Windows compat - Add #[cfg(unix)] to unix_transport module declaration - Add #[cfg(unix)]/#[cfg(not(unix))] branches in app.rs for Unix socket MCP server setup - Remove unused sanitize_error_body import in client.rs tests [skip-regression-check] --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
257 lines
8.0 KiB
Rust
257 lines
8.0 KiB
Rust
//! CLI command handling.
|
|
//!
|
|
//! Provides subcommands for:
|
|
//! - Running the agent (`run`)
|
|
//! - Interactive onboarding wizard (`onboard`)
|
|
//! - Managing configuration (`config list`, `config get`, `config set`)
|
|
//! - Managing WASM tools (`tool install`, `tool list`, `tool remove`)
|
|
//! - Managing MCP servers (`mcp add`, `mcp auth`, `mcp list`, `mcp test`)
|
|
//! - Querying workspace memory (`memory search`, `memory read`, `memory write`)
|
|
//! - Managing OS service (`service install`, `service start`, `service stop`)
|
|
//! - Active health diagnostics (`doctor`)
|
|
//! - Checking system health (`status`)
|
|
|
|
mod completion;
|
|
mod config;
|
|
mod doctor;
|
|
mod mcp;
|
|
pub mod memory;
|
|
pub mod oauth_defaults;
|
|
mod pairing;
|
|
mod registry;
|
|
mod service;
|
|
pub mod status;
|
|
mod tool;
|
|
|
|
pub use completion::Completion;
|
|
pub use config::{ConfigCommand, run_config_command};
|
|
pub use doctor::run_doctor_command;
|
|
pub use mcp::{McpCommand, run_mcp_command};
|
|
pub use memory::MemoryCommand;
|
|
#[cfg(feature = "postgres")]
|
|
pub use memory::run_memory_command;
|
|
pub use memory::run_memory_command_with_db;
|
|
pub use pairing::{PairingCommand, run_pairing_command, run_pairing_command_with_store};
|
|
pub use registry::{RegistryCommand, run_registry_command};
|
|
pub use service::{ServiceCommand, run_service_command};
|
|
pub use status::run_status_command;
|
|
pub use tool::{ToolCommand, run_tool_command};
|
|
|
|
use clap::{ColorChoice, Parser, Subcommand};
|
|
|
|
#[derive(Parser, Debug)]
|
|
#[command(name = "ironclaw")]
|
|
#[command(
|
|
about = "Secure personal AI assistant that protects your data and expands its capabilities"
|
|
)]
|
|
#[command(
|
|
long_about = "IronClaw is a secure AI assistant. Use 'ironclaw <subcommand> --help' for details.\nExamples:\n ironclaw run # Start the agent\n ironclaw config list # List configs"
|
|
)]
|
|
#[command(version)]
|
|
#[command(color = ColorChoice::Auto)] // Enable auto-color for help (if the terminal supports it)
|
|
pub struct Cli {
|
|
#[command(subcommand)]
|
|
pub command: Option<Command>,
|
|
|
|
/// Run in interactive CLI mode only (disable other channels)
|
|
#[arg(long, global = true)]
|
|
pub cli_only: bool,
|
|
|
|
/// Skip database connection (for testing)
|
|
#[arg(long, global = true)]
|
|
pub no_db: bool,
|
|
|
|
/// Single message mode - send one message and exit
|
|
#[arg(short, long, global = true)]
|
|
pub message: Option<String>,
|
|
|
|
/// Configuration file path (optional, uses env vars by default)
|
|
#[arg(short, long, global = true)]
|
|
pub config: Option<std::path::PathBuf>,
|
|
|
|
/// Skip first-run onboarding check
|
|
#[arg(long, global = true)]
|
|
pub no_onboard: bool,
|
|
}
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
pub enum Command {
|
|
/// Run the agent (default if no subcommand given)
|
|
#[command(
|
|
about = "Run the AI agent",
|
|
long_about = "Starts the IronClaw agent in default mode.\nExample: ironclaw run"
|
|
)]
|
|
Run,
|
|
|
|
/// Interactive onboarding wizard
|
|
#[command(
|
|
about = "Run interactive setup wizard",
|
|
long_about = "Guides through initial configuration.\nExamples:\n ironclaw onboard --skip-auth # Skip auth step\n ironclaw onboard --channels-only # Reconfigure channels\n ironclaw onboard --provider-only # Change LLM provider and model"
|
|
)]
|
|
Onboard {
|
|
/// Skip authentication (use existing session)
|
|
#[arg(long)]
|
|
skip_auth: bool,
|
|
|
|
/// Reconfigure channels only
|
|
#[arg(long, conflicts_with = "provider_only")]
|
|
channels_only: bool,
|
|
|
|
/// Reconfigure LLM provider and model only
|
|
#[arg(long, conflicts_with = "channels_only")]
|
|
provider_only: bool,
|
|
},
|
|
|
|
/// Manage configuration settings
|
|
#[command(
|
|
subcommand,
|
|
about = "Manage app configs",
|
|
long_about = "Commands for listing, getting, and setting configurations.\nExample: ironclaw config list"
|
|
)]
|
|
Config(ConfigCommand),
|
|
|
|
/// Manage WASM tools
|
|
#[command(
|
|
subcommand,
|
|
about = "Manage WASM tools",
|
|
long_about = "Install, list, or remove WASM-based tools.\nExample: ironclaw tool install mytool.wasm"
|
|
)]
|
|
Tool(ToolCommand),
|
|
|
|
/// Browse and install extensions from the registry
|
|
#[command(
|
|
subcommand,
|
|
about = "Browse/install extensions",
|
|
long_about = "Interact with extension registry.\nExample: ironclaw registry list"
|
|
)]
|
|
Registry(RegistryCommand),
|
|
|
|
/// Manage MCP servers (hosted tool providers)
|
|
#[command(
|
|
subcommand,
|
|
about = "Manage MCP servers",
|
|
long_about = "Add, auth, list, or test MCP servers.\nExample: ironclaw mcp add notion https://mcp.notion.com"
|
|
)]
|
|
Mcp(Box<McpCommand>),
|
|
|
|
/// Query and manage workspace memory
|
|
#[command(
|
|
subcommand,
|
|
about = "Manage workspace memory",
|
|
long_about = "Search, read, or write to memory.\nExample: ironclaw memory search 'query'"
|
|
)]
|
|
Memory(MemoryCommand),
|
|
|
|
/// DM pairing (approve inbound requests from unknown senders)
|
|
#[command(
|
|
subcommand,
|
|
about = "Manage DM pairing",
|
|
long_about = "Approve or manage pairing requests.\nExamples:\n ironclaw pairing list telegram\n ironclaw pairing approve telegram ABC12345"
|
|
)]
|
|
Pairing(PairingCommand),
|
|
|
|
/// Manage OS service (launchd / systemd)
|
|
#[command(
|
|
subcommand,
|
|
about = "Manage OS service",
|
|
long_about = "Install, start, or stop service.\nExample: ironclaw service install"
|
|
)]
|
|
Service(ServiceCommand),
|
|
|
|
/// Probe external dependencies and validate configuration
|
|
#[command(
|
|
about = "Run diagnostics",
|
|
long_about = "Checks dependencies and config validity.\nExample: ironclaw doctor"
|
|
)]
|
|
Doctor,
|
|
|
|
/// Show system health and diagnostics
|
|
#[command(
|
|
about = "Show system status",
|
|
long_about = "Displays health and diagnostics info.\nExample: ironclaw status"
|
|
)]
|
|
Status,
|
|
|
|
/// Generate shell completion scripts
|
|
#[command(
|
|
about = "Generate completions",
|
|
long_about = "Generates shell completion scripts.\nExample: ironclaw completion --shell bash > ironclaw.bash"
|
|
)]
|
|
Completion(Completion),
|
|
|
|
/// Run as a sandboxed worker inside a Docker container (internal use).
|
|
/// This is invoked automatically by the orchestrator, not by users directly.
|
|
#[command(hide = true)]
|
|
Worker {
|
|
/// Job ID to execute.
|
|
#[arg(long)]
|
|
job_id: uuid::Uuid,
|
|
|
|
/// URL of the orchestrator's internal API.
|
|
#[arg(long, default_value = "http://host.docker.internal:50051")]
|
|
orchestrator_url: String,
|
|
|
|
/// Maximum iterations before stopping.
|
|
#[arg(long, default_value = "50")]
|
|
max_iterations: u32,
|
|
},
|
|
|
|
/// Run as a Claude Code bridge inside a Docker container (internal use).
|
|
/// Spawns the `claude` CLI and streams output back to the orchestrator.
|
|
#[command(hide = true)]
|
|
ClaudeBridge {
|
|
/// Job ID to execute.
|
|
#[arg(long)]
|
|
job_id: uuid::Uuid,
|
|
|
|
/// URL of the orchestrator's internal API.
|
|
#[arg(long, default_value = "http://host.docker.internal:50051")]
|
|
orchestrator_url: String,
|
|
|
|
/// Maximum agentic turns for Claude Code.
|
|
#[arg(long, default_value = "50")]
|
|
max_turns: u32,
|
|
|
|
/// Claude model to use (e.g. "sonnet", "opus").
|
|
#[arg(long, default_value = "sonnet")]
|
|
model: String,
|
|
},
|
|
}
|
|
|
|
impl Cli {
|
|
/// Check if we should run the agent (default behavior or explicit `run` command).
|
|
pub fn should_run_agent(&self) -> bool {
|
|
matches!(self.command, None | Some(Command::Run))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use clap::CommandFactory;
|
|
use insta::assert_snapshot;
|
|
|
|
#[test]
|
|
fn test_version() {
|
|
let cmd = Cli::command();
|
|
assert_eq!(
|
|
cmd.get_version().unwrap_or("unknown"),
|
|
env!("CARGO_PKG_VERSION")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_help_output() {
|
|
let mut cmd = Cli::command();
|
|
let help = cmd.render_help().to_string();
|
|
assert_snapshot!(help);
|
|
}
|
|
|
|
#[test]
|
|
fn test_long_help_output() {
|
|
let mut cmd = Cli::command();
|
|
let help = cmd.render_long_help().to_string();
|
|
assert_snapshot!(help);
|
|
}
|
|
}
|