mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-09-03 01:59:23 +00:00
Merge remote-tracking branch 'origin/main' into ui
# Conflicts: # src/channels/mod.rs # src/main.rs
This commit is contained in:
+65
-128
@@ -8,10 +8,11 @@ use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitEx
|
||||
use ironclaw::{
|
||||
agent::{Agent, AgentDeps, SessionManager},
|
||||
channels::{
|
||||
AppEvent, ChannelManager, GatewayChannel, HttpChannel, ReplChannel, TuiChannel,
|
||||
ChannelManager, GatewayChannel, HttpChannel, ReplChannel, WebhookServer,
|
||||
WebhookServerConfig,
|
||||
wasm::{
|
||||
RegisteredEndpoint, SharedWasmChannel, WasmChannelLoader, WasmChannelRouter,
|
||||
WasmChannelRuntime, WasmChannelRuntimeConfig, WasmChannelServer,
|
||||
WasmChannelRuntime, WasmChannelRuntimeConfig, create_wasm_channel_router,
|
||||
},
|
||||
web::log_layer::{LogBroadcaster, WebLogLayer},
|
||||
},
|
||||
@@ -39,7 +40,7 @@ use ironclaw::{
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Handle non-agent commands first (they don't need TUI/full setup)
|
||||
// Handle non-agent commands first (they don't need full setup)
|
||||
match &cli.command {
|
||||
Some(Command::Tool(tool_cmd)) => {
|
||||
// Simple logging for CLI commands
|
||||
@@ -177,8 +178,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
// Initialize session manager and authenticate BEFORE TUI setup
|
||||
// This allows the auth menu to display cleanly without TUI interference
|
||||
// Initialize session manager and authenticate before channel setup
|
||||
let session_config = SessionConfig {
|
||||
auth_base_url: config.llm.nearai.auth_base_url.clone(),
|
||||
session_path: config.llm.nearai.session_path.clone(),
|
||||
@@ -187,10 +187,9 @@ async fn main() -> anyhow::Result<()> {
|
||||
let session = create_session_manager(session_config).await;
|
||||
|
||||
// Ensure we're authenticated before proceeding (may trigger login flow)
|
||||
// This happens before TUI so the menu displays correctly
|
||||
session.ensure_authenticated().await?;
|
||||
|
||||
// Initialize tracing and channels based on mode
|
||||
// Initialize tracing
|
||||
let env_filter = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new("ironclaw=info,tower_http=debug"));
|
||||
|
||||
@@ -198,53 +197,19 @@ async fn main() -> anyhow::Result<()> {
|
||||
// This gets wired to the gateway's /api/logs/events SSE endpoint later.
|
||||
let log_broadcaster = Arc::new(LogBroadcaster::new());
|
||||
|
||||
// Determine which mode to use: REPL, single message, or TUI
|
||||
let use_repl = cli.repl || cli.message.is_some();
|
||||
tracing_subscriber::registry()
|
||||
.with(env_filter)
|
||||
.with(tracing_subscriber::fmt::layer().with_target(false))
|
||||
.with(WebLogLayer::new(Arc::clone(&log_broadcaster)))
|
||||
.init();
|
||||
|
||||
// Create appropriate channel based on mode
|
||||
let (tui_channel, tui_event_sender, repl_channel) = if use_repl {
|
||||
// REPL mode - use simple stdin/stdout
|
||||
tracing_subscriber::registry()
|
||||
.with(env_filter)
|
||||
.with(tracing_subscriber::fmt::layer().with_target(false))
|
||||
.with(WebLogLayer::new(Arc::clone(&log_broadcaster)))
|
||||
.init();
|
||||
|
||||
let repl = if let Some(ref msg) = cli.message {
|
||||
ReplChannel::with_message(msg.clone())
|
||||
} else {
|
||||
ReplChannel::new()
|
||||
};
|
||||
|
||||
(None, None, Some(repl))
|
||||
// Create CLI channel
|
||||
let repl_channel = if let Some(ref msg) = cli.message {
|
||||
Some(ReplChannel::with_message(msg.clone()))
|
||||
} else if config.channels.cli.enabled {
|
||||
// TUI mode
|
||||
let channel = TuiChannel::new();
|
||||
let log_writer = channel.log_writer();
|
||||
let event_sender = channel.event_sender();
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(env_filter)
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_writer(log_writer)
|
||||
.without_time()
|
||||
.with_target(false)
|
||||
.with_level(true),
|
||||
)
|
||||
.with(WebLogLayer::new(Arc::clone(&log_broadcaster)))
|
||||
.init();
|
||||
|
||||
(Some(channel), Some(event_sender), None)
|
||||
Some(ReplChannel::new())
|
||||
} else {
|
||||
// No CLI - just logging
|
||||
tracing_subscriber::registry()
|
||||
.with(env_filter)
|
||||
.with(tracing_subscriber::fmt::layer().with_target(false))
|
||||
.with(WebLogLayer::new(Arc::clone(&log_broadcaster)))
|
||||
.init();
|
||||
|
||||
(None, None, None)
|
||||
None
|
||||
};
|
||||
|
||||
tracing::info!("Starting IronClaw...");
|
||||
@@ -266,34 +231,6 @@ async fn main() -> anyhow::Result<()> {
|
||||
let llm = create_llm_provider(&config.llm, session.clone())?;
|
||||
tracing::info!("LLM provider initialized: {}", llm.model_name());
|
||||
|
||||
// Fetch available models and send to TUI (async, non-blocking)
|
||||
if let Some(ref event_tx) = tui_event_sender {
|
||||
let llm_for_models = llm.clone();
|
||||
let event_tx = event_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
match llm_for_models.list_models().await {
|
||||
Ok(models) if !models.is_empty() => {
|
||||
let _ = event_tx.send(AppEvent::AvailableModels(models)).await;
|
||||
}
|
||||
Ok(_) => {
|
||||
let _ = event_tx
|
||||
.send(AppEvent::ErrorMessage(
|
||||
"No models available from API".into(),
|
||||
))
|
||||
.await;
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = event_tx
|
||||
.send(AppEvent::ErrorMessage(format!(
|
||||
"Failed to fetch models: {}",
|
||||
e
|
||||
)))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize safety layer
|
||||
let safety = Arc::new(SafetyLayer::new(&config.safety));
|
||||
tracing::info!("Safety layer initialized");
|
||||
@@ -536,7 +473,6 @@ async fn main() -> anyhow::Result<()> {
|
||||
// Initialize channel manager
|
||||
let mut channels = ChannelManager::new();
|
||||
|
||||
// Add REPL channel if in REPL mode
|
||||
if let Some(repl) = repl_channel {
|
||||
channels.add(Box::new(repl));
|
||||
if cli.message.is_some() {
|
||||
@@ -545,25 +481,11 @@ async fn main() -> anyhow::Result<()> {
|
||||
tracing::info!("REPL mode enabled");
|
||||
}
|
||||
}
|
||||
// Add TUI channel if CLI is enabled (already created for logging hookup)
|
||||
else if let Some(tui) = tui_channel {
|
||||
channels.add(Box::new(tui));
|
||||
tracing::info!("TUI channel enabled");
|
||||
}
|
||||
|
||||
// Add HTTP channel if configured and not CLI-only mode
|
||||
if !cli.cli_only && !use_repl {
|
||||
if let Some(ref http_config) = config.channels.http {
|
||||
channels.add(Box::new(HttpChannel::new(http_config.clone())));
|
||||
tracing::info!(
|
||||
"HTTP channel enabled on {}:{}",
|
||||
http_config.host,
|
||||
http_config.port
|
||||
);
|
||||
}
|
||||
}
|
||||
// Collect webhook route fragments; a single WebhookServer hosts them all.
|
||||
let mut webhook_routes: Vec<axum::Router> = Vec::new();
|
||||
|
||||
// Load WASM channels if enabled
|
||||
// Load WASM channels and register their webhook routes.
|
||||
if config.channels.wasm_channels_enabled && config.channels.wasm_channels_dir.exists() {
|
||||
match WasmChannelRuntime::new(WasmChannelRuntimeConfig::default()) {
|
||||
Ok(runtime) => {
|
||||
@@ -575,7 +497,6 @@ async fn main() -> anyhow::Result<()> {
|
||||
.await
|
||||
{
|
||||
Ok(results) => {
|
||||
// Create router for WASM channel webhooks
|
||||
let wasm_router = Arc::new(WasmChannelRouter::new());
|
||||
let mut has_webhook_channels = false;
|
||||
|
||||
@@ -583,10 +504,8 @@ async fn main() -> anyhow::Result<()> {
|
||||
let channel_name = loaded.name().to_string();
|
||||
tracing::info!("Loaded WASM channel: {}", channel_name);
|
||||
|
||||
// Get webhook secret name from capabilities (generic)
|
||||
let secret_name = loaded.webhook_secret_name();
|
||||
|
||||
// Get webhook secret for this channel from secrets store
|
||||
let webhook_secret = if let Some(ref secrets) = secrets_store {
|
||||
secrets
|
||||
.get_decrypted("default", &secret_name)
|
||||
@@ -597,12 +516,9 @@ async fn main() -> anyhow::Result<()> {
|
||||
None
|
||||
};
|
||||
|
||||
// Get the secret header name from capabilities
|
||||
let secret_header =
|
||||
loaded.webhook_secret_header().map(|s| s.to_string());
|
||||
|
||||
// Register channel with router for webhook handling
|
||||
// Use known webhook path based on channel name
|
||||
let webhook_path = format!("/webhook/{}", channel_name);
|
||||
let endpoints = vec![RegisteredEndpoint {
|
||||
channel_name: channel_name.clone(),
|
||||
@@ -613,8 +529,6 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
let channel_arc = Arc::new(loaded.channel);
|
||||
|
||||
// Inject runtime config into the channel (tunnel_url, webhook_secret)
|
||||
// This must be done before start() is called
|
||||
{
|
||||
let mut config_updates = std::collections::HashMap::new();
|
||||
|
||||
@@ -660,7 +574,6 @@ async fn main() -> anyhow::Result<()> {
|
||||
.await;
|
||||
has_webhook_channels = true;
|
||||
|
||||
// Inject credentials for this channel (generic pattern-based injection)
|
||||
if let Some(ref secrets) = secrets_store {
|
||||
match inject_channel_credentials(
|
||||
&channel_arc,
|
||||
@@ -688,32 +601,14 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap in SharedWasmChannel for ChannelManager
|
||||
// Both the router and ChannelManager share the same underlying channel
|
||||
channels.add(Box::new(SharedWasmChannel::new(channel_arc)));
|
||||
}
|
||||
|
||||
// Start WASM channel webhook server if we have channels with webhooks
|
||||
if has_webhook_channels && config.tunnel.public_url.is_some() {
|
||||
let mut server = WasmChannelServer::new(wasm_router);
|
||||
if let Some(ref ext_mgr) = extension_manager {
|
||||
server = server.with_extension_manager(Arc::clone(ext_mgr));
|
||||
}
|
||||
let addr = std::net::SocketAddr::from(([0, 0, 0, 0], 8080));
|
||||
match server.start(addr).await {
|
||||
Ok(_handle) => {
|
||||
tracing::info!(
|
||||
"WASM channel webhook server started on {}",
|
||||
addr
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Failed to start WASM channel webhook server: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
webhook_routes.push(create_wasm_channel_router(
|
||||
wasm_router,
|
||||
extension_manager.as_ref().map(Arc::clone),
|
||||
));
|
||||
}
|
||||
|
||||
for (path, err) in &results.errors {
|
||||
@@ -735,6 +630,43 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// Add HTTP channel if configured and not CLI-only mode.
|
||||
// Extract its routes for the unified server; the channel itself just
|
||||
// provides the mpsc stream.
|
||||
let mut webhook_server_addr: Option<std::net::SocketAddr> = None;
|
||||
if !cli.cli_only {
|
||||
if let Some(ref http_config) = config.channels.http {
|
||||
let http_channel = HttpChannel::new(http_config.clone());
|
||||
webhook_routes.push(http_channel.routes());
|
||||
let (host, port) = http_channel.addr();
|
||||
webhook_server_addr = Some(
|
||||
format!("{}:{}", host, port)
|
||||
.parse()
|
||||
.expect("HttpConfig host:port must be a valid SocketAddr"),
|
||||
);
|
||||
channels.add(Box::new(http_channel));
|
||||
tracing::info!(
|
||||
"HTTP channel enabled on {}:{}",
|
||||
http_config.host,
|
||||
http_config.port
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Start the unified webhook server if any routes were registered.
|
||||
let mut webhook_server = if !webhook_routes.is_empty() {
|
||||
let addr =
|
||||
webhook_server_addr.unwrap_or_else(|| std::net::SocketAddr::from(([0, 0, 0, 0], 8080)));
|
||||
let mut server = WebhookServer::new(WebhookServerConfig { addr });
|
||||
for routes in webhook_routes {
|
||||
server.add_routes(routes);
|
||||
}
|
||||
server.start().await?;
|
||||
Some(server)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Create workspace for agent (shared with memory tools)
|
||||
let workspace = store.as_ref().map(|s| {
|
||||
let mut ws = Workspace::new("default", s.pool());
|
||||
@@ -811,6 +743,11 @@ async fn main() -> anyhow::Result<()> {
|
||||
// Run the agent (blocks until shutdown)
|
||||
agent.run().await?;
|
||||
|
||||
// Shut down the webhook server if one was started
|
||||
if let Some(ref mut server) = webhook_server {
|
||||
server.shutdown().await;
|
||||
}
|
||||
|
||||
tracing::info!("Agent shutdown complete");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user