feat(viking): add local server management for privacy-first deployment
- Add viking_server.rs (Rust) for managing local OpenViking server process - Add viking-server-manager.ts (TypeScript) for server control from UI - Update VikingAdapter to support 'local' mode with auto-start capability - Update documentation for local deployment mode Key features: - Auto-start local server when needed - All data stays in ~/.openviking/ (privacy-first) - Server listens only on 127.0.0.1 - Graceful fallback to remote/localStorage modes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,15 @@
|
||||
// - Port: 4200 (was 18789)
|
||||
// - Binary: openfang (was openclaw)
|
||||
// - Config: ~/.openfang/openfang.toml (was ~/.openclaw/openclaw.json)
|
||||
|
||||
// Viking CLI sidecar module for local memory operations
|
||||
mod viking_commands;
|
||||
mod viking_server;
|
||||
|
||||
// Memory extraction and context building modules (supplement CLI)
|
||||
mod memory;
|
||||
mod llm;
|
||||
|
||||
use serde::Serialize;
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
@@ -1006,7 +1015,27 @@ pub fn run() {
|
||||
gateway_local_auth,
|
||||
gateway_prepare_for_tauri,
|
||||
gateway_approve_device_pairing,
|
||||
gateway_doctor
|
||||
gateway_doctor,
|
||||
// OpenViking CLI sidecar commands
|
||||
viking_commands::viking_status,
|
||||
viking_commands::viking_add,
|
||||
viking_commands::viking_add_inline,
|
||||
viking_commands::viking_find,
|
||||
viking_commands::viking_grep,
|
||||
viking_commands::viking_ls,
|
||||
viking_commands::viking_read,
|
||||
viking_commands::viking_remove,
|
||||
viking_commands::viking_tree,
|
||||
// Viking server management (local deployment)
|
||||
viking_server::viking_server_status,
|
||||
viking_server::viking_server_start,
|
||||
viking_server::viking_server_stop,
|
||||
viking_server::viking_server_restart,
|
||||
// Memory extraction commands (supplement CLI)
|
||||
memory::extractor::extract_session_memories,
|
||||
memory::context_builder::estimate_content_tokens,
|
||||
// LLM commands (for extraction)
|
||||
llm::llm_complete
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
295
desktop/src-tauri/src/viking_server.rs
Normal file
295
desktop/src-tauri/src/viking_server.rs
Normal file
@@ -0,0 +1,295 @@
|
||||
//! OpenViking Local Server Management
|
||||
//!
|
||||
//! Manages a local OpenViking server instance for privacy-first deployment.
|
||||
//! All data is stored locally in ~/.openviking/ - nothing is uploaded to remote servers.
|
||||
//!
|
||||
//! Architecture:
|
||||
//! ┌─────────────────────────────────────────────────────────────────┐
|
||||
//! │ ZCLAW Desktop (Tauri) │
|
||||
//! │ │
|
||||
//! │ ┌─────────────────┐ HTTP ┌─────────────────────────┐ │
|
||||
//! │ │ viking_commands │ ◄────────────►│ openviking-server │ │
|
||||
//! │ │ (Tauri cmds) │ localhost │ (Python, managed here) │ │
|
||||
//! │ └─────────────────┘ └───────────┬─────────────┘ │
|
||||
//! │ │ │
|
||||
//! │ ┌─────────▼─────────────┐ │
|
||||
//! │ │ SQLite + Vector Store │ │
|
||||
//! │ │ ~/.openviking/ │ │
|
||||
//! │ │ (LOCAL DATA ONLY) │ │
|
||||
//! │ └───────────────────────┘ │
|
||||
//! └─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::process::{Child, Command};
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
// === Types ===
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ServerStatus {
|
||||
pub running: bool,
|
||||
pub port: u16,
|
||||
pub pid: Option<u32>,
|
||||
pub data_dir: Option<String>,
|
||||
pub version: Option<String>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ServerConfig {
|
||||
pub port: u16,
|
||||
pub data_dir: String,
|
||||
pub config_file: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ServerConfig {
|
||||
fn default() -> Self {
|
||||
let home = dirs::home_dir()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| ".".to_string());
|
||||
|
||||
Self {
|
||||
port: 1933,
|
||||
data_dir: format!("{}/.openviking/workspace", home),
|
||||
config_file: Some(format!("{}/.openviking/ov.conf", home)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Server Process Management ===
|
||||
|
||||
static SERVER_PROCESS: Mutex<Option<Child>> = Mutex::new(None);
|
||||
|
||||
/// Check if OpenViking server is running
|
||||
fn is_server_running(port: u16) -> bool {
|
||||
// Try to connect to the server
|
||||
let url = format!("http://127.0.0.1:{}/api/v1/status", port);
|
||||
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(Duration::from_secs(2))
|
||||
.build()
|
||||
.ok();
|
||||
|
||||
if let Some(client) = client {
|
||||
if let Ok(resp) = client.get(&url).send() {
|
||||
return resp.status().is_success();
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Find openviking-server executable
|
||||
fn find_server_binary() -> Result<String, String> {
|
||||
// Check environment variable first
|
||||
if let Ok(path) = std::env::var("ZCLAW_VIKING_SERVER_BIN") {
|
||||
if std::path::Path::new(&path).exists() {
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
|
||||
// Check common locations
|
||||
let candidates = vec![
|
||||
"openviking-server".to_string(),
|
||||
"python -m openviking.server".to_string(),
|
||||
];
|
||||
|
||||
// Try to find in PATH
|
||||
for cmd in &candidates {
|
||||
if Command::new("which")
|
||||
.arg(cmd.split_whitespace().next().unwrap_or(""))
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Ok(cmd.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Check Python virtual environment
|
||||
let home = dirs::home_dir()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let venv_candidates = vec![
|
||||
format!("{}/.openviking/venv/bin/openviking-server", home),
|
||||
format!("{}/.local/bin/openviking-server", home),
|
||||
];
|
||||
|
||||
for path in venv_candidates {
|
||||
if std::path::Path::new(&path).exists() {
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: assume it's in PATH via pip install
|
||||
Ok("openviking-server".to_string())
|
||||
}
|
||||
|
||||
// === Tauri Commands ===
|
||||
|
||||
/// Get server status
|
||||
#[tauri::command]
|
||||
pub fn viking_server_status() -> Result<ServerStatus, String> {
|
||||
let config = ServerConfig::default();
|
||||
|
||||
let running = is_server_running(config.port);
|
||||
|
||||
let pid = if running {
|
||||
SERVER_PROCESS
|
||||
.lock()
|
||||
.map(|guard| guard.as_ref().map(|c| c.id()))
|
||||
.ok()
|
||||
.flatten()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Get version if running
|
||||
let version = if running {
|
||||
let url = format!("http://127.0.0.1:{}/api/v1/version", config.port);
|
||||
reqwest::blocking::Client::builder()
|
||||
.timeout(Duration::from_secs(2))
|
||||
.build()
|
||||
.ok()
|
||||
.and_then(|client| client.get(&url).send().ok())
|
||||
.and_then(|resp| resp.text().ok())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(ServerStatus {
|
||||
running,
|
||||
port: config.port,
|
||||
pid,
|
||||
data_dir: Some(config.data_dir),
|
||||
version,
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Start local OpenViking server
|
||||
#[tauri::command]
|
||||
pub fn viking_server_start(config: Option<ServerConfig>) -> Result<ServerStatus, String> {
|
||||
let config = config.unwrap_or_default();
|
||||
|
||||
// Check if already running
|
||||
if is_server_running(config.port) {
|
||||
return Ok(ServerStatus {
|
||||
running: true,
|
||||
port: config.port,
|
||||
pid: None,
|
||||
data_dir: Some(config.data_dir),
|
||||
version: None,
|
||||
error: Some("Server already running".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
// Find server binary
|
||||
let server_bin = find_server_binary()?;
|
||||
|
||||
// Ensure data directory exists
|
||||
std::fs::create_dir_all(&config.data_dir)
|
||||
.map_err(|e| format!("Failed to create data directory: {}", e))?;
|
||||
|
||||
// Set environment variables
|
||||
if let Some(ref config_file) = config.config_file {
|
||||
std::env::set_var("OPENVIKING_CONFIG_FILE", config_file);
|
||||
}
|
||||
|
||||
// Start server process
|
||||
let child = if server_bin.contains("python") {
|
||||
// Use Python module
|
||||
let parts: Vec<&str> = server_bin.split_whitespace().collect();
|
||||
Command::new(parts[0])
|
||||
.args(&parts[1..])
|
||||
.arg("--host")
|
||||
.arg("127.0.0.1")
|
||||
.arg("--port")
|
||||
.arg(config.port.to_string())
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to start server: {}", e))?
|
||||
} else {
|
||||
// Direct binary
|
||||
Command::new(&server_bin)
|
||||
.arg("--host")
|
||||
.arg("127.0.0.1")
|
||||
.arg("--port")
|
||||
.arg(config.port.to_string())
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to start server: {}", e))?
|
||||
};
|
||||
|
||||
let pid = child.id();
|
||||
|
||||
// Store process handle
|
||||
if let Ok(mut guard) = SERVER_PROCESS.lock() {
|
||||
*guard = Some(child);
|
||||
}
|
||||
|
||||
// Wait for server to be ready
|
||||
let mut ready = false;
|
||||
for _ in 0..30 {
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
if is_server_running(config.port) {
|
||||
ready = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !ready {
|
||||
return Err("Server failed to start within 15 seconds".to_string());
|
||||
}
|
||||
|
||||
Ok(ServerStatus {
|
||||
running: true,
|
||||
port: config.port,
|
||||
pid: Some(pid),
|
||||
data_dir: Some(config.data_dir),
|
||||
version: None,
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Stop local OpenViking server
|
||||
#[tauri::command]
|
||||
pub fn viking_server_stop() -> Result<(), String> {
|
||||
if let Ok(mut guard) = SERVER_PROCESS.lock() {
|
||||
if let Some(mut child) = guard.take() {
|
||||
child.kill().map_err(|e| format!("Failed to kill server: {}", e))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Restart local OpenViking server
|
||||
#[tauri::command]
|
||||
pub fn viking_server_restart(config: Option<ServerConfig>) -> Result<ServerStatus, String> {
|
||||
viking_server_stop()?;
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
viking_server_start(config)
|
||||
}
|
||||
|
||||
// === Tests ===
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_server_config_default() {
|
||||
let config = ServerConfig::default();
|
||||
assert_eq!(config.port, 1933);
|
||||
assert!(config.data_dir.contains(".openviking"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_server_running_not_running() {
|
||||
// Should return false when no server is running on port 1933
|
||||
let result = is_server_running(1933);
|
||||
// Just check it doesn't panic
|
||||
assert!(result || !result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user