use serde::{Deserialize, Serialize}; use thiserror::Error; use serde_json; #[cfg(not(target_arch = "wasm32"))] use async_trait::async_trait; // Transport implementations #[cfg(not(target_arch = "wasm32"))] pub mod transports; // Import types from the main supervisor crate // WASM-compatible client module #[cfg(target_arch = "wasm32")] pub mod wasm; // Builder module for type-safe client construction pub mod builder; // Re-export WASM types for convenience #[cfg(target_arch = "wasm32")] pub use wasm::{WasmSupervisorClient, WasmJobType, WasmRunnerType, create_job_canonical_repr, sign_job_canonical}; // Re-export builder for convenience #[cfg(target_arch = "wasm32")] pub use builder::WasmSupervisorClientBuilder; // Re-export HTTP builder for convenience #[cfg(not(target_arch = "wasm32"))] pub use builder::SupervisorClientBuilder; // Native client dependencies #[cfg(not(target_arch = "wasm32"))] use jsonrpsee::{ core::client::ClientT, http_client::HttpClient, rpc_params, }; /// Transport abstraction for supervisor communication /// Allows different transport layers (HTTP, Mycelium, etc.) #[cfg(not(target_arch = "wasm32"))] #[async_trait] pub trait SupervisorTransport: Send + Sync { /// Send a JSON-RPC request and await the response async fn call( &self, method: &str, params: serde_json::Value, ) -> Result; } /// HTTP transport implementation using jsonrpsee #[cfg(not(target_arch = "wasm32"))] #[derive(Clone)] pub struct HttpTransport { client: HttpClient, } #[cfg(not(target_arch = "wasm32"))] #[async_trait] impl SupervisorTransport for HttpTransport { async fn call( &self, method: &str, params: serde_json::Value, ) -> Result { // params is already an array from the caller // jsonrpsee expects params as an array, so pass it directly let result: serde_json::Value = if params.is_array() { // Use the array directly with rpc_params let arr = params.as_array().unwrap(); match arr.len() { 0 => self.client.request(method, rpc_params![]).await?, 1 => self.client.request(method, rpc_params![&arr[0]]).await?, _ => { // For multiple params, we need to pass them as a slice self.client.request(method, rpc_params![arr]).await? } } } else { // Single param not in array self.client.request(method, rpc_params![¶ms]).await? }; Ok(result) } } /// Client for communicating with Hero Supervisor OpenRPC server /// Generic over transport layer (HTTP, Mycelium, etc.) #[cfg(not(target_arch = "wasm32"))] #[derive(Clone)] pub struct SupervisorClient { transport: T, #[allow(dead_code)] secret: String, } /// Legacy type alias for backward compatibility #[cfg(not(target_arch = "wasm32"))] pub type HttpSupervisorClient = SupervisorClient; /// Error types for client operations #[cfg(not(target_arch = "wasm32"))] #[derive(Error, Debug)] pub enum ClientError { #[error("JSON-RPC error: {0}")] JsonRpc(#[from] jsonrpsee::core::ClientError), #[error("Serialization error: {0}")] Serialization(#[from] serde_json::Error), #[error("HTTP client error: {0}")] Http(String), #[error("Server error: {message}")] Server { message: String }, } /// Error types for WASM client operations #[cfg(target_arch = "wasm32")] #[derive(Error, Debug)] pub enum ClientError { #[error("JSON-RPC error: {0}")] JsonRpc(String), #[error("Serialization error: {0}")] Serialization(#[from] serde_json::Error), #[error("HTTP client error: {0}")] Http(String), #[error("Server error: {message}")] Server { message: String }, #[error("JavaScript error: {0}")] JavaScript(String), #[error("Network error: {0}")] Network(String), } // Implement From for jsonrpsee ClientError for WASM #[cfg(target_arch = "wasm32")] impl From for ClientError { fn from(js_val: wasm_bindgen::JsValue) -> Self { ClientError::JavaScript(format!("{:?}", js_val)) } } /// Result type for client operations pub type ClientResult = Result; /// Request parameters for generating API keys (auto-generates key value) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GenerateApiKeyParams { pub name: String, pub scope: String, // "admin", "registrar", or "user" } /// Configuration for a runner #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RunnerConfig { /// 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>, } /// Job result response #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum JobResult { Success { success: String }, Error { error: String }, } /// Job status response #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JobStatusResponse { pub job_id: String, pub status: String, pub created_at: String, pub started_at: Option, pub completed_at: Option, } /// Response from job.run (blocking execution) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JobRunResponse { pub job_id: String, pub status: String, pub result: Option, } /// 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 hero-job crate (both native and WASM) pub use hero_job::{Job, JobStatus, JobError, JobBuilder, JobSignature}; // Note: Job client is now part of hero-supervisor crate // Re-exports removed - use hero_supervisor::job_client directly if needed /// Process status wrapper for OpenRPC serialization (matches server response) #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum ProcessStatusWrapper { Running, Stopped, Starting, Stopping, Error(String), } /// Log information wrapper for OpenRPC serialization (matches server response) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LogInfoWrapper { pub timestamp: String, pub level: String, pub message: String, } /// Supervisor information response containing secret counts and server details #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SupervisorInfo { pub server_url: String, } /// API Key information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ApiKey { pub key: String, pub name: String, pub scope: String, pub created_at: String, } /// Auth verification response #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuthVerifyResponse { pub scope: String, pub name: Option, pub created_at: Option, } /// Type aliases for convenience #[cfg(not(target_arch = "wasm32"))] pub type RunnerStatus = ProcessStatusWrapper; #[cfg(not(target_arch = "wasm32"))] pub type LogInfo = LogInfoWrapper; #[cfg(not(target_arch = "wasm32"))] impl SupervisorClient { /// Create a builder for HTTP-based SupervisorClient pub fn builder() -> SupervisorClientBuilder { SupervisorClientBuilder::new() } } #[cfg(not(target_arch = "wasm32"))] impl SupervisorClient { /// Create a new client with a custom transport pub fn new(transport: T, secret: String) -> Self { Self { transport, secret } } /// Test connection using OpenRPC discovery method /// This calls the standard `rpc.discover` method that should be available on any OpenRPC server pub async fn discover(&self) -> ClientResult { self.transport.call("rpc.discover", serde_json::json!([])).await } /// Register a new runner to the supervisor /// The runner name is also used as the queue name /// Authentication via Authorization header (set during client creation) pub async fn runner_create( &self, name: &str, ) -> ClientResult<()> { let result = self.transport.call("runner.create", serde_json::json!([name])).await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// Create a new job without queuing it to a runner /// Authentication via Authorization header (set during client creation) pub async fn job_create( &self, job: Job, ) -> ClientResult { let result = self.transport.call("job.create", serde_json::json!([job])).await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// List all jobs pub async fn job_list(&self) -> ClientResult> { let result = self.transport.call("job.list", serde_json::json!([])).await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// Run a job on the appropriate runner and wait for the result (blocking) /// This method queues the job and waits for completion before returning /// The secret is sent via Authorization header (set during client creation) pub async fn job_run( &self, job: Job, timeout: Option, ) -> ClientResult { // Server expects Job directly as params, not wrapped in an object let params = if let Some(_t) = timeout { // If timeout is provided, we need to extend the Job with timeout field // For now, just send the job directly and ignore timeout // TODO: Add timeout support to the RPC method signature serde_json::to_value(&job).map_err(ClientError::Serialization)? } else { serde_json::to_value(&job).map_err(ClientError::Serialization)? }; let result = self.transport.call("job.run", params).await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// Start a job without waiting for the result (non-blocking) /// This method queues the job and returns immediately with the job_id /// Authentication via Authorization header (set during client creation) pub async fn job_start( &self, job: Job, ) -> ClientResult { let params = serde_json::json!({ "job": job }); let result = self.transport.call("job.start", serde_json::json!([params])).await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// Get the current status of a job pub async fn job_status(&self, job_id: &str) -> ClientResult { let result = self.transport.call("job.status", serde_json::json!([job_id])).await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// Get the result of a completed job (blocks until result is available) pub async fn job_result(&self, job_id: &str) -> ClientResult { let result = self.transport.call("job.result", serde_json::json!([job_id])).await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// Remove a runner from the supervisor /// Authentication via Authorization header (set during client creation) pub async fn runner_remove(&self, runner_id: &str) -> ClientResult<()> { let result = self.transport.call("runner.remove", serde_json::json!([runner_id])).await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// List all runner IDs pub async fn runner_list(&self) -> ClientResult> { let result = self.transport.call("runner.list", serde_json::json!([])).await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// Start a specific runner /// Authentication via Authorization header (set during client creation) pub async fn start_runner(&self, actor_id: &str) -> ClientResult<()> { let result = self.transport.call("runner.start", serde_json::json!([actor_id])).await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// Add a runner to the supervisor /// Authentication via Authorization header (set during client creation) pub async fn add_runner(&self, config: RunnerConfig) -> ClientResult<()> { let params = serde_json::json!({ "config": config }); let _result = self .transport .call("runner.add", serde_json::json!([params])) .await?; Ok(()) } /// Get status of a specific runner /// Authentication via Authorization header (set during client creation) pub async fn get_runner_status(&self, actor_id: &str) -> ClientResult { let result = self .transport .call("runner.status", serde_json::json!([actor_id])) .await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// Get logs for a specific runner pub async fn get_runner_logs( &self, actor_id: &str, lines: Option, follow: bool, ) -> ClientResult> { let result = self .transport .call("get_runner_logs", serde_json::json!([actor_id, lines, follow])) .await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// Queue a job to a specific runner pub async fn queue_job_to_runner(&self, runner: &str, job: Job) -> ClientResult<()> { let params = serde_json::json!({ "runner": runner, "job": job }); let _result = self .transport .call("queue_job_to_runner", serde_json::json!([params])) .await?; Ok(()) } /// Queue a job and wait for completion pub async fn queue_and_wait(&self, runner: &str, job: Job, timeout_secs: u64) -> ClientResult> { let params = serde_json::json!({ "runner": runner, "job": job, "timeout_secs": timeout_secs }); let result = self .transport .call("queue_and_wait", serde_json::json!([params])) .await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// Run a job on a specific runner pub async fn run_job(&self, job: Job) -> ClientResult { let params = serde_json::json!({ "job": job }); let result = self .transport .call("job.run", serde_json::json!([params])) .await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// Get job result by job ID pub async fn get_job_result(&self, job_id: &str) -> ClientResult> { let result = self .transport .call("get_job_result", serde_json::json!([job_id])) .await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// Get status of all runners pub async fn get_all_runner_status(&self) -> ClientResult> { let result = self .transport .call("get_all_runner_status", serde_json::json!([])) .await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// Start all runners pub async fn start_all(&self) -> ClientResult> { let result = self .transport .call("start_all", serde_json::json!([])) .await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// Stop all runners pub async fn stop_all(&self, force: bool) -> ClientResult> { let result = self .transport .call("stop_all", serde_json::json!([force])) .await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// Get status of all runners (alternative method) pub async fn get_all_status(&self) -> ClientResult> { let result = self .transport .call("get_all_status", serde_json::json!([])) .await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// Add a secret to the supervisor pub async fn add_secret( &self, secret_type: &str, secret_value: &str, ) -> ClientResult<()> { let params = serde_json::json!({ "secret_type": secret_type, "secret_value": secret_value }); let _result = self .transport .call("add_secret", serde_json::json!([params])) .await?; Ok(()) } /// Remove a secret from the supervisor pub async fn remove_secret( &self, secret_type: &str, secret_value: &str, ) -> ClientResult<()> { let params = serde_json::json!({ "secret_type": secret_type, "secret_value": secret_value }); let _result = self .transport .call("remove_secret", serde_json::json!([params])) .await?; Ok(()) } /// List secrets (returns supervisor info including secret counts) pub async fn list_secrets(&self) -> ClientResult { let params = serde_json::json!({}); let result = self .transport .call("list_secrets", serde_json::json!([params])) .await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// Stop a running job pub async fn job_stop(&self, job_id: &str) -> ClientResult<()> { let result = self.transport.call("job.stop", serde_json::json!([job_id])).await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// Delete a job from the system pub async fn job_delete(&self, job_id: &str) -> ClientResult<()> { let result = self.transport.call("job.delete", serde_json::json!([job_id])).await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// Get supervisor information including secret counts pub async fn get_supervisor_info(&self) -> ClientResult { let result = self .transport .call("supervisor.info", serde_json::json!([])) .await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// Get a job by ID pub async fn job_get(&self, job_id: &str) -> ClientResult { let result = self .transport .call("job.get", serde_json::json!([job_id])) .await?; serde_json::from_value(result).map_err(ClientError::Serialization) } // ========== Auth/API Key Methods ========== /// Verify the current API key pub async fn auth_verify(&self) -> ClientResult { let result = self .transport .call("auth.verify", serde_json::json!([])) .await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// Create a new API key (admin only) pub async fn key_create(&self, key: ApiKey) -> ClientResult<()> { let _result = self .transport .call("auth.key.create", serde_json::json!([key])) .await?; Ok(()) } /// Generate a new API key with auto-generated key value (admin only) pub async fn key_generate(&self, params: GenerateApiKeyParams) -> ClientResult { let result = self .transport .call("key.generate", serde_json::json!([params])) .await?; serde_json::from_value(result).map_err(ClientError::Serialization) } /// Remove an API key (admin only) pub async fn key_delete(&self, key_id: String) -> ClientResult<()> { let _result = self .transport .call("key.delete", serde_json::json!([key_id])) .await?; Ok(()) } /// List all API keys (admin only) pub async fn key_list(&self) -> ClientResult> { let result = self .transport .call("key.list", serde_json::json!([])) .await?; serde_json::from_value(result).map_err(ClientError::Serialization) } }