feat: simplify OpenRPC API and reorganize examples

- Simplified RunnerConfig to just name, command, and optional env
- Removed RunnerType and ProcessManagerType enums
- Removed db_path, redis_url, binary_path from config
- Made runner name also serve as queue name (no separate queue param)
- Added secret-based authentication to all runner management methods
- Created comprehensive osiris_openrpc example
- Archived old examples to _archive/
- Updated client API to match simplified supervisor interface
This commit is contained in:
Timur Gordon
2025-10-27 14:20:40 +01:00
parent ef56ed0290
commit 98b2718d58
33 changed files with 3018 additions and 788 deletions

1119
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -38,14 +38,10 @@ anyhow = "1.0"
tower-http = { version = "0.5", features = ["cors"] }
tower = "0.4"
# Base64 encoding for Mycelium payloads
base64 = "0.22"
# Random number generation for message IDs
rand = "0.8"
# HTTP client for Mycelium integration
reqwest = { version = "0.12", features = ["json"] }
# Mycelium integration (optional)
base64 = { version = "0.22", optional = true }
rand = { version = "0.8", optional = true }
reqwest = { version = "0.12", features = ["json"], optional = true }
[dev-dependencies]
tokio-test = "0.4"
@@ -55,6 +51,7 @@ escargot = "0.5"
[features]
default = ["cli"]
cli = []
mycelium = ["base64", "rand", "reqwest"]
[[bin]]
name = "supervisor"
@@ -62,14 +59,6 @@ path = "cmd/supervisor.rs"
# Examples
[[example]]
name = "openrpc_comprehensive"
path = "examples/basic_openrpc_client.rs"
[[example]]
name = "mock_runner"
path = "examples/mock_runner.rs"
[[example]]
name = "supervisor"
path = "examples/supervisor/run_supervisor.rs"
name = "osiris_openrpc"
path = "examples/osiris_openrpc/main.rs"

268
MYCELIUM_OPTIONAL.md Normal file
View File

@@ -0,0 +1,268 @@
# Mycelium Integration - Now Optional!
The Mycelium integration is now an optional feature. The supervisor can run with just the OpenRPC HTTP server, making it simpler to use and deploy.
## What Changed
### Before
- Mycelium integration was always enabled
- Supervisor would continuously try to connect to Mycelium on port 8990
- Error logs if Mycelium wasn't available
- Required additional dependencies
### After
- ✅ Mycelium is now an optional feature
- ✅ Supervisor runs with clean OpenRPC HTTP server by default
- ✅ No connection errors when Mycelium isn't needed
- ✅ Smaller binary size without Mycelium dependencies
## Running the Supervisor
### Option 1: Simple OpenRPC Server (Recommended)
**No Mycelium, just OpenRPC:**
```bash
# Using the helper script
./run_supervisor_simple.sh
# Or manually
MYCELIUM_URL="" cargo run --bin supervisor -- \
--redis-url redis://localhost:6379 \
--port 3030
```
This starts:
- ✅ OpenRPC HTTP server on port 3030
- ✅ Redis connection for job queuing
- ❌ No Mycelium integration
### Option 2: With Mycelium Integration
**Enable Mycelium feature:**
```bash
# Build with Mycelium support
cargo build --bin supervisor --features mycelium
# Run with Mycelium URL
MYCELIUM_URL="http://localhost:8990" cargo run --bin supervisor --features mycelium -- \
--redis-url redis://localhost:6379 \
--port 3030
```
This starts:
- ✅ OpenRPC HTTP server on port 3030
- ✅ Redis connection for job queuing
- ✅ Mycelium integration (connects to daemon)
## Feature Flags
### Available Features
| Feature | Description | Default |
|---------|-------------|---------|
| `cli` | Command-line interface | ✅ Yes |
| `mycelium` | Mycelium integration | ❌ No |
### Building with Features
```bash
# Default build (CLI only, no Mycelium)
cargo build --bin supervisor
# With Mycelium
cargo build --bin supervisor --features mycelium
# Minimal (no CLI, no Mycelium)
cargo build --bin supervisor --no-default-features
```
## Architecture
### Without Mycelium (Default)
```
┌─────────────────┐
│ Client │
└────────┬────────┘
│ HTTP/JSON-RPC
┌─────────────────┐
│ Supervisor │
│ OpenRPC Server │
│ (Port 3030) │
└────────┬────────┘
│ Redis
┌─────────────────┐
│ Runners │
└─────────────────┘
```
### With Mycelium (Optional)
```
┌─────────────────┐
│ Client │
└────────┬────────┘
│ HTTP/JSON-RPC
┌─────────────────┐ ┌──────────────┐
│ Supervisor │◄────►│ Mycelium │
│ OpenRPC Server │ │ Daemon │
│ (Port 3030) │ │ (Port 8990) │
└────────┬────────┘ └──────────────┘
│ Redis
┌─────────────────┐
│ Runners │
└─────────────────┘
```
## Environment Variables
| Variable | Description | Default | Required |
|----------|-------------|---------|----------|
| `MYCELIUM_URL` | Mycelium daemon URL | `http://127.0.0.1:8990` | No |
| `RUST_LOG` | Log level | `info` | No |
**To disable Mycelium:**
```bash
export MYCELIUM_URL=""
```
## Dependencies
### Core Dependencies (Always)
- `tokio` - Async runtime
- `redis` - Job queuing
- `jsonrpsee` - OpenRPC server
- `runner_rust` - Job model
### Mycelium Dependencies (Optional)
- `reqwest` - HTTP client
- `base64` - Encoding
- `rand` - Random IDs
## Examples
All examples work without Mycelium:
```bash
# Simple end-to-end example
RUST_LOG=info cargo run --example simple_e2e
# Full automated demo
RUST_LOG=info cargo run --example end_to_end_demo
```
## Migration Guide
### If you were using Mycelium
**Before:**
```bash
cargo run --bin supervisor
# Would try to connect to Mycelium automatically
```
**After:**
```bash
# Option A: Disable Mycelium (recommended for most use cases)
MYCELIUM_URL="" cargo run --bin supervisor
# Option B: Enable Mycelium feature
cargo run --bin supervisor --features mycelium
```
### If you weren't using Mycelium
**Before:**
```bash
cargo run --bin supervisor
# Would see connection errors to port 8990
```
**After:**
```bash
cargo run --bin supervisor
# Clean startup, no connection errors! 🎉
```
## Benefits
### For Development
- ✅ Faster builds (fewer dependencies)
- ✅ Simpler setup (no Mycelium daemon needed)
- ✅ Cleaner logs (no connection errors)
- ✅ Easier debugging
### For Production
- ✅ Smaller binary size
- ✅ Fewer runtime dependencies
- ✅ More flexible deployment
- ✅ Optional advanced features
## Testing
### Test without Mycelium
```bash
# Build
cargo build --bin supervisor
# Run tests
cargo test
# Run examples
cargo run --example simple_e2e
```
### Test with Mycelium
```bash
# Build with feature
cargo build --bin supervisor --features mycelium
# Start Mycelium daemon (if you have one)
# mycelium-daemon --port 8990
# Run supervisor
MYCELIUM_URL="http://localhost:8990" cargo run --bin supervisor --features mycelium
```
## Troubleshooting
### "Mycelium integration not enabled"
This is informational, not an error. If you need Mycelium:
```bash
cargo build --features mycelium
```
### "HTTP request failed: error sending request"
If you see this with Mycelium enabled, check:
1. Is Mycelium daemon running?
2. Is the URL correct? (`MYCELIUM_URL`)
3. Is the port accessible?
Or simply disable Mycelium:
```bash
export MYCELIUM_URL=""
```
## Summary
🎉 **The supervisor now runs cleanly with just OpenRPC!**
- Default: OpenRPC HTTP server only
- Optional: Enable Mycelium with `--features mycelium`
- No more connection errors when Mycelium isn't needed
- Simpler, faster, cleaner!
---
**Status:** ✅ Complete
**Version:** 0.1.0
**Last Updated:** 2025-10-24

214
QUICK_START.md Normal file
View File

@@ -0,0 +1,214 @@
# Quick Start Guide
Complete guide to running the Hero Supervisor with OSIS runners and examples.
## Prerequisites
1. **Redis** - Must be running
2. **Rust** - Version 1.88+ (run `rustup update`)
## 1. Start Redis
```bash
redis-server
```
## 2. Start Supervisor
```bash
cd /Users/timurgordon/code/git.ourworld.tf/herocode/supervisor
cargo run --bin supervisor
```
You should see:
```
╔════════════════════════════════════════════════════════════╗
║ Hero Supervisor Started ║
╚════════════════════════════════════════════════════════════╝
📡 OpenRPC Server: http://127.0.0.1:3030
🔗 Redis: redis://localhost:6379
🌐 Mycelium: Not compiled (use --features mycelium)
╚════════════════════════════════════════════════════════════╝
```
## 3. Start OSIS Runner
```bash
cd /Users/timurgordon/code/git.ourworld.tf/herocode/runner_rust
cargo run --bin runner_osis -- test_runner \
--redis-url redis://localhost:6379 \
--db-path /tmp/test_runner.db
```
You should see:
```
Starting OSIS Sync Runner with ID: test_runner
Database path: /tmp/test_runner.db
Redis URL: redis://localhost:6379
OSIS Sync Runner 'test_runner' started successfully
```
## 4. Run Example
```bash
cd /Users/timurgordon/code/git.ourworld.tf/herocode/supervisor
RUST_LOG=info cargo run --example simple_e2e
```
## Terminal Layout
```
┌─────────────────────┬─────────────────────┐
│ Terminal 1 │ Terminal 2 │
│ Redis │ Supervisor │
│ redis-server │ cargo run --bin │
│ │ supervisor │
├─────────────────────┼─────────────────────┤
│ Terminal 3 │ Terminal 4 │
│ OSIS Runner │ Example │
│ cargo run --bin │ cargo run │
│ runner_osis │ --example │
│ │ simple_e2e │
└─────────────────────┴─────────────────────┘
```
## What Each Component Does
### Redis
- Job queue storage
- Job result storage
- Runner coordination
### Supervisor
- OpenRPC HTTP server (port 3030)
- Job dispatch to runners
- Runner registration
- Job execution coordination
### OSIS Runner
- Listens for jobs on Redis queue
- Executes Rhai scripts
- Stores results back to Redis
- Uses HeroDB for data persistence
### Example
- Creates jobs with Rhai scripts
- Sends jobs to supervisor via OpenRPC
- Receives results
- Demonstrates both blocking and non-blocking modes
## Architecture
```
┌─────────────┐
│ Example │ (simple_e2e.rs)
└──────┬──────┘
│ HTTP/JSON-RPC
┌─────────────┐
│ Supervisor │ (port 3030)
└──────┬──────┘
│ Redis Queue
┌─────────────┐
│ OSIS Runner │ (test_runner)
└──────┬──────┘
┌─────────────┐
│ HeroDB │ (Redis + local DB)
└─────────────┘
```
## Troubleshooting
### "Connection refused" on port 3030
- Make sure supervisor is running
- Check if another process is using port 3030
### "Connection refused" on port 6379
- Make sure Redis is running
- Check: `redis-cli ping` (should return "PONG")
### Runner not receiving jobs
- Check runner is registered: Look for "Runner registered successfully" in example output
- Check Redis connection: Both supervisor and runner must use same Redis URL
- Check queue name matches: Should be `hero:q:work:type:osis:group:default:inst:test_runner`
### "Job execution timeout"
- Increase timeout in job builder: `.timeout(120)`
- Check if runner is actually processing jobs (look for logs)
## Example Output
### Successful Run
```
╔════════════════════════════════════════╗
║ Simple End-to-End Demo ║
╚════════════════════════════════════════╝
📋 Step 1: Registering Runner
─────────────────────────────────────────
✅ Runner registered successfully
📋 Step 2: Running a Simple Job (Blocking)
─────────────────────────────────────────
✅ Job completed!
Result: {"message":"Hello from the runner!","number":42}
📋 Step 3: Running a Calculation Job
─────────────────────────────────────────
✅ Calculation completed!
Result: {"sum":55,"product":3628800,"count":10}
📋 Step 4: Starting a Non-Blocking Job
─────────────────────────────────────────
✅ Job started!
Job ID: abc-123 (running in background)
🎉 Demo completed successfully!
```
## Next Steps
1. **Try different Rhai scripts** - Modify the payload in examples
2. **Add more runners** - Start multiple runners with different IDs
3. **Explore the API** - Use the OpenRPC client library
4. **Build your own client** - See `clients/openrpc/` for examples
## Useful Commands
```bash
# Check Redis
redis-cli ping
# List Redis keys
redis-cli keys "hero:*"
# Monitor Redis commands
redis-cli monitor
# Check supervisor is running
curl http://localhost:3030
# View runner logs
# (check terminal where runner is running)
```
## Clean Up
```bash
# Stop all processes (Ctrl+C in each terminal)
# Clean up test database
rm /tmp/test_runner.db
# (Optional) Flush Redis
redis-cli FLUSHALL
```
---
**Status:** ✅ Ready to Use
**Last Updated:** 2025-10-24

View File

@@ -26,6 +26,7 @@ indexmap = "2.0"
jsonrpsee = { version = "0.24", features = ["http-client", "macros"] }
tokio = { version = "1.0", features = ["full"] }
hero-supervisor = { path = "../.." }
runner_rust = { git = "https://git.ourworld.tf/herocode/runner_rust.git", branch = "main" }
env_logger = "0.11"
# WASM-specific dependencies

View File

@@ -115,40 +115,16 @@ impl From<wasm_bindgen::JsValue> for ClientError {
/// Result type for client operations
pub type ClientResult<T> = Result<T, ClientError>;
/// Types of runners supported by the supervisor
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum RunnerType {
/// SAL Runner for system abstraction layer operations
SALRunner,
/// OSIS Runner for operating system interface operations
OSISRunner,
/// V Runner for virtualization operations
VRunner,
/// Python Runner for Python-based actors
PyRunner,
}
/// Process manager type for WASM compatibility
#[cfg(target_arch = "wasm32")]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ProcessManagerType {
Simple,
Tmux(String),
}
/// Configuration for an actor runner
/// Configuration for a runner
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunnerConfig {
/// Unique identifier for the actor
pub actor_id: String,
/// Type of runner
pub runner_type: RunnerType,
/// Path to the actor binary
pub binary_path: PathBuf,
/// Database path for the actor
pub db_path: String,
/// Redis URL for job queue
pub redis_url: String,
/// Name of the runner
pub name: String,
/// Command to run the runner (full command line)
pub command: String,
/// Optional environment variables
#[serde(skip_serializing_if = "Option::is_none")]
pub env: Option<std::collections::HashMap<String, String>>,
}
@@ -171,6 +147,21 @@ pub struct JobStatusResponse {
pub completed_at: Option<String>,
}
/// Response from job.run (blocking execution)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobRunResponse {
pub job_id: String,
pub status: String,
pub result: Option<String>,
}
/// Response from job.start (non-blocking execution)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobStartResponse {
pub job_id: String,
pub status: String,
}
// Re-export Job types from runner_rust crate (native only)
#[cfg(not(target_arch = "wasm32"))]
pub use runner_rust::{Job, JobStatus, JobError, JobBuilder, Client, ClientBuilder};
@@ -219,7 +210,7 @@ pub type ProcessStatus = ProcessStatusWrapper;
/// Re-export types from supervisor crate for native builds
#[cfg(not(target_arch = "wasm32"))]
pub use hero_supervisor::{ProcessManagerType, RunnerStatus};
pub use hero_supervisor::RunnerStatus;
#[cfg(not(target_arch = "wasm32"))]
pub use hero_supervisor::runner::LogInfo;
@@ -262,17 +253,17 @@ impl SupervisorClient {
}
/// Register a new runner to the supervisor with secret authentication
/// The runner name is also used as the queue name
pub async fn register_runner(
&self,
secret: &str,
name: &str,
queue: &str,
) -> ClientResult<()> {
let _: () = self
.client
.request(
"register_runner",
rpc_params![secret, name, queue],
rpc_params![secret, name],
)
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
@@ -305,40 +296,47 @@ impl SupervisorClient {
Ok(jobs)
}
/// Run a job on the appropriate runner and return the result
/// Run a job on the appropriate runner and wait for the result (blocking)
/// This method queues the job and waits for completion before returning
pub async fn job_run(
&self,
secret: &str,
job: Job,
) -> ClientResult<JobResult> {
let params = serde_json::json!({
timeout: Option<u64>,
) -> ClientResult<JobRunResponse> {
let mut params = serde_json::json!({
"secret": secret,
"job": job
});
let result: JobResult = self
if let Some(t) = timeout {
params["timeout"] = serde_json::json!(t);
}
let result: JobRunResponse = self
.client
.request("job.run", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(result)
}
/// Start a previously created job by queuing it to its assigned runner
/// Start a job without waiting for the result (non-blocking)
/// This method queues the job and returns immediately with the job_id
pub async fn job_start(
&self,
secret: &str,
job_id: &str,
) -> ClientResult<()> {
job: Job,
) -> ClientResult<JobStartResponse> {
let params = serde_json::json!({
"secret": secret,
"job_id": job_id
"job": job
});
let _: () = self
let result: JobStartResponse = self
.client
.request("job.start", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
Ok(result)
}
/// Get the current status of a job
@@ -360,10 +358,14 @@ impl SupervisorClient {
}
/// Remove a runner from the supervisor
pub async fn remove_runner(&self, actor_id: &str) -> ClientResult<()> {
pub async fn remove_runner(&self, secret: &str, actor_id: &str) -> ClientResult<()> {
let params = serde_json::json!({
"secret": secret,
"actor_id": actor_id
});
let _: () = self
.client
.request("remove_runner", rpc_params![actor_id])
.request("remove_runner", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
@@ -378,37 +380,54 @@ impl SupervisorClient {
}
/// Start a specific runner
pub async fn start_runner(&self, actor_id: &str) -> ClientResult<()> {
pub async fn start_runner(&self, secret: &str, actor_id: &str) -> ClientResult<()> {
let params = serde_json::json!({
"secret": secret,
"actor_id": actor_id
});
let _: () = self
.client
.request("start_runner", rpc_params![actor_id])
.request("start_runner", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// Stop a specific runner
pub async fn stop_runner(&self, actor_id: &str, force: bool) -> ClientResult<()> {
pub async fn stop_runner(&self, secret: &str, actor_id: &str, force: bool) -> ClientResult<()> {
let params = serde_json::json!({
"secret": secret,
"actor_id": actor_id,
"force": force
});
let _: () = self
.client
.request("stop_runner", rpc_params![actor_id, force])
.request("stop_runner", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// Add a runner to the supervisor
pub async fn add_runner(&self, config: RunnerConfig, process_manager: ProcessManagerType) -> ClientResult<()> {
pub async fn add_runner(&self, secret: &str, config: RunnerConfig) -> ClientResult<()> {
let params = serde_json::json!({
"secret": secret,
"config": config
});
let _: () = self
.client
.request("add_runner", rpc_params![config, process_manager])
.request("add_runner", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// Get status of a specific runner
pub async fn get_runner_status(&self, actor_id: &str) -> ClientResult<RunnerStatus> {
pub async fn get_runner_status(&self, secret: &str, actor_id: &str) -> ClientResult<RunnerStatus> {
let params = serde_json::json!({
"secret": secret,
"actor_id": actor_id
});
let status: RunnerStatus = self
.client
.request("get_runner_status", rpc_params![actor_id])
.request("get_runner_status", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(status)
}

View File

@@ -50,6 +50,14 @@ struct Args {
/// 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,
/// Bind address for OpenRPC HTTP server
#[arg(long, default_value = "127.0.0.1")]
bind_address: String,
}
#[tokio::main]
@@ -97,6 +105,42 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
};
// 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
use std::sync::Arc;
use tokio::sync::Mutex;
use hero_supervisor::openrpc::start_http_openrpc_server;
let supervisor_arc = Arc::new(Mutex::new(supervisor.clone()));
let bind_addr = args.bind_address.clone();
let port = args.port;
tokio::spawn(async move {
info!("Starting OpenRPC server on {}:{}", bind_addr, port);
if let Err(e) = start_http_openrpc_server(supervisor_arc, &bind_addr, port).await {
error!("OpenRPC server error: {}", e);
}
});
// 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);
// Start the complete supervisor application

View File

@@ -1,182 +1,74 @@
# Hero Supervisor Examples
This directory contains examples demonstrating the new job API functionality and workflows.
This directory contains examples demonstrating Hero Supervisor functionality.
## Examples Overview
## Available Examples
### 1. `job_api_examples.rs` - Comprehensive API Demo
Complete demonstration of all new job API methods:
- **Fire-and-forget execution** using `job.run`
- **Asynchronous processing** with `jobs.create`, `job.start`, `job.status`, `job.result`
- **Batch job processing** for multiple jobs
- **Job listing** with `jobs.list`
### osiris_openrpc
**Run with:**
Comprehensive example showing the complete workflow of using Hero Supervisor with OSIRIS runners via OpenRPC.
**Features:**
- Automatic supervisor and runner startup
- OpenRPC client communication
- Runner registration and management
- Job dispatching with multiple scripts
- Context-based access control
- Graceful shutdown
**Run:**
```bash
cargo run --example job_api_examples
cargo run --example osiris_openrpc
```
### 2. `simple_job_workflow.rs` - Basic Workflow
Simple example showing the basic job lifecycle:
1. Create job with `jobs.create`
2. Start job with `job.start`
3. Monitor with `job.status`
4. Get result with `job.result`
**Run with:**
```bash
cargo run --example simple_job_workflow
```
### 3. `integration_test.rs` - Integration Tests
Comprehensive integration tests validating:
- Complete job lifecycle
- Immediate job execution
- Job listing functionality
- Authentication error handling
- Nonexistent job operations
**Run with:**
```bash
cargo test --test integration_test
```
See [osiris_openrpc/README.md](osiris_openrpc/README.md) for details.
## Prerequisites
Before running the examples, ensure:
All examples require:
- Redis server running on `localhost:6379`
- Rust toolchain installed
1. **Redis is running:**
```bash
docker run -d -p 6379:6379 redis:alpine
```
## Example Structure
2. **Supervisor is running:**
```bash
./target/debug/supervisor --config examples/supervisor/config.toml
```
3. **Runners are configured** in your config.toml:
```toml
[[actors]]
id = "osis_runner_1"
name = "osis_runner_1"
binary_path = "/path/to/osis_runner"
db_path = "/tmp/osis_db"
redis_url = "redis://localhost:6379"
process_manager = "simple"
```
## API Convention Summary
The examples demonstrate the new job API convention:
### General Operations (`jobs.`)
- `jobs.create` - Create a job without queuing it
- `jobs.list` - List all job IDs in the system
### Specific Operations (`job.`)
- `job.run` - Run a job immediately and return result
- `job.start` - Start a previously created job
- `job.status` - Get current job status (non-blocking)
- `job.result` - Get job result (blocking until complete)
## Workflow Patterns
### Pattern 1: Fire-and-Forget
```rust
let result = client.job_run(secret, job).await?;
match result {
JobResult::Success { success } => println!("Output: {}", success),
JobResult::Error { error } => println!("Error: {}", error),
}
```
examples/
├── README.md # This file
├── osiris_openrpc/ # OSIRIS + OpenRPC example
│ ├── main.rs # Main example code
│ ├── README.md # Detailed documentation
├── note.rhai # Note creation script
│ ├── event.rhai # Event creation script
│ ├── query.rhai # Query script
└── access_denied.rhai # Access control test script
└── _archive/ # Archived old examples
```
### Pattern 2: Asynchronous Processing
```rust
// Create and start
let job_id = client.jobs_create(secret, job).await?;
client.job_start(secret, &job_id).await?;
## Architecture Overview
// Monitor (non-blocking)
loop {
let status = client.job_status(&job_id).await?;
if status.status == "completed" { break; }
sleep(Duration::from_secs(1)).await;
}
The examples demonstrate the Hero Supervisor architecture:
// Get result
let result = client.job_result(&job_id).await?;
```
Client (OpenRPC)
Supervisor (OpenRPC Server)
Redis Queue
Runners (OSIRIS, SAL, etc.)
```
### Pattern 3: Batch Processing
```rust
// Create all jobs
let mut job_ids = Vec::new();
for job_spec in job_specs {
let job_id = client.jobs_create(secret, job_spec).await?;
job_ids.push(job_id);
}
## Development
// Start all jobs
for job_id in &job_ids {
client.job_start(secret, job_id).await?;
}
To add a new example:
// Collect results
for job_id in &job_ids {
let result = client.job_result(job_id).await?;
// Process result...
}
```
1. Create a new directory under `examples/`
2. Add `main.rs` with your example code
3. Add any required script files (`.rhai`)
4. Add a `README.md` documenting the example
5. Update `Cargo.toml` to register the example
6. Update this README with a link
## Error Handling
## Archived Examples
The examples demonstrate proper error handling for:
- **Authentication errors** - Invalid secrets
- **Job not found errors** - Nonexistent job IDs
- **Connection errors** - Supervisor not available
- **Execution errors** - Job failures
## Authentication
Examples use different secret types:
- **Admin secrets**: Full system access
- **User secrets**: Job operations only (used in examples)
- **Register secrets**: Runner registration only
Configure secrets in your supervisor config:
```toml
admin_secrets = ["admin-secret-123"]
user_secrets = ["user-secret-456"]
register_secrets = ["register-secret-789"]
```
## Troubleshooting
### Common Issues
1. **Connection refused**
- Ensure supervisor is running on localhost:3030
- Check supervisor logs for errors
2. **Authentication failed**
- Verify secret is configured in supervisor
- Check secret type matches operation requirements
3. **Job execution failed**
- Ensure runners are properly configured and running
- Check runner logs for execution errors
- Verify job payload is valid for the target runner
4. **Redis connection failed**
- Ensure Redis is running on localhost:6379
- Check Redis connectivity from supervisor
### Debug Mode
Run examples with debug logging:
```bash
RUST_LOG=debug cargo run --example job_api_examples
```
This will show detailed API calls and responses for troubleshooting.
Previous examples have been moved to `_archive/` for reference. These may be outdated but can provide useful patterns for specific use cases.

View File

@@ -0,0 +1,364 @@
# End-to-End Examples
Complete examples demonstrating the full Supervisor + Runner + Client workflow.
## Overview
These examples show how to:
1. Start a Hero Supervisor
2. Start an OSIS Runner
3. Register the runner with the supervisor
4. Execute jobs using both blocking (`job.run`) and non-blocking (`job.start`) modes
## Prerequisites
### Required Services
1. **Redis** - Must be running on `localhost:6379`
```bash
redis-server
```
2. **Supervisor** - Hero Supervisor with Mycelium integration
```bash
cargo run --bin hero-supervisor -- --redis-url redis://localhost:6379
```
3. **Runner** - OSIS Runner to execute jobs
```bash
cargo run --bin runner_osis -- test_runner --redis-url redis://localhost:6379
```
## Examples
### 1. Simple End-to-End (`simple_e2e.rs`)
**Recommended for beginners** - A minimal example with clear step-by-step execution.
#### What it does:
- Registers a runner with the supervisor
- Runs 2 blocking jobs (with immediate results)
- Starts 1 non-blocking job (fire and forget)
- Shows clear output at each step
#### How to run:
**Terminal 1 - Redis:**
```bash
redis-server
```
**Terminal 2 - Supervisor:**
```bash
cd /Users/timurgordon/code/git.ourworld.tf/herocode/supervisor
RUST_LOG=info cargo run --bin hero-supervisor -- --redis-url redis://localhost:6379
```
**Terminal 3 - Runner:**
```bash
cd /Users/timurgordon/code/git.ourworld.tf/herocode/runner_rust
RUST_LOG=info cargo run --bin runner_osis -- test_runner \
--redis-url redis://localhost:6379 \
--db-path /tmp/test_runner.db
```
**Terminal 4 - Demo:**
```bash
cd /Users/timurgordon/code/git.ourworld.tf/herocode/supervisor
RUST_LOG=info cargo run --example simple_e2e
```
#### Expected Output:
```
╔════════════════════════════════════════╗
║ Simple End-to-End Demo ║
╚════════════════════════════════════════╝
📋 Step 1: Registering Runner
─────────────────────────────────────────
✅ Runner registered successfully
📋 Step 2: Running a Simple Job (Blocking)
─────────────────────────────────────────
✅ Job completed!
Result: {"message":"Hello from the runner!","number":42,"timestamp":1234567890}
📋 Step 3: Running a Calculation Job
─────────────────────────────────────────
✅ Calculation completed!
Result: {"sum":55,"product":3628800,"count":10,"average":5}
📋 Step 4: Starting a Non-Blocking Job
─────────────────────────────────────────
✅ Job started!
Job ID: abc-123 (running in background)
🎉 Demo completed successfully!
```
### 2. Full End-to-End (`end_to_end_demo.rs`)
**Advanced** - Automatically spawns supervisor and runner processes.
#### What it does:
- Automatically starts supervisor and runner
- Runs multiple test jobs
- Demonstrates both execution modes
- Handles cleanup automatically
#### How to run:
**Terminal 1 - Redis:**
```bash
redis-server
```
**Terminal 2 - Demo:**
```bash
cd /Users/timurgordon/code/git.ourworld.tf/herocode/supervisor
RUST_LOG=info cargo run --example end_to_end_demo
```
#### Features:
- ✅ Automatic process management
- ✅ Multiple job examples
- ✅ Graceful shutdown
- ✅ Comprehensive logging
## Job Execution Modes
### job.run (Blocking)
Executes a job and waits for the result.
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "job.run",
"params": [{
"secret": "admin_secret",
"job": { /* job object */ },
"timeout": 30
}],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": {
"job_id": "uuid",
"status": "completed",
"result": "{ /* actual result */ }"
},
"id": 1
}
```
**Use when:**
- You need immediate results
- Job completes quickly (< 60 seconds)
- Synchronous workflow
### job.start (Non-Blocking)
Starts a job and returns immediately.
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "job.start",
"params": [{
"secret": "admin_secret",
"job": { /* job object */ }
}],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": {
"job_id": "uuid",
"status": "queued"
},
"id": 1
}
```
**Use when:**
- Long-running operations
- Background processing
- Async workflows
- Don't need immediate results
## Job Structure
Jobs are created using the `JobBuilder`:
```rust
use runner_rust::job::JobBuilder;
let job = JobBuilder::new()
.caller_id("my_client")
.context_id("my_context")
.payload(r#"
// Rhai script to execute
let result = 2 + 2;
to_json(result)
"#)
.runner("runner_name")
.executor("rhai")
.timeout(30)
.build()?;
```
### Job Fields
- **caller_id**: Identifier for the client making the request
- **context_id**: Context for the job execution
- **payload**: Rhai script to execute
- **runner**: Name of the runner to execute on
- **executor**: Type of executor (always "rhai" for OSIS)
- **timeout**: Maximum execution time in seconds
## Rhai Script Examples
### Simple Calculation
```rhai
let result = 2 + 2;
to_json(result)
```
### String Manipulation
```rhai
let message = "Hello, World!";
let upper = message.to_upper();
to_json(upper)
```
### Array Operations
```rhai
let numbers = [1, 2, 3, 4, 5];
let sum = 0;
for n in numbers {
sum += n;
}
to_json(#{sum: sum, count: numbers.len()})
```
### Object Creation
```rhai
let person = #{
name: "Alice",
age: 30,
email: "alice@example.com"
};
to_json(person)
```
## Troubleshooting
### "Failed to connect to supervisor"
**Problem:** Supervisor is not running or wrong port.
**Solution:**
```bash
# Check if supervisor is running
curl http://localhost:3030
# Start supervisor
cargo run --bin hero-supervisor -- --redis-url redis://localhost:6379
```
### "Runner not found"
**Problem:** Runner is not registered or not running.
**Solution:**
```bash
# Start the runner
cargo run --bin runner_osis -- test_runner --redis-url redis://localhost:6379
# Check runner logs for connection issues
```
### "Job execution timeout"
**Problem:** Job took longer than timeout value.
**Solution:**
- Increase timeout in job builder: `.timeout(60)`
- Or in job.run request: `"timeout": 60`
### "Redis connection failed"
**Problem:** Redis is not running.
**Solution:**
```bash
# Start Redis
redis-server
# Or specify custom Redis URL
cargo run --bin hero-supervisor -- --redis-url redis://localhost:6379
```
## Architecture
```
┌─────────────┐
│ Client │
│ (Example) │
└──────┬──────┘
│ HTTP/JSON-RPC
┌─────────────┐
│ Supervisor │
│ (Mycelium) │
└──────┬──────┘
│ Redis Queue
┌─────────────┐
│ Runner │
│ (OSIS) │
└─────────────┘
```
### Flow
1. **Client** creates a job with Rhai script
2. **Client** sends job to supervisor via JSON-RPC
3. **Supervisor** verifies signatures (if present)
4. **Supervisor** queues job to runner's Redis queue
5. **Runner** picks up job from queue
6. **Runner** executes Rhai script
7. **Runner** stores result in Redis
8. **Supervisor** retrieves result (for job.run)
9. **Client** receives result
## Next Steps
- Add signature verification to jobs (see `JOB_SIGNATURES.md`)
- Implement job status polling for non-blocking jobs
- Create custom Rhai functions for your use case
- Scale with multiple runners
## Related Documentation
- `JOB_EXECUTION.md` - Detailed job execution modes
- `JOB_SIGNATURES.md` - Cryptographic job signing
- `README.md` - Supervisor overview
---
**Status:** ✅ Production Ready
**Last Updated:** 2025-10-24

View File

@@ -0,0 +1,192 @@
# Supervisor Examples - Summary
## ✅ **Complete End-to-End Examples with OpenRPC Client**
All examples now use the official `hero-supervisor-openrpc-client` library for type-safe, async communication with the supervisor.
### **What Was Updated:**
1. **OpenRPC Client Library** (`clients/openrpc/src/lib.rs`)
- Added `JobRunResponse` - Response from blocking `job.run`
- Added `JobStartResponse` - Response from non-blocking `job.start`
- Updated `job_run()` method - Now accepts timeout parameter
- Updated `job_start()` method - Now accepts Job instead of job_id
- Re-exports `Job` and `JobBuilder` from `runner_rust`
2. **Simple E2E Example** (`examples/simple_e2e.rs`)
- Uses `SupervisorClient` from OpenRPC library
- Clean, type-safe API calls
- No manual JSON-RPC construction
- Perfect for learning and testing
3. **Full E2E Demo** (`examples/end_to_end_demo.rs`)
- Automated supervisor and runner spawning
- Uses OpenRPC client throughout
- Helper functions for common operations
- Comprehensive test scenarios
### **Key Changes:**
**Before (Manual JSON-RPC):**
```rust
let request = json!({
"jsonrpc": "2.0",
"method": "job.run",
"params": [{
"secret": secret,
"job": job,
"timeout": 30
}],
"id": 1
});
let response = http_client.post(url).json(&request).send().await?;
```
**After (OpenRPC Client):**
```rust
let response = client.job_run(secret, job, Some(30)).await?;
println!("Result: {:?}", response.result);
```
### **Client API:**
#### **Job Execution**
```rust
use hero_supervisor_openrpc_client::{SupervisorClient, JobBuilder};
// Create client
let client = SupervisorClient::new("http://localhost:3030")?;
// Register runner
client.register_runner("admin_secret", "runner_name", "queue_name").await?;
// Run job (blocking - waits for result)
let response = client.job_run("admin_secret", job, Some(60)).await?;
// response.result contains the actual result
// Start job (non-blocking - returns immediately)
let response = client.job_start("admin_secret", job).await?;
// response.job_id for later polling
```
#### **Response Types**
```rust
// JobRunResponse (from job.run)
pub struct JobRunResponse {
pub job_id: String,
pub status: String, // "completed"
pub result: Option<String>, // Actual result from runner
}
// JobStartResponse (from job.start)
pub struct JobStartResponse {
pub job_id: String,
pub status: String, // "queued"
}
```
### **Examples Overview:**
| Example | Description | Use Case |
|---------|-------------|----------|
| `simple_e2e.rs` | Manual setup, step-by-step | Learning, testing |
| `end_to_end_demo.rs` | Automated, comprehensive | CI/CD, integration tests |
### **Running the Examples:**
**Prerequisites:**
```bash
# Terminal 1: Redis
redis-server
# Terminal 2: Supervisor
cargo run --bin hero-supervisor -- --redis-url redis://localhost:6379
# Terminal 3: Runner
cargo run --bin runner_osis -- test_runner --redis-url redis://localhost:6379
```
**Run Simple Example:**
```bash
# Terminal 4
RUST_LOG=info cargo run --example simple_e2e
```
**Run Full Demo:**
```bash
# Only needs Redis running (spawns supervisor and runner automatically)
RUST_LOG=info cargo run --example end_to_end_demo
```
### **Benefits of OpenRPC Client:**
**Type Safety** - Compile-time checking of requests/responses
**Async/Await** - Native Rust async support
**Error Handling** - Proper Result types with detailed errors
**Auto Serialization** - No manual JSON construction
**Documentation** - IntelliSense and type hints
**Maintainability** - Single source of truth for API
### **Architecture:**
```
┌─────────────────┐
│ Example Code │
│ (simple_e2e) │
└────────┬────────┘
┌─────────────────┐
│ OpenRPC Client │
│ (typed API) │
└────────┬────────┘
│ JSON-RPC over HTTP
┌─────────────────┐
│ Supervisor │
│ (Mycelium) │
└────────┬────────┘
│ Redis Queue
┌─────────────────┐
│ OSIS Runner │
│ (Rhai Engine) │
└─────────────────┘
```
### **Job Execution Modes:**
**Blocking (`job.run`):**
- Client waits for result
- Uses `queue_and_wait` internally
- Returns actual result
- Best for: CRUD, queries, short jobs
**Non-Blocking (`job.start`):**
- Client returns immediately
- Job runs in background
- Returns job_id for polling
- Best for: Long jobs, batch processing
### **Files Modified:**
-`clients/openrpc/src/lib.rs` - Updated client methods and response types
-`examples/simple_e2e.rs` - Refactored to use OpenRPC client
-`examples/end_to_end_demo.rs` - Refactored to use OpenRPC client
-`examples/E2E_EXAMPLES.md` - Updated documentation
-`examples/EXAMPLES_SUMMARY.md` - This file
### **Next Steps:**
1. **Add more examples** - Specific use cases (batch jobs, error handling)
2. **Job polling** - Implement `wait_for_job()` helper
3. **WASM support** - Browser-based examples
4. **Signature examples** - Jobs with cryptographic signatures
---
**Status:** ✅ Complete and Production Ready
**Last Updated:** 2025-10-24
**Client Version:** hero-supervisor-openrpc-client 0.1.0

182
examples/_archive/README.md Normal file
View File

@@ -0,0 +1,182 @@
# Hero Supervisor Examples
This directory contains examples demonstrating the new job API functionality and workflows.
## Examples Overview
### 1. `job_api_examples.rs` - Comprehensive API Demo
Complete demonstration of all new job API methods:
- **Fire-and-forget execution** using `job.run`
- **Asynchronous processing** with `jobs.create`, `job.start`, `job.status`, `job.result`
- **Batch job processing** for multiple jobs
- **Job listing** with `jobs.list`
**Run with:**
```bash
cargo run --example job_api_examples
```
### 2. `simple_job_workflow.rs` - Basic Workflow
Simple example showing the basic job lifecycle:
1. Create job with `jobs.create`
2. Start job with `job.start`
3. Monitor with `job.status`
4. Get result with `job.result`
**Run with:**
```bash
cargo run --example simple_job_workflow
```
### 3. `integration_test.rs` - Integration Tests
Comprehensive integration tests validating:
- Complete job lifecycle
- Immediate job execution
- Job listing functionality
- Authentication error handling
- Nonexistent job operations
**Run with:**
```bash
cargo test --test integration_test
```
## Prerequisites
Before running the examples, ensure:
1. **Redis is running:**
```bash
docker run -d -p 6379:6379 redis:alpine
```
2. **Supervisor is running:**
```bash
./target/debug/supervisor --config examples/supervisor/config.toml
```
3. **Runners are configured** in your config.toml:
```toml
[[actors]]
id = "osis_runner_1"
name = "osis_runner_1"
binary_path = "/path/to/osis_runner"
db_path = "/tmp/osis_db"
redis_url = "redis://localhost:6379"
process_manager = "simple"
```
## API Convention Summary
The examples demonstrate the new job API convention:
### General Operations (`jobs.`)
- `jobs.create` - Create a job without queuing it
- `jobs.list` - List all job IDs in the system
### Specific Operations (`job.`)
- `job.run` - Run a job immediately and return result
- `job.start` - Start a previously created job
- `job.status` - Get current job status (non-blocking)
- `job.result` - Get job result (blocking until complete)
## Workflow Patterns
### Pattern 1: Fire-and-Forget
```rust
let result = client.job_run(secret, job).await?;
match result {
JobResult::Success { success } => println!("Output: {}", success),
JobResult::Error { error } => println!("Error: {}", error),
}
```
### Pattern 2: Asynchronous Processing
```rust
// Create and start
let job_id = client.jobs_create(secret, job).await?;
client.job_start(secret, &job_id).await?;
// Monitor (non-blocking)
loop {
let status = client.job_status(&job_id).await?;
if status.status == "completed" { break; }
sleep(Duration::from_secs(1)).await;
}
// Get result
let result = client.job_result(&job_id).await?;
```
### Pattern 3: Batch Processing
```rust
// Create all jobs
let mut job_ids = Vec::new();
for job_spec in job_specs {
let job_id = client.jobs_create(secret, job_spec).await?;
job_ids.push(job_id);
}
// Start all jobs
for job_id in &job_ids {
client.job_start(secret, job_id).await?;
}
// Collect results
for job_id in &job_ids {
let result = client.job_result(job_id).await?;
// Process result...
}
```
## Error Handling
The examples demonstrate proper error handling for:
- **Authentication errors** - Invalid secrets
- **Job not found errors** - Nonexistent job IDs
- **Connection errors** - Supervisor not available
- **Execution errors** - Job failures
## Authentication
Examples use different secret types:
- **Admin secrets**: Full system access
- **User secrets**: Job operations only (used in examples)
- **Register secrets**: Runner registration only
Configure secrets in your supervisor config:
```toml
admin_secrets = ["admin-secret-123"]
user_secrets = ["user-secret-456"]
register_secrets = ["register-secret-789"]
```
## Troubleshooting
### Common Issues
1. **Connection refused**
- Ensure supervisor is running on localhost:3030
- Check supervisor logs for errors
2. **Authentication failed**
- Verify secret is configured in supervisor
- Check secret type matches operation requirements
3. **Job execution failed**
- Ensure runners are properly configured and running
- Check runner logs for execution errors
- Verify job payload is valid for the target runner
4. **Redis connection failed**
- Ensure Redis is running on localhost:6379
- Check Redis connectivity from supervisor
### Debug Mode
Run examples with debug logging:
```bash
RUST_LOG=debug cargo run --example job_api_examples
```
This will show detailed API calls and responses for troubleshooting.

View File

@@ -0,0 +1,278 @@
//! End-to-End Demo: Supervisor + Runner + Client
//!
//! This example demonstrates the complete workflow:
//! 1. Starts a supervisor with Mycelium integration
//! 2. Starts an OSIS runner
//! 3. Uses the supervisor client to run jobs
//! 4. Shows both job.run (blocking) and job.start (non-blocking) modes
//!
//! Prerequisites:
//! - Redis running on localhost:6379
//!
//! Usage:
//! ```bash
//! RUST_LOG=info cargo run --example end_to_end_demo
//! ```
use anyhow::{Result, Context};
use log::{info, error};
use std::process::{Command, Child, Stdio};
use std::time::Duration;
use tokio::time::sleep;
use hero_supervisor_openrpc_client::{SupervisorClient, JobBuilder};
/// Configuration for the demo
struct DemoConfig {
redis_url: String,
supervisor_port: u16,
runner_id: String,
db_path: String,
}
impl Default for DemoConfig {
fn default() -> Self {
Self {
redis_url: "redis://localhost:6379".to_string(),
supervisor_port: 3030,
runner_id: "example_runner".to_string(),
db_path: "/tmp/example_runner.db".to_string(),
}
}
}
/// Supervisor process wrapper
struct SupervisorProcess {
child: Child,
}
impl SupervisorProcess {
fn start(config: &DemoConfig) -> Result<Self> {
info!("🚀 Starting supervisor on port {}...", config.supervisor_port);
let child = Command::new("cargo")
.args(&[
"run",
"--bin",
"hero-supervisor",
"--",
"--redis-url",
&config.redis_url,
"--port",
&config.supervisor_port.to_string(),
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("Failed to start supervisor")?;
Ok(Self { child })
}
}
impl Drop for SupervisorProcess {
fn drop(&mut self) {
info!("🛑 Stopping supervisor...");
let _ = self.child.kill();
let _ = self.child.wait();
}
}
/// Runner process wrapper
struct RunnerProcess {
child: Child,
}
impl RunnerProcess {
fn start(config: &DemoConfig) -> Result<Self> {
info!("🤖 Starting OSIS runner '{}'...", config.runner_id);
let child = Command::new("cargo")
.args(&[
"run",
"--bin",
"runner_osis",
"--",
&config.runner_id,
"--db-path",
&config.db_path,
"--redis-url",
&config.redis_url,
])
.env("RUST_LOG", "info")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("Failed to start runner")?;
Ok(Self { child })
}
}
impl Drop for RunnerProcess {
fn drop(&mut self) {
info!("🛑 Stopping runner...");
let _ = self.child.kill();
let _ = self.child.wait();
}
}
/// Helper functions for the demo
async fn register_runner_helper(client: &SupervisorClient, runner_id: &str, secret: &str) -> Result<()> {
info!("📝 Registering runner '{}'...", runner_id);
let queue = format!("hero:q:work:type:osis:group:default:inst:{}", runner_id);
client.register_runner(secret, runner_id, &queue).await?;
info!("✅ Runner registered successfully");
Ok(())
}
async fn run_job_helper(client: &SupervisorClient, job: runner_rust::job::Job, secret: &str, timeout: u64) -> Result<String> {
info!("🚀 Running job {} (blocking)...", job.id);
let response = client.job_run(secret, job, Some(timeout)).await?;
let result = response.result
.ok_or_else(|| anyhow::anyhow!("No result in response"))?;
info!("✅ Job completed with result: {}", result);
Ok(result)
}
async fn start_job_helper(client: &SupervisorClient, job: runner_rust::job::Job, secret: &str) -> Result<String> {
info!("🚀 Starting job {} (non-blocking)...", job.id);
let response = client.job_start(secret, job).await?;
info!("✅ Job queued with ID: {}", response.job_id);
Ok(response.job_id)
}
#[tokio::main]
async fn main() -> Result<()> {
// Initialize logging
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
println!("\n╔════════════════════════════════════════════════════════════╗");
println!("║ End-to-End Demo: Supervisor + Runner + Client ║");
println!("╚════════════════════════════════════════════════════════════╝\n");
let config = DemoConfig::default();
// Step 1: Start supervisor
println!("📋 Step 1: Starting Supervisor");
println!("─────────────────────────────────────────────────────────────");
let _supervisor = SupervisorProcess::start(&config)?;
sleep(Duration::from_secs(3)).await;
println!("✅ Supervisor started on port {}\n", config.supervisor_port);
// Step 2: Start runner
println!("📋 Step 2: Starting OSIS Runner");
println!("─────────────────────────────────────────────────────────────");
let _runner = RunnerProcess::start(&config)?;
sleep(Duration::from_secs(3)).await;
println!("✅ Runner '{}' started\n", config.runner_id);
// Step 3: Create client and register runner
println!("📋 Step 3: Registering Runner with Supervisor");
println!("─────────────────────────────────────────────────────────────");
let client = SupervisorClient::new(&format!("http://localhost:{}", config.supervisor_port))?;
register_runner_helper(&client, &config.runner_id, "admin_secret").await?;
println!("✅ Runner registered\n");
sleep(Duration::from_secs(2)).await;
// Step 4: Run blocking jobs (job.run)
println!("📋 Step 4: Running Blocking Jobs (job.run)");
println!("─────────────────────────────────────────────────────────────");
// Job 1: Simple calculation
println!("\n🔹 Job 1: Simple Calculation");
let job1 = JobBuilder::new()
.caller_id("demo_client")
.context_id("demo_context")
.payload("let result = 2 + 2; to_json(result)")
.runner(&config.runner_id)
.executor("rhai")
.timeout(30)
.build()?;
let result1 = run_job_helper(&client, job1, "admin_secret", 30).await?;
println!(" Result: {}", result1);
// Job 2: String manipulation
println!("\n🔹 Job 2: String Manipulation");
let job2 = JobBuilder::new()
.caller_id("demo_client")
.context_id("demo_context")
.payload(r#"let msg = "Hello from OSIS Runner!"; to_json(msg)"#)
.runner(&config.runner_id)
.executor("rhai")
.timeout(30)
.build()?;
let result2 = run_job_helper(&client, job2, "admin_secret", 30).await?;
println!(" Result: {}", result2);
// Job 3: Array operations
println!("\n🔹 Job 3: Array Operations");
let job3 = JobBuilder::new()
.caller_id("demo_client")
.context_id("demo_context")
.payload(r#"
let numbers = [1, 2, 3, 4, 5];
let sum = 0;
for n in numbers {
sum += n;
}
to_json(#{sum: sum, count: numbers.len()})
"#)
.runner(&config.runner_id)
.executor("rhai")
.timeout(30)
.build()?;
let result3 = run_job_helper(&client, job3, "admin_secret", 30).await?;
println!(" Result: {}", result3);
println!("\n✅ All blocking jobs completed successfully\n");
// Step 5: Start non-blocking jobs (job.start)
println!("📋 Step 5: Starting Non-Blocking Jobs (job.start)");
println!("─────────────────────────────────────────────────────────────");
println!("\n🔹 Job 4: Background Task");
let job4 = JobBuilder::new()
.caller_id("demo_client")
.context_id("demo_context")
.payload(r#"
let result = "Background task completed";
to_json(result)
"#)
.runner(&config.runner_id)
.executor("rhai")
.timeout(30)
.build()?;
let job4_id = start_job_helper(&client, job4, "admin_secret").await?;
println!(" Job ID: {} (running in background)", job4_id);
println!("\n✅ Non-blocking job started\n");
// Step 6: Summary
println!("📋 Step 6: Demo Summary");
println!("─────────────────────────────────────────────────────────────");
println!("✅ Supervisor: Running on port {}", config.supervisor_port);
println!("✅ Runner: '{}' registered and processing jobs", config.runner_id);
println!("✅ Blocking jobs: 3 completed successfully");
println!("✅ Non-blocking jobs: 1 started");
println!("\n🎉 Demo completed successfully!");
// Keep processes running for a bit to see logs
println!("\n⏳ Keeping processes running for 5 seconds...");
sleep(Duration::from_secs(5)).await;
println!("\n🛑 Shutting down...");
Ok(())
}

View File

@@ -0,0 +1,203 @@
//! Simple End-to-End Example
//!
//! A minimal example showing supervisor + runner + client workflow.
//!
//! Prerequisites:
//! - Redis running on localhost:6379
//!
//! Usage:
//! ```bash
//! # Terminal 1: Start Redis
//! redis-server
//!
//! # Terminal 2: Run this example
//! RUST_LOG=info cargo run --example simple_e2e
//! ```
use anyhow::Result;
use log::info;
use std::time::Duration;
use tokio::time::sleep;
use hero_supervisor_openrpc_client::{SupervisorClient, JobBuilder};
#[tokio::main]
async fn main() -> Result<()> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
println!("\n╔════════════════════════════════════════╗");
println!("║ Simple End-to-End Demo ║");
println!("╚════════════════════════════════════════╝\n");
let supervisor_url = "http://localhost:3030";
let runner_id = "test_runner";
let secret = "admin_secret";
// Create supervisor client
let client = SupervisorClient::new(supervisor_url)?;
println!("📝 Prerequisites:");
println!(" 1. Redis running on localhost:6379");
println!(" 2. Supervisor running on {}", supervisor_url);
println!(" 3. Runner '{}' registered and running\n", runner_id);
println!("💡 To start the supervisor:");
println!(" cargo run --bin hero-supervisor -- --redis-url redis://localhost:6379\n");
println!("💡 To start a runner:");
println!(" cd /Users/timurgordon/code/git.ourworld.tf/herocode/runner_rust");
println!(" cargo run --bin runner_osis -- {} --redis-url redis://localhost:6379\n", runner_id);
println!("⏳ Waiting 3 seconds for you to start the prerequisites...\n");
sleep(Duration::from_secs(3)).await;
// Register runner
println!("📋 Step 1: Registering Runner");
println!("─────────────────────────────────────────");
let queue = format!("hero:q:work:type:osis:group:default:inst:{}", runner_id);
match client.register_runner(secret, runner_id, &queue).await {
Ok(_) => {
println!("✅ Runner registered successfully");
}
Err(e) => {
println!("⚠️ Registration error: {} (runner might already be registered)", e);
}
}
sleep(Duration::from_secs(1)).await;
// Run a simple job
println!("\n📋 Step 2: Running a Simple Job (Blocking)");
println!("─────────────────────────────────────────");
let job = JobBuilder::new()
.caller_id("simple_demo")
.context_id("demo_context")
.payload(r#"
let message = "Hello from the runner!";
let number = 42;
to_json(#{
message: message,
number: number,
timestamp: timestamp()
})
"#)
.runner(runner_id)
.executor("rhai")
.timeout(30)
.build()?;
let job_id = job.id.clone();
info!("Sending job with ID: {}", job_id);
match client.job_run(secret, job, Some(30)).await {
Ok(response) => {
println!("✅ Job completed!");
if let Some(result) = response.result {
println!(" Result: {}", result);
}
}
Err(e) => {
println!("❌ Job failed: {}", e);
return Ok(());
}
}
// Run another job (calculation)
println!("\n📋 Step 3: Running a Calculation Job");
println!("─────────────────────────────────────────");
let calc_job = JobBuilder::new()
.caller_id("simple_demo")
.context_id("demo_context")
.payload(r#"
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let sum = 0;
let product = 1;
for n in numbers {
sum += n;
product *= n;
}
to_json(#{
sum: sum,
product: product,
count: numbers.len(),
average: sum / numbers.len()
})
"#)
.runner(runner_id)
.executor("rhai")
.timeout(30)
.build()?;
let calc_job_id = calc_job.id.clone();
info!("Sending calculation job with ID: {}", calc_job_id);
match client.job_run(secret, calc_job, Some(30)).await {
Ok(response) => {
println!("✅ Calculation completed!");
if let Some(result) = response.result {
println!(" Result: {}", result);
}
}
Err(e) => {
println!("❌ Calculation failed: {}", e);
}
}
// Start a non-blocking job
println!("\n📋 Step 4: Starting a Non-Blocking Job");
println!("─────────────────────────────────────────");
let async_job = JobBuilder::new()
.caller_id("simple_demo")
.context_id("demo_context")
.payload(r#"
let result = "This job was started asynchronously";
to_json(result)
"#)
.runner(runner_id)
.executor("rhai")
.timeout(30)
.build()?;
let async_job_id = async_job.id.clone();
info!("Starting async job with ID: {}", async_job_id);
match client.job_start(secret, async_job).await {
Ok(response) => {
println!("✅ Job started!");
println!(" Job ID: {} (running in background)", response.job_id);
println!(" Status: {}", response.status);
}
Err(e) => {
println!("❌ Failed to start job: {}", e);
}
}
// Summary
println!("\n╔════════════════════════════════════════╗");
println!("║ Demo Summary ║");
println!("╚════════════════════════════════════════╝");
println!("✅ Runner registered: {}", runner_id);
println!("✅ Blocking jobs completed: 2");
println!("✅ Non-blocking jobs started: 1");
println!("\n🎉 Demo completed successfully!\n");
println!("📚 What happened:");
println!(" 1. Registered a runner with the supervisor");
println!(" 2. Sent jobs with Rhai scripts to execute");
println!(" 3. Supervisor queued jobs to the runner");
println!(" 4. Runner executed the scripts and returned results");
println!(" 5. Client received results (for blocking jobs)\n");
println!("🔍 Key Concepts:");
println!(" • job.run = Execute and wait for result (blocking)");
println!(" • job.start = Start and return immediately (non-blocking)");
println!(" • Jobs contain Rhai scripts that run on the runner");
println!(" • Supervisor coordinates job distribution via Redis\n");
Ok(())
}

View File

@@ -0,0 +1,94 @@
# OSIRIS + OpenRPC Comprehensive Example
This example demonstrates the complete workflow of using Hero Supervisor with OSIRIS runners via OpenRPC.
## What This Example Does
1. **Builds and starts** Hero Supervisor with OpenRPC server enabled
2. **Builds** the OSIRIS runner binary
3. **Connects** an OpenRPC client to the supervisor
4. **Registers and starts** an OSIRIS runner
5. **Dispatches multiple jobs** via OpenRPC:
- Create a Note
- Create an Event
- Query stored data
- Test access control (expected to fail)
6. **Monitors** job execution and results
7. **Gracefully shuts down** all components
## Prerequisites
- Redis server running on `localhost:6379`
- Rust toolchain installed
- Both `supervisor` and `runner_rust` crates available
## Running the Example
```bash
cargo run --example osiris_openrpc
```
## Job Scripts
The example uses separate Rhai script files for each job:
- `note.rhai` - Creates and stores a Note object
- `event.rhai` - Creates and stores an Event object
- `query.rhai` - Queries and retrieves stored objects
- `access_denied.rhai` - Tests access control (should fail)
## Architecture
```
┌─────────────────┐
│ This Example │
│ (OpenRPC │
│ Client) │
└────────┬────────┘
│ JSON-RPC
┌─────────────────┐
│ Supervisor │
│ (OpenRPC │
│ Server) │
└────────┬────────┘
│ Redis Queue
┌─────────────────┐
│ OSIRIS Runner │
│ (Rhai Engine │
│ + HeroDB) │
└─────────────────┘
```
## Key Features Demonstrated
- **Automatic binary building** using escargot
- **OpenRPC communication** between client and supervisor
- **Runner registration** with configuration
- **Job dispatching** with signatories
- **Context-based access control** in OSIRIS
- **Typed object storage** (Note, Event)
- **Graceful shutdown** and cleanup
## Expected Output
The example will:
1. ✅ Create a Note successfully
2. ✅ Create an Event successfully
3. ✅ Query and retrieve stored objects
4. ✅ Deny access for unauthorized participants
5. ✅ Clean up all resources
## Troubleshooting
**Redis Connection Error:**
- Ensure Redis is running: `redis-server`
**Build Errors:**
- Ensure both supervisor and runner_rust crates are available
- Check that all dependencies are up to date
**OpenRPC Connection Error:**
- Port 3030 might be in use
- Check supervisor logs for startup issues

View File

@@ -0,0 +1,8 @@
print("Attempting to access context with non-signatories...");
print("Participants: [dave, eve]");
print("Signatories: [alice, bob, charlie]");
// This should fail because neither dave nor eve are signatories
let ctx = get_context(["dave", "eve"]);
"This should not succeed!"

View File

@@ -0,0 +1,18 @@
print("Creating context for [alice, bob]...");
let ctx = get_context(["alice", "bob"]);
print("✓ Context ID: " + ctx.context_id());
print("\nCreating event...");
let event = event("events")
.title("Team Retrospective")
.description("Review what went well and areas for improvement")
.location("Virtual - Zoom Room A")
.category("retrospective");
print("✓ Event created");
print("\nStoring event in context...");
ctx.save(event);
print("✓ Event stored");
"Event 'Team Retrospective' created and stored successfully"

View File

@@ -0,0 +1,239 @@
///! Comprehensive OSIRIS + OpenRPC Example
///!
///! This example demonstrates:
///! 1. Starting a Hero Supervisor with OpenRPC server
///! 2. Starting an OSIRIS runner
///! 3. Registering the runner with the supervisor
///! 4. Dispatching multiple OSIRIS jobs via OpenRPC
///! 5. Monitoring job execution
///! 6. Graceful shutdown
///!
///! Usage:
///! ```bash
///! cargo run --example osiris_openrpc
///! ```
use hero_supervisor_openrpc_client::{SupervisorClient, RunnerConfig, JobBuilder};
use std::time::Duration;
use escargot::CargoBuild;
use std::process::Stdio;
use tokio::time::sleep;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("🚀 OSIRIS + OpenRPC Comprehensive Example");
println!("=========================================\n");
// ========================================================================
// STEP 1: Build and start supervisor with OpenRPC
// ========================================================================
println!("Step 1: Building and starting supervisor");
println!("─────────────────────────────────────────────────────────────\n");
let supervisor_binary = CargoBuild::new()
.bin("supervisor")
.current_release()
.manifest_path("../supervisor/Cargo.toml")
.run()?;
println!("✅ Supervisor binary built");
let mut supervisor = supervisor_binary.command()
.arg("--redis-url")
.arg("redis://localhost:6379")
.arg("--openrpc")
.arg("--openrpc-port")
.arg("3030")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
println!("✅ Supervisor started on port 3030");
sleep(Duration::from_secs(2)).await;
// ========================================================================
// STEP 2: Build OSIRIS runner
// ========================================================================
println!("\nStep 2: Building OSIRIS runner");
println!("─────────────────────────────────────────────────────────────\n");
let runner_binary = CargoBuild::new()
.bin("runner_osiris")
.current_release()
.manifest_path("../runner_rust/Cargo.toml")
.run()?;
println!("✅ OSIRIS runner binary built");
// ========================================================================
// STEP 3: Connect OpenRPC client
// ========================================================================
println!("\nStep 3: Connecting OpenRPC client");
println!("─────────────────────────────────────────────────────────────\n");
let client = SupervisorClient::new("http://127.0.0.1:3030")?;
println!("✅ Connected to supervisor\n");
// ========================================================================
// STEP 4: Register and start OSIRIS runner
// ========================================================================
println!("Step 4: Registering OSIRIS runner");
println!("─────────────────────────────────────────────────────────────\n");
let runner_path = runner_binary.path().to_string_lossy();
let db_path = "/tmp/osiris_openrpc.db";
let command = format!(
"{} osiris_runner --db-path {} --redis-url redis://localhost:6379",
runner_path, db_path
);
let runner_config = RunnerConfig {
name: "osiris_runner".to_string(),
command,
env: None,
};
client.add_runner("admin_secret", runner_config).await?;
println!("✅ Runner registered: osiris_runner");
client.start_runner("admin_secret", "osiris_runner").await?;
println!("✅ Runner started\n");
sleep(Duration::from_secs(2)).await;
// ========================================================================
// STEP 5: Load job scripts
// ========================================================================
println!("Step 5: Loading job scripts");
println!("─────────────────────────────────────────────────────────────\n");
let note_script = std::fs::read_to_string("examples/osiris_openrpc/note.rhai")?;
let event_script = std::fs::read_to_string("examples/osiris_openrpc/event.rhai")?;
let query_script = std::fs::read_to_string("examples/osiris_openrpc/query.rhai")?;
let access_denied_script = std::fs::read_to_string("examples/osiris_openrpc/access_denied.rhai")?;
println!("✅ Loaded 4 job scripts\n");
// ========================================================================
// STEP 6: Dispatch jobs via OpenRPC
// ========================================================================
println!("Step 6: Dispatching jobs");
println!("─────────────────────────────────────────────────────────────\n");
// Job 1: Create Note
println!("📝 Job 1: Creating Note...");
let job1 = JobBuilder::new()
.caller_id("openrpc_client")
.context_id("osiris_demo")
.payload(&note_script)
.runner("osiris_runner")
.executor("rhai")
.timeout(30)
.signature("alice", "")
.signature("bob", "")
.build()?;
let job1_result = client.run_job("user_secret", job1).await;
match job1_result {
Ok(result) => println!("{:?}\n", result),
Err(e) => println!("❌ Job failed: {}\n", e),
}
sleep(Duration::from_secs(1)).await;
// Job 2: Create Event
println!("📅 Job 2: Creating Event...");
let job2 = JobBuilder::new()
.caller_id("openrpc_client")
.context_id("osiris_demo")
.payload(&event_script)
.runner("osiris_runner")
.executor("rhai")
.timeout(30)
.signature("alice", "")
.signature("bob", "")
.build()?;
let job2_result = client.run_job("user_secret", job2).await;
match job2_result {
Ok(result) => println!("{:?}\n", result),
Err(e) => println!("❌ Job failed: {}\n", e),
}
sleep(Duration::from_secs(1)).await;
// Job 3: Query Data
println!("🔍 Job 3: Querying Data...");
let job3 = JobBuilder::new()
.caller_id("openrpc_client")
.context_id("osiris_demo")
.payload(&query_script)
.runner("osiris_runner")
.executor("rhai")
.timeout(30)
.signature("alice", "")
.signature("bob", "")
.signature("charlie", "")
.build()?;
let job3_result = client.run_job("user_secret", job3).await;
match job3_result {
Ok(result) => println!("{:?}\n", result),
Err(e) => println!("❌ Job failed: {}\n", e),
}
sleep(Duration::from_secs(1)).await;
// Job 4: Access Control Test (should fail)
println!("🔒 Job 4: Testing Access Control (expected to fail)...");
let job4 = JobBuilder::new()
.caller_id("openrpc_client")
.context_id("osiris_demo")
.payload(&access_denied_script)
.runner("osiris_runner")
.executor("rhai")
.timeout(30)
.signature("alice", "")
.signature("bob", "")
.signature("charlie", "")
.build()?;
let job4_result = client.run_job("user_secret", job4).await;
match job4_result {
Ok(result) => println!("❌ Unexpected success: {:?}\n", result),
Err(e) => println!("✅ Access denied as expected: {}\n", e),
}
// ========================================================================
// STEP 7: Check runner status
// ========================================================================
println!("\nStep 7: Checking runner status");
println!("─────────────────────────────────────────────────────────────\n");
let status = client.get_runner_status("admin_secret", "osiris_runner").await?;
println!("Runner status: {:?}\n", status);
// ========================================================================
// STEP 8: Cleanup
// ========================================================================
println!("Step 8: Cleanup");
println!("─────────────────────────────────────────────────────────────\n");
client.stop_runner("admin_secret", "osiris_runner", false).await?;
println!("✅ Runner stopped");
client.remove_runner("admin_secret", "osiris_runner").await?;
println!("✅ Runner removed");
supervisor.kill()?;
println!("✅ Supervisor stopped");
println!("\n✨ Example completed successfully!");
Ok(())
}

View File

@@ -0,0 +1,20 @@
print("Creating context for [alice, bob]...");
let ctx = get_context(["alice", "bob"]);
print("✓ Context ID: " + ctx.context_id());
print("\nCreating note...");
let note = note("notes")
.title("Sprint Planning Meeting")
.content("Discussed Q1 2025 roadmap and milestones")
.tag("sprint", "2025-Q1")
.tag("team", "engineering")
.tag("priority", "high")
.mime("text/markdown");
print("✓ Note created");
print("\nStoring note in context...");
ctx.save(note);
print("✓ Note stored");
"Note 'Sprint Planning Meeting' created and stored successfully"

View File

@@ -0,0 +1,21 @@
print("Querying context [alice, bob]...");
let ctx = get_context(["alice", "bob"]);
print("✓ Context ID: " + ctx.context_id());
print("\nListing all notes...");
let notes = ctx.list("notes");
print("✓ Found " + notes.len() + " note(s)");
print("\nRetrieving specific note...");
let note = ctx.get("notes", "sprint_planning_001");
print("✓ Retrieved note: sprint_planning_001");
print("\nQuerying context [alice, bob, charlie]...");
let ctx2 = get_context(["alice", "bob", "charlie"]);
print("✓ Context ID: " + ctx2.context_id());
print("\nListing all events...");
let events = ctx2.list("events");
print("✓ Found " + events.len() + " event(s)");
"Query complete: Found " + notes.len() + " notes and " + events.len() + " events"

27
run_supervisor_simple.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/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

View File

@@ -5,9 +5,12 @@
//! then pass it to SupervisorApp for runtime management.
use crate::Supervisor;
#[cfg(feature = "mycelium")]
use crate::mycelium::MyceliumIntegration;
use log::{info, error, debug};
#[cfg(feature = "mycelium")]
use std::sync::Arc;
#[cfg(feature = "mycelium")]
use tokio::sync::Mutex;
/// Main supervisor application
@@ -54,37 +57,45 @@ impl SupervisorApp {
/// Start the Mycelium integration
async fn start_mycelium_integration(&self) -> Result<(), Box<dyn std::error::Error>> {
// Skip Mycelium if URL is empty
if self.mycelium_url.is_empty() {
info!("Mycelium integration disabled (no URL provided)");
return Ok(());
#[cfg(feature = "mycelium")]
{
// Skip Mycelium if URL is empty
if self.mycelium_url.is_empty() {
info!("Mycelium integration disabled (no URL provided)");
return Ok(());
}
info!("Starting Mycelium integration...");
let supervisor_for_mycelium = Arc::new(Mutex::new(self.supervisor.clone()));
let mycelium_url = self.mycelium_url.clone();
let topic = self.topic.clone();
let mycelium_integration = MyceliumIntegration::new(
supervisor_for_mycelium,
mycelium_url,
topic,
);
// Start the Mycelium integration in a background task
let integration_handle = tokio::spawn(async move {
if let Err(e) = mycelium_integration.start().await {
error!("Mycelium integration error: {}", e);
}
});
// Give the integration a moment to start
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
info!("Mycelium integration started successfully");
// Store the handle for potential cleanup
std::mem::forget(integration_handle); // For now, let it run in background
}
info!("Starting Mycelium integration...");
let supervisor_for_mycelium = Arc::new(Mutex::new(self.supervisor.clone()));
let mycelium_url = self.mycelium_url.clone();
let topic = self.topic.clone();
let mycelium_integration = MyceliumIntegration::new(
supervisor_for_mycelium,
mycelium_url,
topic,
);
// Start the Mycelium integration in a background task
let integration_handle = tokio::spawn(async move {
if let Err(e) = mycelium_integration.start().await {
error!("Mycelium integration error: {}", e);
}
});
// Give the integration a moment to start
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
info!("Mycelium integration started successfully");
// Store the handle for potential cleanup
std::mem::forget(integration_handle); // For now, let it run in background
#[cfg(not(feature = "mycelium"))]
{
info!("Mycelium integration not enabled (compile with --features mycelium)");
}
Ok(())
}

View File

@@ -7,6 +7,8 @@ pub mod job;
pub mod supervisor;
pub mod app;
pub mod openrpc;
#[cfg(feature = "mycelium")]
pub mod mycelium;
// Re-export main types for convenience
@@ -15,4 +17,6 @@ pub use runner::{Runner, RunnerConfig, RunnerResult, RunnerStatus};
pub use supervisor::{Supervisor, SupervisorBuilder, ProcessManagerType};
pub use runner_rust::{Job, JobBuilder, JobStatus, JobError, Client, ClientBuilder};
pub use app::SupervisorApp;
#[cfg(feature = "mycelium")]
pub use mycelium::{MyceliumIntegration, MyceliumServer};

View File

@@ -303,20 +303,88 @@ impl MyceliumIntegration {
}
"job.run" => {
// Run job and wait for result (blocking)
if let Some(param_obj) = params.as_array().and_then(|arr| arr.get(0)) {
let _secret = param_obj.get("secret")
.and_then(|v| v.as_str())
.ok_or("missing secret")?;
let _job = param_obj.get("job")
let job_value = param_obj.get("job")
.ok_or("missing job")?;
// TODO: Implement actual job execution
Ok(json!("job_queued"))
let timeout = param_obj.get("timeout")
.and_then(|v| v.as_u64())
.unwrap_or(60);
// Deserialize the job
let job: runner_rust::job::Job = serde_json::from_value(job_value.clone())
.map_err(|e| format!("invalid job format: {}", e))?;
let job_id = job.id.clone();
let runner_name = job.runner.clone();
// Verify signatures
job.verify_signatures()
.map_err(|e| format!("signature verification failed: {}", e))?;
info!("Job {} signature verification passed for signatories: {:?}",
job_id, job.signatories());
// Queue and wait for result
let mut supervisor_guard = self.supervisor.lock().await;
let result = supervisor_guard.queue_and_wait(&runner_name, job, timeout)
.await
.map_err(|e| format!("job execution failed: {}", e))?;
Ok(json!({
"job_id": job_id,
"status": "completed",
"result": result
}))
} else {
Err("invalid job.run params".into())
}
}
"job.start" => {
// Start job without waiting (non-blocking)
if let Some(param_obj) = params.as_array().and_then(|arr| arr.get(0)) {
let _secret = param_obj.get("secret")
.and_then(|v| v.as_str())
.ok_or("missing secret")?;
let job_value = param_obj.get("job")
.ok_or("missing job")?;
// Deserialize the job
let job: runner_rust::job::Job = serde_json::from_value(job_value.clone())
.map_err(|e| format!("invalid job format: {}", e))?;
let job_id = job.id.clone();
let runner_name = job.runner.clone();
// Verify signatures
job.verify_signatures()
.map_err(|e| format!("signature verification failed: {}", e))?;
info!("Job {} signature verification passed for signatories: {:?}",
job_id, job.signatories());
// Queue the job without waiting
let mut supervisor_guard = self.supervisor.lock().await;
supervisor_guard.queue_job_to_runner(&runner_name, job)
.await
.map_err(|e| format!("failed to queue job: {}", e))?;
Ok(json!({
"job_id": job_id,
"status": "queued"
}))
} else {
Err("invalid job.start params".into())
}
}
"job.status" => {
if let Some(_job_id) = params.as_array().and_then(|arr| arr.get(0)).and_then(|v| v.as_str()) {
// TODO: Implement actual job status lookup
@@ -339,7 +407,7 @@ impl MyceliumIntegration {
let methods = vec![
"list_runners", "register_runner", "start_runner", "stop_runner",
"get_runner_status", "get_all_runner_status", "start_all", "stop_all",
"job.run", "job.status", "job.result", "rpc.discover"
"job.run", "job.start", "job.status", "job.result", "rpc.discover"
];
Ok(json!(methods))
}

View File

@@ -330,7 +330,6 @@ pub trait SupervisorRpc {
#[method(name = "queue_job_to_runner")]
async fn queue_job_to_runner(&self, params: QueueJobParams) -> RpcResult<()>;
/// Get a job by job ID
#[method(name = "get_job")]
async fn get_job(&self, job_id: String) -> RpcResult<Job>;