Files
openhuman/src-tauri/src/core_server.rs
T
Steven EnamakelandGitHub 37318bc73d fix: stabilize daemon service lifecycle and align Agent Status UI (#6)
* Update Welcome page to integrate OAuth provider and adjust branding

- Added OAuthProviderButton to the Welcome component for Google authentication.
- Updated the branding from AlphaHuman to OpenHuman in the FeaturesStep component.
- Removed unnecessary Lottie animation from the Onboarding page for a cleaner layout.
- Changed the backend URL to point to the new API endpoint for TinyHumans.

* Refactor routing and sidebar components for improved clarity

- Commented out the Mnemonic route and related logic in AppRoutes for future consideration.
- Updated MiniSidebar to comment out the Invite Friends section.
- Removed unused navigation logic and upgrade call-to-action from the Home page.
- Adjusted Onboarding navigation to redirect to Home instead of Mnemonic.

* Comment out unused settings menu items for future consideration in SettingsHome component

* Add core process and RPC functionality for Alphahuman

- Introduced new binaries: `alphahuman-core` and `alphahuman-cli` for core process management and command-line interaction.
- Implemented `CoreProcessHandle` for managing the lifecycle of the core process.
- Added `core_rpc` module for handling RPC requests and responses.
- Created `core_server` module to manage core server logic and routing.
- Updated `alphahuman` commands to utilize the new RPC structure for health checks, security policies, and configuration management.
- Refactored existing code to streamline interactions with the core process and improve overall architecture.

* Update dependencies and enhance CLI functionality

- Added `clap` for command-line argument parsing in the `alphahuman-cli`.
- Updated `Cargo.lock` and `Cargo.toml` to include `clap` and its features.
- Refactored `alphahuman-cli` to utilize structured command handling with subcommands.
- Improved core process management and logging in `core_process.rs`.
- Enhanced routing and error handling in `core_server.rs` with new root and not found handlers.
- Updated various commands in `alphahuman.rs` to ensure core process is running before executing RPC calls.

* Enhance service management and CLI functionality

- Added new functions for daemon program arguments and command line construction in the service module.
- Updated macOS and Linux installation functions to dynamically generate program arguments and command lines.
- Introduced a new `Reinstall` command in the CLI for easier service management.
- Refactored service command handling to utilize local service functions for improved clarity and maintainability.

* Refactor daemon references to agent in components and hooks

- Updated terminology from "Daemon" to "Agent" in MiniSidebar, DaemonHealthPanel, and TauriCommandsPanel for consistency.
- Modified useDaemonHealth hook to probe agent status and handle agent lifecycle management.
- Introduced new agent server status interface and function in Tauri commands for improved status checking.
- Adjusted related comments and error handling to reflect the changes in terminology.

* Refactor Home component and enhance macOS service management

- Updated Home component to navigate to in-app conversations instead of opening a Telegram bot link.
- Improved macOS service management by implementing modern lifecycle commands and adding compatibility fallbacks for service control.
- Introduced new utility functions for handling macOS GUI domain and service targets.

* Refactor Home component to enhance navigation

- Removed SkillsGrid component and replaced it with a button that navigates to the Skills page.
- Improved layout with additional margin for better spacing in the Home component.
2026-03-24 21:04:19 -07:00

729 lines
23 KiB
Rust

use anyhow::Result;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use axum::{Json, Router};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::alphahuman::config::Config;
use crate::alphahuman::health;
use crate::alphahuman::security::{SecretStore, SecurityPolicy};
use crate::alphahuman::{doctor, hardware, integrations, migration, onboard, service};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandResponse<T> {
pub result: T,
pub logs: Vec<String>,
}
fn command_response<T>(result: T, logs: Vec<String>) -> CommandResponse<T> {
CommandResponse { result, logs }
}
#[derive(Debug, Deserialize)]
struct RpcRequest {
#[allow(dead_code)]
jsonrpc: String,
id: serde_json::Value,
method: String,
#[serde(default)]
params: serde_json::Value,
}
#[derive(Debug, Serialize)]
struct RpcSuccess {
jsonrpc: &'static str,
id: serde_json::Value,
result: serde_json::Value,
}
#[derive(Debug, Serialize)]
struct RpcFailure {
jsonrpc: &'static str,
id: serde_json::Value,
error: RpcError,
}
#[derive(Debug, Serialize)]
struct RpcError {
code: i64,
message: String,
data: Option<serde_json::Value>,
}
#[derive(Clone)]
struct AppState {
core_version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigSnapshot {
pub config: serde_json::Value,
pub workspace_dir: String,
pub config_path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelSettingsUpdate {
pub api_key: Option<String>,
pub api_url: Option<String>,
pub default_provider: Option<String>,
pub default_model: Option<String>,
pub default_temperature: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemorySettingsUpdate {
pub backend: Option<String>,
pub auto_save: Option<bool>,
pub embedding_provider: Option<String>,
pub embedding_model: Option<String>,
pub embedding_dimensions: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GatewaySettingsUpdate {
pub host: Option<String>,
pub port: Option<u16>,
pub require_pairing: Option<bool>,
pub allow_public_bind: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuntimeSettingsUpdate {
pub kind: Option<String>,
pub reasoning_enabled: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrowserSettingsUpdate {
pub enabled: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuntimeFlags {
pub browser_allow_all: bool,
pub log_prompts: bool,
}
#[derive(Debug, Deserialize)]
struct AgentChatParams {
message: String,
provider_override: Option<String>,
model_override: Option<String>,
temperature: Option<f64>,
}
#[derive(Debug, Deserialize)]
struct DoctorModelsParams {
provider_override: Option<String>,
use_cache: Option<bool>,
}
#[derive(Debug, Deserialize)]
struct IntegrationInfoParams {
name: String,
}
#[derive(Debug, Deserialize)]
struct ModelsRefreshParams {
provider_override: Option<String>,
force: Option<bool>,
}
#[derive(Debug, Deserialize)]
struct MigrateOpenClawParams {
source_workspace: Option<String>,
dry_run: Option<bool>,
}
#[derive(Debug, Deserialize)]
struct HardwareIntrospectParams {
path: String,
}
#[derive(Debug, Deserialize)]
struct EncryptSecretParams {
plaintext: String,
}
#[derive(Debug, Deserialize)]
struct DecryptSecretParams {
ciphertext: String,
}
#[derive(Debug, Deserialize)]
struct SetBrowserAllowAllParams {
enabled: bool,
}
async fn load_alphahuman_config() -> Result<Config, String> {
let timeout_duration = std::time::Duration::from_secs(30);
match tokio::time::timeout(timeout_duration, Config::load_or_init()).await {
Ok(Ok(config)) => Ok(config),
Ok(Err(e)) => Err(e.to_string()),
Err(_) => Err("Config loading timed out".to_string()),
}
}
fn snapshot_config(config: &Config) -> Result<ConfigSnapshot, String> {
let value = serde_json::to_value(config).map_err(|e| e.to_string())?;
Ok(ConfigSnapshot {
config: value,
workspace_dir: config.workspace_dir.display().to_string(),
config_path: config.config_path.display().to_string(),
})
}
fn env_flag_enabled(key: &str) -> bool {
matches!(
std::env::var(key).ok().as_deref(),
Some("1") | Some("true") | Some("TRUE") | Some("yes") | Some("YES")
)
}
fn secret_store_for_config(config: &Config) -> SecretStore {
let data_dir = config
.config_path
.parent()
.map_or_else(|| std::path::PathBuf::from("."), std::path::PathBuf::from);
SecretStore::new(&data_dir, true)
}
fn parse_params<T: DeserializeOwned>(params: serde_json::Value) -> Result<T, String> {
serde_json::from_value(params).map_err(|e| format!("invalid params: {e}"))
}
fn rpc_error_response(id: serde_json::Value, code: i64, message: String) -> Response {
(
StatusCode::OK,
Json(RpcFailure {
jsonrpc: "2.0",
id,
error: RpcError {
code,
message,
data: None,
},
}),
)
.into_response()
}
fn to_rpc_success(id: serde_json::Value, value: serde_json::Value) -> Response {
(
StatusCode::OK,
Json(RpcSuccess {
jsonrpc: "2.0",
id,
result: value,
}),
)
.into_response()
}
fn to_json_value<T: Serialize>(value: T) -> Result<serde_json::Value, String> {
serde_json::to_value(value).map_err(|e| e.to_string())
}
async fn rpc_handler(State(state): State<AppState>, Json(req): Json<RpcRequest>) -> Response {
let id = req.id.clone();
let result = dispatch(state, req.method.as_str(), req.params).await;
match result {
Ok(value) => to_rpc_success(id, value),
Err(message) => rpc_error_response(id, -32000, message),
}
}
async fn dispatch(
state: AppState,
method: &str,
params: serde_json::Value,
) -> Result<serde_json::Value, String> {
match method {
"core.ping" => to_json_value(json!({ "ok": true })),
"core.version" => to_json_value(json!({ "version": state.core_version })),
"alphahuman.health_snapshot" => to_json_value(command_response(
health::snapshot_json(),
vec!["health_snapshot requested".to_string()],
)),
"alphahuman.security_policy_info" => {
let policy = SecurityPolicy::default();
let payload = json!({
"autonomy": policy.autonomy,
"workspace_only": policy.workspace_only,
"allowed_commands": policy.allowed_commands,
"max_actions_per_hour": policy.max_actions_per_hour,
"require_approval_for_medium_risk": policy.require_approval_for_medium_risk,
"block_high_risk_commands": policy.block_high_risk_commands,
});
to_json_value(command_response(
payload,
vec!["security_policy_info computed".to_string()],
))
}
"alphahuman.get_config" => {
let config = load_alphahuman_config().await?;
let snapshot = snapshot_config(&config)?;
to_json_value(command_response(
snapshot,
vec![format!(
"config loaded from {}",
config.config_path.display()
)],
))
}
"alphahuman.update_model_settings" => {
let update: ModelSettingsUpdate = parse_params(params)?;
let mut config = load_alphahuman_config().await?;
if let Some(api_key) = update.api_key {
config.api_key = if api_key.trim().is_empty() {
None
} else {
Some(api_key)
};
}
if let Some(api_url) = update.api_url {
config.api_url = if api_url.trim().is_empty() {
None
} else {
Some(api_url)
};
}
if let Some(provider) = update.default_provider {
config.default_provider = if provider.trim().is_empty() {
None
} else {
Some(provider)
};
}
if let Some(model) = update.default_model {
config.default_model = if model.trim().is_empty() {
None
} else {
Some(model)
};
}
if let Some(temp) = update.default_temperature {
config.default_temperature = temp;
}
config.save().await.map_err(|e| e.to_string())?;
let snapshot = snapshot_config(&config)?;
to_json_value(command_response(
snapshot,
vec![format!(
"model settings saved to {}",
config.config_path.display()
)],
))
}
"alphahuman.update_memory_settings" => {
let update: MemorySettingsUpdate = parse_params(params)?;
let mut config = load_alphahuman_config().await?;
if let Some(backend) = update.backend {
config.memory.backend = backend;
}
if let Some(auto_save) = update.auto_save {
config.memory.auto_save = auto_save;
}
if let Some(provider) = update.embedding_provider {
config.memory.embedding_provider = provider;
}
if let Some(model) = update.embedding_model {
config.memory.embedding_model = model;
}
if let Some(dimensions) = update.embedding_dimensions {
config.memory.embedding_dimensions = dimensions;
}
config.save().await.map_err(|e| e.to_string())?;
let snapshot = snapshot_config(&config)?;
to_json_value(command_response(
snapshot,
vec![format!(
"memory settings saved to {}",
config.config_path.display()
)],
))
}
"alphahuman.update_gateway_settings" => {
let update: GatewaySettingsUpdate = parse_params(params)?;
let mut config = load_alphahuman_config().await?;
if let Some(host) = update.host {
config.gateway.host = host;
}
if let Some(port) = update.port {
config.gateway.port = port;
}
if let Some(require_pairing) = update.require_pairing {
config.gateway.require_pairing = require_pairing;
}
if let Some(allow_public_bind) = update.allow_public_bind {
config.gateway.allow_public_bind = allow_public_bind;
}
config.save().await.map_err(|e| e.to_string())?;
let snapshot = snapshot_config(&config)?;
to_json_value(command_response(
snapshot,
vec![format!(
"gateway settings saved to {}",
config.config_path.display()
)],
))
}
"alphahuman.update_tunnel_settings" => {
let tunnel: crate::alphahuman::config::TunnelConfig = parse_params(params)?;
let mut config = load_alphahuman_config().await?;
config.tunnel = tunnel;
config.save().await.map_err(|e| e.to_string())?;
let snapshot = snapshot_config(&config)?;
to_json_value(command_response(
snapshot,
vec![format!(
"tunnel settings saved to {}",
config.config_path.display()
)],
))
}
"alphahuman.update_runtime_settings" => {
let update: RuntimeSettingsUpdate = parse_params(params)?;
let mut config = load_alphahuman_config().await?;
if let Some(kind) = update.kind {
config.runtime.kind = kind;
}
if let Some(reasoning_enabled) = update.reasoning_enabled {
config.runtime.reasoning_enabled = Some(reasoning_enabled);
}
config.save().await.map_err(|e| e.to_string())?;
let snapshot = snapshot_config(&config)?;
to_json_value(command_response(
snapshot,
vec![format!(
"runtime settings saved to {}",
config.config_path.display()
)],
))
}
"alphahuman.update_browser_settings" => {
let update: BrowserSettingsUpdate = parse_params(params)?;
let mut config = load_alphahuman_config().await?;
if let Some(enabled) = update.enabled {
config.browser.enabled = enabled;
}
config.save().await.map_err(|e| e.to_string())?;
let snapshot = snapshot_config(&config)?;
to_json_value(command_response(
snapshot,
vec![format!(
"browser settings saved to {}",
config.config_path.display()
)],
))
}
"alphahuman.get_runtime_flags" => {
let flags = RuntimeFlags {
browser_allow_all: env_flag_enabled("ALPHAHUMAN_BROWSER_ALLOW_ALL"),
log_prompts: env_flag_enabled("ALPHAHUMAN_LOG_PROMPTS"),
};
to_json_value(command_response(
flags,
vec!["runtime flags read".to_string()],
))
}
"alphahuman.set_browser_allow_all" => {
let p: SetBrowserAllowAllParams = parse_params(params)?;
if p.enabled {
std::env::set_var("ALPHAHUMAN_BROWSER_ALLOW_ALL", "1");
} else {
std::env::remove_var("ALPHAHUMAN_BROWSER_ALLOW_ALL");
}
let flags = RuntimeFlags {
browser_allow_all: env_flag_enabled("ALPHAHUMAN_BROWSER_ALLOW_ALL"),
log_prompts: env_flag_enabled("ALPHAHUMAN_LOG_PROMPTS"),
};
to_json_value(command_response(
flags,
vec!["browser allow-all flag updated".to_string()],
))
}
"alphahuman.agent_chat" => {
let p: AgentChatParams = parse_params(params)?;
let mut config = load_alphahuman_config().await?;
if let Some(provider) = p.provider_override {
config.default_provider = Some(provider);
}
if let Some(model) = p.model_override {
config.default_model = Some(model);
}
if let Some(temp) = p.temperature {
config.default_temperature = temp;
}
let mut agent =
crate::alphahuman::agent::Agent::from_config(&config).map_err(|e| e.to_string())?;
let response = agent
.run_single(&p.message)
.await
.map_err(|e| e.to_string())?;
to_json_value(command_response(
response,
vec!["agent chat completed".to_string()],
))
}
"alphahuman.encrypt_secret" => {
let p: EncryptSecretParams = parse_params(params)?;
let config = load_alphahuman_config().await?;
let store = secret_store_for_config(&config);
let ciphertext = store.encrypt(&p.plaintext).map_err(|e| e.to_string())?;
to_json_value(command_response(
ciphertext,
vec!["secret encrypted".to_string()],
))
}
"alphahuman.decrypt_secret" => {
let p: DecryptSecretParams = parse_params(params)?;
let config = load_alphahuman_config().await?;
let store = secret_store_for_config(&config);
let plaintext = store.decrypt(&p.ciphertext).map_err(|e| e.to_string())?;
to_json_value(command_response(
plaintext,
vec!["secret decrypted".to_string()],
))
}
"alphahuman.doctor_report" => {
let config = load_alphahuman_config().await?;
let report = doctor::run(&config).map_err(|e| e.to_string())?;
to_json_value(command_response(
report,
vec!["doctor report generated".to_string()],
))
}
"alphahuman.doctor_models" => {
let p: DoctorModelsParams = parse_params(params)?;
let config = load_alphahuman_config().await?;
let use_cache = p.use_cache.unwrap_or(true);
let report = doctor::run_models(&config, p.provider_override.as_deref(), use_cache)
.map_err(|e| e.to_string())?;
to_json_value(command_response(
report,
vec!["model probes completed".to_string()],
))
}
"alphahuman.list_integrations" => {
let config = load_alphahuman_config().await?;
to_json_value(command_response(
integrations::list_integrations(&config),
vec!["integrations listed".to_string()],
))
}
"alphahuman.get_integration_info" => {
let p: IntegrationInfoParams = parse_params(params)?;
let config = load_alphahuman_config().await?;
let info =
integrations::get_integration_info(&config, &p.name).map_err(|e| e.to_string())?;
to_json_value(command_response(
info,
vec![format!("integration loaded: {}", p.name)],
))
}
"alphahuman.models_refresh" => {
let p: ModelsRefreshParams = parse_params(params)?;
let config = load_alphahuman_config().await?;
let result = onboard::run_models_refresh(
&config,
p.provider_override.as_deref(),
p.force.unwrap_or(false),
)
.map_err(|e| e.to_string())?;
to_json_value(command_response(
result,
vec!["model refresh completed".to_string()],
))
}
"alphahuman.migrate_openclaw" => {
let p: MigrateOpenClawParams = parse_params(params)?;
let config = load_alphahuman_config().await?;
let source = p.source_workspace.map(std::path::PathBuf::from);
let report =
migration::migrate_openclaw_memory(&config, source, p.dry_run.unwrap_or(true))
.await
.map_err(|e| e.to_string())?;
to_json_value(command_response(
report,
vec!["migration completed".to_string()],
))
}
"alphahuman.hardware_discover" => to_json_value(command_response(
hardware::discover_hardware(),
vec!["hardware discovery complete".to_string()],
)),
"alphahuman.hardware_introspect" => {
let p: HardwareIntrospectParams = parse_params(params)?;
let info = hardware::introspect_device(&p.path).map_err(|e| e.to_string())?;
to_json_value(command_response(
info,
vec![format!("introspected {}", p.path)],
))
}
"alphahuman.service_install" => {
let config = load_alphahuman_config().await?;
let status = service::install(&config).map_err(|e| e.to_string())?;
to_json_value(command_response(
status,
vec!["service install completed".to_string()],
))
}
"alphahuman.service_start" => {
let config = load_alphahuman_config().await?;
let status = service::start(&config).map_err(|e| e.to_string())?;
to_json_value(command_response(
status,
vec!["service start completed".to_string()],
))
}
"alphahuman.service_stop" => {
let config = load_alphahuman_config().await?;
let status = service::stop(&config).map_err(|e| e.to_string())?;
to_json_value(command_response(
status,
vec!["service stop completed".to_string()],
))
}
"alphahuman.service_status" => {
let config = load_alphahuman_config().await?;
let status = service::status(&config).map_err(|e| e.to_string())?;
to_json_value(command_response(
status,
vec!["service status fetched".to_string()],
))
}
"alphahuman.service_uninstall" => {
let config = load_alphahuman_config().await?;
let status = service::uninstall(&config).map_err(|e| e.to_string())?;
to_json_value(command_response(
status,
vec!["service uninstall completed".to_string()],
))
}
_ => Err(format!("unknown method: {method}")),
}
}
async fn health_handler() -> impl IntoResponse {
(StatusCode::OK, Json(json!({ "ok": true })))
}
async fn root_handler() -> impl IntoResponse {
(
StatusCode::OK,
Json(json!({
"name": "alphahuman-core",
"ok": true,
"endpoints": {
"health": "/health",
"rpc": "/rpc"
},
"usage": {
"jsonrpc": {
"version": "2.0",
"method": "core.ping",
"params": {}
}
}
})),
)
}
async fn not_found_handler() -> impl IntoResponse {
(
StatusCode::NOT_FOUND,
Json(json!({
"ok": false,
"error": "not_found",
"message": "Route not found. Try /, /health, or /rpc."
})),
)
}
fn core_port() -> u16 {
std::env::var("ALPHAHUMAN_CORE_PORT")
.ok()
.and_then(|v| v.parse::<u16>().ok())
.unwrap_or(7788)
}
pub async fn run_server(port: Option<u16>) -> Result<()> {
let port = port.unwrap_or_else(core_port);
let bind_addr = format!("127.0.0.1:{port}");
let listener = tokio::net::TcpListener::bind(&bind_addr).await?;
let app = Router::new()
.route("/", get(root_handler))
.route("/health", get(health_handler))
.route("/rpc", post(rpc_handler))
.fallback(not_found_handler)
.with_state(AppState {
core_version: env!("CARGO_PKG_VERSION").to_string(),
});
log::info!("[core] listening on http://{bind_addr}");
axum::serve(listener, app).await?;
Ok(())
}
pub fn run_from_cli_args(args: &[String]) -> Result<()> {
let mut port: Option<u16> = None;
let mut idx = 0;
while idx < args.len() {
match args[idx].as_str() {
"serve" => {}
"--port" => {
let value = args
.get(idx + 1)
.ok_or_else(|| anyhow::anyhow!("missing value for --port"))?;
port = Some(value.parse::<u16>()?);
idx += 1;
}
_ => {}
}
idx += 1;
}
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
runtime.block_on(run_server(port))
}