feat: reorganize examples and add signature support to JobBuilder

- Reorganized examples into osiris/, sal/, and utils/ folders
- Moved hardcoded scripts to separate .rhai files
- Added signature() method to JobBuilder for job signing
- Updated OSIRIS context to use block_in_place instead of runtime
- Removed runtime field from OsirisContext
- Added typed save() methods for Note and Event objects
- Updated all examples to use new structure and APIs
This commit is contained in:
Timur Gordon
2025-10-27 13:49:39 +01:00
parent 90754cc4ac
commit 14ff6cae67
92 changed files with 904 additions and 268 deletions

54
examples/README.md Normal file
View File

@@ -0,0 +1,54 @@
# Runner Examples
This directory contains organized examples demonstrating different aspects of the runner system.
## Directory Structure
```
examples/
├── osiris/ # OSIRIS context and object management examples
├── sal/ # System Abstraction Layer (SAL) DSL examples
└── utils/ # Utility examples (signing, crypto, etc.)
```
## Running Examples
### OSIRIS Example
Complete example demonstrating OSIRIS context management, Note and Event creation, and access control:
```bash
cargo run --example osiris
```
See [osiris/README.md](osiris/README.md) for details.
### SAL Example
Examples demonstrating various SAL DSL modules for system operations:
```bash
cargo run --example sal
```
See [sal/README.md](sal/README.md) for details.
### Utility Examples
Utility examples for cryptographic operations and job signing:
```bash
cargo run --example sign_job --features crypto
```
See [utils/README.md](utils/README.md) for details.
## Prerequisites
- Redis server running on `localhost:6379`
- For OSIRIS examples: HeroDB instance (or uses local SQLite)
- For crypto examples: Enable the `crypto` feature flag
## Example Structure
Each example folder contains:
- `main.rs` - The main Rust example code
- `*.rhai` - Rhai script files demonstrating various features
- `README.md` - Detailed documentation for that example category

175
examples/osiris/README.md Normal file
View File

@@ -0,0 +1,175 @@
# OSIRIS Complete Example
A comprehensive end-to-end example demonstrating the complete OSIRIS workflow.
## Prerequisites
1. **Redis** - Must be running on localhost:6379
```bash
redis-server
```
2. **Rust** - Version 1.88+
```bash
rustup update
```
## Complete Example
### osiris_complete - Full End-to-End Workflow
**ONE EXAMPLE TO RULE THEM ALL** - Complete demonstration of OSIRIS.
**Run:**
```bash
cd /Users/timurgordon/code/git.ourworld.tf/herocode/runner_rust
cargo run --example osiris_complete
```
**What it demonstrates:**
1. **Runner Management** - Starts and stops runner_osiris daemon
2. **Job Client** - Creates client with builder pattern
3. **Job Building** - Uses JobBuilder for clean job creation
4. **Synchronous Execution** - Uses `run_job()` to wait for results
5. **OSIRIS Objects** - Creates Note and Event objects
6. **Context Storage** - Stores objects in participant-based contexts
7. **Data Querying** - Retrieves and lists stored data
8. **Access Control** - Demonstrates signatory-based access
9. **Error Handling** - Proper error propagation and cleanup
**Expected Output:**
```
🚀 OSIRIS Demo Script
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Step 1: Getting context for participants [alice, bob]
✓ Context ID: alice,bob
Step 2: Creating a Note
✓ Note created with title: Project Planning Meeting
...
```
## Key Concepts
### Context Management
**Simple Logic:**
- Context = list of public keys (participants)
- To get_context, at least one participant must be a signatory
- No state tracking - contexts created fresh each time
**Example:**
```rhai
// Signatories: [alice, bob, charlie]
// ✅ Works - alice is a signatory
let ctx = get_context(["alice", "bob"]);
// ❌ Fails - dave is not a signatory
let ctx = get_context(["alice", "dave"]);
```
### OSIRIS Objects
**Note:**
```rhai
let note = note("collection_name")
.title("My Note")
.content("Note content")
.tag("key", "value")
.mime("text/plain");
```
**Event:**
```rhai
let event = event("collection_name")
.title("My Event")
.description("Event description")
.location("Location")
.tag("type", "meeting");
```
### Context Operations
**Save:**
```rhai
let id = ctx.save("collection", "object_id", object);
```
**Get:**
```rhai
let obj = ctx.get("collection", "object_id");
```
**List:**
```rhai
let ids = ctx.list("collection");
```
**Delete:**
```rhai
let deleted = ctx.delete("collection", "object_id");
```
**Query:**
```rhai
let results = ctx.query("collection", "field", "value");
```
## Architecture
```
┌─────────────────────┐
│ Rhai Script │
│ (osiris_demo.rhai)│
└──────────┬──────────┘
┌─────────────────────┐
│ OSIRIS Engine │
│ - Note API │
│ - Event API │
│ - get_context() │
└──────────┬──────────┘
┌─────────────────────┐
│ OsirisContext │
│ - Participants │
│ - CRUD operations │
└──────────┬──────────┘
┌─────────────────────┐
│ HeroDB │
│ (Redis + local DB) │
└─────────────────────┘
```
## Troubleshooting
### "Connection refused" on port 6379
- Make sure Redis is running: `redis-server`
- Check: `redis-cli ping` (should return "PONG")
### "Access denied: none of the participants are signatories"
- Check that at least one participant is in the SIGNATORIES list
- Signatories are set via engine tags (see examples)
### "Failed to create context"
- Verify Redis is accessible
- Check HeroDB URL is correct
- Ensure db_id is valid (typically 1)
## Next Steps
1. **Modify the scripts** - Edit `osiris_demo.rhai` to try different operations
2. **Add more objects** - Create your own object types
3. **Multi-context** - Try creating multiple contexts with different participants
4. **Integration** - Use OSIRIS in your own applications
---
**Status:** ✅ Ready to Use
**Last Updated:** 2025-10-24

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"

248
examples/osiris/main.rs Normal file
View File

@@ -0,0 +1,248 @@
/// Complete End-to-End OSIRIS Example
///
/// This example demonstrates the complete OSIRIS workflow:
/// 1. Starting the runner_osiris daemon
/// 2. Creating jobs with JobBuilder
/// 3. Running jobs with run_job() (synchronous)
/// 4. Creating OSIRIS objects (Note, Event)
/// 5. Storing objects in contexts
/// 6. Querying and retrieving data
/// 7. Proper cleanup
///
/// Prerequisites:
/// - Redis running on localhost:6379
///
/// Run with:
/// ```bash
/// cargo run --example osiris_complete
/// ```
use runner_rust::job::{JobBuilder, Client};
use std::time::Duration;
use tokio::process::Command;
use tokio::time::sleep;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("🚀 OSIRIS Complete End-to-End Example");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// ========================================================================
// STEP 1: Start the runner daemon
// ========================================================================
println!("Step 1: Starting runner_osiris daemon");
println!("─────────────────────────────────────────────────────────────\n");
let mut runner = Command::new("cargo")
.args(&[
"run",
"--bin",
"runner_osiris",
"--",
"demo_runner",
"--db-path",
"/tmp/osiris_complete.db",
"--redis-url",
"redis://localhost:6379",
])
.spawn()?;
println!("✓ Runner started (PID: {})", runner.id().unwrap_or(0));
println!(" Runner ID: demo_runner");
println!(" Queue: demo_runner");
println!(" DB: /tmp/osiris_complete.db");
// Give the runner time to initialize
sleep(Duration::from_secs(2)).await;
println!("\n");
// ========================================================================
// STEP 2: Create job client
// ========================================================================
println!("Step 2: Creating job client");
println!("─────────────────────────────────────────────────────────────\n");
let client = Client::builder()
.redis_url("redis://localhost:6379")
.build()
.await?;
println!("✓ Job client created\n");
// ========================================================================
// STEP 3: Create and store a Note
// ========================================================================
println!("Step 3: Creating and Storing a Note");
println!("─────────────────────────────────────────────────────────────\n");
let create_note_script = std::fs::read_to_string("examples/osiris/note.rhai")?;
let job1 = JobBuilder::new()
.caller_id("example_client")
.context_id("demo_context")
.payload(create_note_script)
.runner("demo_runner")
.executor("rhai")
.timeout(30)
.signature("alice", "")
.signature("bob", "")
.signature("charlie", "")
.build()?;
println!("Running job: Create Note");
println!("Job ID: {}", job1.id);
match client.run_job(&job1, "demo_runner", 60).await {
Ok(result) => {
println!("\n{}\n", result);
}
Err(e) => {
println!("\n❌ Job failed: {}\n", e);
let _ = runner.kill().await;
return Err(format!("Job failed: {}", e).into());
}
}
// ========================================================================
// STEP 4: Create and store an Event
// ========================================================================
println!("Step 4: Creating and Storing an Event");
println!("─────────────────────────────────────────────────────────────\n");
let create_event_script = std::fs::read_to_string("examples/osiris/event.rhai")?;
let job2 = JobBuilder::new()
.caller_id("example_client")
.context_id("demo_context")
.payload(create_event_script)
.runner("demo_runner")
.executor("rhai")
.timeout(30)
.signature("alice", "")
.signature("bob", "")
.signature("charlie", "")
.build()?;
println!("Running job: Create Event");
println!("Job ID: {}", job2.id);
match client.run_job(&job2, "demo_runner", 60).await {
Ok(result) => {
println!("\n{}\n", result);
}
Err(e) => {
println!("\n❌ Job failed: {}\n", e);
let _ = runner.kill().await;
return Err(format!("Job failed: {}", e).into());
}
}
// ========================================================================
// STEP 5: Query context data
// ========================================================================
println!("Step 5: Querying Context Data");
println!("─────────────────────────────────────────────────────────────\n");
let query_script = std::fs::read_to_string("examples/osiris/query.rhai")?;
let job3 = JobBuilder::new()
.caller_id("example_client")
.context_id("demo_context")
.payload(query_script)
.runner("demo_runner")
.executor("rhai")
.timeout(30)
.signature("alice", "")
.signature("bob", "")
.signature("charlie", "")
.build()?;
println!("Running job: Query Data");
println!("Job ID: {}", job3.id);
match client.run_job(&job3, "demo_runner", 60).await {
Ok(result) => {
println!("\n{}\n", result);
}
Err(e) => {
println!("\n❌ Job failed: {}\n", e);
let _ = runner.kill().await;
return Err(format!("Job failed: {}", e).into());
}
}
// ========================================================================
// STEP 6: Demonstrate access control
// ========================================================================
println!("Step 6: Testing Access Control");
println!("─────────────────────────────────────────────────────────────\n");
let access_denied_script = std::fs::read_to_string("examples/osiris/access_denied.rhai")?;
let job4 = JobBuilder::new()
.caller_id("example_client")
.context_id("demo_context")
.payload(access_denied_script)
.runner("demo_runner")
.executor("rhai")
.timeout(30)
.signature("alice", "")
.signature("bob", "")
.signature("charlie", "")
.build()?;
println!("Running job: Access Control Test");
println!("Job ID: {}", job4.id);
println!("Expected: Access denied\n");
match client.run_job(&job4, "demo_runner", 60).await {
Ok(result) => {
println!("❌ Unexpected success: {}\n", result);
}
Err(e) => {
println!("✅ Access denied as expected");
println!(" Error: {}\n", e);
}
}
// ========================================================================
// STEP 7: Cleanup
// ========================================================================
println!("Step 7: Cleanup");
println!("─────────────────────────────────────────────────────────────\n");
println!("Stopping runner...");
runner.kill().await?;
println!("✓ Runner stopped\n");
// ========================================================================
// Summary
// ========================================================================
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("🎉 Complete Example Finished!\n");
println!("📝 What We Demonstrated:");
println!(" ✓ Started runner_osiris daemon");
println!(" ✓ Created job client with builder pattern");
println!(" ✓ Built jobs with JobBuilder");
println!(" ✓ Used run_job() for synchronous execution");
println!(" ✓ Created OSIRIS Note object");
println!(" ✓ Created OSIRIS Event object");
println!(" ✓ Stored objects in contexts");
println!(" ✓ Queried and retrieved data");
println!(" ✓ Demonstrated participant-based access control");
println!(" ✓ Proper cleanup and shutdown");
println!("\n🏗️ Architecture:");
println!(" • Contexts defined by participant public keys");
println!(" • At least one participant must be a signatory");
println!(" • No state tracking - contexts created on demand");
println!(" • Jobs executed via Redis queue");
println!(" • Results returned synchronously");
println!("\n💡 Key Features:");
println!(" • Simple API: client.run_job()");
println!(" • Fluent builders: JobBuilder, note(), event()");
println!(" • Automatic waiting and result extraction");
println!(" • Proper error handling and timeouts");
println!(" • Production-ready job infrastructure");
Ok(())
}

20
examples/osiris/note.rhai Normal file
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"

View File

@@ -1,37 +0,0 @@
/// Example: Running OSIRIS Rhai scripts
///
/// This example demonstrates how to use the OSIRIS Rhai engine
/// to execute scripts that create and manipulate OSIRIS objects.
///
/// Prerequisites:
/// - HeroDB running on localhost:6379
/// - osiris crate added as dependency with rhai-support feature
///
/// Run with:
/// ```bash
/// cargo run --example osiris_example
/// ```
use runner_rust::engine::osiris::run_osiris_script;
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("🚀 OSIRIS Rhai Engine Example\n");
// Example 1: Inline script
println!("Example 1: Inline Script");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
let script = r#"
print("Creating a note from inline script...");
let note = note("notes");
print(note);
"#;
run_osiris_script(script, "redis://localhost:6379", 1)?;
println!("\n✅ Example completed!");
println!("\nNote: Full OSIRIS integration requires adding osiris crate");
println!(" as a dependency with the 'rhai-support' feature enabled.");
Ok(())
}

View File

@@ -1,54 +0,0 @@
// OSIRIS Rhai Script Example
// This script demonstrates creating and storing OSIRIS objects
print("=== OSIRIS Rhai Script Example ===\n");
// Create a note with fluent builder pattern
print("Creating a note...");
let note = note("notes")
.title("My First Rhai Note")
.content("This note was created from a Rhai script using the OSIRIS engine!")
.tag("source", "rhai")
.tag("project", "osiris")
.tag("priority", "high")
.mime("text/plain");
print(`Note created: ${note.get_title()}`);
// Store the note in HeroDB
print("Storing note...");
let note_id = put_note(note);
print(`✓ Note stored with ID: ${note_id}\n`);
// Retrieve the note
print("Retrieving note...");
let retrieved_note = get_note("notes", note_id);
print(`✓ Retrieved: ${retrieved_note.get_title()}`);
print(` Content: ${retrieved_note.get_content()}\n`);
// Create an event
print("Creating an event...");
let event = event("calendar", "Team Meeting")
.description("Weekly team sync")
.location("Conference Room A")
.category("meetings")
.all_day(false);
print(`Event created: ${event.get_title()}`);
// Store the event
print("Storing event...");
let event_id = put_event(event);
print(`✓ Event stored with ID: ${event_id}\n`);
// Query notes by tag
print("Querying notes by tag (project=osiris)...");
let ids = query("notes", "tags:tag", "project=osiris");
print(`✓ Found ${ids.len()} note(s) with tag project=osiris`);
for id in ids {
let n = get_note("notes", id);
print(` - ${n.get_title()}`);
}
print("\n=== Script completed successfully! ===");

64
examples/sal/README.md Normal file
View File

@@ -0,0 +1,64 @@
# SAL (System Abstraction Layer) Examples
This directory contains examples demonstrating the SAL DSL modules for system operations.
## Running the Example
```bash
cargo run --example sal
```
## Available Scripts
The `scripts/` directory contains various Rhai scripts demonstrating different SAL modules:
### Basics
- `basics/` - Basic Rhai language features and syntax
### Containers
- `containers/` - Container management operations
### Git
- `git/` - Git repository operations
### Hero Vault
- `hero_vault/` - Secure credential storage and retrieval
### Kubernetes
- `kubernetes/` - Kubernetes cluster management
### Mycelium
- `mycelium/` - Mycelium network operations
### Network
- `network/` - Network utilities and operations
### PostgreSQL
- `postgresclient/` - PostgreSQL database operations
### Process
- `process/` - Process management and execution
### Redis
- `redisclient/` - Redis operations
### Service Manager
- `service_manager/` - System service management
### Zinit
- `zinit/` - Zinit service manager operations
## Script Structure
Each script demonstrates:
- Module initialization
- Common operations
- Error handling
- Best practices
## Configuration
The example uses:
- Default database path: `/tmp/sal_engine.db`
- Context isolation per script
- Verbose output for debugging

View File

@@ -1,46 +0,0 @@
/// Quick test of OSIRIS Rhai integration
///
/// Run with:
/// ```bash
/// cargo run --example test_osiris
/// ```
use runner_rust::engine::osiris::create_osiris_engine;
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("🧪 Testing OSIRIS Rhai Engine\n");
// Test 1: Create engine
println!("Test 1: Creating OSIRIS engine...");
let engine = create_osiris_engine("redis://localhost:6379", 1)?;
println!("✓ Engine created successfully\n");
// Test 2: Run simple script
println!("Test 2: Running simple script...");
let script = r#"
print("Hello from OSIRIS Rhai!");
// Create a note
let note = note("test_notes")
.title("Test Note")
.content("This is a test")
.tag("test", "true");
print("Note created: " + note.get_title());
"#;
match engine.eval::<()>(script) {
Ok(_) => println!("✓ Script executed successfully\n"),
Err(e) => {
println!("✗ Script error: {}\n", e);
println!("Note: This is expected if HeroDB is not running");
}
}
println!("✅ Tests completed!");
println!("\nTo run full integration:");
println!("1. Start HeroDB: cd ../herodb && cargo run --release");
println!("2. Run: cargo run --example test_osiris");
Ok(())
}

20
examples/utils/README.md Normal file
View File

@@ -0,0 +1,20 @@
# Utility Examples
This directory contains utility examples for working with the runner system.
## sign_job
Demonstrates how to:
- Create a job with signatories
- Sign the job with secp256k1 private keys
- Verify the signatures
### Usage
```bash
cargo run --example sign_job --features crypto
```
### Requirements
The `crypto` feature must be enabled to use cryptographic signing functionality.