forked from herocode/horus
628 lines
21 KiB
Rust
628 lines
21 KiB
Rust
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<serde_json::Value, ClientError>;
|
|
}
|
|
|
|
/// 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<serde_json::Value, ClientError> {
|
|
// 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<T: SupervisorTransport = HttpTransport> {
|
|
transport: T,
|
|
#[allow(dead_code)]
|
|
secret: String,
|
|
}
|
|
|
|
/// Legacy type alias for backward compatibility
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub type HttpSupervisorClient = SupervisorClient<HttpTransport>;
|
|
|
|
/// 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<wasm_bindgen::JsValue> for ClientError {
|
|
fn from(js_val: wasm_bindgen::JsValue) -> Self {
|
|
ClientError::JavaScript(format!("{:?}", js_val))
|
|
}
|
|
}
|
|
|
|
/// Result type for client operations
|
|
pub type ClientResult<T> = Result<T, ClientError>;
|
|
|
|
/// 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<std::collections::HashMap<String, String>>,
|
|
}
|
|
|
|
|
|
|
|
/// 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<String>,
|
|
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 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<String>,
|
|
pub created_at: Option<String>,
|
|
}
|
|
|
|
/// 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<HttpTransport> {
|
|
/// Create a builder for HTTP-based SupervisorClient
|
|
pub fn builder() -> SupervisorClientBuilder {
|
|
SupervisorClientBuilder::new()
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
impl<T: SupervisorTransport> SupervisorClient<T> {
|
|
/// 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<serde_json::Value> {
|
|
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<String> {
|
|
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<Vec<Job>> {
|
|
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<u64>,
|
|
) -> ClientResult<JobRunResponse> {
|
|
// 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<JobStartResponse> {
|
|
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<JobStatus> {
|
|
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<JobResult> {
|
|
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<Vec<String>> {
|
|
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<RunnerStatus> {
|
|
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<usize>,
|
|
follow: bool,
|
|
) -> ClientResult<Vec<LogInfo>> {
|
|
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<Option<String>> {
|
|
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<JobResult> {
|
|
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<Option<String>> {
|
|
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<Vec<(String, RunnerStatus)>> {
|
|
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<Vec<(String, bool)>> {
|
|
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<Vec<(String, bool)>> {
|
|
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<Vec<(String, RunnerStatus)>> {
|
|
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<SupervisorInfo> {
|
|
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<SupervisorInfo> {
|
|
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<Job> {
|
|
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<AuthVerifyResponse> {
|
|
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<ApiKey> {
|
|
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<Vec<ApiKey>> {
|
|
let result = self
|
|
.transport
|
|
.call("key.list", serde_json::json!([]))
|
|
.await?;
|
|
serde_json::from_value(result).map_err(ClientError::Serialization)
|
|
}
|
|
} |