diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..22246e0 --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +# Hero Supervisor Configuration + +# Redis connection URL +REDIS_URL=redis://127.0.0.1:6379 + +# OpenRPC Server Configuration +BIND_ADDRESS=127.0.0.1 +PORT=3030 + +# Authentication Secrets (generate with: ./scripts/generate_secret.sh) +# At least one admin secret is required +ADMIN_SECRETS=your_admin_secret_here + +# Optional: Additional secrets for different access levels +# USER_SECRETS=user_secret_1,user_secret_2 +# REGISTER_SECRETS=register_secret_1 + +# Optional: Mycelium network URL (requires mycelium feature) +# MYCELIUM_URL=http://127.0.0.1:8989 diff --git a/.gitignore b/.gitignore index ac79e64..b8c474b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ target -.bin \ No newline at end of file +.bin +.env +/tmp/supervisor-*.log \ No newline at end of file diff --git a/core/src/bin/supervisor.rs b/core/src/bin/supervisor.rs index f9c608c..f2789ab 100644 --- a/core/src/bin/supervisor.rs +++ b/core/src/bin/supervisor.rs @@ -1,38 +1,24 @@ -//! # Hero Supervisor Binary -//! -//! Main supervisor binary that manages multiple actors and listens to jobs over Redis. -//! The supervisor builds with actor configuration, starts actors, and dispatches jobs -//! to the appropriate runners based on the job's runner field. - - +//! Hero Supervisor Binary use hero_supervisor::{SupervisorApp, SupervisorBuilder}; use clap::Parser; -use log::{info, error}; -use std::path::PathBuf; +use log::error; - - - -/// Command line arguments for the supervisor +/// Hero Supervisor - manages actors and dispatches jobs #[derive(Parser, Debug)] #[command(name = "supervisor")] -#[command(about = "Hero Supervisor - manages multiple actors and dispatches jobs")] +#[command(about = "Hero Supervisor - manages actors and dispatches jobs")] struct Args { - /// Path to the configuration TOML file - #[arg(short, long, value_name = "FILE")] - config: Option, - /// Redis URL for job queue - #[arg(long, default_value = "redis://localhost:6379")] + #[arg(long, default_value = "redis://127.0.0.1:6379")] redis_url: String, /// Namespace for Redis keys #[arg(long, default_value = "")] namespace: String, - /// Admin secrets (can be specified multiple times) - #[arg(long = "admin-secret", value_name = "SECRET")] + /// Admin secrets (required, can be specified multiple times) + #[arg(long = "admin-secret", value_name = "SECRET", required = true)] admin_secrets: Vec, /// User secrets (can be specified multiple times) @@ -43,14 +29,6 @@ struct Args { #[arg(long = "register-secret", value_name = "SECRET")] register_secrets: Vec, - /// Mycelium daemon URL - #[arg(long, default_value = "http://127.0.0.1:8990")] - mycelium_url: String, - - /// Mycelium topic for supervisor RPC messages - #[arg(long, default_value = "supervisor.rpc")] - topic: String, - /// Port for OpenRPC HTTP server #[arg(long, default_value = "3030")] port: u16, @@ -59,88 +37,37 @@ struct Args { #[arg(long, default_value = "127.0.0.1")] bind_address: String, - /// Bootstrap an initial admin API key with the given name - #[arg(long = "bootstrap-admin-key", value_name = "NAME")] - bootstrap_admin_key: Option, + /// Mycelium daemon URL (optional) + #[arg(long, default_value = "")] + mycelium_url: String, + + /// Mycelium topic for supervisor RPC messages + #[arg(long, default_value = "supervisor.rpc")] + topic: String, } #[tokio::main] async fn main() -> Result<(), Box> { - // Initialize logging env_logger::init(); - - info!("Starting Hero Supervisor"); - - // Parse command line arguments let args = Args::parse(); - - - // Create and initialize supervisor using builder pattern + // Build supervisor let mut builder = SupervisorBuilder::new() .redis_url(&args.redis_url) - .namespace(&args.namespace); - - // Add secrets from CLI arguments - if !args.admin_secrets.is_empty() { - info!("Adding {} admin secret(s)", args.admin_secrets.len()); - builder = builder.admin_secrets(args.admin_secrets); - } + .namespace(&args.namespace) + .admin_secrets(args.admin_secrets); if !args.user_secrets.is_empty() { - info!("Adding {} user secret(s)", args.user_secrets.len()); builder = builder.user_secrets(args.user_secrets); } if !args.register_secrets.is_empty() { - info!("Adding {} register secret(s)", args.register_secrets.len()); builder = builder.register_secrets(args.register_secrets); } - let supervisor = match args.config { - Some(_config_path) => { - info!("Loading configuration from config file not yet implemented"); - // For now, use CLI configuration - builder.build().await? - } - None => { - info!("Using CLI configuration"); - builder.build().await? - } - }; + let supervisor = builder.build().await?; - // Bootstrap admin key if requested - if let Some(admin_key_name) = args.bootstrap_admin_key { - info!("Bootstrapping admin API key: {}", admin_key_name); - let admin_key = supervisor.bootstrap_admin_key(admin_key_name).await; - println!("\n╔════════════════════════════════════════════════════════════╗"); - println!("║ 🔑 Admin API Key Created ║"); - println!("╚════════════════════════════════════════════════════════════╝"); - println!(" Name: {}", admin_key.name); - println!(" Key: {}", admin_key.key); - println!(" Scope: {}", admin_key.scope.as_str()); - println!(" ⚠️ SAVE THIS KEY - IT WILL NOT BE SHOWN AGAIN!"); - println!("╚════════════════════════════════════════════════════════════╝\n"); - } - - // Print startup information - let server_url = format!("http://{}:{}", args.bind_address, args.port); - println!("\n╔════════════════════════════════════════════════════════════╗"); - println!("║ Hero Supervisor Started ║"); - println!("╚════════════════════════════════════════════════════════════╝"); - println!(" 📡 OpenRPC Server: {}", server_url); - println!(" 🔗 Redis: {}", args.redis_url); - #[cfg(feature = "mycelium")] - if !args.mycelium_url.is_empty() { - println!(" 🌐 Mycelium: {}", args.mycelium_url); - } else { - println!(" 🌐 Mycelium: Disabled"); - } - #[cfg(not(feature = "mycelium"))] - println!(" 🌐 Mycelium: Not compiled (use --features mycelium)"); - println!("╚════════════════════════════════════════════════════════════╝\n"); - - // Start OpenRPC server in background + // Start OpenRPC server use std::sync::Arc; use tokio::sync::Mutex; use hero_supervisor::openrpc::start_http_openrpc_server; @@ -150,11 +77,8 @@ async fn main() -> Result<(), Box> { let port = args.port; tokio::spawn(async move { - info!("Starting OpenRPC server on {}:{}", bind_addr, port); match start_http_openrpc_server(supervisor_arc, &bind_addr, port).await { Ok(handle) => { - info!("OpenRPC server started successfully"); - // Keep the server running by holding the handle handle.stopped().await; error!("OpenRPC server stopped unexpectedly"); } @@ -164,12 +88,16 @@ async fn main() -> Result<(), Box> { } }); - // Give the server a moment to start tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; - let mut app = SupervisorApp::new(supervisor, args.mycelium_url, args.topic); + // Print minimal startup info + println!("📡 http://{}:{}", args.bind_address, args.port); + #[cfg(feature = "mycelium")] + if !args.mycelium_url.is_empty() { + println!("🌐 {}", args.mycelium_url); + } - // Start the complete supervisor application + let mut app = SupervisorApp::new(supervisor, args.mycelium_url, args.topic); app.start().await?; Ok(()) diff --git a/scripts/environment.sh b/scripts/environment.sh new file mode 100755 index 0000000..79afc6f --- /dev/null +++ b/scripts/environment.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Load environment variables from .env file + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +PROJECT_DIR=$(cd "$SCRIPT_DIR/.." && pwd) +ENV_FILE="$PROJECT_DIR/.env" + +if [ -f "$ENV_FILE" ]; then + # Export variables from .env file + set -a + source "$ENV_FILE" + set +a + echo "✅ Loaded environment from .env" +else + echo "⚠️ No .env file found at $ENV_FILE" + echo " Copy .env.example to .env and configure your settings" + exit 1 +fi diff --git a/scripts/generate_secret.sh b/scripts/generate_secret.sh new file mode 100755 index 0000000..721990b --- /dev/null +++ b/scripts/generate_secret.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# Generate a supervisor secret key in the correct format + +# Generate a random 32-byte hex string +SECRET=$(openssl rand -hex 32) + +echo "Generated supervisor secret:" +echo "$SECRET" +echo "" +echo "Add this to your .env file:" +echo "SUPERVISOR_ADMIN_SECRET=$SECRET" diff --git a/scripts/run.sh b/scripts/run.sh index 5186e09..a40abb2 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -1,64 +1,134 @@ #!/bin/bash -set -e SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) PROJECT_DIR=$(cd "$SCRIPT_DIR/.." && pwd) -# Build first -"$SCRIPT_DIR/build.sh" +# Load environment variables +source "$SCRIPT_DIR/environment.sh" -# Configuration -REDIS_URL="${REDIS_URL:-redis://localhost:6379}" +# Spinner function +spinner() { + local pid=$1 + local delay=0.1 + local spinstr='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' + while ps -p $pid > /dev/null 2>&1; do + local temp=${spinstr#?} + printf " [%c] " "$spinstr" + local spinstr=$temp${spinstr%"$temp"} + sleep $delay + printf "\b\b\b\b\b\b" + done + printf " \b\b\b\b" +} + +echo "Starting Hero Supervisor" +echo "" + +# Build +printf "🔨 Building... " +if "$SCRIPT_DIR/build.sh" > /tmp/supervisor-run-build.log 2>&1 & spinner $!; wait $!; then + echo "✅" +else + echo "❌" + echo " Error: Build failed. Check /tmp/supervisor-run-build.log" + exit 1 +fi + +# Validate required environment variables +if [ -z "$ADMIN_SECRETS" ]; then + echo "❌ Error: ADMIN_SECRETS not set in .env" + echo " Generate a secret with: ./scripts/generate_secret.sh" + exit 1 +fi + +# Set defaults +REDIS_URL="${REDIS_URL:-redis://127.0.0.1:6379}" PORT="${PORT:-3030}" BIND_ADDRESS="${BIND_ADDRESS:-127.0.0.1}" -BOOTSTRAP_ADMIN_KEY="${BOOTSTRAP_ADMIN_KEY:-admin}" ADMIN_UI_PORT="${ADMIN_UI_PORT:-8080}" -LOG_LEVEL="${LOG_LEVEL:-info}" +LOG_LEVEL="${LOG_LEVEL:-error}" # Cleanup function cleanup() { - echo "Shutting down..." + echo "" + printf "🛑 Stopping... " kill $(jobs -p) 2>/dev/null || true + echo "✅" exit 0 } trap cleanup SIGINT SIGTERM -echo "Starting Hero Supervisor..." +# Start supervisor +printf "📡 Supervisor... " cd "$PROJECT_DIR" -# Start supervisor in background -RUST_LOG="$LOG_LEVEL" RUST_LOG_STYLE=never \ -target/release/supervisor \ - --bootstrap-admin-key "$BOOTSTRAP_ADMIN_KEY" \ - --redis-url "$REDIS_URL" \ - --port "$PORT" \ - --bind-address "$BIND_ADDRESS" & +# Build command with flags +SUPERVISOR_CMD="target/release/supervisor --redis-url $REDIS_URL --port $PORT --bind-address $BIND_ADDRESS" -SUPERVISOR_PID=$! +# Add admin secrets +IFS=',' read -ra SECRETS <<< "$ADMIN_SECRETS" +for secret in "${SECRETS[@]}"; do + SUPERVISOR_CMD="$SUPERVISOR_CMD --admin-secret $secret" +done -# Wait for supervisor to start -sleep 2 - -# Check if supervisor is running -if ! ps -p $SUPERVISOR_PID > /dev/null 2>&1; then - echo "Failed to start supervisor" - exit 1 +# Add user secrets if provided +if [ ! -z "$USER_SECRETS" ]; then + IFS=',' read -ra SECRETS <<< "$USER_SECRETS" + for secret in "${SECRETS[@]}"; do + SUPERVISOR_CMD="$SUPERVISOR_CMD --user-secret $secret" + done fi +# Add register secrets if provided +if [ ! -z "$REGISTER_SECRETS" ]; then + IFS=',' read -ra SECRETS <<< "$REGISTER_SECRETS" + for secret in "${SECRETS[@]}"; do + SUPERVISOR_CMD="$SUPERVISOR_CMD --register-secret $secret" + done +fi + +# Add mycelium URL if provided +if [ ! -z "$MYCELIUM_URL" ]; then + SUPERVISOR_CMD="$SUPERVISOR_CMD --mycelium-url $MYCELIUM_URL" +fi + +RUST_LOG="$LOG_LEVEL" RUST_LOG_STYLE=never $SUPERVISOR_CMD > /tmp/supervisor-run.log 2>&1 & +SUPERVISOR_PID=$! + +sleep 2 + +if ! ps -p $SUPERVISOR_PID > /dev/null 2>&1; then + echo "❌" + echo " Error: Supervisor failed to start. Check /tmp/supervisor-run.log" + exit 1 +fi +echo "✅" + # Start admin UI -echo "Starting Admin UI on port $ADMIN_UI_PORT..." +printf "🎨 Admin UI... " cd "$PROJECT_DIR/ui" -trunk serve --port "$ADMIN_UI_PORT" & -ADMIN_UI_PID=$! +if ! command -v trunk &> /dev/null; then + echo "⚠️ (trunk not installed)" +else + trunk serve --port "$ADMIN_UI_PORT" > /tmp/supervisor-ui.log 2>&1 & + ADMIN_UI_PID=$! + sleep 1 + if ps -p $ADMIN_UI_PID > /dev/null 2>&1; then + echo "✅" + else + echo "❌" + fi +fi echo "" -echo "✅ Hero Supervisor system started" -echo " 📡 Supervisor API: http://$BIND_ADDRESS:$PORT" -echo " 🎨 Admin UI: http://127.0.0.1:$ADMIN_UI_PORT" +echo "📡 http://$BIND_ADDRESS:$PORT" +if [ ! -z "$ADMIN_UI_PID" ] && ps -p $ADMIN_UI_PID > /dev/null 2>&1; then + echo "🎨 http://127.0.0.1:$ADMIN_UI_PORT" +fi echo "" -echo "Press Ctrl+C to stop all services" +echo "Press Ctrl+C to stop" -# Wait for both processes +# Wait for processes wait \ No newline at end of file diff --git a/scripts/run_supervisor_simple.sh b/scripts/run_supervisor_simple.sh deleted file mode 100755 index 32079d2..0000000 --- a/scripts/run_supervisor_simple.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -# Run Hero Supervisor with OpenRPC server only (no Mycelium) -# -# This starts the supervisor with: -# - OpenRPC HTTP server on port 3030 -# - Redis connection for job queuing -# - No Mycelium integration -# -# Usage: -# ./run_supervisor_simple.sh - -echo "🚀 Starting Hero Supervisor (OpenRPC only)" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo " OpenRPC Server: http://localhost:3030" -echo " Redis: redis://localhost:6379" -echo " Mycelium: Disabled" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo "" - -# Set environment variables -export RUST_LOG=info -export MYCELIUM_URL="" # Disable Mycelium - -# Build and run -cargo run --bin supervisor --no-default-features --features cli -- \ - --redis-url redis://localhost:6379 \ - --port 3030