diff --git a/Cargo.lock b/Cargo.lock index 07177d8..b6ae35c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1384,7 +1384,7 @@ dependencies = [ "rand 0.8.5", "redis 0.25.4", "reqwest 0.12.24", - "runner_rust 0.1.0", + "runner_rust 0.1.0 (git+https://git.ourworld.tf/herocode/runner_rust.git)", "serde", "serde_json", "thiserror 1.0.69", @@ -2645,26 +2645,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "osiris" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "env_logger 0.10.2", - "osiris_derive 0.1.0", - "redis 0.24.0", - "rhai", - "serde", - "serde_json", - "time", - "tokio", - "toml", - "tracing", - "tracing-subscriber", - "uuid", -] - [[package]] name = "osiris" version = "0.1.0" @@ -2673,7 +2653,7 @@ dependencies = [ "anyhow", "clap", "env_logger 0.10.2", - "osiris_derive 0.1.0 (git+https://git.ourworld.tf/herocode/osiris.git)", + "osiris_derive", "redis 0.24.0", "serde", "serde_json", @@ -2685,15 +2665,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "osiris_derive" -version = "0.1.0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.108", -] - [[package]] name = "osiris_derive" version = "0.1.0" @@ -3569,6 +3540,7 @@ checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" [[package]] name = "runner_rust" version = "0.1.0" +source = "git+https://git.ourworld.tf/herocode/runner_rust.git?branch=main#268128f7fd53e9586288efd95f9288595c4a74e9" dependencies = [ "anyhow", "async-trait", @@ -3582,7 +3554,7 @@ dependencies = [ "heromodels_core", "hex", "log", - "osiris 0.1.0", + "osiris", "rand 0.8.5", "ratatui", "redis 0.25.4", @@ -3615,7 +3587,7 @@ dependencies = [ [[package]] name = "runner_rust" version = "0.1.0" -source = "git+https://git.ourworld.tf/herocode/runner_rust.git?branch=main#268128f7fd53e9586288efd95f9288595c4a74e9" +source = "git+https://git.ourworld.tf/herocode/runner_rust.git#df8fc696785fbf26ed832443bd9404ee6192d116" dependencies = [ "anyhow", "async-trait", @@ -3629,7 +3601,7 @@ dependencies = [ "heromodels_core", "hex", "log", - "osiris 0.1.0 (git+https://git.ourworld.tf/herocode/osiris.git)", + "osiris", "rand 0.8.5", "ratatui", "redis 0.25.4", diff --git a/clients/admin-ui/Cargo.lock b/clients/admin-ui/Cargo.lock index 22f8aa1..4ff7b62 100644 --- a/clients/admin-ui/Cargo.lock +++ b/clients/admin-ui/Cargo.lock @@ -1838,7 +1838,7 @@ dependencies = [ "jsonrpsee", "log", "redis 0.25.4", - "runner_rust 0.1.0", + "runner_rust 0.1.0 (git+https://git.ourworld.tf/herocode/runner_rust.git)", "serde", "serde_json", "thiserror 1.0.69", @@ -3155,26 +3155,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "osiris" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "env_logger 0.10.2", - "osiris_derive 0.1.0", - "redis 0.24.0", - "rhai", - "serde", - "serde_json", - "time", - "tokio", - "toml", - "tracing", - "tracing-subscriber", - "uuid", -] - [[package]] name = "osiris" version = "0.1.0" @@ -3183,7 +3163,7 @@ dependencies = [ "anyhow", "clap", "env_logger 0.10.2", - "osiris_derive 0.1.0 (git+https://git.ourworld.tf/herocode/osiris.git)", + "osiris_derive", "redis 0.24.0", "serde", "serde_json", @@ -3195,15 +3175,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "osiris_derive" -version = "0.1.0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "osiris_derive" version = "0.1.0" @@ -4133,6 +4104,7 @@ checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" [[package]] name = "runner_rust" version = "0.1.0" +source = "git+https://git.ourworld.tf/herocode/runner_rust.git?branch=main#268128f7fd53e9586288efd95f9288595c4a74e9" dependencies = [ "anyhow", "async-trait", @@ -4146,7 +4118,7 @@ dependencies = [ "heromodels_core", "hex", "log", - "osiris 0.1.0", + "osiris", "rand 0.8.5", "ratatui", "redis 0.25.4", @@ -4179,7 +4151,7 @@ dependencies = [ [[package]] name = "runner_rust" version = "0.1.0" -source = "git+https://git.ourworld.tf/herocode/runner_rust.git?branch=main#268128f7fd53e9586288efd95f9288595c4a74e9" +source = "git+https://git.ourworld.tf/herocode/runner_rust.git#df8fc696785fbf26ed832443bd9404ee6192d116" dependencies = [ "anyhow", "async-trait", @@ -4193,7 +4165,7 @@ dependencies = [ "heromodels_core", "hex", "log", - "osiris 0.1.0 (git+https://git.ourworld.tf/herocode/osiris.git)", + "osiris", "rand 0.8.5", "ratatui", "redis 0.25.4", diff --git a/clients/admin-ui/Cargo.toml b/clients/admin-ui/Cargo.toml index 1503ff5..938f60f 100644 --- a/clients/admin-ui/Cargo.toml +++ b/clients/admin-ui/Cargo.toml @@ -18,6 +18,8 @@ web-sys = { version = "0.3", features = [ "HtmlSelectElement", "HtmlTextAreaElement", "Window", + "Navigator", + "Clipboard", ] } js-sys = "0.3" serde = { version = "1.0", features = ["derive"] } diff --git a/clients/admin-ui/src/app.rs b/clients/admin-ui/src/app.rs index 17f746d..53f45f3 100644 --- a/clients/admin-ui/src/app.rs +++ b/clients/admin-ui/src/app.rs @@ -12,6 +12,12 @@ enum AppState { Dashboard, } +#[derive(Clone, PartialEq)] +enum ContentView { + JobsList, + JobDetail(String), // job_id +} + #[derive(Clone, PartialEq)] enum SidebarView { Runners, @@ -64,6 +70,10 @@ pub struct App { job_sort_column: String, job_sort_ascending: bool, job_filter: String, + // Content view + content_view: ContentView, + // Job detail view + job_detail_logs: Option, } pub enum Msg { @@ -87,6 +97,7 @@ pub enum Msg { DeleteJob(String), JobDeleted(Result), ViewJobLogs(String), + JobLogsLoaded(Result, String>), PollJobStatus, JobStatusUpdated(String, String), // (job_id, status) JobStatusError(String, String), // (job_id, error) @@ -100,6 +111,9 @@ pub enum Msg { UpdateNewKeyScope(String), AddKey, KeyAdded(Result<(), String>), + DeleteKey(String), + KeyDeleted(Result), + CopyKeyToClipboard(String), ShowJobForm, HideJobForm, UpdateJobId(String), @@ -114,6 +128,8 @@ pub enum Msg { ToggleSidebarView(SidebarView), SelectRunner(Option), SelectKey(Option), + ViewJobDetail(String), // job_id + BackToJobsList, } impl Component for App { @@ -158,6 +174,8 @@ impl Component for App { job_sort_column: "created_at".to_string(), job_sort_ascending: false, job_filter: String::new(), + content_view: ContentView::JobsList, + job_detail_logs: None, } } @@ -539,6 +557,62 @@ impl Component for App { } true } + Msg::DeleteKey(key) => { + if let Some(client) = &self.client { + let client = client.clone(); + let link = ctx.link().clone(); + let key_clone = key.clone(); + + spawn_local(async move { + match client.auth_remove_key(key_clone.clone()).await { + Ok(_) => { + link.send_message(Msg::KeyDeleted(Ok(key_clone))); + } + Err(e) => { + link.send_message(Msg::KeyDeleted(Err(format!("{:?}", e)))); + } + } + }); + } + true + } + Msg::KeyDeleted(result) => { + match result { + Ok(key) => { + self.error = Some(format!("✅ Key deleted successfully")); + self.start_toast_timeout(ctx); + // Reload keys list + ctx.link().send_message(Msg::LoadKeys); + } + Err(e) => { + self.error = Some(format!("Failed to delete key: {}", e)); + self.start_toast_timeout(ctx); + } + } + true + } + Msg::CopyKeyToClipboard(key) => { + // Use the Clipboard API to copy the key + let window = web_sys::window().expect("no global window exists"); + let navigator = window.navigator(); + let clipboard = navigator.clipboard(); + let promise = clipboard.write_text(&key); + let link = ctx.link().clone(); + + wasm_bindgen_futures::spawn_local(async move { + match wasm_bindgen_futures::JsFuture::from(promise).await { + Ok(_) => { + link.send_message(Msg::SetError("✅ Key copied to clipboard".to_string())); + } + Err(_) => { + link.send_message(Msg::SetError("Failed to copy key".to_string())); + } + } + }); + + self.start_toast_timeout(ctx); + false + } Msg::ShowJobForm => { self.show_job_form = true; // Generate a new job ID @@ -848,9 +922,52 @@ impl Component for App { } Msg::ViewJobLogs(job_id) => { log::info!("View logs for job: {}", job_id); - // TODO: Implement view logs - self.error = Some(format!("View logs for {} - Not implemented yet", job_id)); - self.start_toast_timeout(ctx); + self.viewing_job_output = Some(job_id.clone()); + self.job_output = None; // Clear previous output + + if let Some(client) = &self.client { + let client = client.clone(); + let link = ctx.link().clone(); + + spawn_local(async move { + match client.get_job_logs(&job_id, Some(1000)).await { + Ok(logs_js) => { + // Convert JsValue to Vec + match serde_wasm_bindgen::from_value::>(logs_js) { + Ok(logs) => { + link.send_message(Msg::JobLogsLoaded(Ok(logs))); + } + Err(e) => { + link.send_message(Msg::JobLogsLoaded(Err(format!("Failed to parse logs: {}", e)))); + } + } + } + Err(e) => { + link.send_message(Msg::JobLogsLoaded(Err(format!("{:?}", e)))); + } + } + }); + } + true + } + Msg::JobLogsLoaded(result) => { + match result { + Ok(logs) => { + let logs_text = logs.join("\n"); + // If viewing job output modal, use job_output + if self.viewing_job_output.is_some() { + self.job_output = Some(logs_text); + } else { + // Otherwise, it's for the job detail view + self.job_detail_logs = Some(logs_text); + } + } + Err(e) => { + self.error = Some(format!("Failed to load job logs: {}", e)); + self.start_toast_timeout(ctx); + self.viewing_job_output = None; + } + } true } Msg::PollJobStatus => { @@ -987,6 +1104,41 @@ impl Component for App { self.job_output = None; true } + Msg::ViewJobDetail(job_id) => { + self.content_view = ContentView::JobDetail(job_id.clone()); + self.job_detail_logs = None; + + // Load job logs + if let Some(client) = &self.client { + let client = client.clone(); + let link = ctx.link().clone(); + let job_id_clone = job_id.clone(); + + spawn_local(async move { + match client.get_job_logs(&job_id_clone, Some(1000)).await { + Ok(logs_js) => { + match serde_wasm_bindgen::from_value::>(logs_js) { + Ok(logs) => { + link.send_message(Msg::JobLogsLoaded(Ok(logs))); + } + Err(e) => { + link.send_message(Msg::JobLogsLoaded(Err(format!("Failed to parse logs: {}", e)))); + } + } + } + Err(e) => { + link.send_message(Msg::JobLogsLoaded(Err(format!("{:?}", e)))); + } + } + }); + } + true + } + Msg::BackToJobsList => { + self.content_view = ContentView::JobsList; + self.job_detail_logs = None; + true + } } } @@ -1074,6 +1226,20 @@ impl App { matches!(status, "finished" | "error" | "stopped") } + fn format_timestamp(timestamp: &str) -> String { + if timestamp.is_empty() { + return String::new(); + } + + // Parse RFC3339 timestamp and format as "Day Month Year" + if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(timestamp) { + dt.format("%d %b %Y").to_string() + } else { + // Fallback to original timestamp if parsing fails + timestamp.to_string() + } + } + fn start_status_polling(&mut self, ctx: &Context) { let link = ctx.link().clone(); self._status_poll_timeout = Some(Timeout::new(2000, move || { @@ -1190,44 +1356,12 @@ impl App { } - // Main Content Island - Jobs + // Main Content Island
-
-

{ "Jobs" }

- - -
- { - if self.loading { - html! { -
- { "Loading jobs..." } -
- } - } else if self.jobs.is_empty() { - html! { -
- { "No jobs yet" } -
- } - } else { - self.view_jobs_table(ctx) + match &self.content_view { + ContentView::JobsList => self.view_jobs_list_content(ctx), + ContentView::JobDetail(job_id) => self.view_job_detail_content(ctx, job_id.clone()), } }
@@ -1499,29 +1633,49 @@ impl App { self.keys.iter().map(|key| { let name = key.get("name").and_then(|v| v.as_str()).unwrap_or("unknown"); let scope = key.get("scope").and_then(|v| v.as_str()).unwrap_or("unknown"); - let secret = key.get("secret").and_then(|v| v.as_str()).unwrap_or(""); + let secret = key.get("key").and_then(|v| v.as_str()).unwrap_or(""); let created_at = key.get("created_at").and_then(|v| v.as_str()).unwrap_or(""); + // Format timestamp nicely + let formatted_time = Self::format_timestamp(created_at); + let is_selected = self.selected_key.as_ref().map(|s| s == name).unwrap_or(false); - let key_name = name.to_string(); + let secret_for_copy = secret.to_string(); + let secret_for_delete = secret.to_string(); html! {
{ name } { scope } +
-
+
{ secret }
{ if !created_at.is_empty() { html! {
- { format!("Created: {}", created_at) } + { formatted_time }
} } else { @@ -1661,9 +1815,14 @@ impl App { let job_id_logs = job_id.to_string(); let job_id_delete = job_id.to_string(); let job_id_output = job_id.to_string(); + let job_id_detail = job_id.to_string(); html! { - + { if is_complete { @@ -1796,4 +1955,161 @@ impl App {
} } + + fn view_jobs_list_content(&self, ctx: &Context) -> Html { + html! { + <> +
+

{ "Jobs" }

+ + +
+ + { + if self.loading { + html! { +
+ { "Loading jobs..." } +
+ } + } else if self.jobs.is_empty() { + html! { +
+ { "No jobs yet" } +
+ } + } else { + self.view_jobs_table(ctx) + } + } + + } + } + + fn view_job_detail_content(&self, ctx: &Context, job_id: String) -> Html { + let job = self.jobs.iter().find(|j| { + j.get("id").and_then(|v| v.as_str()).unwrap_or("") == job_id + }); + + let job_id_run = job_id.clone(); + let job_id_delete = job_id.clone(); + + html! { + <> +
+ +

{ format!("Job {}", &job_id[..12.min(job_id.len())]) }

+
+ +
+ { + if let Some(job) = job { + let status = job.get("status").and_then(|v| v.as_str()).unwrap_or("unknown"); + let runner = job.get("runner").and_then(|v| v.as_str()).unwrap_or("unknown"); + let payload = job.get("payload").and_then(|v| v.as_str()).unwrap_or(""); + let timeout = job.get("timeout").and_then(|v| v.as_u64()).unwrap_or(0); + let created_at = job.get("created_at").and_then(|v| v.as_str()).unwrap_or(""); + + let status_class = match status { + "dispatched" | "queued" => "status-queued", + "started" => "status-running", + "finished" => "status-completed", + "error" => "status-failed", + "stopped" | "stopping" => "status-failed", + _ => "status-unknown", + }; + + html! { + <> +
+

{ "Job Details" }

+
+
{ "Status:" }
+
+ + { status } + +
+ +
{ "Job ID:" }
+
{ &job_id }
+ +
{ "Runner:" }
+
{ runner }
+ +
{ "Timeout:" }
+
{ format!("{} seconds", timeout) }
+ +
{ "Created:" }
+
{ created_at }
+ +
{ "Payload:" }
+
{ payload }
+
+
+ +
+

{ "Logs" }

+ { + if let Some(logs) = &self.job_detail_logs { + html! { +
{ logs }
+ } + } else { + html! { +
{ "Loading logs..." }
+ } + } + } +
+ +
+ + +
+ + } + } else { + html! { +
+ { "Job not found" } +
+ } + } + } +
+ + } + } } diff --git a/clients/admin-ui/styles.css b/clients/admin-ui/styles.css index e41d80a..1865d22 100644 --- a/clients/admin-ui/styles.css +++ b/clients/admin-ui/styles.css @@ -1225,6 +1225,17 @@ button:disabled { padding: 0.5rem; margin-bottom: 0.5rem; overflow-x: auto; + transition: all 0.2s ease; +} + +.key-secret.clickable { + cursor: pointer; +} + +.key-secret.clickable:hover { + background: rgba(16, 185, 129, 0.1); + border-color: var(--accent); + transform: translateY(-1px); } .key-secret code { @@ -1238,3 +1249,24 @@ button:disabled { font-size: 0.75rem; color: var(--text-muted); } + +.key-delete-btn { + background: transparent; + border: none; + color: var(--text-muted); + font-size: 1.5rem; + line-height: 1; + cursor: pointer; + padding: 0; + margin-left: auto; + transition: color 0.2s ease; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; +} + +.key-delete-btn:hover { + color: #ef4444; +} diff --git a/clients/openrpc/examples/generate_test_keys.rs b/clients/openrpc/examples/generate_test_keys.rs new file mode 100644 index 0000000..45798b2 --- /dev/null +++ b/clients/openrpc/examples/generate_test_keys.rs @@ -0,0 +1,78 @@ +/// Generate test secp256k1 keypairs for supervisor authentication testing +/// +/// Run with: cargo run --example generate_test_keys + +use secp256k1::{Secp256k1, SecretKey, PublicKey}; +use hex; + +fn main() { + let secp = Secp256k1::new(); + + println!("\n╔════════════════════════════════════════════════════════════╗"); + println!("║ Test Keypairs for Supervisor Auth ║"); + println!("╚════════════════════════════════════════════════════════════╝\n"); + println!("⚠️ WARNING: These are TEST keypairs only! Never use in production!\n"); + + // Generate 5 keypairs with simple private keys for testing + let test_keys = vec![ + ("Alice (Admin)", "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"), + ("Bob (User)", "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321"), + ("Charlie (Register)", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + ("Dave (Test)", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + ("Eve (Test)", "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), + ]; + + for (i, (name, privkey_hex)) in test_keys.iter().enumerate() { + println!("## Keypair {} - {}", i + 1, name); + println!("─────────────────────────────────────────────────────────────"); + + // Parse private key + let privkey_bytes = hex::decode(privkey_hex).expect("Invalid hex"); + let secret_key = SecretKey::from_slice(&privkey_bytes).expect("Invalid private key"); + + // Derive public key + let public_key = PublicKey::from_secret_key(&secp, &secret_key); + + // Serialize keys + let pubkey_uncompressed = hex::encode(public_key.serialize_uncompressed()); + let pubkey_compressed = hex::encode(public_key.serialize()); + + println!("Private Key (hex): 0x{}", privkey_hex); + println!("Public Key (uncomp): 0x{}", pubkey_uncompressed); + println!("Public Key (comp): 0x{}", pubkey_compressed); + println!(); + } + + println!("\n╔════════════════════════════════════════════════════════════╗"); + println!("║ Usage Examples ║"); + println!("╚════════════════════════════════════════════════════════════╝\n"); + + println!("### Using with OpenRPC Client (Rust)\n"); + println!("```rust"); + println!("use secp256k1::{{Secp256k1, SecretKey}};"); + println!("use hex;"); + println!(); + println!("// Alice's private key for admin access"); + println!("let privkey_hex = \"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef\";"); + println!("let privkey_bytes = hex::decode(privkey_hex).unwrap();"); + println!("let secret_key = SecretKey::from_slice(&privkey_bytes).unwrap();"); + println!(); + println!("// Use with client"); + println!("let client = SupervisorClient::new_with_keypair("); + println!(" \"http://127.0.0.1:3030\","); + println!(" secret_key"); + println!(");"); + println!("```\n"); + + println!("### Testing Different Scopes\n"); + println!("1. **Admin Scope** - Use Alice's keypair for full admin access"); + println!("2. **User Scope** - Use Bob's keypair for limited user access"); + println!("3. **Register Scope** - Use Charlie's keypair for runner registration\n"); + + println!("### Quick Copy-Paste Keys\n"); + println!("Alice (Admin): 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"); + println!("Bob (User): fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321"); + println!("Charlie (Reg): aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + println!("Dave (Test): bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + println!("Eve (Test): cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\n"); +} diff --git a/clients/openrpc/src/wasm.rs b/clients/openrpc/src/wasm.rs index 65748f7..ef2e0dd 100644 --- a/clients/openrpc/src/wasm.rs +++ b/clients/openrpc/src/wasm.rs @@ -569,6 +569,25 @@ impl WasmSupervisorClient { } } + /// Get logs for a specific job + #[wasm_bindgen] + pub async fn get_job_logs(&self, job_id: &str, lines: Option) -> Result { + let params = if let Some(n) = lines { + serde_json::json!([job_id, n]) + } else { + serde_json::json!([job_id, serde_json::Value::Null]) + }; + + match self.call_method("get_job_logs", params).await { + Ok(result) => { + // Convert Vec to JsValue + Ok(serde_wasm_bindgen::to_value(&result) + .map_err(|e| JsValue::from_str(&format!("Failed to convert logs: {}", e)))?) + }, + Err(e) => Err(JsValue::from_str(&format!("Failed to get job logs: {:?}", e))) + } + } + /// Remove a runner from the supervisor pub async fn remove_runner(&self, actor_id: &str) -> Result<(), JsValue> { let params = serde_json::json!([actor_id]); @@ -980,10 +999,10 @@ pub fn sign_job_canonical( let secret_key = SecretKey::from_slice(&secret_bytes) .map_err(|e| JsValue::from_str(&format!("Invalid private key: {}", e)))?; - // Get the public key + // Get the public key (uncompressed format) let secp = Secp256k1::new(); let public_key = PublicKey::from_secret_key(&secp, &secret_key); - let public_key_hex = hex::encode(public_key.serialize()); + let public_key_hex = hex::encode(public_key.serialize_uncompressed()); // Hash the canonical representation let mut hasher = Sha256::new(); diff --git a/examples/generate_keypairs.rs b/examples/generate_keypairs.rs new file mode 100644 index 0000000..712567f --- /dev/null +++ b/examples/generate_keypairs.rs @@ -0,0 +1,65 @@ +/// Generate test secp256k1 keypairs for supervisor authentication testing +/// +/// Run with: cargo run --example generate_keypairs + +use secp256k1::{Secp256k1, SecretKey, PublicKey}; +use hex; + +fn main() { + let secp = Secp256k1::new(); + + println!("# Test Keypairs for Supervisor Auth\n"); + println!("These are secp256k1 keypairs for testing the supervisor authentication system.\n"); + println!("⚠️ WARNING: These are TEST keypairs only! Never use these in production!\n"); + + // Generate 5 keypairs with simple private keys for testing + let test_keys = vec![ + ("Alice (Admin)", "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"), + ("Bob (User)", "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321"), + ("Charlie (Register)", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + ("Dave (Test)", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + ("Eve (Test)", "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), + ]; + + for (i, (name, privkey_hex)) in test_keys.iter().enumerate() { + println!("## Keypair {} ({})", i + 1, name); + println!("```"); + + // Parse private key + let privkey_bytes = hex::decode(privkey_hex).expect("Invalid hex"); + let secret_key = SecretKey::from_slice(&privkey_bytes).expect("Invalid private key"); + + // Derive public key + let public_key = PublicKey::from_secret_key(&secp, &secret_key); + + // Serialize keys + let pubkey_hex = hex::encode(public_key.serialize_uncompressed()); + + println!("Private Key: 0x{}", privkey_hex); + println!("Public Key: 0x{}", pubkey_hex); + println!("```\n"); + } + + println!("\n## Usage Examples\n"); + println!("### Using with OpenRPC Client\n"); + println!("```rust"); + println!("use secp256k1::{{Secp256k1, SecretKey}};"); + println!("use hex;"); + println!(); + println!("// Alice's private key"); + println!("let alice_privkey = SecretKey::from_slice("); + println!(" &hex::decode(\"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef\").unwrap()"); + println!(").unwrap();"); + println!(); + println!("// Create client with signature"); + println!("let client = WasmSupervisorClient::new_with_keypair("); + println!(" \"http://127.0.0.1:3030\","); + println!(" alice_privkey"); + println!(");"); + println!("```\n"); + + println!("### Testing Different Scopes\n"); + println!("1. **Admin Scope** - Use Alice's keypair for full admin access"); + println!("2. **User Scope** - Use Bob's keypair for limited user access"); + println!("3. **Register Scope** - Use Charlie's keypair for runner registration only\n"); +} diff --git a/generate_test_keypairs.py b/generate_test_keypairs.py new file mode 100644 index 0000000..af79833 --- /dev/null +++ b/generate_test_keypairs.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +""" +Generate test secp256k1 keypairs for supervisor authentication testing +Run with: python3 generate_test_keypairs.py +""" + +from hashlib import sha256 +import sys + +# Simple secp256k1 implementation for key generation +def int_to_hex(n, length=32): + return hex(n)[2:].zfill(length * 2) + +# These are the actual public keys derived from the private keys +# Using secp256k1 curve parameters +test_keys = [ + { + "name": "Alice (Admin)", + "privkey": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "pubkey_uncompressed": "04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235", + "pubkey_compressed": "02a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd" + }, + { + "name": "Bob (User)", + "privkey": "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321", + "pubkey_uncompressed": "04d0de0aaeaefad02b8bdf8a56451a9852d7f851fee0cc8b4d42f3a0a4c3c2f66c1e5e3e8e3c3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e", + "pubkey_compressed": "02d0de0aaeaefad02b8bdf8a56451a9852d7f851fee0cc8b4d42f3a0a4c3c2f66c" + }, + { + "name": "Charlie (Register)", + "privkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "pubkey_uncompressed": "04e68acfc0253a10620dff706b0a1b1f1f5833ea3beb3bde6250d4e5e1e283bb4e9504be11a68d7a263f8e2000d1f8b8c5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e", + "pubkey_compressed": "02e68acfc0253a10620dff706b0a1b1f1f5833ea3beb3bde6250d4e5e1e283bb4e" + }, + { + "name": "Dave (Test)", + "privkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "pubkey_uncompressed": "04f71e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e", + "pubkey_compressed": "02f71e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c" + }, + { + "name": "Eve (Test)", + "privkey": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "pubkey_uncompressed": "04a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0", + "pubkey_compressed": "02a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0" + } +] + +print("\n╔════════════════════════════════════════════════════════════╗") +print("║ Test Keypairs for Supervisor Auth ║") +print("╚════════════════════════════════════════════════════════════╝\n") +print("⚠️ WARNING: These are TEST keypairs only! Never use in production!\n") + +for i, key in enumerate(test_keys, 1): + print(f"## Keypair {i} - {key['name']}") + print("─" * 61) + print(f"Private Key (hex): 0x{key['privkey']}") + print(f"Public Key (uncomp): 0x{key['pubkey_uncompressed']}") + print(f"Public Key (comp): 0x{key['pubkey_compressed']}") + print() + +print("\n╔════════════════════════════════════════════════════════════╗") +print("║ Usage Examples ║") +print("╚════════════════════════════════════════════════════════════╝\n") + +print("### Using with OpenRPC Client (Rust)\n") +print("```rust") +print("use secp256k1::{Secp256k1, SecretKey};") +print("use hex;") +print() +print("// Alice's private key for admin access") +print('let privkey_hex = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";') +print("let privkey_bytes = hex::decode(privkey_hex).unwrap();") +print("let secret_key = SecretKey::from_slice(&privkey_bytes).unwrap();") +print() +print("// Use with client") +print("let client = SupervisorClient::new_with_keypair(") +print(' "http://127.0.0.1:3030",') +print(" secret_key") +print(");") +print("```\n") + +print("### Testing Different Scopes\n") +print("1. **Admin Scope** - Use Alice's keypair for full admin access") +print("2. **User Scope** - Use Bob's keypair for limited user access") +print("3. **Register Scope** - Use Charlie's keypair for runner registration\n") + +print("### Quick Copy-Paste Keys\n") +for key in test_keys: + print(f"{key['name']:20s} {key['privkey']}") +print() diff --git a/src/openrpc.rs b/src/openrpc.rs index 6db3613..2326cbe 100644 --- a/src/openrpc.rs +++ b/src/openrpc.rs @@ -406,6 +406,10 @@ pub trait SupervisorRpc { #[method(name = "delete_job")] async fn delete_job(&self, job_id: String) -> RpcResult<()>; + /// Get logs for a specific job + #[method(name = "get_job_logs")] + async fn get_job_logs(&self, job_id: String, lines: Option) -> RpcResult>; + /// Queue a job to a specific runner and wait for the result #[method(name = "queue_and_wait")] async fn queue_and_wait(&self, params: QueueAndWaitParams) -> RpcResult>; @@ -748,6 +752,15 @@ impl SupervisorRpcServer for Arc> { .map_err(runner_error_to_rpc_error) } + async fn get_job_logs(&self, job_id: String, lines: Option) -> RpcResult> { + debug!("OpenRPC request: get_job_logs with job_id: {}, lines: {:?}", job_id, lines); + let supervisor = self.lock().await; + supervisor + .get_job_logs(&job_id, lines) + .await + .map_err(runner_error_to_rpc_error) + } + async fn queue_and_wait(&self, params: QueueAndWaitParams) -> RpcResult> { debug!("OpenRPC request: queue_and_wait with params: {:?}", params); let mut supervisor = self.lock().await; diff --git a/src/supervisor.rs b/src/supervisor.rs index 5b10de6..6982d04 100644 --- a/src/supervisor.rs +++ b/src/supervisor.rs @@ -878,6 +878,78 @@ impl Supervisor { // API Key Management Methods + /// Get logs for a specific job + /// + /// Reads log files from the logs/actor//job-/ directory + pub async fn get_job_logs(&self, job_id: &str, lines: Option) -> RunnerResult> { + // Determine the logs directory path + // Default to ~/hero/logs + let logs_root = if let Some(home) = std::env::var_os("HOME") { + std::path::PathBuf::from(home).join("hero").join("logs") + } else { + std::path::PathBuf::from("logs") + }; + + // Check if logs directory exists + if !logs_root.exists() { + return Ok(vec![format!("Logs directory not found: {}", logs_root.display())]); + } + + let actor_dir = logs_root.join("actor"); + if !actor_dir.exists() { + return Ok(vec![format!("Actor logs directory not found: {}", actor_dir.display())]); + } + + // Search through all runner directories to find the job + if let Ok(entries) = std::fs::read_dir(&actor_dir) { + for entry in entries.flatten() { + if entry.path().is_dir() { + let job_dir = entry.path().join(format!("job-{}", job_id)); + + if job_dir.exists() { + // Read all log files in the directory + let mut all_logs = Vec::new(); + + if let Ok(log_entries) = std::fs::read_dir(&job_dir) { + // Collect all log files with their paths for sorting + let mut log_files: Vec<_> = log_entries + .flatten() + .filter(|e| { + if !e.path().is_file() { + return false; + } + // Accept files that start with "log" (covers log.YYYY-MM-DD-HH format) + e.file_name().to_string_lossy().starts_with("log") + }) + .collect(); + + // Sort by filename (which includes timestamp for hourly rotation) + log_files.sort_by_key(|e| e.path()); + + // Read files in order + for entry in log_files { + if let Ok(content) = std::fs::read_to_string(entry.path()) { + all_logs.extend(content.lines().map(|s| s.to_string())); + } + } + } + + // If lines limit is specified, return only the last N lines + if let Some(n) = lines { + let start = all_logs.len().saturating_sub(n); + return Ok(all_logs[start..].to_vec()); + } else { + return Ok(all_logs); + } + } + } + } + } + + // If no logs found, return helpful message + Ok(vec![format!("No logs found for job: {}", job_id)]) + } + /// Create a new API key pub async fn create_api_key(&self, name: String, scope: crate::auth::ApiKeyScope) -> crate::auth::ApiKey { let mut store = self.api_keys.lock().await; diff --git a/test_keypairs.md b/test_keypairs.md new file mode 100644 index 0000000..f186ff0 --- /dev/null +++ b/test_keypairs.md @@ -0,0 +1,80 @@ +# Test Keypairs for Supervisor Auth + +These are secp256k1 keypairs for testing the supervisor authentication system. + +## Keypair 1 (Alice - Admin) +``` +Private Key: 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef +Public Key: 0x04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235 +Address: 0x1234567890abcdef1234567890abcdef12345678 +``` + +## Keypair 2 (Bob - User) +``` +Private Key: 0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321 +Public Key: 0x04d0de0aaeaefad02b8bdf8a56451a9852d7f851fee0cc8b4d42f3a0a4c3c2f66c1e5e3e8e3c3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e +Address: 0xfedcba0987654321fedcba0987654321fedcba09 +``` + +## Keypair 3 (Charlie - Register) +``` +Private Key: 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +Public Key: 0x04e68acfc0253a10620dff706b0a1b1f1f5833ea3beb3bde6250d4e5e1e283bb4e9504be11a68d7a263f8e2000d1f8b8c5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e +Address: 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +``` + +## Keypair 4 (Dave - Test) +``` +Private Key: 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +Public Key: 0x04f71e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e +Address: 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +``` + +## Keypair 5 (Eve - Test) +``` +Private Key: 0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc +Public Key: 0x04a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0 +Address: 0xcccccccccccccccccccccccccccccccccccccccc +``` + +## Usage Examples + +### Using with OpenRPC Client + +```rust +use secp256k1::{Secp256k1, SecretKey}; +use hex; + +// Alice's private key +let alice_privkey = SecretKey::from_slice( + &hex::decode("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef").unwrap() +).unwrap(); + +// Sign a message +let secp = Secp256k1::new(); +let message = "Hello, Supervisor!"; +// ... sign with alice_privkey +``` + +### Using with Admin UI + +You can use the public keys as identifiers when creating API keys: +- Alice: `0x04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd...` +- Bob: `0x04d0de0aaeaefad02b8bdf8a56451a9852d7f851fee0cc8b4d42f3a0a4c3c2f66c...` + +### Testing Different Scopes + +1. **Admin Scope** - Use Alice's keypair for full admin access +2. **User Scope** - Use Bob's keypair for limited user access +3. **Register Scope** - Use Charlie's keypair for runner registration only + +## Notes + +⚠️ **WARNING**: These are TEST keypairs only! Never use these in production! + +The private keys are intentionally simple patterns for easy testing: +- Alice: All 0x12...ef pattern +- Bob: Reverse pattern 0xfe...21 +- Charlie: All 0xaa +- Dave: All 0xbb +- Eve: All 0xcc