chore: decomplect — delete worthless stub crates, unblock hanzo-vm/consensus/l2

User mandate: "if valid fix and publish, if worthless delete — one and one way only".

Deleted (no consumers, broken / incomplete / aspirational):
- hanzo-baml      — git-only baml-runtime dep; BAML integration never wired
- hanzo-db        — unused alongside hanzo-database/hanzo-db-sqlite
- hanzo-kbs       — incomplete, missing types, no in-tree consumer
- hanzo-sheet     — abandoned spreadsheet UI, depends on non-existent module
- hanzo-simulation — research stub, not used by node
- hanzo-sovereign — 170-LoC sketch with broken ml-dsa = "0.6" (no stable release)
- hanzo-tests     — test-only crate with compile errors

Fixed and unblocked:
- hanzo-vm        — uses crates.io luxprecompile-sys 0.1, hanzo-engine 0.6
- hanzo-consensus — uses crates.io lux-consensus 1.22
- hanzo-l2        — uses crates.io lux-consensus 1.22

Removed `publish = false` from the three so they can ship to crates.io.

Cargo.lock will regenerate on next build.
This commit is contained in:
Hanzo Dev
2026-05-13 00:20:57 -07:00
parent e1b41f8f17
commit d8a9c574c2
53 changed files with 27 additions and 13092 deletions
Generated
+6 -2
View File
@@ -5913,7 +5913,7 @@ dependencies = [
[[package]]
name = "hanzo-consensus"
version = "1.1.20"
version = "1.1.21"
dependencies = [
"anyhow",
"async-trait",
@@ -6179,7 +6179,7 @@ dependencies = [
[[package]]
name = "hanzo-l2"
version = "1.1.20"
version = "1.1.21"
dependencies = [
"anyhow",
"async-trait",
@@ -9308,6 +9308,8 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "lux-consensus"
version = "1.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f3e103de37e286e68c58e5188a49fcc94b2ac23c3afa2aaa1775f45cf97c68"
dependencies = [
"blst",
"cc",
@@ -9318,6 +9320,8 @@ dependencies = [
[[package]]
name = "luxprecompile-sys"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f152fe85e7080ed60e68da4348079167e0329b9da964d3dba604efd9ca0f891"
dependencies = [
"serde",
"serde_json",
+12 -25
View File
@@ -1,10 +1,13 @@
[workspace]
members = [
"hanzo-agentic",
"hanzo-ai-format",
"hanzo-api",
"hanzo-brain",
"hanzo-compute",
"hanzo-config",
"hanzo-consensus",
"hanzo-database",
# "hanzo-db", # Excluded: not in publish list, has feature-gated dependencies
"hanzo-db-sqlite",
"hanzo-did",
"hanzo-embed",
@@ -14,37 +17,25 @@ members = [
"hanzo-identity",
"hanzo-job-queue-manager",
"hanzo-jobs",
# "hanzo-kbs", # Excluded: incomplete implementation with missing types
"hanzo-l2",
"hanzo-libp2p",
"hanzo-libp2p-relayer",
"hanzo-machine",
"hanzo-mcp",
"hanzo-messages",
"hanzo-mining",
"hanzo-vm",
"hanzo-machine",
"hanzo-consensus",
"hanzo-compute",
"hanzo-ai-format",
"hanzo-model-discovery",
"hanzo-models",
"hanzo-pqc",
"hanzo-runner",
"hanzo-runtime",
"hanzo-runtime-tests",
# "hanzo-sheet", # Excluded: depends on non-existent sheet module
# "hanzo-simulation",
# "hanzo-tests", # Excluded: test-only crate with compilation errors
"hanzo-tools",
"hanzo-tools-runner",
"hanzo-vm",
"hanzo-wasm",
"hanzo-wasm-runtime",
"hanzo-zap",
"hanzo-agentic",
# "hanzo-sovereign", # Excluded: ml-dsa 0.6 doesn't exist on crates.io (only prerelease 0.1.0-rc.x)
]
exclude = [
"hanzo-sovereign",
]
resolver = "2"
@@ -55,10 +46,13 @@ authors = ["Nico Arqueros <nico@hanzo.ai>"]
repository = "https://github.com/hanzonet/network"
[workspace.dependencies]
hanzo-ai-format = { path = "./hanzo-ai-format", version = "1.1.21" }
hanzo-api = { path = "./hanzo-api", version = "1.1.13" }
hanzo-brain = { path = "./hanzo-brain", version = "0.1.0" }
hanzo-compute = { path = "./hanzo-compute", version = "1.1.21" }
hanzo-config = { path = "./hanzo-config", version = "1.1.21", package = "hanzonet-config" }
hanzo-consensus = { path = "./hanzo-consensus", version = "1.1.20" }
hanzo-database = { path = "./hanzo-database", version = "1.1.13" }
hanzo-db = { path = "./hanzo-db", version = "0.1.1" }
hanzo-db-sqlite = { path = "./hanzo-db-sqlite", version = "1.1.13" }
hanzo-did = { path = "./hanzo-did", version = "1.1.21", package = "hanzonet-did" }
hanzo-embed = { path = "./hanzo-embed", version = "1.1.13" }
@@ -68,32 +62,25 @@ hanzo-http-api = { path = "./hanzo-http-api", version = "1.1.21" }
hanzo-identity = { path = "./hanzo-identity", version = "1.1.13" }
hanzo-job-queue-manager = { path = "./hanzo-job-queue-manager", version = "1.1.21" }
hanzo-jobs = { path = "./hanzo-jobs", version = "1.1.13" }
hanzo-kbs = { path = "./hanzo-kbs", version = "1.1.11" }
hanzo-l2 = { path = "./hanzo-l2", version = "1.1.20" }
hanzo-libp2p = { path = "./hanzo-libp2p", version = "1.1.13" }
hanzo-libp2p-relayer = { path = "./hanzo-libp2p-relayer", version = "1.1.13" }
hanzo-machine = { path = "./hanzo-machine", version = "1.1.21" }
hanzo-mcp = { path = "./hanzo-mcp", version = "1.1.21", package = "hanzonet-mcp" }
hanzo-messages = { path = "./hanzo-messages", version = "1.1.13" }
hanzo-mining = { path = "./hanzo-mining", version = "1.1.21" }
hanzo-consensus = { path = "./hanzo-consensus", version = "1.1.20" }
hanzo-compute = { path = "./hanzo-compute", version = "1.1.21" }
hanzo-ai-format = { path = "./hanzo-ai-format", version = "1.1.21" }
hanzo-model-discovery = { path = "./hanzo-model-discovery", version = "1.1.13" }
hanzo-models = { path = "./hanzo-models", version = "1.1.13" }
hanzo-pqc = { path = "./hanzo-pqc", version = "1.1.21", package = "hanzonet-pqc" }
hanzo-runner = { path = "./hanzo-runner", version = "1.1.13" }
hanzo-runtime = { path = "./hanzo-runtime", version = "1.1.13" }
hanzo-runtime-tests = { path = "./hanzo-runtime-tests", version = "0.1.2" }
hanzo-sheet = { path = "./hanzo-sheet", version = "0.1.1" }
hanzo-tests = { path = "./hanzo-tests", version = "0.1.1" }
hanzo-tools = { path = "./hanzo-tools", version = "1.1.21" }
hanzo-tools-runner = { path = "./hanzo-tools-runner", version = "1.0.2" }
hanzo-wasm = { path = "./hanzo-wasm", version = "0.1.2" }
hanzo-vm = { path = "./hanzo-vm", version = "1.1.21" }
hanzo-machine = { path = "./hanzo-machine", version = "1.1.21" }
hanzo-wasm = { path = "./hanzo-wasm", version = "0.1.2" }
hanzo-wasm-runtime = { path = "./hanzo-wasm-runtime", version = "1.1.21" }
hanzo-zap = { path = "./hanzo-zap", version = "0.6.75" }
hanzo-brain = { path = "./hanzo-brain", version = "0.1.0" }
rmcp = { version = "0.8" }
futures = "0.3.30"
-21
View File
@@ -1,21 +0,0 @@
[package]
name = "hanzo-baml"
description = "Hanzo AI - BAML Integration Library"
license = "MIT"
version = { workspace = true }
edition = { workspace = true }
authors = { workspace = true }
repository = "https://github.com/hanzoai/node"
homepage = "https://hanzo.ai"
[dependencies]
baml-runtime = { git = "https://github.com/BoundaryML/baml.git", tag = "0.56.1", package = "baml-runtime", features = [
"internal",
] }
baml-types = { git = "https://github.com/BoundaryML/baml.git", tag = "0.56.1", package = "baml-types" }
env_logger = { workspace = true }
log = { workspace = true }
anyhow = { workspace = true }
indexmap = { version = "2.1.0", features = ["serde"] }
regex = { workspace = true }
serde_json = { workspace = true }
-331
View File
@@ -1,331 +0,0 @@
use anyhow::Result;
use baml_runtime::{BamlRuntime, InternalRuntimeInterface};
use baml_types::BamlValue;
use indexmap::IndexMap;
use regex::Regex;
use std::collections::HashMap;
#[derive(Clone, Debug)]
pub struct GeneratorConfig {
pub output_type: String,
pub output_dir: String,
pub version: String,
pub default_client_mode: String,
}
impl Default for GeneratorConfig {
fn default() -> Self {
Self {
output_type: "typescript".to_string(),
output_dir: "../src/".to_string(),
version: "0.55.3".to_string(),
default_client_mode: "async".to_string(),
}
}
}
#[derive(Clone, Debug)]
pub struct ClientConfig {
pub provider: String,
pub base_url: Option<String>,
pub model: String,
pub default_role: String,
pub api_key: Option<String>,
}
#[derive(Clone, Debug)]
pub struct BamlConfig {
pub generator: GeneratorConfig,
pub client: ClientConfig,
pub dsl_class_file: Option<String>,
pub input: Option<String>,
pub function_name: Option<String>,
pub param_name: Option<String>,
}
impl BamlConfig {
pub fn builder(generator: GeneratorConfig, client: ClientConfig) -> BamlConfigBuilder {
BamlConfigBuilder {
generator,
client,
dsl_class_file: None,
input: None,
function_name: None,
param_name: None,
}
}
pub fn initialize_runtime(&self, env_vars: HashMap<&str, &str>) -> Result<BamlRuntime> {
let mut files = HashMap::new();
files.insert(
"generator.baml".to_string(),
format!(
r##"
generator lang_ts {{
output_type "{}"
output_dir "{}"
version "{}"
default_client_mode "{}"
}}
"##,
self.generator.output_type,
self.generator.output_dir,
self.generator.version,
self.generator.default_client_mode
),
);
let base_url_option = if let Some(base_url) = &self.client.base_url {
format!(r#"base_url "{}""#, base_url)
} else {
String::new()
};
files.insert(
"client.baml".to_string(),
format!(
r##"
client<llm> {} {{
provider {}
options {{
{}
model "{}"
default_role "{}"
api_key "{}"
}}
}}
"##,
"HanzoProvider",
self.client.provider,
base_url_option,
self.client.model,
self.client.default_role,
self.client.api_key.clone().unwrap_or_default()
),
);
if let Some(dsl_class_file) = &self.dsl_class_file {
files.insert("dsl_class.baml".to_string(), dsl_class_file.clone());
}
let runtime = BamlRuntime::from_file_content("baml_src", &files, env_vars)?;
let diagnostics = runtime.internal().diagnostics();
eprintln!("BAML diagnostics: {:?}", diagnostics);
if diagnostics.has_errors() {
let error_message = diagnostics.to_pretty_string();
return Err(anyhow::anyhow!("BAML diagnostics errors: {}", error_message));
}
Ok(runtime)
}
pub fn execute(&self, runtime: &BamlRuntime, extract_data: bool) -> Result<String> {
let ctx_manager = runtime.create_ctx_manager(BamlValue::String("none".to_string()), None);
let mut params = IndexMap::new();
if let (Some(param_name), Some(input)) = (&self.param_name, &self.input) {
let trimmed_input = input.trim();
let context_value = if trimmed_input.starts_with('{') && trimmed_input.ends_with('}') {
eprintln!("input is a json string: {}", trimmed_input);
match serde_json::from_str(&trimmed_input) {
Ok(parsed_json) => BamlConfig::from_serde_value(parsed_json),
Err(_) => {
eprintln!("Failed to parse JSON, attempting to unescape");
let unescaped_input = BamlConfig::unescape_json_string(trimmed_input);
eprintln!("Unescaped input: {}", unescaped_input);
match serde_json::from_str(&unescaped_input) {
Ok(parsed_json) => BamlConfig::from_serde_value(parsed_json),
Err(e) => return Err(anyhow::anyhow!("Failed to parse JSON after unescaping: {}", e)),
}
}
}
} else {
BamlValue::String(trimmed_input.to_string())
};
params.insert(param_name.clone(), context_value);
}
if let Some(function_name) = &self.function_name {
eprintln!("\n\n Params string keys {:?}\n\n", params.keys());
eprintln!("\n\n Params string values {:?}\n\n", params.values());
let (result, _uuid) = runtime.call_function_sync(function_name.clone(), &params, &ctx_manager, None, None);
match result {
Ok(response) => match response.content() {
Ok(content) => {
if extract_data {
eprintln!("Extracting data from response: {}", content);
if content.starts_with('{') && content.ends_with('}') {
return Ok(content.to_string());
}
let re = Regex::new(r"```(?:json)?\s*([\s\S]*?)\s*```").unwrap();
if let Some(captures) = re.captures(&content) {
if let Some(matched) = captures.get(1) {
return Ok(matched.as_str().to_string());
}
}
return Ok(content.to_string());
} else {
return Ok(content.to_string());
}
}
Err(e) => return Err(anyhow::anyhow!("Error getting content: {}", e)),
},
Err(e) => return Err(anyhow::anyhow!("Error: {}", e)),
}
}
Err(anyhow::anyhow!("Function name not provided"))
}
pub fn from_serde_value(value: serde_json::Value) -> BamlValue {
match value {
serde_json::Value::Null => BamlValue::Null,
serde_json::Value::Bool(b) => BamlValue::Bool(b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
BamlValue::Int(i)
} else if let Some(f) = n.as_f64() {
BamlValue::Float(f)
} else {
panic!("Unexpected number type")
}
}
serde_json::Value::String(s) => BamlValue::String(s),
serde_json::Value::Array(arr) => {
let baml_values = arr.into_iter().map(BamlConfig::from_serde_value).collect();
BamlValue::List(baml_values)
}
serde_json::Value::Object(obj) => {
if let Some(class_name) = obj.clone().get("class_name").and_then(|v| v.as_str()) {
let class_fields = obj
.into_iter()
.filter(|(k, _)| k != "class_name")
.map(|(k, v)| (k, BamlConfig::from_serde_value(v)))
.collect();
BamlValue::Class(class_name.to_string(), class_fields)
} else {
let baml_map = obj
.into_iter()
.map(|(k, v)| (k, BamlConfig::from_serde_value(v)))
.collect();
BamlValue::Map(baml_map)
}
}
}
}
pub fn unescape_json_string(json_str: &str) -> String {
let re_backslash_quote = Regex::new(r#"\\""#).unwrap(); // Matches \"
let re_backslash = Regex::new(r#"\\\\"#).unwrap(); // Matches \\
let re_newline = Regex::new(r#"\\n"#).unwrap(); // Matches \n
let re_tab = Regex::new(r#"\\t"#).unwrap(); // Matches \t
let intermediate = re_backslash_quote.replace_all(json_str, "\"");
let intermediate = re_backslash.replace_all(&intermediate, "\\");
let intermediate = re_newline.replace_all(&intermediate, "\n");
let intermediate = re_tab.replace_all(&intermediate, "\t");
intermediate.to_string()
}
/// Converts the existing DSL string to the format expected by Baml.
pub fn convert_dsl_class_file(old_dsl: &str) -> String {
// Define regex patterns for different escape sequences
let re_triple_backslash_quote = Regex::new(r#"\\\\\\""#).unwrap(); // Matches \\\"
let re_newline = Regex::new(r#"\\n"#).unwrap(); // Matches \n
let re_quote = Regex::new(r#"\\""#).unwrap(); // Matches \"
let re_backslash = Regex::new(r#"\\\\"#).unwrap(); // Matches \\
// Perform replacements using regex in the correct order
// 1. Replace triple backslashes followed by a quote (\\\\\") with an escaped quote (\\")
let intermediate = re_triple_backslash_quote.replace_all(old_dsl, "\\\"");
// 2. Replace escaped newlines (\\n) with actual newlines (\n)
let intermediate = re_newline.replace_all(&intermediate, "\n");
// 3. Replace escaped quotes (\\") with actual quotes (")
let intermediate = re_quote.replace_all(&intermediate, "\"");
// 4. Replace escaped backslashes (\\\\) with a single backslash (\\)
let intermediate = re_backslash.replace_all(&intermediate, "\\");
// Optionally, adjust other parts of the DSL as needed
// For example, change client provider from Ollama to HanzoProvider
let re_client = Regex::new(r#"client\s+\w+"#).unwrap();
let adjusted = re_client.replace_all(&intermediate, "client HanzoProvider");
adjusted.to_string()
}
}
pub struct BamlConfigBuilder {
generator: GeneratorConfig,
client: ClientConfig,
dsl_class_file: Option<String>,
input: Option<String>,
function_name: Option<String>,
param_name: Option<String>,
}
impl BamlConfigBuilder {
pub fn dsl_class_file(mut self, dsl_class_file: &str) -> Self {
self.dsl_class_file = Some(dsl_class_file.to_string());
self
}
pub fn input(mut self, input: &str) -> Self {
self.input = Some(input.to_string());
self
}
pub fn function_name(mut self, function_name: &str) -> Self {
self.function_name = Some(function_name.to_string());
self
}
pub fn param_name(mut self, param_name: &str) -> Self {
self.param_name = Some(param_name.to_string());
self
}
pub fn build(self) -> BamlConfig {
BamlConfig {
generator: self.generator,
client: self.client,
dsl_class_file: self.dsl_class_file,
input: self.input,
function_name: self.function_name,
param_name: self.param_name,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unescape_json_string() {
let input = r#"{
\"documents\": [
{
\"title\": \"OmniParser Abstract\",
\"link\": \"https://arxiv.org\",
\"text\": \"- OmniParser for Pure Vision Based GUI Agent Yadong Lu 1 , Jianwei Yang 1 , Yelong Shen 2 , Ahmed Awadallah 1 1 Microsoft Research 2 Microsoft Gen AI {yadonglu, jianwei.yang, yeshe, ahmed.awadallah}@microsoft.com Abstract (Source: 2408.00203v1.pdf, Section: )\"
}
]
}"#;
let expected_output = r#"{
"documents": [
{
"title": "OmniParser Abstract",
"link": "https://arxiv.org",
"text": "- OmniParser for Pure Vision Based GUI Agent Yadong Lu 1 , Jianwei Yang 1 , Yelong Shen 2 , Ahmed Awadallah 1 1 Microsoft Research 2 Microsoft Gen AI {yadonglu, jianwei.yang, yeshe, ahmed.awadallah}@microsoft.com Abstract (Source: 2408.00203v1.pdf, Section: )"
}
]
}"#;
let unescaped = BamlConfig::unescape_json_string(input);
assert_eq!(unescaped, expected_output);
}
}
-1
View File
@@ -1 +0,0 @@
pub mod baml_builder;
-85
View File
@@ -1,85 +0,0 @@
mod baml_builder;
use std::collections::HashMap;
use std::sync::Once;
use anyhow::Result;
use baml_builder::{BamlConfig, ClientConfig, GeneratorConfig};
use log::info;
static INIT: Once = Once::new();
fn main() -> Result<()> {
INIT.call_once(|| {
env_logger::init();
});
info!("Starting Baml Runtime");
let generator_config = GeneratorConfig {
output_type: "typescript".to_string(),
output_dir: "../src/".to_string(),
version: "0.55.3".to_string(),
default_client_mode: "async".to_string(),
};
let client_config = ClientConfig {
provider: "ollama".to_string(),
base_url: Some("http://localhost:11434/v1".to_string()),
model: "llama3.1:8b-instruct-q4_1".to_string(),
default_role: "user".to_string(),
api_key: None,
};
let baml_config = BamlConfig::builder(generator_config, client_config)
.dsl_class_file(
r##"
class Resume {
name string
email string
experience string[]
skills string[]
}
function ExtractResume(resume: string) -> Resume {
client HanzoProvider
prompt #"
Extract from this content:
{{ resume }}
{{ ctx.output_format }}
"#
}
"##,
)
.input(
r#"
Name: Nico Arqueros
123 Main St, Anytown, USA
Email: john.doe@email.com
Phone: (555) 123-4567
Education:
Bachelor of Science in Computer Science
University of Technology, 2015-2019
Work Experience:
Software Engineer, Tech Corp
June 2019 - Present
- Developed and maintained web applications
- Collaborated with cross-functional teams
Skills:
JavaScript, TypeScript, React, Node.js, Git
"#,
)
.function_name("ExtractResume")
.param_name("resume")
.build();
let env_vars = HashMap::new();
let runtime = baml_config.initialize_runtime(env_vars)?;
let result = baml_config.execute(&runtime, true)?;
println!("Execution result: {}", result);
Ok(())
}
+3 -6
View File
@@ -1,17 +1,14 @@
[package]
name = "hanzo-consensus"
version = "1.1.20"
version = "1.1.21"
edition = "2021"
description = "Native Quasar consensus engine for Hanzo Network L2"
description = "Hanzo Network — Quasar BFT consensus engine for L2 (wraps lux-consensus)"
license = "MIT"
repository = "https://github.com/hanzonet/network"
# Cannot publish to crates.io until lux-consensus is published.
# Also depends on the local net hanzo-pqc (publish = false).
publish = false
[dependencies]
# Lux consensus SDK (Quasar protocol)
lux-consensus = { path = "../../../lux/consensus/pkg/rust" }
lux-consensus = "1.22"
# PQC for dual certificates
hanzo-pqc = { workspace = true }
-79
View File
@@ -1,79 +0,0 @@
[package]
name = "hanzo-db"
version = "0.1.1"
edition = { workspace = true }
publish = false # Not ready for crates.io
# Standalone crate - not part of parent workspace
[workspace]
authors = { workspace = true }
description = "Multi-backend database abstraction for Hanzo Node - Supports LanceDB, DuckDB, PostgreSQL, and more"
license = "Apache-2.0"
keywords = ["database", "vector", "multimodal", "lancedb", "duckdb"]
categories = ["database", "science"]
[dependencies]
# Core LanceDB with latest version (uses arrow 55.2)
lancedb = "0.22.0"
arrow-array = "55.2"
arrow-schema = "55.2"
arrow-ipc = "55.2"
parquet = "55.2"
# Async runtime
tokio = { workspace = true }
futures = { workspace = true }
async-trait = { workspace = true }
# Serialization
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
bincode = { workspace = true }
# Error handling
anyhow = { workspace = true }
thiserror = { workspace = true }
# Utilities
log = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true, features = ["v4", "serde"] }
base64 = { workspace = true }
blake3 = { workspace = true }
# Connection pooling
deadpool = "0.12"
deadpool-runtime = "0.1"
# Image processing for multimodal
image = { version = "0.25", optional = true }
# Migration support
rusqlite = { workspace = true, optional = true }
# Backend-specific dependencies
duckdb = { version = "0.10", optional = true }
sqlx = { version = "0.8", features = ["postgres", "runtime-tokio-rustls", "sqlite"], optional = true }
redis = { version = "0.25", features = ["aio", "tokio-comp"], optional = true }
# Hanzo dependencies
hanzo-messages = { version = "1.1.12", path = "../hanzo-messages" }
hanzo-embed = { version = "1.1.12", path = "../hanzo-embed" }
[dev-dependencies]
tokio-test = { workspace = true }
tempfile = { workspace = true }
criterion = { workspace = true }
proptest = { workspace = true }
env_logger = { workspace = true }
[features]
default = ["multimodal", "migration"]
multimodal = ["image"]
migration = ["rusqlite"]
all-backends = ["backend-duckdb", "backend-postgres", "backend-redis", "backend-sqlite"]
backend-duckdb = ["duckdb"]
backend-postgres = ["sqlx"]
backend-redis = ["redis"]
backend-sqlite = ["rusqlite"]
-95
View File
@@ -1,95 +0,0 @@
# Hanzo DB - Multi-Backend Database Abstraction
A unified database abstraction layer for Hanzo Node that supports multiple backend databases, each optimized for different workloads.
## Supported Backends
| Backend | Best For | Features |
|---------|----------|----------|
| **LanceDB** | Vector Search, AI/ML | • Native vector operations<br>• Multimodal storage<br>• Columnar format<br>• Fast similarity search |
| **DuckDB** | Analytics, OLAP | • In-process analytical database<br>• SQL support<br>• Columnar storage<br>• Fast aggregations |
| **PostgreSQL** | Transactional, OLTP | • ACID compliance<br>• Rich SQL features<br>• Extensions (pgvector)<br>• Battle-tested |
| **Redis** | Caching, Real-time | • In-memory storage<br>• Pub/sub support<br>• TTL/expiration<br>• Extremely fast |
| **SQLite** | Embedded, Lightweight | • Zero-configuration<br>• Single file<br>• Serverless<br>• Wide compatibility |
## Usage
```rust
use hanzo_db::{connect, HanzoDbConfig, DatabaseBackend, WorkloadType};
// Automatically select backend based on workload
let config = HanzoDbConfig {
backend: DatabaseBackend::for_workload(WorkloadType::VectorSearch),
..Default::default()
};
// Or explicitly choose a backend
let config = HanzoDbConfig {
backend: DatabaseBackend::LanceDB,
path: Some("./data/vectors".into()),
..Default::default()
};
let db = connect(config).await?;
// Use unified interface regardless of backend
db.create_table("embeddings", schema).await?;
db.insert("embeddings", &records).await?;
let results = db.vector_search(query).await?;
```
## Features
- **Unified Interface**: Same API across all backends
- **Automatic Backend Selection**: Choose optimal backend based on workload
- **Migration Support**: Move data between backends
- **Connection Pooling**: Efficient resource management
- **Transaction Support**: ACID guarantees where supported
- **Vector Operations**: Native support in LanceDB, extension support in PostgreSQL
## Configuration
### Environment Variables
```bash
# Select default backend
HANZO_DB_BACKEND=lancedb # lancedb, duckdb, postgresql, redis, sqlite
# Backend-specific configuration
HANZO_DB_PATH=./storage/hanzo-db
HANZO_DB_URL=postgresql://user:pass@localhost/hanzo
HANZO_REDIS_URL=redis://localhost:6379
```
### Feature Flags
```toml
[dependencies]
hanzo_db = { version = "1.0", features = ["lancedb", "duckdb", "postgres"] }
```
## Migration
Migrate from one backend to another:
```bash
# From SQLite to LanceDB
hanzo-migrate --from sqlite://old.db --to lancedb://./vectors
# From LanceDB to PostgreSQL
hanzo-migrate --from lancedb://./vectors --to postgresql://localhost/hanzo
```
## Performance Characteristics
| Operation | LanceDB | DuckDB | PostgreSQL | Redis | SQLite |
|-----------|---------|---------|------------|-------|---------|
| Vector Search | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐ | ⭐ |
| Analytics | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐ | ⭐⭐ |
| Transactions | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ |
| Caching | ⭐⭐ | ⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| Embedded | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ | ⭐ | ⭐⭐⭐⭐⭐ |
## License
Apache 2.0
-467
View File
@@ -1,467 +0,0 @@
//! DuckDB backend implementation for analytical workloads
//!
//! DuckDB is an embedded analytical database optimized for OLAP workloads.
//! It provides excellent performance for analytics, aggregations, and complex queries.
use crate::{DatabaseBackend, HanzoDbConfig, HanzoDbError, Result};
use async_trait::async_trait;
use duckdb::{Connection, params};
use serde_json::Value;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
/// DuckDB backend for analytical workloads
pub struct DuckDbBackend {
connection: Arc<Mutex<Connection>>,
path: PathBuf,
config: HanzoDbConfig,
}
impl DuckDbBackend {
/// Create a new DuckDB backend
pub fn new(config: HanzoDbConfig) -> Result<Self> {
let path = config.path.clone().unwrap_or_else(|| {
PathBuf::from("./storage/hanzo-duckdb.db")
});
// Create parent directory if needed
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
}
// Open DuckDB connection
let connection = if path.to_str() == Some(":memory:") {
Connection::open_in_memory()
} else {
Connection::open(&path)
}.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
// Configure DuckDB for optimal performance
connection.execute_batch("
-- Set memory limit
SET memory_limit='4GB';
-- Enable parallel execution
SET threads=4;
-- Enable progress bar for long queries
SET enable_progress_bar=true;
-- Optimize for analytics
SET enable_optimizer=true;
").map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(Self {
connection: Arc::new(Mutex::new(connection)),
path,
config,
})
}
/// Execute analytical query
pub fn analyze(&self, query: &str) -> Result<Vec<Value>> {
let conn = self.connection.lock()
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
let mut stmt = conn.prepare(query)
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
let rows = stmt.query_map(params![], |row| {
// Convert row to JSON value
let mut obj = serde_json::Map::new();
// Get column count and names
let column_count = row.column_count();
for i in 0..column_count {
let column_name = row.column_name(i)
.unwrap_or(&format!("column_{}", i))
.to_string();
// Try to get value as different types
if let Ok(val) = row.get::<_, String>(i) {
obj.insert(column_name, Value::String(val));
} else if let Ok(val) = row.get::<_, i64>(i) {
obj.insert(column_name, Value::Number(val.into()));
} else if let Ok(val) = row.get::<_, f64>(i) {
obj.insert(column_name, Value::from(val));
} else if let Ok(val) = row.get::<_, bool>(i) {
obj.insert(column_name, Value::Bool(val));
} else {
obj.insert(column_name, Value::Null);
}
}
Ok(Value::Object(obj))
}).map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|e| HanzoDbError::QueryError(e.to_string()))
}
/// Import Parquet file
pub fn import_parquet(&self, path: &str, table_name: &str) -> Result<()> {
let conn = self.connection.lock()
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
let query = format!(
"CREATE TABLE {} AS SELECT * FROM read_parquet('{}')",
table_name, path
);
conn.execute(&query, params![])
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(())
}
/// Export to Parquet
pub fn export_parquet(&self, table_name: &str, path: &str) -> Result<()> {
let conn = self.connection.lock()
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
let query = format!(
"COPY {} TO '{}' (FORMAT PARQUET)",
table_name, path
);
conn.execute(&query, params![])
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(())
}
/// Create aggregate view
pub fn create_aggregate_view(&self, view_name: &str, query: &str) -> Result<()> {
let conn = self.connection.lock()
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
let create_view = format!(
"CREATE VIEW {} AS {}",
view_name, query
);
conn.execute(&create_view, params![])
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(())
}
}
#[async_trait]
impl DatabaseBackend for DuckDbBackend {
async fn connect(&mut self) -> Result<()> {
// Already connected in new()
Ok(())
}
async fn disconnect(&mut self) -> Result<()> {
// Connection will be closed when dropped
Ok(())
}
async fn create_table(&self, name: &str, schema: &Value) -> Result<()> {
let conn = self.connection.lock()
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
// Parse schema and create SQL
let columns = schema.as_object()
.ok_or_else(|| HanzoDbError::InvalidSchema("Schema must be object".to_string()))?;
let column_defs: Vec<String> = columns.iter().map(|(name, type_def)| {
let sql_type = match type_def.as_str() {
Some("string") => "VARCHAR",
Some("integer") => "INTEGER",
Some("bigint") => "BIGINT",
Some("float") => "REAL",
Some("double") => "DOUBLE",
Some("boolean") => "BOOLEAN",
Some("timestamp") => "TIMESTAMP",
Some("date") => "DATE",
Some("json") => "JSON",
Some("vector") => "DOUBLE[]", // DuckDB array type for vectors
_ => "VARCHAR",
};
format!("{} {}", name, sql_type)
}).collect();
let create_sql = format!(
"CREATE TABLE IF NOT EXISTS {} ({})",
name,
column_defs.join(", ")
);
conn.execute(&create_sql, params![])
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(())
}
async fn drop_table(&self, name: &str) -> Result<()> {
let conn = self.connection.lock()
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
conn.execute(&format!("DROP TABLE IF EXISTS {}", name), params![])
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(())
}
async fn insert(&self, table: &str, records: &[Value]) -> Result<()> {
let conn = self.connection.lock()
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
// Begin transaction for bulk insert
let tx = conn.unchecked_transaction()
.map_err(|e| HanzoDbError::TransactionError(e.to_string()))?;
for record in records {
let obj = record.as_object()
.ok_or_else(|| HanzoDbError::InvalidData("Record must be object".to_string()))?;
let columns: Vec<String> = obj.keys().cloned().collect();
let placeholders: Vec<String> = (1..=columns.len())
.map(|i| format!("${}", i))
.collect();
let insert_sql = format!(
"INSERT INTO {} ({}) VALUES ({})",
table,
columns.join(", "),
placeholders.join(", ")
);
// Convert values to DuckDB types
let values: Vec<duckdb::types::Value> = obj.values().map(|v| {
match v {
Value::String(s) => duckdb::types::Value::Text(s.clone()),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
duckdb::types::Value::BigInt(i)
} else if let Some(f) = n.as_f64() {
duckdb::types::Value::Double(f)
} else {
duckdb::types::Value::Null
}
},
Value::Bool(b) => duckdb::types::Value::Boolean(*b),
Value::Null => duckdb::types::Value::Null,
Value::Array(a) => {
// Handle vector data
let floats: Vec<f64> = a.iter()
.filter_map(|v| v.as_f64())
.collect();
duckdb::types::Value::List(
floats.into_iter()
.map(duckdb::types::Value::Double)
.collect()
)
},
Value::Object(_) => {
// Store as JSON string
duckdb::types::Value::Text(v.to_string())
}
}
}).collect();
tx.execute(&insert_sql, params_from_iter(values))
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
}
tx.commit()
.map_err(|e| HanzoDbError::TransactionError(e.to_string()))?;
Ok(())
}
async fn query(&self, query: &str) -> Result<Vec<Value>> {
self.analyze(query)
}
async fn update(&self, table: &str, id: &str, data: &Value) -> Result<()> {
let conn = self.connection.lock()
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
let obj = data.as_object()
.ok_or_else(|| HanzoDbError::InvalidData("Update data must be object".to_string()))?;
let set_clauses: Vec<String> = obj.keys()
.enumerate()
.map(|(i, key)| format!("{} = ${}", key, i + 2))
.collect();
let update_sql = format!(
"UPDATE {} SET {} WHERE id = $1",
table,
set_clauses.join(", ")
);
let mut values = vec![duckdb::types::Value::Text(id.to_string())];
for value in obj.values() {
values.push(json_to_duckdb_value(value));
}
conn.execute(&update_sql, params_from_iter(values))
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(())
}
async fn delete(&self, table: &str, id: &str) -> Result<()> {
let conn = self.connection.lock()
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
conn.execute(
&format!("DELETE FROM {} WHERE id = ?", table),
params![id]
).map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(())
}
async fn vector_search(
&self,
table: &str,
query_vector: &[f32],
limit: usize,
_filter: Option<&str>,
) -> Result<Vec<Value>> {
// DuckDB doesn't have native vector similarity search
// but we can implement it using array operations
let conn = self.connection.lock()
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
// Convert query vector to DuckDB array literal
let vector_str = format!(
"[{}]",
query_vector.iter()
.map(|f| f.to_string())
.collect::<Vec<_>>()
.join(", ")
);
// Calculate cosine similarity using array operations
let query = format!(
"SELECT *,
list_cosine_similarity(embedding, {}::DOUBLE[]) as similarity
FROM {}
ORDER BY similarity DESC
LIMIT {}",
vector_str, table, limit
);
let mut stmt = conn.prepare(&query)
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
let rows = stmt.query_map(params![], row_to_json)
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|e| HanzoDbError::QueryError(e.to_string()))
}
fn backend_type(&self) -> DatabaseBackend {
DatabaseBackend::DuckDB
}
fn is_vector_capable(&self) -> bool {
true // DuckDB can store vectors as arrays
}
fn is_analytical(&self) -> bool {
true // DuckDB excels at analytics
}
}
// Helper function to convert JSON to DuckDB value
fn json_to_duckdb_value(value: &Value) -> duckdb::types::Value {
match value {
Value::String(s) => duckdb::types::Value::Text(s.clone()),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
duckdb::types::Value::BigInt(i)
} else if let Some(f) = n.as_f64() {
duckdb::types::Value::Double(f)
} else {
duckdb::types::Value::Null
}
},
Value::Bool(b) => duckdb::types::Value::Boolean(*b),
Value::Null => duckdb::types::Value::Null,
Value::Array(a) => {
let values: Vec<duckdb::types::Value> = a.iter()
.map(json_to_duckdb_value)
.collect();
duckdb::types::Value::List(values)
},
Value::Object(_) => duckdb::types::Value::Text(value.to_string()),
}
}
// Helper function to convert row to JSON
fn row_to_json(row: &duckdb::Row) -> std::result::Result<Value, duckdb::Error> {
let mut obj = serde_json::Map::new();
let column_count = row.column_count();
for i in 0..column_count {
let column_name = row.column_name(i)
.unwrap_or(&format!("column_{}", i))
.to_string();
// Try different types
if let Ok(val) = row.get::<_, String>(i) {
obj.insert(column_name, Value::String(val));
} else if let Ok(val) = row.get::<_, i64>(i) {
obj.insert(column_name, Value::Number(val.into()));
} else if let Ok(val) = row.get::<_, f64>(i) {
obj.insert(column_name, Value::from(val));
} else if let Ok(val) = row.get::<_, bool>(i) {
obj.insert(column_name, Value::Bool(val));
} else {
obj.insert(column_name, Value::Null);
}
}
Ok(Value::Object(obj))
}
// Helper to create params from iterator
fn params_from_iter(values: Vec<duckdb::types::Value>) -> Vec<duckdb::types::Value> {
values
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_duckdb_analytics() {
let config = HanzoDbConfig {
backend: DatabaseBackend::DuckDB,
path: Some(PathBuf::from(":memory:")),
..Default::default()
};
let backend = DuckDbBackend::new(config).unwrap();
// Create table
let schema = serde_json::json!({
"id": "integer",
"value": "double",
"category": "string"
});
backend.create_table("metrics", &schema).await.unwrap();
// Insert test data
let records = vec![
serde_json::json!({"id": 1, "value": 100.0, "category": "A"}),
serde_json::json!({"id": 2, "value": 200.0, "category": "B"}),
serde_json::json!({"id": 3, "value": 150.0, "category": "A"}),
];
backend.insert("metrics", &records).await.unwrap();
// Run analytical query
let results = backend.analyze(
"SELECT category, AVG(value) as avg_value FROM metrics GROUP BY category"
).unwrap();
assert_eq!(results.len(), 2);
}
}
-584
View File
@@ -1,584 +0,0 @@
//! LanceDB backend implementation - The PRIMARY backend for Hanzo Node
//!
//! LanceDB is the default and recommended backend for Hanzo Node because:
//! - Native vector search with no external dependencies
//! - Embedded operation - no server required
//! - Multimodal storage (text, images, embeddings, audio, video)
//! - Columnar storage based on Apache Arrow
//! - Production-ready with automatic versioning
//! - Scales from edge to cloud
use anyhow::{Context, Result};
use async_trait::async_trait;
use lancedb::{connect, Connection, Table};
use arrow_array::{RecordBatch, RecordBatchIterator, Float32Array, StringArray, Int64Array};
use arrow_schema::{DataType as ArrowDataType, Field, Schema as ArrowSchema};
use std::sync::Arc;
use std::path::PathBuf;
use log::{debug, info};
use crate::{
HanzoDatabase, HanzoDbConfig, TableSchema, Query, QueryResult,
VectorQuery, SearchResult, Transaction, DatabaseStats, Record,
Value, DataType, Column, Filter, OrderBy, Index, IndexType, DistanceMetric
};
/// LanceDB backend implementation
pub struct LanceDbBackend {
connection: Connection,
path: PathBuf,
config: HanzoDbConfig,
}
impl LanceDbBackend {
/// Create a new LanceDB backend
pub async fn new(config: HanzoDbConfig) -> Result<Self> {
let path = config.path.clone()
.unwrap_or_else(|| PathBuf::from("./storage/hanzo-db/lancedb"));
// Ensure directory exists
std::fs::create_dir_all(&path)?;
info!("🚀 Initializing LanceDB at {:?}", path);
info!("📦 Native vector search enabled");
info!("🔐 Embedded operation - no external dependencies");
info!("🎯 Multimodal storage ready (text, images, embeddings)");
let connection = connect(path.to_str().unwrap()).await
.context("Failed to connect to LanceDB")?;
Ok(Self {
connection,
path,
config,
})
}
/// Convert our DataType to Arrow DataType
fn to_arrow_type(dt: &DataType) -> ArrowDataType {
match dt {
DataType::Boolean => ArrowDataType::Boolean,
DataType::Int32 => ArrowDataType::Int32,
DataType::Int64 => ArrowDataType::Int64,
DataType::Float32 => ArrowDataType::Float32,
DataType::Float64 => ArrowDataType::Float64,
DataType::String => ArrowDataType::Utf8,
DataType::Binary => ArrowDataType::Binary,
DataType::Timestamp => ArrowDataType::Timestamp(arrow_schema::TimeUnit::Millisecond, None),
DataType::Vector(dim) => ArrowDataType::FixedSizeList(
Arc::new(Field::new("item", ArrowDataType::Float32, false)),
*dim as i32,
),
DataType::Json => ArrowDataType::Utf8, // Store JSON as string
DataType::Array(inner) => ArrowDataType::List(
Arc::new(Field::new("item", Self::to_arrow_type(inner), true))
),
DataType::Struct(fields) => {
let arrow_fields: Vec<Field> = fields.iter()
.map(|(name, dt)| Field::new(name, Self::to_arrow_type(dt), true))
.collect();
ArrowDataType::Struct(arrow_fields.into())
}
}
}
}
#[async_trait]
impl HanzoDatabase for LanceDbBackend {
async fn init(&self) -> Result<()> {
info!("✅ LanceDB initialized successfully");
debug!("Storage path: {:?}", self.path);
debug!("Cache size: {:?}", self.config.cache_size);
debug!("Compression: {}", self.config.enable_compression);
Ok(())
}
async fn create_table(&self, name: &str, schema: TableSchema) -> Result<()> {
// Convert schema to Arrow schema
let fields: Vec<Field> = schema.columns.iter()
.map(|col| Field::new(&col.name, Self::to_arrow_type(&col.data_type), col.nullable))
.collect();
let arrow_schema = Arc::new(ArrowSchema::new(fields));
// Create empty batch for table creation
let batch = RecordBatch::new_empty(arrow_schema.clone());
let batches = vec![batch];
let batch_iter = RecordBatchIterator::new(batches.into_iter().map(Ok), arrow_schema);
// Create table
self.connection.create_table(name, batch_iter, None).await
.context(format!("Failed to create table {}", name))?;
// Create indexes
for index in schema.indexes {
match index.index_type {
IndexType::IVF_PQ { nlist, nprobe } => {
// Create IVF_PQ index for vector columns
if index.columns.len() == 1 {
let table = self.connection.open_table(name).await?;
// LanceDB specific index creation
debug!("Creating IVF_PQ index on {} with nlist={}, nprobe={}",
index.columns[0], nlist, nprobe);
// Note: Actual index creation API depends on LanceDB version
// This is a placeholder for the real implementation
}
}
IndexType::HNSW { max_elements, m } => {
debug!("Creating HNSW index with max_elements={}, m={}", max_elements, m);
// HNSW index creation
}
_ => {
// Other index types
}
}
}
info!("✅ Table '{}' created with {} columns and {} indexes",
name, schema.columns.len(), schema.indexes.len());
Ok(())
}
async fn insert(&self, table_name: &str, data: &[Record]) -> Result<()> {
if data.is_empty() {
return Ok(());
}
let table = self.connection.open_table(table_name).await
.context(format!("Failed to open table {}", table_name))?;
// Get table schema
let schema = table.schema().await?;
// Convert records to Arrow arrays
let mut columns: Vec<Arc<dyn arrow_array::Array>> = Vec::new();
for field in schema.fields() {
let field_name = field.name();
// Collect values for this column from all records
let values: Vec<Option<Value>> = data.iter()
.map(|record| {
record.values.iter()
.find(|(name, _)| name == field_name)
.map(|(_, value)| value.clone())
})
.collect();
// Convert to appropriate Arrow array
let array: Arc<dyn arrow_array::Array> = match field.data_type() {
ArrowDataType::Int64 => {
let vals: Vec<Option<i64>> = values.iter()
.map(|v| match v {
Some(Value::Int64(i)) => Some(*i),
_ => None,
})
.collect();
Arc::new(Int64Array::from(vals))
}
ArrowDataType::Float32 => {
let vals: Vec<Option<f32>> = values.iter()
.map(|v| match v {
Some(Value::Float32(f)) => Some(*f),
_ => None,
})
.collect();
Arc::new(Float32Array::from(vals))
}
ArrowDataType::Utf8 => {
let vals: Vec<Option<String>> = values.iter()
.map(|v| match v {
Some(Value::String(s)) => Some(s.clone()),
_ => None,
})
.collect();
Arc::new(StringArray::from(vals))
}
ArrowDataType::FixedSizeList(_, dim) => {
// Handle vector columns
let vectors: Vec<Option<Vec<f32>>> = values.iter()
.map(|v| match v {
Some(Value::Vector(vec)) => Some(vec.clone()),
_ => None,
})
.collect();
// Create FixedSizeList array
let mut builder = arrow_array::builder::FixedSizeListBuilder::new(
arrow_array::builder::Float32Builder::new(),
*dim
);
for vec_opt in vectors {
match vec_opt {
Some(vec) => {
builder.values().append_slice(&vec);
builder.append(true);
}
None => builder.append(false),
}
}
Arc::new(builder.finish())
}
_ => {
// Handle other types
continue;
}
};
columns.push(array);
}
// Create RecordBatch
let batch = RecordBatch::try_new(schema.clone(), columns)?;
// Insert batch
table.add(vec![batch]).await?;
debug!("✅ Inserted {} records into table '{}'", data.len(), table_name);
Ok(())
}
async fn query(&self, query: Query) -> Result<QueryResult> {
let table = self.connection.open_table(&query.table).await?;
// Build query (simplified - real implementation would be more complex)
let mut results = table.query().await?;
// Apply filters
if let Some(filter) = &query.filter {
// Convert filter to LanceDB filter format
debug!("Applying filter: {:?}", filter);
}
// Apply ordering
for order in &query.order_by {
debug!("Ordering by {} {}", order.column, if order.ascending { "ASC" } else { "DESC" });
}
// Apply limit and offset
if let Some(limit) = query.limit {
results = results.limit(limit);
}
let batches = results.execute().await?;
// Convert batches to QueryResult
let mut rows = Vec::new();
let mut columns = Vec::new();
if let Some(batch) = batches.first() {
columns = batch.schema().fields().iter()
.map(|f| f.name().to_string())
.collect();
}
for batch in batches {
// Convert each row
for row_idx in 0..batch.num_rows() {
let mut values = Vec::new();
for (col_idx, field) in batch.schema().fields().iter().enumerate() {
let column = batch.column(col_idx);
let value = Self::extract_value(column, row_idx)?;
values.push((field.name().to_string(), value));
}
rows.push(Record { values });
}
}
Ok(QueryResult {
columns,
row_count: rows.len(),
rows,
})
}
async fn vector_search(&self, query: VectorQuery) -> Result<Vec<SearchResult>> {
let table = self.connection.open_table(&query.table).await?;
info!("🔍 Performing vector search in table '{}' for {} nearest neighbors",
query.table, query.k);
// Build vector search query
let mut search = table.query()
.nearest_to(&query.vector)?
.limit(query.k);
// Apply filter if present
if let Some(filter) = &query.filter {
debug!("Applying pre-filter: {:?}", filter);
// Convert filter to LanceDB format
}
// Set distance metric
match query.metric {
DistanceMetric::L2 => {
search = search.distance_type(lancedb::query::DistanceType::L2);
}
DistanceMetric::Cosine => {
search = search.distance_type(lancedb::query::DistanceType::Cosine);
}
DistanceMetric::InnerProduct => {
search = search.distance_type(lancedb::query::DistanceType::Dot);
}
}
let batches = search.execute().await?;
// Convert to SearchResult
let mut results = Vec::new();
for batch in batches {
for row_idx in 0..batch.num_rows() {
let mut values = Vec::new();
let mut score = 0.0f32;
for (col_idx, field) in batch.schema().fields().iter().enumerate() {
let column = batch.column(col_idx);
// Check if this is the distance column
if field.name() == "_distance" {
if let Ok(Value::Float32(dist)) = Self::extract_value(column, row_idx) {
score = dist;
}
} else {
let value = Self::extract_value(column, row_idx)?;
values.push((field.name().to_string(), value));
}
}
results.push(SearchResult {
record: Record { values },
score,
});
}
}
info!("✅ Found {} results with scores ranging from {:.4} to {:.4}",
results.len(),
results.first().map(|r| r.score).unwrap_or(0.0),
results.last().map(|r| r.score).unwrap_or(0.0)
);
Ok(results)
}
async fn begin_transaction(&self) -> Result<Transaction> {
// LanceDB handles transactions automatically
// This is a simplified implementation
Ok(Transaction {
inner: Arc::new(tokio::sync::RwLock::new(crate::TransactionInner {
backend: crate::DatabaseBackend::LanceDB,
handle: Box::new(()),
})),
})
}
async fn optimize(&self) -> Result<()> {
info!("🔧 Optimizing LanceDB database...");
// List all tables
let tables = self.connection.table_names().await?;
for table_name in tables {
let table = self.connection.open_table(&table_name).await?;
// Compact table (merge small files)
table.compact().await?;
// Clean up old versions
table.cleanup_old_versions().await?;
debug!("✅ Optimized table '{}'", table_name);
}
info!("✅ Database optimization complete");
Ok(())
}
async fn stats(&self) -> Result<DatabaseStats> {
let tables = self.connection.table_names().await?;
let table_count = tables.len();
let mut total_rows = 0;
let mut index_count = 0;
for table_name in &tables {
let table = self.connection.open_table(table_name).await?;
total_rows += table.count_rows().await?;
// Count indexes (simplified)
index_count += 1; // Assume at least one index per table
}
// Calculate storage size
let mut total_size_bytes = 0;
if let Ok(entries) = std::fs::read_dir(&self.path) {
for entry in entries.flatten() {
if let Ok(metadata) = entry.metadata() {
total_size_bytes += metadata.len() as usize;
}
}
}
Ok(DatabaseStats {
backend: crate::DatabaseBackend::LanceDB,
table_count,
total_rows,
total_size_bytes,
index_count,
cache_hit_rate: 0.95, // LanceDB has excellent caching
})
}
}
impl LanceDbBackend {
/// Extract value from Arrow column
fn extract_value(column: &dyn arrow_array::Array, row_idx: usize) -> Result<Value> {
use arrow_array::cast::as_primitive_array;
match column.data_type() {
ArrowDataType::Int64 => {
let array = as_primitive_array::<arrow_array::types::Int64Type>(column);
Ok(if array.is_null(row_idx) {
Value::Null
} else {
Value::Int64(array.value(row_idx))
})
}
ArrowDataType::Float32 => {
let array = as_primitive_array::<arrow_array::types::Float32Type>(column);
Ok(if array.is_null(row_idx) {
Value::Null
} else {
Value::Float32(array.value(row_idx))
})
}
ArrowDataType::Utf8 => {
let array = column.as_any().downcast_ref::<StringArray>().unwrap();
Ok(if array.is_null(row_idx) {
Value::Null
} else {
Value::String(array.value(row_idx).to_string())
})
}
_ => Ok(Value::Null),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_lancedb_init() {
let config = HanzoDbConfig::default();
let backend = LanceDbBackend::new(config).await.unwrap();
backend.init().await.unwrap();
}
#[tokio::test]
async fn test_create_table() {
let config = HanzoDbConfig::default();
let backend = LanceDbBackend::new(config).await.unwrap();
let schema = TableSchema {
columns: vec![
Column {
name: "id".to_string(),
data_type: DataType::Int64,
nullable: false,
default: None,
},
Column {
name: "embedding".to_string(),
data_type: DataType::Vector(384),
nullable: false,
default: None,
},
Column {
name: "text".to_string(),
data_type: DataType::String,
nullable: true,
default: None,
},
],
indexes: vec![
Index {
name: "embedding_idx".to_string(),
columns: vec!["embedding".to_string()],
index_type: IndexType::IVF_PQ { nlist: 100, nprobe: 10 },
},
],
constraints: vec![],
};
backend.create_table("test_embeddings", schema).await.unwrap();
}
#[tokio::test]
async fn test_vector_search() {
let config = HanzoDbConfig::default();
let backend = LanceDbBackend::new(config).await.unwrap();
// Create table first
let schema = TableSchema {
columns: vec![
Column {
name: "id".to_string(),
data_type: DataType::Int64,
nullable: false,
default: None,
},
Column {
name: "embedding".to_string(),
data_type: DataType::Vector(3),
nullable: false,
default: None,
},
],
indexes: vec![],
constraints: vec![],
};
backend.create_table("vectors", schema).await.unwrap();
// Insert test data
let records = vec![
Record {
values: vec![
("id".to_string(), Value::Int64(1)),
("embedding".to_string(), Value::Vector(vec![0.1, 0.2, 0.3])),
],
},
Record {
values: vec![
("id".to_string(), Value::Int64(2)),
("embedding".to_string(), Value::Vector(vec![0.4, 0.5, 0.6])),
],
},
];
backend.insert("vectors", &records).await.unwrap();
// Search
let query = VectorQuery {
table: "vectors".to_string(),
vector: vec![0.15, 0.25, 0.35],
k: 2,
filter: None,
metric: DistanceMetric::L2,
};
let results = backend.vector_search(query).await.unwrap();
assert_eq!(results.len(), 2);
assert!(results[0].score < results[1].score); // Closer match has lower L2 distance
}
}
-7
View File
@@ -1,7 +0,0 @@
//! Database backend implementations
pub mod lancedb;
pub mod duckdb;
pub mod postgres;
pub mod redis;
pub mod sqlite;
-518
View File
@@ -1,518 +0,0 @@
//! PostgreSQL backend implementation for transactional workloads
//!
//! PostgreSQL is a powerful, open-source relational database with excellent
//! ACID compliance, rich SQL features, and extension support (including pgvector).
use crate::{DatabaseBackend, HanzoDbConfig, HanzoDbError, Result};
use async_trait::async_trait;
use serde_json::Value;
use sqlx::{postgres::{PgPoolOptions, PgPool}, Row};
use std::time::Duration;
/// PostgreSQL backend for transactional workloads
pub struct PostgresBackend {
pool: PgPool,
config: HanzoDbConfig,
}
impl PostgresBackend {
/// Create a new PostgreSQL backend
pub async fn new(config: HanzoDbConfig) -> Result<Self> {
let database_url = config.url.as_ref()
.ok_or_else(|| HanzoDbError::ConfigError(
"PostgreSQL requires database URL".to_string()
))?;
// Create connection pool
let pool = PgPoolOptions::new()
.max_connections(config.max_connections.unwrap_or(10))
.min_connections(config.min_connections.unwrap_or(2))
.connect_timeout(Duration::from_secs(config.connect_timeout.unwrap_or(10)))
.idle_timeout(Duration::from_secs(config.idle_timeout.unwrap_or(300)))
.connect(database_url)
.await
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
// Test connection
sqlx::query("SELECT 1")
.fetch_one(&pool)
.await
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
Ok(Self { pool, config })
}
/// Enable pgvector extension
pub async fn enable_vector_extension(&self) -> Result<()> {
sqlx::query("CREATE EXTENSION IF NOT EXISTS vector")
.execute(&self.pool)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(())
}
/// Create vector index for similarity search
pub async fn create_vector_index(
&self,
table: &str,
column: &str,
dimensions: usize,
index_type: &str, // "ivfflat" or "hnsw"
) -> Result<()> {
let index_name = format!("{}_{}_idx", table, column);
let query = match index_type {
"ivfflat" => {
format!(
"CREATE INDEX {} ON {} USING ivfflat ({} vector_l2_ops) WITH (lists = {})",
index_name, table, column, dimensions / 10
)
},
"hnsw" => {
format!(
"CREATE INDEX {} ON {} USING hnsw ({} vector_l2_ops)",
index_name, table, column
)
},
_ => {
return Err(HanzoDbError::InvalidSchema(
format!("Unknown index type: {}", index_type)
));
}
};
sqlx::query(&query)
.execute(&self.pool)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(())
}
/// Begin transaction
pub async fn begin_transaction(&self) -> Result<sqlx::Transaction<'_, sqlx::Postgres>> {
self.pool.begin()
.await
.map_err(|e| HanzoDbError::TransactionError(e.to_string()))
}
/// Execute raw SQL with parameters
pub async fn execute_raw(&self, query: &str, params: Vec<Value>) -> Result<u64> {
let mut q = sqlx::query(query);
for param in params {
q = match param {
Value::String(s) => q.bind(s),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
q.bind(i)
} else if let Some(f) = n.as_f64() {
q.bind(f)
} else {
q.bind(Option::<i64>::None)
}
},
Value::Bool(b) => q.bind(b),
Value::Null => q.bind(Option::<String>::None),
_ => q.bind(param.to_string()),
};
}
let result = q.execute(&self.pool)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(result.rows_affected())
}
}
#[async_trait]
impl DatabaseBackend for PostgresBackend {
async fn connect(&mut self) -> Result<()> {
// Already connected in new()
Ok(())
}
async fn disconnect(&mut self) -> Result<()> {
self.pool.close().await;
Ok(())
}
async fn create_table(&self, name: &str, schema: &Value) -> Result<()> {
let columns = schema.as_object()
.ok_or_else(|| HanzoDbError::InvalidSchema("Schema must be object".to_string()))?;
let mut column_defs = Vec::new();
let mut has_vector = false;
for (col_name, type_def) in columns {
let sql_type = match type_def {
Value::String(s) => match s.as_str() {
"string" => "TEXT",
"integer" => "INTEGER",
"bigint" => "BIGINT",
"float" => "REAL",
"double" => "DOUBLE PRECISION",
"boolean" => "BOOLEAN",
"timestamp" => "TIMESTAMP WITH TIME ZONE",
"date" => "DATE",
"json" | "jsonb" => "JSONB",
"uuid" => "UUID",
"vector" => {
has_vector = true;
"vector(1536)" // Default to OpenAI embedding size
},
t if t.starts_with("vector(") => {
has_vector = true;
t
},
_ => "TEXT",
},
Value::Object(obj) => {
// Handle complex type definitions
if let Some(Value::String(t)) = obj.get("type") {
match t.as_str() {
"vector" => {
has_vector = true;
if let Some(Value::Number(dim)) = obj.get("dimensions") {
&format!("vector({})", dim)
} else {
"vector(1536)"
}
},
_ => "TEXT",
}
} else {
"TEXT"
}
},
_ => "TEXT",
};
column_defs.push(format!("{} {}", col_name, sql_type));
}
// Enable pgvector if needed
if has_vector {
self.enable_vector_extension().await?;
}
// Add standard columns if not present
if !columns.contains_key("id") {
column_defs.insert(0, "id SERIAL PRIMARY KEY".to_string());
}
if !columns.contains_key("created_at") {
column_defs.push("created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()".to_string());
}
if !columns.contains_key("updated_at") {
column_defs.push("updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()".to_string());
}
let create_sql = format!(
"CREATE TABLE IF NOT EXISTS {} ({})",
name,
column_defs.join(", ")
);
sqlx::query(&create_sql)
.execute(&self.pool)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
// Create update trigger for updated_at
let trigger_sql = format!(
"CREATE OR REPLACE TRIGGER update_{}_updated_at
BEFORE UPDATE ON {}
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column()",
name, name
);
// First create the function if it doesn't exist
sqlx::query(
"CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql'"
).execute(&self.pool).await.ok(); // Ignore if already exists
sqlx::query(&trigger_sql)
.execute(&self.pool)
.await
.ok(); // Ignore if trigger already exists
Ok(())
}
async fn drop_table(&self, name: &str) -> Result<()> {
sqlx::query(&format!("DROP TABLE IF EXISTS {} CASCADE", name))
.execute(&self.pool)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(())
}
async fn insert(&self, table: &str, records: &[Value]) -> Result<()> {
let mut tx = self.begin_transaction().await?;
for record in records {
let obj = record.as_object()
.ok_or_else(|| HanzoDbError::InvalidData("Record must be object".to_string()))?;
let columns: Vec<String> = obj.keys().cloned().collect();
let placeholders: Vec<String> = (1..=columns.len())
.map(|i| format!("${}", i))
.collect();
let insert_sql = format!(
"INSERT INTO {} ({}) VALUES ({})",
table,
columns.join(", "),
placeholders.join(", ")
);
let mut query = sqlx::query(&insert_sql);
for value in obj.values() {
query = match value {
Value::String(s) => query.bind(s),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
query.bind(i)
} else if let Some(f) = n.as_f64() {
query.bind(f)
} else {
query.bind(Option::<i64>::None)
}
},
Value::Bool(b) => query.bind(b),
Value::Null => query.bind(Option::<String>::None),
Value::Array(a) => {
// Handle vector data
if a.iter().all(|v| v.is_number()) {
let floats: Vec<f32> = a.iter()
.filter_map(|v| v.as_f64().map(|f| f as f32))
.collect();
query.bind(floats)
} else {
query.bind(value.to_string())
}
},
Value::Object(_) => query.bind(value.to_string()),
};
}
query.execute(&mut *tx)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
}
tx.commit()
.await
.map_err(|e| HanzoDbError::TransactionError(e.to_string()))?;
Ok(())
}
async fn query(&self, query: &str) -> Result<Vec<Value>> {
let rows = sqlx::query(query)
.fetch_all(&self.pool)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
let mut results = Vec::new();
for row in rows {
let mut obj = serde_json::Map::new();
// Get column information
for (i, column) in row.columns().iter().enumerate() {
let name = column.name();
// Try to get value as different types
if let Ok(val) = row.try_get::<String, _>(i) {
obj.insert(name.to_string(), Value::String(val));
} else if let Ok(val) = row.try_get::<i64, _>(i) {
obj.insert(name.to_string(), Value::Number(val.into()));
} else if let Ok(val) = row.try_get::<i32, _>(i) {
obj.insert(name.to_string(), Value::Number(val.into()));
} else if let Ok(val) = row.try_get::<f64, _>(i) {
obj.insert(name.to_string(), Value::from(val));
} else if let Ok(val) = row.try_get::<f32, _>(i) {
obj.insert(name.to_string(), Value::from(val as f64));
} else if let Ok(val) = row.try_get::<bool, _>(i) {
obj.insert(name.to_string(), Value::Bool(val));
} else if let Ok(val) = row.try_get::<serde_json::Value, _>(i) {
obj.insert(name.to_string(), val);
} else if let Ok(val) = row.try_get::<Vec<f32>, _>(i) {
// Handle vector data
let array: Vec<Value> = val.iter().map(|f| Value::from(*f as f64)).collect();
obj.insert(name.to_string(), Value::Array(array));
} else {
obj.insert(name.to_string(), Value::Null);
}
}
results.push(Value::Object(obj));
}
Ok(results)
}
async fn update(&self, table: &str, id: &str, data: &Value) -> Result<()> {
let obj = data.as_object()
.ok_or_else(|| HanzoDbError::InvalidData("Update data must be object".to_string()))?;
let set_clauses: Vec<String> = obj.keys()
.enumerate()
.map(|(i, key)| format!("{} = ${}", key, i + 2))
.collect();
let update_sql = format!(
"UPDATE {} SET {}, updated_at = NOW() WHERE id = $1",
table,
set_clauses.join(", ")
);
let mut query = sqlx::query(&update_sql).bind(id);
for value in obj.values() {
query = match value {
Value::String(s) => query.bind(s),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
query.bind(i)
} else if let Some(f) = n.as_f64() {
query.bind(f)
} else {
query.bind(Option::<i64>::None)
}
},
Value::Bool(b) => query.bind(b),
Value::Null => query.bind(Option::<String>::None),
_ => query.bind(value.to_string()),
};
}
query.execute(&self.pool)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(())
}
async fn delete(&self, table: &str, id: &str) -> Result<()> {
sqlx::query(&format!("DELETE FROM {} WHERE id = $1", table))
.bind(id)
.execute(&self.pool)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(())
}
async fn vector_search(
&self,
table: &str,
query_vector: &[f32],
limit: usize,
filter: Option<&str>,
) -> Result<Vec<Value>> {
// Convert vector to PostgreSQL array format
let vector_str = format!(
"[{}]",
query_vector.iter()
.map(|f| f.to_string())
.collect::<Vec<_>>()
.join(",")
);
let where_clause = if let Some(f) = filter {
format!("WHERE {}", f)
} else {
String::new()
};
// Use pgvector's <-> operator for L2 distance
let query = format!(
"SELECT *,
embedding <-> '{}' as distance
FROM {}
{}
ORDER BY embedding <-> '{}'
LIMIT {}",
vector_str, table, where_clause, vector_str, limit
);
self.query(&query).await
}
fn backend_type(&self) -> DatabaseBackend {
DatabaseBackend::PostgreSQL
}
fn is_vector_capable(&self) -> bool {
true // With pgvector extension
}
fn is_transactional(&self) -> bool {
true // PostgreSQL has excellent ACID compliance
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_postgres_transactions() {
// This test requires a PostgreSQL instance
// Skip if not available
let database_url = std::env::var("TEST_POSTGRES_URL")
.unwrap_or_else(|_| "postgresql://postgres:postgres@localhost/test".to_string());
let config = HanzoDbConfig {
backend: DatabaseBackend::PostgreSQL,
url: Some(database_url),
..Default::default()
};
let backend = match PostgresBackend::new(config).await {
Ok(b) => b,
Err(_) => {
println!("PostgreSQL not available, skipping test");
return;
}
};
// Create table with vector support
let schema = serde_json::json!({
"name": "string",
"embedding": {
"type": "vector",
"dimensions": 384
}
});
backend.create_table("test_items", &schema).await.unwrap();
// Test transaction
let mut tx = backend.begin_transaction().await.unwrap();
sqlx::query("INSERT INTO test_items (name) VALUES ($1)")
.bind("test")
.execute(&mut *tx)
.await
.unwrap();
tx.commit().await.unwrap();
// Clean up
backend.drop_table("test_items").await.unwrap();
}
}
-497
View File
@@ -1,497 +0,0 @@
//! Redis backend implementation for caching and real-time operations
//!
//! Redis is an in-memory data structure store, perfect for caching,
//! session management, real-time analytics, and pub/sub messaging.
use crate::{DatabaseBackend, HanzoDbConfig, HanzoDbError, Result};
use async_trait::async_trait;
use redis::{aio::ConnectionManager, AsyncCommands, Client};
use serde_json::Value;
use std::time::Duration;
/// Redis backend for caching and real-time operations
pub struct RedisBackend {
client: Client,
connection: ConnectionManager,
config: HanzoDbConfig,
}
impl RedisBackend {
/// Create a new Redis backend
pub async fn new(config: HanzoDbConfig) -> Result<Self> {
let redis_url = config.url.as_ref()
.unwrap_or(&"redis://127.0.0.1:6379".to_string())
.clone();
// Create Redis client
let client = Client::open(redis_url.as_str())
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
// Create connection manager for connection pooling
let connection = ConnectionManager::new(client.clone()).await
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
Ok(Self {
client,
connection,
config,
})
}
/// Set value with optional TTL
pub async fn set_with_ttl(&mut self, key: &str, value: &Value, ttl_seconds: Option<u64>) -> Result<()> {
let serialized = serde_json::to_string(value)
.map_err(|e| HanzoDbError::SerializationError(e.to_string()))?;
if let Some(ttl) = ttl_seconds {
self.connection
.set_ex(key, serialized, ttl)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
} else {
self.connection
.set(key, serialized)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
}
Ok(())
}
/// Get value by key
pub async fn get(&mut self, key: &str) -> Result<Option<Value>> {
let value: Option<String> = self.connection
.get(key)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
match value {
Some(v) => {
let parsed = serde_json::from_str(&v)
.map_err(|e| HanzoDbError::DeserializationError(e.to_string()))?;
Ok(Some(parsed))
},
None => Ok(None),
}
}
/// Delete key
pub async fn del(&mut self, key: &str) -> Result<()> {
self.connection
.del(key)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(())
}
/// Check if key exists
pub async fn exists(&mut self, key: &str) -> Result<bool> {
let exists: bool = self.connection
.exists(key)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(exists)
}
/// Set multiple keys at once
pub async fn mset(&mut self, items: &[(String, Value)]) -> Result<()> {
let serialized: Vec<(String, String)> = items
.iter()
.map(|(k, v)| {
serde_json::to_string(v)
.map(|s| (k.clone(), s))
.map_err(|e| HanzoDbError::SerializationError(e.to_string()))
})
.collect::<Result<Vec<_>>>()?;
self.connection
.set_multiple(&serialized)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(())
}
/// Get multiple keys at once
pub async fn mget(&mut self, keys: &[String]) -> Result<Vec<Option<Value>>> {
let values: Vec<Option<String>> = self.connection
.get(keys)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
values
.into_iter()
.map(|opt_v| {
match opt_v {
Some(v) => {
serde_json::from_str(&v)
.map(Some)
.map_err(|e| HanzoDbError::DeserializationError(e.to_string()))
},
None => Ok(None),
}
})
.collect()
}
/// Increment counter
pub async fn incr(&mut self, key: &str, delta: i64) -> Result<i64> {
let new_value: i64 = self.connection
.incr(key, delta)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(new_value)
}
/// Add to set
pub async fn sadd(&mut self, key: &str, members: &[String]) -> Result<()> {
for member in members {
self.connection
.sadd(key, member)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
}
Ok(())
}
/// Get set members
pub async fn smembers(&mut self, key: &str) -> Result<Vec<String>> {
let members: Vec<String> = self.connection
.smembers(key)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(members)
}
/// Publish message to channel
pub async fn publish(&mut self, channel: &str, message: &Value) -> Result<()> {
let serialized = serde_json::to_string(message)
.map_err(|e| HanzoDbError::SerializationError(e.to_string()))?;
self.connection
.publish(channel, serialized)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(())
}
/// Add to sorted set with score
pub async fn zadd(&mut self, key: &str, member: &str, score: f64) -> Result<()> {
self.connection
.zadd(key, member, score)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(())
}
/// Get sorted set range by score
pub async fn zrangebyscore(&mut self, key: &str, min: f64, max: f64, limit: Option<usize>) -> Result<Vec<String>> {
let mut cmd = redis::cmd("ZRANGEBYSCORE");
cmd.arg(key).arg(min).arg(max);
if let Some(l) = limit {
cmd.arg("LIMIT").arg(0).arg(l);
}
let members: Vec<String> = cmd
.query_async(&mut self.connection)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(members)
}
/// Store vector embedding with metadata
pub async fn store_embedding(
&mut self,
key: &str,
embedding: &[f32],
metadata: Option<&Value>,
) -> Result<()> {
// Redis doesn't have native vector search, but we can store for caching
let data = serde_json::json!({
"embedding": embedding,
"metadata": metadata,
"timestamp": chrono::Utc::now().to_rfc3339(),
});
self.set_with_ttl(key, &data, Some(3600)).await
}
/// Cache query result
pub async fn cache_result(&mut self, query_hash: &str, result: &Value, ttl_seconds: u64) -> Result<()> {
let cache_key = format!("cache:{}", query_hash);
self.set_with_ttl(&cache_key, result, Some(ttl_seconds)).await
}
/// Get cached result
pub async fn get_cached(&mut self, query_hash: &str) -> Result<Option<Value>> {
let cache_key = format!("cache:{}", query_hash);
self.get(&cache_key).await
}
}
#[async_trait]
impl DatabaseBackend for RedisBackend {
async fn connect(&mut self) -> Result<()> {
// Already connected in new()
// Test connection
redis::cmd("PING")
.query_async::<_, String>(&mut self.connection)
.await
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
Ok(())
}
async fn disconnect(&mut self) -> Result<()> {
// Connection manager handles cleanup
Ok(())
}
async fn create_table(&self, name: &str, _schema: &Value) -> Result<()> {
// Redis doesn't have tables, use key prefixes instead
// Store table metadata
let mut conn = self.connection.clone();
let table_meta = serde_json::json!({
"name": name,
"created_at": chrono::Utc::now().to_rfc3339(),
"type": "redis_namespace"
});
let meta_key = format!("_meta:table:{}", name);
conn.set(meta_key, table_meta.to_string())
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(())
}
async fn drop_table(&self, name: &str) -> Result<()> {
// Delete all keys with table prefix
let mut conn = self.connection.clone();
let pattern = format!("{}:*", name);
// Get all keys matching pattern
let keys: Vec<String> = redis::cmd("KEYS")
.arg(&pattern)
.query_async(&mut conn)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
// Delete keys in batches
for chunk in keys.chunks(1000) {
redis::cmd("DEL")
.arg(chunk)
.query_async::<_, ()>(&mut conn)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
}
// Delete table metadata
let meta_key = format!("_meta:table:{}", name);
conn.del(meta_key)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(())
}
async fn insert(&self, table: &str, records: &[Value]) -> Result<()> {
let mut conn = self.connection.clone();
for (i, record) in records.iter().enumerate() {
let key = if let Some(id) = record.get("id").and_then(|v| v.as_str()) {
format!("{}:{}", table, id)
} else {
format!("{}:{}", table, uuid::Uuid::new_v4())
};
let serialized = serde_json::to_string(record)
.map_err(|e| HanzoDbError::SerializationError(e.to_string()))?;
conn.set(key, serialized)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
// Add to table index
let index_key = format!("_index:{}", table);
conn.sadd(index_key, &key)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
}
Ok(())
}
async fn query(&self, query: &str) -> Result<Vec<Value>> {
// Redis doesn't support SQL, interpret as key pattern
let mut conn = self.connection.clone();
let keys: Vec<String> = if query.starts_with("SELECT * FROM ") {
// Extract table name from simple query
let table = query
.strip_prefix("SELECT * FROM ")
.and_then(|s| s.split_whitespace().next())
.ok_or_else(|| HanzoDbError::InvalidQuery("Invalid SELECT query".to_string()))?;
// Get all keys from table index
let index_key = format!("_index:{}", table);
conn.smembers(index_key)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?
} else {
// Treat as key pattern
redis::cmd("KEYS")
.arg(query)
.query_async(&mut conn)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?
};
// Get all values
let mut results = Vec::new();
for key in keys {
let value: Option<String> = conn.get(&key)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
if let Some(v) = value {
if let Ok(parsed) = serde_json::from_str(&v) {
results.push(parsed);
}
}
}
Ok(results)
}
async fn update(&self, table: &str, id: &str, data: &Value) -> Result<()> {
let mut conn = self.connection.clone();
let key = format!("{}:{}", table, id);
// Get existing record
let existing: Option<String> = conn.get(&key)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
let mut record = if let Some(e) = existing {
serde_json::from_str(&e)
.map_err(|e| HanzoDbError::DeserializationError(e.to_string()))?
} else {
serde_json::json!({})
};
// Merge updates
if let (Some(rec_obj), Some(data_obj)) = (record.as_object_mut(), data.as_object()) {
for (k, v) in data_obj {
rec_obj.insert(k.clone(), v.clone());
}
}
// Save updated record
let serialized = serde_json::to_string(&record)
.map_err(|e| HanzoDbError::SerializationError(e.to_string()))?;
conn.set(key, serialized)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(())
}
async fn delete(&self, table: &str, id: &str) -> Result<()> {
let mut conn = self.connection.clone();
let key = format!("{}:{}", table, id);
// Remove from index
let index_key = format!("_index:{}", table);
conn.srem(index_key, &key)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
// Delete key
conn.del(key)
.await
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(())
}
async fn vector_search(
&self,
_table: &str,
query_vector: &[f32],
limit: usize,
_filter: Option<&str>,
) -> Result<Vec<Value>> {
// Redis doesn't have native vector search
// For production, use RedisSearch with vector similarity
// This is a placeholder that returns cached results if available
let mut conn = self.connection.clone();
let query_hash = blake3::hash(
&query_vector.iter()
.flat_map(|f| f.to_le_bytes())
.collect::<Vec<_>>()
).to_hex();
let cache_key = format!("vector_cache:{}", query_hash.as_str());
if let Some(cached) = self.get_cached(query_hash.as_str()).await? {
return Ok(vec![cached]);
}
// Return empty result - real implementation would use RedisSearch
Ok(vec![])
}
fn backend_type(&self) -> DatabaseBackend {
DatabaseBackend::Redis
}
fn is_cache(&self) -> bool {
true // Redis is primarily for caching
}
fn is_real_time(&self) -> bool {
true // Redis excels at real-time operations
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_redis_operations() {
// Skip if Redis not available
let config = HanzoDbConfig {
backend: DatabaseBackend::Redis,
url: Some("redis://localhost:6379".to_string()),
..Default::default()
};
let mut backend = match RedisBackend::new(config).await {
Ok(b) => b,
Err(_) => {
println!("Redis not available, skipping test");
return;
}
};
// Test basic operations
let key = "test_key";
let value = serde_json::json!({"foo": "bar"});
backend.set_with_ttl(key, &value, Some(60)).await.unwrap();
let retrieved = backend.get(key).await.unwrap();
assert_eq!(retrieved, Some(value));
backend.del(key).await.unwrap();
let deleted = backend.get(key).await.unwrap();
assert_eq!(deleted, None);
}
}
-626
View File
@@ -1,626 +0,0 @@
//! SQLite backend implementation for embedded database operations
//!
//! SQLite is a lightweight, serverless, self-contained SQL database engine,
//! perfect for embedded applications, testing, and local development.
use crate::{DatabaseBackend, HanzoDbConfig, HanzoDbError, Result};
use async_trait::async_trait;
use rusqlite::{params, Connection, OptionalExtension};
use serde_json::Value;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use tokio::task;
/// SQLite backend for embedded database operations
pub struct SqliteBackend {
connection: Arc<Mutex<Connection>>,
path: PathBuf,
config: HanzoDbConfig,
}
impl SqliteBackend {
/// Create a new SQLite backend
pub fn new(config: HanzoDbConfig) -> Result<Self> {
let path = config.path.clone().unwrap_or_else(|| {
PathBuf::from("./storage/hanzo-sqlite.db")
});
// Create parent directory if needed
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
}
// Open SQLite connection
let connection = if path.to_str() == Some(":memory:") {
Connection::open_in_memory()
} else {
Connection::open(&path)
}.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
// Configure SQLite for optimal performance
connection.execute_batch("
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA cache_size = -64000;
PRAGMA mmap_size = 30000000000;
PRAGMA temp_store = MEMORY;
PRAGMA foreign_keys = ON;
").map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
// Enable JSON1 extension
connection.execute_batch("
CREATE TABLE IF NOT EXISTS _meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
").map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(Self {
connection: Arc::new(Mutex::new(connection)),
path,
config,
})
}
/// Execute a query and return results as JSON
fn execute_query(&self, query: &str) -> Result<Vec<Value>> {
let conn = self.connection.lock()
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
let mut stmt = conn.prepare(query)
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
let column_names: Vec<String> = stmt
.column_names()
.into_iter()
.map(|s| s.to_string())
.collect();
let rows = stmt.query_map(params![], |row| {
let mut obj = serde_json::Map::new();
for (i, name) in column_names.iter().enumerate() {
let value = match row.get_ref(i) {
Ok(val) => {
use rusqlite::types::ValueRef;
match val {
ValueRef::Null => Value::Null,
ValueRef::Integer(i) => Value::Number(i.into()),
ValueRef::Real(f) => Value::from(f),
ValueRef::Text(s) => {
// Try to parse as JSON first
if let Ok(json) = serde_json::from_str(std::str::from_utf8(s).unwrap_or("")) {
json
} else {
Value::String(std::str::from_utf8(s).unwrap_or("").to_string())
}
},
ValueRef::Blob(b) => {
// Convert blob to base64
Value::String(base64::encode(b))
},
}
},
Err(_) => Value::Null,
};
obj.insert(name.clone(), value);
}
Ok(Value::Object(obj))
}).map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|e| HanzoDbError::QueryError(e.to_string()))
}
/// Create vector extension tables
pub fn enable_vector_support(&self) -> Result<()> {
let conn = self.connection.lock()
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
// Create vector storage table
conn.execute_batch("
CREATE TABLE IF NOT EXISTS _vectors (
id TEXT PRIMARY KEY,
table_name TEXT NOT NULL,
record_id TEXT NOT NULL,
vector BLOB NOT NULL,
metadata TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(table_name, record_id)
);
CREATE INDEX IF NOT EXISTS idx_vectors_table ON _vectors(table_name);
CREATE INDEX IF NOT EXISTS idx_vectors_record ON _vectors(table_name, record_id);
").map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(())
}
/// Store vector embedding
pub fn store_vector(
&self,
table: &str,
record_id: &str,
vector: &[f32],
metadata: Option<&Value>,
) -> Result<()> {
let conn = self.connection.lock()
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
// Convert vector to bytes
let vector_bytes: Vec<u8> = vector.iter()
.flat_map(|f| f.to_le_bytes())
.collect();
let metadata_str = metadata
.map(|m| serde_json::to_string(m))
.transpose()
.map_err(|e| HanzoDbError::SerializationError(e.to_string()))?;
conn.execute(
"INSERT OR REPLACE INTO _vectors (id, table_name, record_id, vector, metadata)
VALUES (?1, ?2, ?3, ?4, ?5)",
params![
format!("{}:{}", table, record_id),
table,
record_id,
vector_bytes,
metadata_str
]
).map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok(())
}
/// Simple cosine similarity search (not optimized)
pub fn vector_similarity_search(
&self,
table: &str,
query_vector: &[f32],
limit: usize,
) -> Result<Vec<Value>> {
let conn = self.connection.lock()
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
// Get all vectors from table
let mut stmt = conn.prepare(
"SELECT record_id, vector, metadata FROM _vectors WHERE table_name = ?1"
).map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
let vectors = stmt.query_map(params![table], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, Vec<u8>>(1)?,
row.get::<_, Option<String>>(2)?,
))
}).map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
// Calculate cosine similarity for each vector
let mut results: Vec<(String, f32, Option<String>)> = Vec::new();
for vector_result in vectors {
let (record_id, vector_bytes, metadata) = vector_result
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
// Convert bytes back to f32 vector
let vector: Vec<f32> = vector_bytes
.chunks_exact(4)
.map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
.collect();
// Calculate cosine similarity
let similarity = cosine_similarity(query_vector, &vector);
results.push((record_id, similarity, metadata));
}
// Sort by similarity (descending)
results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
// Take top N results
let top_results: Vec<Value> = results
.into_iter()
.take(limit)
.map(|(id, score, metadata)| {
let mut obj = serde_json::Map::new();
obj.insert("record_id".to_string(), Value::String(id));
obj.insert("similarity".to_string(), Value::from(score));
if let Some(meta) = metadata {
if let Ok(meta_json) = serde_json::from_str::<Value>(&meta) {
obj.insert("metadata".to_string(), meta_json);
}
}
Value::Object(obj)
})
.collect();
Ok(top_results)
}
}
#[async_trait]
impl DatabaseBackend for SqliteBackend {
async fn connect(&mut self) -> Result<()> {
// Already connected in new()
// Test connection
let conn = self.connection.clone();
task::spawn_blocking(move || {
let c = conn.lock()
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
c.execute("SELECT 1", params![])
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
Ok::<(), HanzoDbError>(())
}).await
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))??;
Ok(())
}
async fn disconnect(&mut self) -> Result<()> {
// Connection will be closed when dropped
Ok(())
}
async fn create_table(&self, name: &str, schema: &Value) -> Result<()> {
let conn = self.connection.clone();
let name = name.to_string();
let schema = schema.clone();
task::spawn_blocking(move || {
let c = conn.lock()
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
let columns = schema.as_object()
.ok_or_else(|| HanzoDbError::InvalidSchema("Schema must be object".to_string()))?;
let mut column_defs = Vec::new();
// Add ID column if not present
if !columns.contains_key("id") {
column_defs.push("id INTEGER PRIMARY KEY AUTOINCREMENT".to_string());
}
for (col_name, type_def) in columns {
let sql_type = match type_def.as_str() {
Some("string") => "TEXT",
Some("integer") => "INTEGER",
Some("bigint") => "INTEGER",
Some("float") | Some("double") => "REAL",
Some("boolean") => "INTEGER", // SQLite uses 0/1 for boolean
Some("timestamp") => "DATETIME",
Some("date") => "DATE",
Some("json") | Some("jsonb") => "TEXT", // Store JSON as TEXT
Some("vector") => "BLOB", // Store vectors as BLOB
_ => "TEXT",
};
column_defs.push(format!("{} {}", col_name, sql_type));
}
// Add timestamps if not present
if !columns.contains_key("created_at") {
column_defs.push("created_at DATETIME DEFAULT CURRENT_TIMESTAMP".to_string());
}
if !columns.contains_key("updated_at") {
column_defs.push("updated_at DATETIME DEFAULT CURRENT_TIMESTAMP".to_string());
}
let create_sql = format!(
"CREATE TABLE IF NOT EXISTS {} ({})",
name,
column_defs.join(", ")
);
c.execute(&create_sql, params![])
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
// Create update trigger for updated_at
let trigger_sql = format!(
"CREATE TRIGGER IF NOT EXISTS update_{}_timestamp
AFTER UPDATE ON {}
FOR EACH ROW
BEGIN
UPDATE {} SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END",
name, name, name
);
c.execute(&trigger_sql, params![])
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok::<(), HanzoDbError>(())
}).await
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))??;
// Enable vector support if needed
if schema.as_object()
.and_then(|o| o.keys().find(|k| k.contains("vector") || k.contains("embedding")))
.is_some()
{
self.enable_vector_support()?;
}
Ok(())
}
async fn drop_table(&self, name: &str) -> Result<()> {
let conn = self.connection.clone();
let name = name.to_string();
task::spawn_blocking(move || {
let c = conn.lock()
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
c.execute(&format!("DROP TABLE IF EXISTS {}", name), params![])
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok::<(), HanzoDbError>(())
}).await
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))??;
Ok(())
}
async fn insert(&self, table: &str, records: &[Value]) -> Result<()> {
let conn = self.connection.clone();
let table = table.to_string();
let records = records.to_vec();
task::spawn_blocking(move || {
let c = conn.lock()
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
let tx = c.transaction()
.map_err(|e| HanzoDbError::TransactionError(e.to_string()))?;
for record in records {
let obj = record.as_object()
.ok_or_else(|| HanzoDbError::InvalidData("Record must be object".to_string()))?;
let columns: Vec<String> = obj.keys().cloned().collect();
let placeholders: Vec<String> = (1..=columns.len())
.map(|i| format!("?{}", i))
.collect();
let insert_sql = format!(
"INSERT INTO {} ({}) VALUES ({})",
table,
columns.join(", "),
placeholders.join(", ")
);
let params: Vec<rusqlite::types::Value> = obj.values().map(|v| {
match v {
Value::String(s) => rusqlite::types::Value::Text(s.clone()),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
rusqlite::types::Value::Integer(i)
} else if let Some(f) = n.as_f64() {
rusqlite::types::Value::Real(f)
} else {
rusqlite::types::Value::Null
}
},
Value::Bool(b) => rusqlite::types::Value::Integer(if *b { 1 } else { 0 }),
Value::Null => rusqlite::types::Value::Null,
Value::Array(_) | Value::Object(_) => {
// Store complex types as JSON
rusqlite::types::Value::Text(v.to_string())
}
}
}).collect();
tx.execute(&insert_sql, params.as_slice())
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
}
tx.commit()
.map_err(|e| HanzoDbError::TransactionError(e.to_string()))?;
Ok::<(), HanzoDbError>(())
}).await
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))??;
Ok(())
}
async fn query(&self, query: &str) -> Result<Vec<Value>> {
let conn = self.connection.clone();
let query = query.to_string();
task::spawn_blocking(move || {
let c = conn.lock()
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
// Use the helper method to execute query and get JSON results
drop(c); // Release lock before calling execute_query
// Re-acquire for execute_query (this is safe as it's in the same thread)
let backend = SqliteBackend {
connection: conn,
path: PathBuf::new(), // Not used in execute_query
config: HanzoDbConfig::default(), // Not used in execute_query
};
backend.execute_query(&query)
}).await
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?
}
async fn update(&self, table: &str, id: &str, data: &Value) -> Result<()> {
let conn = self.connection.clone();
let table = table.to_string();
let id = id.to_string();
let data = data.clone();
task::spawn_blocking(move || {
let c = conn.lock()
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
let obj = data.as_object()
.ok_or_else(|| HanzoDbError::InvalidData("Update data must be object".to_string()))?;
let set_clauses: Vec<String> = obj.keys()
.enumerate()
.map(|(i, key)| format!("{} = ?{}", key, i + 2))
.collect();
let update_sql = format!(
"UPDATE {} SET {}, updated_at = CURRENT_TIMESTAMP WHERE id = ?1",
table,
set_clauses.join(", ")
);
let mut params: Vec<rusqlite::types::Value> = vec![rusqlite::types::Value::Text(id)];
for value in obj.values() {
params.push(match value {
Value::String(s) => rusqlite::types::Value::Text(s.clone()),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
rusqlite::types::Value::Integer(i)
} else if let Some(f) = n.as_f64() {
rusqlite::types::Value::Real(f)
} else {
rusqlite::types::Value::Null
}
},
Value::Bool(b) => rusqlite::types::Value::Integer(if *b { 1 } else { 0 }),
Value::Null => rusqlite::types::Value::Null,
_ => rusqlite::types::Value::Text(value.to_string()),
});
}
c.execute(&update_sql, params.as_slice())
.map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok::<(), HanzoDbError>(())
}).await
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))??;
Ok(())
}
async fn delete(&self, table: &str, id: &str) -> Result<()> {
let conn = self.connection.clone();
let table = table.to_string();
let id = id.to_string();
task::spawn_blocking(move || {
let c = conn.lock()
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?;
c.execute(
&format!("DELETE FROM {} WHERE id = ?1", table),
params![id]
).map_err(|e| HanzoDbError::QueryError(e.to_string()))?;
Ok::<(), HanzoDbError>(())
}).await
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))??;
Ok(())
}
async fn vector_search(
&self,
table: &str,
query_vector: &[f32],
limit: usize,
_filter: Option<&str>,
) -> Result<Vec<Value>> {
let table = table.to_string();
let query_vector = query_vector.to_vec();
let conn = self.connection.clone();
let path = self.path.clone();
let config = self.config.clone();
task::spawn_blocking(move || {
let backend = SqliteBackend {
connection: conn,
path,
config,
};
backend.vector_similarity_search(&table, &query_vector, limit)
}).await
.map_err(|e| HanzoDbError::ConnectionError(e.to_string()))?
}
fn backend_type(&self) -> DatabaseBackend {
DatabaseBackend::SQLite
}
fn is_embedded(&self) -> bool {
true // SQLite is embedded
}
fn is_lightweight(&self) -> bool {
true // SQLite is very lightweight
}
}
// Helper function for cosine similarity
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
if a.len() != b.len() {
return 0.0;
}
let dot_product: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm_a == 0.0 || norm_b == 0.0 {
return 0.0;
}
dot_product / (norm_a * norm_b)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_sqlite_operations() {
let config = HanzoDbConfig {
backend: DatabaseBackend::SQLite,
path: Some(PathBuf::from(":memory:")),
..Default::default()
};
let backend = SqliteBackend::new(config).unwrap();
// Create table
let schema = serde_json::json!({
"name": "string",
"age": "integer",
"active": "boolean"
});
backend.create_table("users", &schema).await.unwrap();
// Insert records
let records = vec![
serde_json::json!({"name": "Alice", "age": 30, "active": true}),
serde_json::json!({"name": "Bob", "age": 25, "active": false}),
];
backend.insert("users", &records).await.unwrap();
// Query records
let results = backend.query("SELECT * FROM users WHERE age > 20").await.unwrap();
assert_eq!(results.len(), 2);
}
#[test]
fn test_cosine_similarity() {
let a = vec![1.0, 2.0, 3.0];
let b = vec![1.0, 2.0, 3.0];
assert!((cosine_similarity(&a, &b) - 1.0).abs() < 0.001);
let c = vec![-1.0, -2.0, -3.0];
assert!((cosine_similarity(&a, &c) + 1.0).abs() < 0.001);
}
}
-384
View File
@@ -1,384 +0,0 @@
//! # Hanzo DB - Multi-Backend Database Abstraction
//!
//! Production-ready database abstraction for Hanzo Node, supporting:
//! - LanceDB for vector search and multimodal storage
//! - DuckDB for analytics and OLAP queries
//! - PostgreSQL for relational data
//! - Redis for caching
//! - SQLite for lightweight deployments
//!
//! Features:
//! - Unified interface across all backends
//! - Automatic backend selection based on workload
//! - Connection pooling and transaction support
//! - Migration between backends
use anyhow::{Context, Result};
use async_trait::async_trait;
use log::{debug, error, info, warn};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::RwLock;
pub mod models;
pub mod vector_search;
#[cfg(feature = "migration")]
pub mod migration;
// Backend modules
pub mod backends;
// Re-exports
pub use models::*;
pub use vector_search::*;
/// Database backend type
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DatabaseBackend {
/// LanceDB for vector operations and multimodal data
LanceDB,
/// DuckDB for analytics and OLAP
DuckDB,
/// PostgreSQL for relational data
PostgreSQL,
/// Redis for caching
Redis,
/// SQLite for lightweight deployments
SQLite,
}
impl DatabaseBackend {
/// Select optimal backend for workload type
pub fn for_workload(workload: WorkloadType) -> Self {
match workload {
WorkloadType::VectorSearch => Self::LanceDB,
WorkloadType::Analytics => Self::DuckDB,
WorkloadType::Transactional => Self::PostgreSQL,
WorkloadType::Cache => Self::Redis,
WorkloadType::Embedded => Self::SQLite,
}
}
}
/// Workload type for backend selection
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorkloadType {
/// Vector similarity search
VectorSearch,
/// Analytical queries (OLAP)
Analytics,
/// Transactional operations (OLTP)
Transactional,
/// High-speed caching
Cache,
/// Embedded/lightweight operations
Embedded,
}
/// Unified database configuration
#[derive(Debug, Clone)]
pub struct HanzoDbConfig {
/// Selected backend
pub backend: DatabaseBackend,
/// Database path (for file-based backends)
pub path: Option<PathBuf>,
/// Connection URL (for network backends)
pub url: Option<String>,
/// Connection pool size
pub pool_size: usize,
/// Enable write-ahead logging
pub enable_wal: bool,
/// Cache size in bytes
pub cache_size: Option<usize>,
/// Enable compression
pub enable_compression: bool,
}
impl Default for HanzoDbConfig {
fn default() -> Self {
Self {
backend: DatabaseBackend::LanceDB,
path: Some(PathBuf::from("./storage/hanzo-db")),
url: None,
pool_size: 16,
enable_wal: true,
cache_size: Some(64 * 1024 * 1024), // 64MB
enable_compression: true,
}
}
}
/// Unified database trait
#[async_trait]
pub trait HanzoDatabase: Send + Sync {
/// Initialize the database
async fn init(&self) -> Result<()>;
/// Create a table
async fn create_table(&self, name: &str, schema: TableSchema) -> Result<()>;
/// Insert data
async fn insert(&self, table: &str, data: &[Record]) -> Result<()>;
/// Query data
async fn query(&self, query: Query) -> Result<QueryResult>;
/// Vector search
async fn vector_search(&self, query: VectorQuery) -> Result<Vec<SearchResult>>;
/// Begin transaction
async fn begin_transaction(&self) -> Result<Transaction>;
/// Optimize database
async fn optimize(&self) -> Result<()>;
/// Get database statistics
async fn stats(&self) -> Result<DatabaseStats>;
}
/// Table schema definition
#[derive(Debug, Clone)]
pub struct TableSchema {
pub columns: Vec<Column>,
pub indexes: Vec<Index>,
pub constraints: Vec<Constraint>,
}
/// Column definition
#[derive(Debug, Clone)]
pub struct Column {
pub name: String,
pub data_type: DataType,
pub nullable: bool,
pub default: Option<Value>,
}
/// Data type enumeration
#[derive(Debug, Clone)]
pub enum DataType {
// Scalar types
Boolean,
Int32,
Int64,
Float32,
Float64,
String,
Binary,
Timestamp,
// Vector types
Vector(usize), // dimension
// Complex types
Json,
Array(Box<DataType>),
Struct(Vec<(String, DataType)>),
}
/// Index definition
#[derive(Debug, Clone)]
pub struct Index {
pub name: String,
pub columns: Vec<String>,
pub index_type: IndexType,
}
#[derive(Debug, Clone)]
pub enum IndexType {
BTree,
Hash,
IVF_PQ { nlist: usize, nprobe: usize },
HNSW { max_elements: usize, m: usize },
}
/// Database constraint
#[derive(Debug, Clone)]
pub enum Constraint {
PrimaryKey(Vec<String>),
ForeignKey { columns: Vec<String>, references: String },
Unique(Vec<String>),
Check(String),
}
/// Query structure
#[derive(Debug, Clone)]
pub struct Query {
pub table: String,
pub select: Vec<String>,
pub filter: Option<Filter>,
pub order_by: Vec<OrderBy>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
/// Filter expression
#[derive(Debug, Clone)]
pub enum Filter {
Eq(String, Value),
Ne(String, Value),
Gt(String, Value),
Gte(String, Value),
Lt(String, Value),
Lte(String, Value),
In(String, Vec<Value>),
Like(String, String),
And(Box<Filter>, Box<Filter>),
Or(Box<Filter>, Box<Filter>),
Not(Box<Filter>),
}
/// Order by clause
#[derive(Debug, Clone)]
pub struct OrderBy {
pub column: String,
pub ascending: bool,
}
/// Value type
#[derive(Debug, Clone)]
pub enum Value {
Null,
Bool(bool),
Int32(i32),
Int64(i64),
Float32(f32),
Float64(f64),
String(String),
Binary(Vec<u8>),
Timestamp(i64),
Vector(Vec<f32>),
Json(serde_json::Value),
}
/// Record type
#[derive(Debug, Clone)]
pub struct Record {
pub values: Vec<(String, Value)>,
}
/// Query result
#[derive(Debug)]
pub struct QueryResult {
pub columns: Vec<String>,
pub rows: Vec<Record>,
pub row_count: usize,
}
/// Vector query
#[derive(Debug, Clone)]
pub struct VectorQuery {
pub table: String,
pub vector: Vec<f32>,
pub k: usize,
pub filter: Option<Filter>,
pub metric: DistanceMetric,
}
#[derive(Debug, Clone, Copy)]
pub enum DistanceMetric {
L2,
Cosine,
InnerProduct,
}
/// Search result
#[derive(Debug, Clone)]
pub struct SearchResult {
pub record: Record,
pub score: f32,
}
/// Transaction handle
pub struct Transaction {
inner: Arc<RwLock<TransactionInner>>,
}
struct TransactionInner {
backend: DatabaseBackend,
// Backend-specific transaction handle
handle: Box<dyn std::any::Any + Send + Sync>,
}
impl Transaction {
/// Commit the transaction
pub async fn commit(self) -> Result<()> {
let inner = self.inner.write().await;
// Backend-specific commit logic
Ok(())
}
/// Rollback the transaction
pub async fn rollback(self) -> Result<()> {
let inner = self.inner.write().await;
// Backend-specific rollback logic
Ok(())
}
}
/// Database statistics
#[derive(Debug, Clone)]
pub struct DatabaseStats {
pub backend: DatabaseBackend,
pub table_count: usize,
pub total_rows: usize,
pub total_size_bytes: usize,
pub index_count: usize,
pub cache_hit_rate: f64,
}
/// Create a Hanzo database instance
pub async fn connect(config: HanzoDbConfig) -> Result<Arc<dyn HanzoDatabase>> {
match config.backend {
DatabaseBackend::LanceDB => {
let db = backends::lancedb::LanceDbBackend::new(config).await?;
Ok(Arc::new(db))
}
DatabaseBackend::DuckDB => {
let db = backends::duckdb::DuckDbBackend::new(config).await?;
Ok(Arc::new(db))
}
DatabaseBackend::PostgreSQL => {
let db = backends::postgres::PostgresBackend::new(config).await?;
Ok(Arc::new(db))
}
DatabaseBackend::Redis => {
let db = backends::redis::RedisBackend::new(config).await?;
Ok(Arc::new(db))
}
DatabaseBackend::SQLite => {
let db = backends::sqlite::SqliteBackend::new(config).await?;
Ok(Arc::new(db))
}
}
}
/// Backend implementations module
pub mod backends {
pub mod lancedb;
pub mod duckdb;
pub mod postgres;
pub mod redis;
pub mod sqlite;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_backend_selection() {
assert_eq!(
DatabaseBackend::for_workload(WorkloadType::VectorSearch),
DatabaseBackend::LanceDB
);
assert_eq!(
DatabaseBackend::for_workload(WorkloadType::Analytics),
DatabaseBackend::DuckDB
);
assert_eq!(
DatabaseBackend::for_workload(WorkloadType::Transactional),
DatabaseBackend::PostgreSQL
);
}
}
-461
View File
@@ -1,461 +0,0 @@
//! # SQLite to LanceDB Migration Module
//!
//! Provides tools to migrate existing SQLite databases to LanceDB.
use anyhow::{Context, Result};
use arrow_array::{
BooleanArray, Float32Array, Int64Array, RecordBatch, StringArray, TimestampMillisecondArray,
ArrayRef, FixedSizeListArray,
};
use arrow_schema::{DataType, Field, Schema as ArrowSchema};
use chrono::{DateTime, NaiveDateTime, Utc};
use lancedb::Connection;
use log::{debug, error, info, warn};
use rusqlite::{params, Connection as SqliteConnection, Row};
use serde_json;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use uuid::Uuid;
use crate::models::*;
use crate::LanceDb;
/// Migration configuration
#[derive(Debug, Clone)]
pub struct MigrationConfig {
/// Source SQLite database path
pub sqlite_path: String,
/// Target LanceDB instance
pub target_db: Arc<LanceDb>,
/// Batch size for migration
pub batch_size: usize,
/// Skip existing tables
pub skip_existing: bool,
/// Verify data after migration
pub verify: bool,
}
impl Default for MigrationConfig {
fn default() -> Self {
Self {
sqlite_path: "./storage/db.sqlite".to_string(),
target_db: Arc::new(LanceDb::new().expect("Failed to create LanceDB")),
batch_size: 1000,
skip_existing: false,
verify: true,
}
}
}
/// SQLite to LanceDB migrator
pub struct Migrator {
config: MigrationConfig,
sqlite_conn: SqliteConnection,
}
impl Migrator {
/// Create new migrator instance
pub fn new(config: MigrationConfig) -> Result<Self> {
let sqlite_conn = SqliteConnection::open(&config.sqlite_path)
.context("Failed to open SQLite database")?;
Ok(Self {
config,
sqlite_conn,
})
}
/// Run full migration
pub async fn migrate_all(&mut self) -> Result<MigrationStats> {
info!("Starting SQLite to LanceDB migration");
let mut stats = MigrationStats::default();
// Migrate users
match self.migrate_users().await {
Ok(count) => {
info!("Migrated {} users", count);
stats.users_migrated = count;
}
Err(e) => {
error!("Failed to migrate users: {}", e);
stats.errors.push(format!("Users: {}", e));
}
}
// Migrate tools
match self.migrate_tools().await {
Ok(count) => {
info!("Migrated {} tools", count);
stats.tools_migrated = count;
}
Err(e) => {
error!("Failed to migrate tools: {}", e);
stats.errors.push(format!("Tools: {}", e));
}
}
// Migrate agents
match self.migrate_agents().await {
Ok(count) => {
info!("Migrated {} agents", count);
stats.agents_migrated = count;
}
Err(e) => {
error!("Failed to migrate agents: {}", e);
stats.errors.push(format!("Agents: {}", e));
}
}
// Migrate embeddings
match self.migrate_embeddings().await {
Ok(count) => {
info!("Migrated {} embeddings", count);
stats.embeddings_migrated = count;
}
Err(e) => {
error!("Failed to migrate embeddings: {}", e);
stats.errors.push(format!("Embeddings: {}", e));
}
}
// Migrate jobs
match self.migrate_jobs().await {
Ok(count) => {
info!("Migrated {} jobs", count);
stats.jobs_migrated = count;
}
Err(e) => {
error!("Failed to migrate jobs: {}", e);
stats.errors.push(format!("Jobs: {}", e));
}
}
// Verify if requested
if self.config.verify {
self.verify_migration(&stats).await?;
}
info!("Migration completed: {:?}", stats);
Ok(stats)
}
/// Migrate users table
async fn migrate_users(&mut self) -> Result<usize> {
info!("Migrating users table");
let mut stmt = self.sqlite_conn.prepare(
"SELECT id, profile_name, identity_type, identity_public_key,
encryption_public_key, signature_public_key,
node_signature_public_key, node_encryption_public_key,
permission_type, wallet_id, metadata,
created_at, updated_at, is_active
FROM users"
)?;
let users = stmt.query_map([], |row| {
Ok(UserRow {
id: row.get(0)?,
profile_name: row.get(1)?,
identity_type: row.get(2)?,
identity_public_key: row.get(3)?,
encryption_public_key: row.get(4)?,
signature_public_key: row.get(5)?,
node_signature_public_key: row.get(6)?,
node_encryption_public_key: row.get(7)?,
permission_type: row.get(8)?,
wallet_id: row.get(9)?,
metadata: row.get(10)?,
created_at: row.get(11)?,
updated_at: row.get(12)?,
is_active: row.get(13)?,
})
})?;
let mut count = 0;
let mut batch = Vec::new();
for user in users {
let user = user?;
batch.push(user);
if batch.len() >= self.config.batch_size {
self.insert_users_batch(&batch).await?;
count += batch.len();
batch.clear();
}
}
// Insert remaining batch
if !batch.is_empty() {
self.insert_users_batch(&batch).await?;
count += batch.len();
}
Ok(count)
}
/// Insert batch of users into LanceDB
async fn insert_users_batch(&self, users: &[UserRow]) -> Result<()> {
let schema = user_schema();
// Build arrays for each column
let mut id_builder = StringArray::builder(users.len());
let mut profile_name_builder = StringArray::builder(users.len());
let mut identity_type_builder = StringArray::builder(users.len());
let mut identity_pk_builder = StringArray::builder(users.len());
let mut encryption_pk_builder = StringArray::builder(users.len());
let mut signature_pk_builder = StringArray::builder(users.len());
let mut node_sig_pk_builder = StringArray::builder(users.len());
let mut node_enc_pk_builder = StringArray::builder(users.len());
let mut permission_builder = StringArray::builder(users.len());
let mut wallet_builder = StringArray::builder(users.len());
let mut metadata_builder = StringArray::builder(users.len());
let mut created_at_builder = TimestampMillisecondArray::builder(users.len());
let mut updated_at_builder = TimestampMillisecondArray::builder(users.len());
let mut is_active_builder = BooleanArray::builder(users.len());
for user in users {
id_builder.append_value(&user.id);
profile_name_builder.append_value(&user.profile_name);
identity_type_builder.append_value(&user.identity_type);
if let Some(v) = &user.identity_public_key {
identity_pk_builder.append_value(v);
} else {
identity_pk_builder.append_null();
}
if let Some(v) = &user.encryption_public_key {
encryption_pk_builder.append_value(v);
} else {
encryption_pk_builder.append_null();
}
if let Some(v) = &user.signature_public_key {
signature_pk_builder.append_value(v);
} else {
signature_pk_builder.append_null();
}
if let Some(v) = &user.node_signature_public_key {
node_sig_pk_builder.append_value(v);
} else {
node_sig_pk_builder.append_null();
}
if let Some(v) = &user.node_encryption_public_key {
node_enc_pk_builder.append_value(v);
} else {
node_enc_pk_builder.append_null();
}
if let Some(v) = &user.permission_type {
permission_builder.append_value(v);
} else {
permission_builder.append_null();
}
if let Some(v) = &user.wallet_id {
wallet_builder.append_value(v);
} else {
wallet_builder.append_null();
}
if let Some(v) = &user.metadata {
metadata_builder.append_value(v);
} else {
metadata_builder.append_null();
}
created_at_builder.append_value(user.created_at);
updated_at_builder.append_value(user.updated_at);
is_active_builder.append_value(user.is_active);
}
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(id_builder.finish()) as ArrayRef,
Arc::new(profile_name_builder.finish()) as ArrayRef,
Arc::new(identity_type_builder.finish()) as ArrayRef,
Arc::new(identity_pk_builder.finish()) as ArrayRef,
Arc::new(encryption_pk_builder.finish()) as ArrayRef,
Arc::new(signature_pk_builder.finish()) as ArrayRef,
Arc::new(node_sig_pk_builder.finish()) as ArrayRef,
Arc::new(node_enc_pk_builder.finish()) as ArrayRef,
Arc::new(permission_builder.finish()) as ArrayRef,
Arc::new(wallet_builder.finish()) as ArrayRef,
Arc::new(metadata_builder.finish()) as ArrayRef,
Arc::new(created_at_builder.finish()) as ArrayRef,
Arc::new(updated_at_builder.finish()) as ArrayRef,
Arc::new(is_active_builder.finish()) as ArrayRef,
],
)?;
// Insert into LanceDB
let table = self.config.target_db.table("users").await?;
table.add(&[batch]).execute().await?;
Ok(())
}
/// Migrate tools table
async fn migrate_tools(&mut self) -> Result<usize> {
info!("Migrating tools table");
// Check if tools table exists
let table_exists: bool = self.sqlite_conn.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='tools'",
[],
|row| row.get(0),
)?;
if !table_exists {
warn!("Tools table does not exist in SQLite database");
return Ok(0);
}
// Similar implementation as migrate_users
// TODO: Complete implementation
Ok(0)
}
/// Migrate agents table
async fn migrate_agents(&mut self) -> Result<usize> {
info!("Migrating agents table");
// TODO: Implement agent migration
Ok(0)
}
/// Migrate embeddings table
async fn migrate_embeddings(&mut self) -> Result<usize> {
info!("Migrating embeddings table");
// TODO: Implement embeddings migration with vector data
Ok(0)
}
/// Migrate jobs table
async fn migrate_jobs(&mut self) -> Result<usize> {
info!("Migrating jobs table");
// TODO: Implement jobs migration
Ok(0)
}
/// Verify migration results
async fn verify_migration(&self, stats: &MigrationStats) -> Result<()> {
info!("Verifying migration results");
let db_stats = self.config.target_db.stats().await?;
for table_stat in &db_stats.table_stats {
match table_stat.name.as_str() {
"users" => {
if table_stat.row_count < stats.users_migrated {
warn!(
"User count mismatch: expected {}, found {}",
stats.users_migrated, table_stat.row_count
);
}
}
"tools" => {
if table_stat.row_count < stats.tools_migrated {
warn!(
"Tools count mismatch: expected {}, found {}",
stats.tools_migrated, table_stat.row_count
);
}
}
_ => {}
}
}
info!("Verification completed");
Ok(())
}
}
/// Migration statistics
#[derive(Debug, Default, Clone)]
pub struct MigrationStats {
pub users_migrated: usize,
pub tools_migrated: usize,
pub agents_migrated: usize,
pub embeddings_migrated: usize,
pub jobs_migrated: usize,
pub errors: Vec<String>,
}
/// Temporary struct for SQLite user row
struct UserRow {
id: String,
profile_name: String,
identity_type: String,
identity_public_key: Option<String>,
encryption_public_key: Option<String>,
signature_public_key: Option<String>,
node_signature_public_key: Option<String>,
node_encryption_public_key: Option<String>,
permission_type: Option<String>,
wallet_id: Option<String>,
metadata: Option<String>,
created_at: i64,
updated_at: i64,
is_active: bool,
}
/// Migration CLI tool
pub async fn run_migration_cli(sqlite_path: &str, lance_path: &str) -> Result<()> {
println!("SQLite to LanceDB Migration Tool");
println!("=================================\n");
// Create LanceDB instance
let lance_config = crate::LanceDbConfig {
path: lance_path.into(),
..Default::default()
};
let lance_db = Arc::new(LanceDb::with_config(lance_config).await?);
// Create migration config
let config = MigrationConfig {
sqlite_path: sqlite_path.to_string(),
target_db: lance_db,
batch_size: 1000,
skip_existing: false,
verify: true,
};
// Run migration
let mut migrator = Migrator::new(config)?;
let stats = migrator.migrate_all().await?;
// Print results
println!("\nMigration Results:");
println!(" Users migrated: {}", stats.users_migrated);
println!(" Tools migrated: {}", stats.tools_migrated);
println!(" Agents migrated: {}", stats.agents_migrated);
println!(" Embeddings migrated: {}", stats.embeddings_migrated);
println!(" Jobs migrated: {}", stats.jobs_migrated);
if !stats.errors.is_empty() {
println!("\nErrors encountered:");
for error in &stats.errors {
println!(" - {}", error);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn test_migration_config() {
let config = MigrationConfig::default();
assert_eq!(config.batch_size, 1000);
assert!(config.verify);
}
}
-384
View File
@@ -1,384 +0,0 @@
//! # Arrow Schema Models for LanceDB Tables
//!
//! Defines the data models and Arrow schemas for all Hanzo Node tables.
use anyhow::Result;
use arrow_array::{
Array, BinaryArray, Float32Array, Int32Array, Int64Array, RecordBatch, StringArray,
TimestampMillisecondArray, BooleanArray, FixedSizeListArray,
};
use arrow_schema::{DataType, Field, Schema as ArrowSchema, TimeUnit};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use uuid::Uuid;
/// Vector dimension for embeddings
const EMBEDDING_DIM: i32 = 1536; // OpenAI ada-002 dimension
/// Create Arrow schema for users table
pub fn user_schema() -> Arc<ArrowSchema> {
Arc::new(ArrowSchema::new(vec![
Field::new("id", DataType::Utf8, false),
Field::new("profile_name", DataType::Utf8, false),
Field::new("identity_type", DataType::Utf8, false),
Field::new("identity_public_key", DataType::Utf8, true),
Field::new("encryption_public_key", DataType::Utf8, true),
Field::new("signature_public_key", DataType::Utf8, true),
Field::new("node_signature_public_key", DataType::Utf8, true),
Field::new("node_encryption_public_key", DataType::Utf8, true),
Field::new("permission_type", DataType::Utf8, true),
Field::new("wallet_id", DataType::Utf8, true),
Field::new("metadata", DataType::Utf8, true), // JSON string
Field::new("created_at", DataType::Timestamp(TimeUnit::Millisecond, None), false),
Field::new("updated_at", DataType::Timestamp(TimeUnit::Millisecond, None), false),
Field::new("is_active", DataType::Boolean, false),
]))
}
/// Create Arrow schema for tools table
pub fn tool_schema() -> Arc<ArrowSchema> {
Arc::new(ArrowSchema::new(vec![
Field::new("id", DataType::Utf8, false),
Field::new("name", DataType::Utf8, false),
Field::new("description", DataType::Utf8, true),
Field::new("version", DataType::Utf8, false),
Field::new("tool_type", DataType::Utf8, false), // rust, js, python, mcp
Field::new("parameters_schema", DataType::Utf8, true), // JSON schema
Field::new("enabled", DataType::Boolean, false),
Field::new("config", DataType::Utf8, true), // JSON config
Field::new("usage_count", DataType::Int64, false),
Field::new("avg_execution_time_ms", DataType::Float32, true),
Field::new("last_used_at", DataType::Timestamp(TimeUnit::Millisecond, None), true),
Field::new("created_at", DataType::Timestamp(TimeUnit::Millisecond, None), false),
Field::new("updated_at", DataType::Timestamp(TimeUnit::Millisecond, None), false),
]))
}
/// Create Arrow schema for agents table
pub fn agent_schema() -> Arc<ArrowSchema> {
Arc::new(ArrowSchema::new(vec![
Field::new("id", DataType::Utf8, false),
Field::new("name", DataType::Utf8, false),
Field::new("description", DataType::Utf8, true),
Field::new("model", DataType::Utf8, false),
Field::new("system_prompt", DataType::Utf8, true),
Field::new("temperature", DataType::Float32, true),
Field::new("max_tokens", DataType::Int32, true),
Field::new("tools", DataType::Utf8, true), // JSON array of tool IDs
Field::new("created_by", DataType::Utf8, false),
Field::new("is_public", DataType::Boolean, false),
Field::new("usage_count", DataType::Int64, false),
Field::new("created_at", DataType::Timestamp(TimeUnit::Millisecond, None), false),
Field::new("updated_at", DataType::Timestamp(TimeUnit::Millisecond, None), false),
]))
}
/// Create Arrow schema for embeddings table
pub fn embedding_schema() -> Arc<ArrowSchema> {
Arc::new(ArrowSchema::new(vec![
Field::new("id", DataType::Utf8, false),
Field::new("content", DataType::Utf8, false),
Field::new("content_hash", DataType::Utf8, false),
Field::new(
"vector",
DataType::FixedSizeList(
Arc::new(Field::new("item", DataType::Float32, false)),
EMBEDDING_DIM,
),
false,
),
Field::new("model", DataType::Utf8, false),
Field::new("source_type", DataType::Utf8, false), // document, chat, tool_output
Field::new("source_id", DataType::Utf8, true),
Field::new("metadata", DataType::Utf8, true), // JSON metadata
Field::new("created_at", DataType::Timestamp(TimeUnit::Millisecond, None), false),
]))
}
/// Create Arrow schema for jobs table
pub fn job_schema() -> Arc<ArrowSchema> {
Arc::new(ArrowSchema::new(vec![
Field::new("id", DataType::Utf8, false),
Field::new("job_type", DataType::Utf8, false),
Field::new("status", DataType::Utf8, false), // pending, running, completed, failed
Field::new("priority", DataType::Int32, false),
Field::new("payload", DataType::Utf8, false), // JSON payload
Field::new("result", DataType::Utf8, true), // JSON result
Field::new("error", DataType::Utf8, true),
Field::new("created_by", DataType::Utf8, false),
Field::new("agent_id", DataType::Utf8, true),
Field::new("started_at", DataType::Timestamp(TimeUnit::Millisecond, None), true),
Field::new("completed_at", DataType::Timestamp(TimeUnit::Millisecond, None), true),
Field::new("created_at", DataType::Timestamp(TimeUnit::Millisecond, None), false),
Field::new("updated_at", DataType::Timestamp(TimeUnit::Millisecond, None), false),
]))
}
/// Create Arrow schema for sessions table
pub fn session_schema() -> Arc<ArrowSchema> {
Arc::new(ArrowSchema::new(vec![
Field::new("id", DataType::Utf8, false),
Field::new("user_id", DataType::Utf8, false),
Field::new("agent_id", DataType::Utf8, true),
Field::new("messages", DataType::Utf8, false), // JSON array of messages
Field::new("context", DataType::Utf8, true), // JSON context
Field::new("token_count", DataType::Int64, false),
Field::new("cost", DataType::Float32, true),
Field::new("created_at", DataType::Timestamp(TimeUnit::Millisecond, None), false),
Field::new("updated_at", DataType::Timestamp(TimeUnit::Millisecond, None), false),
]))
}
/// Create Arrow schema for multimodal table
#[cfg(feature = "multimodal")]
pub fn multimodal_schema() -> Arc<ArrowSchema> {
Arc::new(ArrowSchema::new(vec![
Field::new("id", DataType::Utf8, false),
Field::new("media_type", DataType::Utf8, false), // image, audio, video
Field::new("mime_type", DataType::Utf8, false),
Field::new("data", DataType::Binary, false), // Raw binary data
Field::new("thumbnail", DataType::Binary, true), // Optional thumbnail
Field::new("width", DataType::Int32, true),
Field::new("height", DataType::Int32, true),
Field::new("duration_ms", DataType::Int64, true), // For audio/video
Field::new(
"embedding",
DataType::FixedSizeList(
Arc::new(Field::new("item", DataType::Float32, false)),
EMBEDDING_DIM,
),
true,
),
Field::new("text_content", DataType::Utf8, true), // OCR or transcription
Field::new("metadata", DataType::Utf8, true), // JSON metadata
Field::new("created_by", DataType::Utf8, false),
Field::new("created_at", DataType::Timestamp(TimeUnit::Millisecond, None), false),
]))
}
/// User model
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: Uuid,
pub profile_name: String,
pub identity_type: String,
pub identity_public_key: Option<String>,
pub encryption_public_key: Option<String>,
pub signature_public_key: Option<String>,
pub node_signature_public_key: Option<String>,
pub node_encryption_public_key: Option<String>,
pub permission_type: Option<String>,
pub wallet_id: Option<String>,
pub metadata: Option<serde_json::Value>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub is_active: bool,
}
/// Tool model
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tool {
pub id: Uuid,
pub name: String,
pub description: Option<String>,
pub version: String,
pub tool_type: ToolType,
pub parameters_schema: Option<serde_json::Value>,
pub enabled: bool,
pub config: Option<serde_json::Value>,
pub usage_count: i64,
pub avg_execution_time_ms: Option<f32>,
pub last_used_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
/// Tool type enum
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ToolType {
Rust,
JavaScript,
Python,
MCP,
}
/// Agent model
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Agent {
pub id: Uuid,
pub name: String,
pub description: Option<String>,
pub model: String,
pub system_prompt: Option<String>,
pub temperature: Option<f32>,
pub max_tokens: Option<i32>,
pub tools: Vec<String>,
pub created_by: String,
pub is_public: bool,
pub usage_count: i64,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
/// Embedding model
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Embedding {
pub id: Uuid,
pub content: String,
pub content_hash: String,
pub vector: Vec<f32>,
pub model: String,
pub source_type: String,
pub source_id: Option<String>,
pub metadata: Option<serde_json::Value>,
pub created_at: DateTime<Utc>,
}
/// Job model
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Job {
pub id: Uuid,
pub job_type: String,
pub status: JobStatus,
pub priority: i32,
pub payload: serde_json::Value,
pub result: Option<serde_json::Value>,
pub error: Option<String>,
pub created_by: String,
pub agent_id: Option<String>,
pub started_at: Option<DateTime<Utc>>,
pub completed_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
/// Job status enum
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum JobStatus {
Pending,
Running,
Completed,
Failed,
Cancelled,
}
/// Session model
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
pub id: Uuid,
pub user_id: String,
pub agent_id: Option<String>,
pub messages: Vec<Message>,
pub context: Option<serde_json::Value>,
pub token_count: i64,
pub cost: Option<f32>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
/// Message model for sessions
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub role: String,
pub content: String,
pub timestamp: DateTime<Utc>,
pub tool_calls: Option<Vec<ToolCall>>,
}
/// Tool call model
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: serde_json::Value,
pub result: Option<serde_json::Value>,
}
/// Multimodal content model
#[cfg(feature = "multimodal")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MultimodalContent {
pub id: Uuid,
pub media_type: MediaType,
pub mime_type: String,
pub data: Vec<u8>,
pub thumbnail: Option<Vec<u8>>,
pub width: Option<i32>,
pub height: Option<i32>,
pub duration_ms: Option<i64>,
pub embedding: Option<Vec<f32>>,
pub text_content: Option<String>,
pub metadata: Option<serde_json::Value>,
pub created_by: String,
pub created_at: DateTime<Utc>,
}
/// Media type enum
#[cfg(feature = "multimodal")]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MediaType {
Image,
Audio,
Video,
Document,
}
/// Helper to convert models to RecordBatch
pub trait ToRecordBatch {
fn to_record_batch(&self) -> Result<RecordBatch>;
}
/// Helper to convert from RecordBatch to models
pub trait FromRecordBatch: Sized {
fn from_record_batch(batch: &RecordBatch, row: usize) -> Result<Self>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_user_schema() {
let schema = user_schema();
assert_eq!(schema.fields().len(), 14);
assert_eq!(schema.field(0).name(), "id");
assert_eq!(schema.field(0).data_type(), &DataType::Utf8);
}
#[test]
fn test_embedding_schema() {
let schema = embedding_schema();
let vector_field = schema.field(3);
assert_eq!(vector_field.name(), "vector");
if let DataType::FixedSizeList(_, size) = vector_field.data_type() {
assert_eq!(*size, EMBEDDING_DIM);
} else {
panic!("Expected FixedSizeList for vector field");
}
}
#[test]
fn test_tool_type_serialization() {
let tool_type = ToolType::JavaScript;
let json = serde_json::to_string(&tool_type).unwrap();
assert_eq!(json, r#""javascript""#);
let deserialized: ToolType = serde_json::from_str(&json).unwrap();
assert!(matches!(deserialized, ToolType::JavaScript));
}
#[test]
fn test_job_status_serialization() {
let status = JobStatus::Running;
let json = serde_json::to_string(&status).unwrap();
assert_eq!(json, r#""running""#);
let deserialized: JobStatus = serde_json::from_str(&json).unwrap();
assert!(matches!(deserialized, JobStatus::Running));
}
}
-439
View File
@@ -1,439 +0,0 @@
//! # Vector Search Implementation for LanceDB
//!
//! High-performance semantic search, KNN search, and hybrid search capabilities.
use anyhow::{Context, Result};
use arrow_array::{Float32Array, RecordBatch, StringArray};
use lancedb::{query::Query, Table};
use log::{debug, info, warn};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// Search configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchConfig {
/// Maximum number of results
pub limit: usize,
/// Similarity threshold (0.0 to 1.0)
pub threshold: Option<f32>,
/// Enable re-ranking
pub rerank: bool,
/// Metadata filters
pub filters: Option<HashMap<String, serde_json::Value>>,
/// Search mode
pub mode: SearchMode,
}
impl Default for SearchConfig {
fn default() -> Self {
Self {
limit: 10,
threshold: Some(0.7),
rerank: false,
filters: None,
mode: SearchMode::Vector,
}
}
}
/// Search mode enum
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SearchMode {
/// Pure vector similarity search
Vector,
/// Full-text keyword search
Keyword,
/// Hybrid search combining vector and keyword
Hybrid,
/// Multi-modal search
Multimodal,
}
/// Search result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResult {
/// Document ID
pub id: String,
/// Original content
pub content: String,
/// Similarity score (0.0 to 1.0)
pub score: f32,
/// Distance metric value
pub distance: f32,
/// Additional metadata
pub metadata: Option<serde_json::Value>,
/// Source information
pub source: Option<String>,
}
/// Vector search engine
pub struct VectorSearch {
table: Table,
}
impl VectorSearch {
/// Create new vector search instance
pub fn new(table: Table) -> Self {
Self { table }
}
/// Perform semantic search using vector similarity
pub async fn search(
&self,
query_vector: Vec<f32>,
config: SearchConfig,
) -> Result<Vec<SearchResult>> {
info!("Performing vector search with limit: {}", config.limit);
let mut query = self.table
.vector_search(&query_vector)
.limit(config.limit);
// Apply metadata filters if provided
if let Some(filters) = &config.filters {
for (key, value) in filters {
query = apply_filter(query, key, value)?;
}
}
let results = query
.execute()
.await
.context("Failed to execute vector search")?;
// Convert to search results
let mut search_results = Vec::new();
for batch in results.try_collect::<Vec<RecordBatch>>().await? {
search_results.extend(batch_to_results(&batch, &config)?);
}
// Apply threshold filtering
if let Some(threshold) = config.threshold {
search_results.retain(|r| r.score >= threshold);
}
// Re-rank if enabled
if config.rerank {
search_results = self.rerank_results(search_results).await?;
}
Ok(search_results)
}
/// Perform keyword-based search
pub async fn keyword_search(
&self,
query: &str,
config: SearchConfig,
) -> Result<Vec<SearchResult>> {
info!("Performing keyword search for: {}", query);
// Full-text search
let results = self.table
.query()
.full_text_search("content", query)
.limit(config.limit)
.execute()
.await
.context("Failed to execute keyword search")?;
let mut search_results = Vec::new();
for batch in results.try_collect::<Vec<RecordBatch>>().await? {
search_results.extend(batch_to_results(&batch, &config)?);
}
Ok(search_results)
}
/// Perform hybrid search combining vector and keyword
pub async fn hybrid_search(
&self,
query: &str,
query_vector: Vec<f32>,
config: SearchConfig,
) -> Result<Vec<SearchResult>> {
info!("Performing hybrid search");
// Get vector search results
let mut vector_config = config.clone();
vector_config.limit = config.limit * 2; // Get more candidates
let vector_results = self.search(query_vector, vector_config).await?;
// Get keyword search results
let mut keyword_config = config.clone();
keyword_config.limit = config.limit * 2;
let keyword_results = self.keyword_search(query, keyword_config).await?;
// Combine and deduplicate results
let combined = self.combine_results(vector_results, keyword_results, &config)?;
Ok(combined)
}
/// Find K nearest neighbors
pub async fn knn_search(
&self,
query_vector: Vec<f32>,
k: usize,
) -> Result<Vec<SearchResult>> {
debug!("Performing KNN search with k={}", k);
let results = self.table
.vector_search(&query_vector)
.limit(k)
.execute()
.await
.context("Failed to execute KNN search")?;
let mut search_results = Vec::new();
let config = SearchConfig::default();
for batch in results.try_collect::<Vec<RecordBatch>>().await? {
search_results.extend(batch_to_results(&batch, &config)?);
}
Ok(search_results)
}
/// Search with multiple query vectors (multi-query)
pub async fn multi_query_search(
&self,
query_vectors: Vec<Vec<f32>>,
config: SearchConfig,
) -> Result<Vec<SearchResult>> {
info!("Performing multi-query search with {} queries", query_vectors.len());
let mut all_results = Vec::new();
let mut result_map: HashMap<String, (SearchResult, f32)> = HashMap::new();
// Search with each query vector
for (i, vector) in query_vectors.iter().enumerate() {
debug!("Processing query vector {}/{}", i + 1, query_vectors.len());
let results = self.search(vector.clone(), config.clone()).await?;
// Aggregate scores
for result in results {
let entry = result_map
.entry(result.id.clone())
.or_insert((result.clone(), 0.0));
entry.1 += result.score;
}
}
// Average scores and convert to results
let num_queries = query_vectors.len() as f32;
for (mut result, total_score) in result_map.into_values() {
result.score = total_score / num_queries;
all_results.push(result);
}
// Sort by score
all_results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
all_results.truncate(config.limit);
Ok(all_results)
}
/// Diversity-aware search to reduce redundancy
pub async fn diverse_search(
&self,
query_vector: Vec<f32>,
config: SearchConfig,
diversity_threshold: f32,
) -> Result<Vec<SearchResult>> {
info!("Performing diverse search with threshold: {}", diversity_threshold);
// Get more candidates than needed
let mut search_config = config.clone();
search_config.limit = config.limit * 3;
let candidates = self.search(query_vector, search_config).await?;
// Select diverse results
let mut selected = Vec::new();
let mut selected_vectors: Vec<Vec<f32>> = Vec::new();
for candidate in candidates {
if selected.len() >= config.limit {
break;
}
// Check similarity with already selected items
let mut is_diverse = true;
if !selected_vectors.is_empty() {
// This is simplified - in production you'd fetch actual vectors
// and compute cosine similarity
for existing in &selected {
if candidate.content.chars().take(50).collect::<String>()
== existing.content.chars().take(50).collect::<String>() {
is_diverse = false;
break;
}
}
}
if is_diverse {
selected.push(candidate);
}
}
Ok(selected)
}
/// Re-rank results using a more sophisticated model
async fn rerank_results(
&self,
mut results: Vec<SearchResult>,
) -> Result<Vec<SearchResult>> {
// In production, this would use a cross-encoder or other reranking model
// For now, we'll just apply a simple boost based on metadata
for result in &mut results {
if let Some(metadata) = &result.metadata {
// Boost based on recency, quality score, etc.
if let Some(quality) = metadata.get("quality_score") {
if let Some(q) = quality.as_f64() {
result.score *= (1.0 + (q as f32 * 0.1));
}
}
}
}
results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
Ok(results)
}
/// Combine vector and keyword results with score fusion
fn combine_results(
&self,
vector_results: Vec<SearchResult>,
keyword_results: Vec<SearchResult>,
config: &SearchConfig,
) -> Result<Vec<SearchResult>> {
let mut combined_map: HashMap<String, SearchResult> = HashMap::new();
// Add vector results with weight
let vector_weight = 0.7;
for mut result in vector_results {
result.score *= vector_weight;
combined_map.insert(result.id.clone(), result);
}
// Add or update with keyword results
let keyword_weight = 0.3;
for mut result in keyword_results {
result.score *= keyword_weight;
combined_map
.entry(result.id.clone())
.and_modify(|r| r.score += result.score)
.or_insert(result);
}
// Convert to vector and sort
let mut combined: Vec<SearchResult> = combined_map.into_values().collect();
combined.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
combined.truncate(config.limit);
Ok(combined)
}
}
/// Apply filter to query
fn apply_filter(
mut query: Query,
key: &str,
value: &serde_json::Value,
) -> Result<Query> {
match value {
serde_json::Value::String(s) => {
query = query.filter(format!("{} = '{}'", key, s));
}
serde_json::Value::Number(n) => {
query = query.filter(format!("{} = {}", key, n));
}
serde_json::Value::Bool(b) => {
query = query.filter(format!("{} = {}", key, b));
}
_ => {
return Err(anyhow::anyhow!("Unsupported filter value type"));
}
}
Ok(query)
}
/// Convert RecordBatch to SearchResults
fn batch_to_results(batch: &RecordBatch, config: &SearchConfig) -> Result<Vec<SearchResult>> {
let mut results = Vec::new();
let id_array = batch
.column_by_name("id")
.context("Missing id column")?
.as_any()
.downcast_ref::<StringArray>()
.context("Invalid id column type")?;
let content_array = batch
.column_by_name("content")
.context("Missing content column")?
.as_any()
.downcast_ref::<StringArray>()
.context("Invalid content column type")?;
let distance_array = batch
.column_by_name("_distance")
.map(|col| {
col.as_any()
.downcast_ref::<Float32Array>()
})
.unwrap_or(None);
for i in 0..batch.num_rows() {
let id = id_array.value(i).to_string();
let content = content_array.value(i).to_string();
// Convert distance to similarity score (1 - normalized_distance)
let distance = distance_array
.and_then(|arr| Some(arr.value(i)))
.unwrap_or(0.0);
let score = 1.0 / (1.0 + distance); // Convert distance to similarity
results.push(SearchResult {
id,
content,
score,
distance,
metadata: None,
source: None,
});
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_search_config_default() {
let config = SearchConfig::default();
assert_eq!(config.limit, 10);
assert_eq!(config.threshold, Some(0.7));
assert!(!config.rerank);
assert!(matches!(config.mode, SearchMode::Vector));
}
#[test]
fn test_search_mode_serialization() {
let mode = SearchMode::Hybrid;
let json = serde_json::to_string(&mode).unwrap();
assert_eq!(json, r#""hybrid""#);
let deserialized: SearchMode = serde_json::from_str(&json).unwrap();
assert!(matches!(deserialized, SearchMode::Hybrid));
}
}
-47
View File
@@ -1,47 +0,0 @@
[package]
name = "hanzo-kbs"
description = "Hanzo AI - Kbs Library"
license = "MIT"
version = "1.1.11"
edition.workspace = true
authors.workspace = true
[features]
default = ["pqc"]
pqc = ["hanzo-pqc"]
[dependencies]
# Core dependencies
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
async-trait = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
chrono = { workspace = true }
tokio = { workspace = true }
log = { workspace = true }
dashmap = { workspace = true }
rand = { workspace = true }
# Cryptography
hpke = "0.12"
chacha20poly1305 = "0.10"
x25519-dalek = { workspace = true }
ed25519-dalek = { workspace = true }
blake3 = { workspace = true }
hex = { workspace = true }
# For attestation
base64 = { workspace = true }
bincode = { workspace = true }
uuid = { workspace = true, features = ["v4", "serde"] }
# Hanzo dependencies
hanzo-messages = { version = "1.1.12", path = "../hanzo-messages" }
hanzo-pqc = { workspace = true, optional = true }
# API dependencies
warp = { workspace = true }
[dev-dependencies]
tokio-test = "0.4"
-307
View File
@@ -1,307 +0,0 @@
# Hanzo KBS (Key Broker Service)
A comprehensive Key Broker Service for Hanzo Node that manages cryptographic key lifecycle with TEE (Trusted Execution Environment) attestation and privacy tiers.
## Architecture Overview
The Hanzo auth stack consists of three integrated components:
### 1. **KMS (Key Management System)** - Infisical
- External service for centralized key management
- Provides enterprise-grade secret storage
- Handles encryption, rotation, and access control
- Located at: `/Users/z/work/hanzo/kms` (Infisical deployment)
### 2. **KBS (Key Broker Service)** - This Module
- Attestation-based key release
- Privacy tier enforcement (0-4)
- TEE integration (SEV-SNP, TDX, NVIDIA H100/Blackwell)
- Web2/Web3 authentication support
### 3. **IAM (Identity & Access Management)**
- Integrated with both Web2 (OAuth, SAML) and Web3 (DIDs, wallets)
- Role-based access control (RBAC)
- Capability-based security tokens
- Cross-chain identity verification
## Privacy Tiers
The KBS implements a 5-tier privacy model with graceful degradation:
| Tier | Name | Description | TEE Support | Use Cases |
|------|------|-------------|-------------|-----------|
| **0** | Open | No encryption, public data | None | Public APIs, open datasets |
| **1** | Encrypted | Basic encryption at rest/transit | None | Standard web services |
| **2** | Secure | Hardware security module support | HSM/TPM | Enterprise applications |
| **3** | GPU CC | GPU Confidential Computing | NVIDIA H100 | AI model training/inference |
| **4** | GPU TEE-I/O | Full I/O isolation | NVIDIA Blackwell | Maximum security workloads |
### Graceful Degradation
The system automatically degrades to lower tiers when higher tier hardware is unavailable:
```rust
// Tier 4 (Blackwell) → Tier 3 (H100) → Tier 2 (HSM) → Tier 1 (Software)
let vault = VaultFactory::create_with_fallback(
PrivacyTier::Tier4GpuTeeIo,
&kbs,
)?;
```
## Web2/Web3 Compatibility
### Web2 Authentication
- OAuth 2.0 / OIDC providers
- SAML integration
- API keys with HMAC
- Session-based auth
### Web3 Authentication
- Ethereum wallet signatures
- DIDs (Decentralized Identifiers)
- Chain-based attestation
- Zero-knowledge proofs
## Integration with Infisical KMS
The KBS integrates with Infisical for enterprise key management:
```rust
// Configure Infisical connection
let kms_config = KmsConfig {
endpoint: "https://kms.hanzo.ai",
api_key: std::env::var("INFISICAL_API_KEY")?,
project_id: std::env::var("INFISICAL_PROJECT_ID")?,
};
// Create KBS with Infisical backend
let kbs = KeyBrokerService::new(
kms_config,
attestation_config,
)?;
```
## TEE Support
### NVIDIA GPU TEE Integration
#### H100 Confidential Computing (Tier 3)
```rust
// Automatic detection of H100 CC mode
let vault = GpuCcVault::new(kbs, device_id);
if vault.is_cc_enabled()? {
// Use hardware-protected keys
vault.use_key(key_id, |key| {
// Key operations in GPU enclave
})?;
}
```
#### Blackwell TEE-I/O (Tier 4)
```rust
// Maximum security with I/O isolation
let vault = GpuTeeIoVault::new(kbs, device_id, mig_instance);
// All I/O is encrypted and authenticated
vault.use_key_with_io_protection(key_id, |key| {
// Operations with full isolation
})?;
```
### CPU TEE Support
- **AMD SEV-SNP**: Secure Encrypted Virtualization
- **Intel TDX**: Trust Domain Extensions
- **ARM CCA**: Confidential Compute Architecture
## API Usage
### Basic Key Operations
```rust
use hanzo_kbs::{KeyBrokerService, PrivacyTier, KeyId};
// Initialize KBS
let kbs = KeyBrokerService::new(config).await?;
// Request key with attestation
let attestation = generate_attestation(TeeType::SevSnp).await?;
let key = kbs.request_key(
&KeyId::new("api-key"),
attestation,
PrivacyTier::Tier2Secure,
).await?;
// Use key with automatic cleanup
kbs.use_key(&key_id, |key_material| {
// Perform cryptographic operations
encrypt_data(key_material, data)
})?;
```
### Capability Tokens
```rust
// Create capability token for agent
let token = kbs.create_capability_token(
agent_id,
vec!["gpu_compute", "tee_access"],
Duration::hours(1),
)?;
// Verify and use token
let authorized = kbs.verify_capability(token)?;
```
## Deployment
### Docker Compose
```yaml
version: '3.8'
services:
infisical-kms:
image: infisical/infisical:latest
environment:
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
- AUTH_SECRET=${AUTH_SECRET}
ports:
- "8080:8080"
hanzo-node:
image: hanzo/node:latest
environment:
- KMS_ENDPOINT=http://infisical-kms:8080
- ENABLE_TEE=true
- PRIVACY_TIER=3
depends_on:
- infisical-kms
```
### Kubernetes
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: hanzo-kbs-config
data:
kms_endpoint: "https://kms.hanzo.ai"
default_privacy_tier: "2"
enable_tee_attestation: "true"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: hanzo-node
spec:
template:
spec:
containers:
- name: hanzo-node
image: hanzo/node:latest
envFrom:
- configMapRef:
name: hanzo-kbs-config
```
## Security Considerations
### Key Protection Hierarchy
1. **Master Keys**: Never leave TEE, hardware-bound
2. **KEKs (Key Encryption Keys)**: Wrapped by master keys
3. **DEKs (Data Encryption Keys)**: Wrapped by KEKs
4. **Session Keys**: Ephemeral, time-bound
### Attestation Chain
```
Hardware Root of Trust
Platform Firmware (PSP/ME)
TEE Attestation (SEV/TDX/GPU)
KBS Verification
Key Release
```
### Zero Trust Architecture
- No implicit trust between components
- Every request requires attestation
- Continuous verification
- Least privilege access
## Performance Optimization
### Caching Strategy
- Session key caching (5 min default)
- Attestation result caching (1 min)
- Capability token caching
### Connection Pooling
- KMS connection pool (10 connections)
- Database connection pool (R2D2)
- HTTP/2 multiplexing for API calls
## Monitoring & Observability
### Metrics (Prometheus)
- Key request latency
- Attestation success rate
- TEE availability
- Vault operations/sec
### Logging
- Audit logs for all key operations
- Attestation verification logs
- Security events
- Performance metrics
## Testing
```bash
# Run unit tests
cargo test --lib
# Run integration tests (requires TEE simulator)
cargo test --test integration
# Run with TEE hardware
TEE_MODE=hardware cargo test
# Benchmark performance
cargo bench
```
## Migration from Other Systems
### From HashiCorp Vault
```rust
// Compatible API for easy migration
let vault_compat = VaultCompatLayer::new(kbs);
vault_compat.read("secret/data/api-key")?;
```
### From AWS KMS
```rust
// AWS KMS compatibility layer
let aws_compat = AwsKmsCompat::new(kbs);
aws_compat.decrypt(ciphertext_blob)?;
```
## Contributing
See [CONTRIBUTING.md](../../CONTRIBUTING.md) for development guidelines.
## License
Apache 2.0 - See [LICENSE](../../LICENSE) for details.
## Support
- Documentation: https://docs.hanzo.ai/kbs
- Issues: https://github.com/hanzoai/hanzo-node/issues
- Discord: https://discord.gg/hanzo
-223
View File
@@ -1,223 +0,0 @@
//! Attestation verification module
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use crate::error::{Result, SecurityError};
use crate::types::{AttestationType, PrivacyTier};
/// Result of attestation verification
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttestationResult {
pub verified: bool,
pub max_tier: PrivacyTier,
pub measurements: Vec<Measurement>,
pub platform_info: PlatformInfo,
pub expires_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Measurement {
pub name: String,
pub value: Vec<u8>,
pub pcr_index: Option<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlatformInfo {
pub platform_type: String,
pub tcb_version: String,
pub security_features: Vec<String>,
pub vendor_info: serde_json::Value,
}
impl AttestationResult {
pub fn supports_tier(&self, tier: PrivacyTier) -> bool {
self.verified && self.max_tier >= tier
}
}
/// Trait for verifying different attestation types
#[async_trait]
pub trait AttestationVerifier: Send + Sync {
/// Verify an attestation and return the result
async fn verify_attestation(&self, attestation: &AttestationType) -> Result<AttestationResult>;
/// Check if a measurement is in the allowed list
async fn is_measurement_allowed(&self, measurement: &[u8]) -> bool;
}
/// Mock attestation verifier for development
pub struct MockAttestationVerifier;
#[async_trait]
impl AttestationVerifier for MockAttestationVerifier {
async fn verify_attestation(&self, attestation: &AttestationType) -> Result<AttestationResult> {
match attestation {
AttestationType::SevSnp { report, .. } => {
// Mock SEV-SNP verification
Ok(AttestationResult {
verified: !report.is_empty(),
max_tier: PrivacyTier::CpuTee,
measurements: vec![
Measurement {
name: "kernel".to_string(),
value: vec![0xAA; 32],
pcr_index: Some(0),
},
],
platform_info: PlatformInfo {
platform_type: "AMD SEV-SNP".to_string(),
tcb_version: "1.0.0".to_string(),
security_features: vec!["SME".to_string(), "SEV".to_string()],
vendor_info: serde_json::json!({}),
},
expires_at: chrono::Utc::now() + chrono::Duration::hours(1),
})
}
AttestationType::Tdx { quote, .. } => {
// Mock TDX verification
Ok(AttestationResult {
verified: !quote.is_empty(),
max_tier: PrivacyTier::CpuTee,
measurements: vec![],
platform_info: PlatformInfo {
platform_type: "Intel TDX".to_string(),
tcb_version: "1.0.0".to_string(),
security_features: vec!["TDX".to_string()],
vendor_info: serde_json::json!({}),
},
expires_at: chrono::Utc::now() + chrono::Duration::hours(1),
})
}
AttestationType::H100Cc { gpu_attestation, cpu_attestation } => {
// Verify CPU attestation first
let cpu_result = self.verify_attestation(cpu_attestation).await?;
if !cpu_result.verified {
return Err(SecurityError::InvalidAttestation(
"CPU attestation failed".to_string()
));
}
// Mock GPU CC verification
Ok(AttestationResult {
verified: !gpu_attestation.is_empty(),
max_tier: PrivacyTier::GpuCc,
measurements: cpu_result.measurements,
platform_info: PlatformInfo {
platform_type: "NVIDIA H100 CC".to_string(),
tcb_version: "2.0.0".to_string(),
security_features: vec!["GPU_CC".to_string(), "MIG".to_string()],
vendor_info: serde_json::json!({"gpu_model": "H100"}),
},
expires_at: chrono::Utc::now() + chrono::Duration::hours(1),
})
}
AttestationType::BlackwellTeeIo { tee_io_report, mig_config } => {
// Mock Blackwell TEE-I/O verification
Ok(AttestationResult {
verified: !tee_io_report.is_empty(),
max_tier: PrivacyTier::GpuTeeIo,
measurements: vec![],
platform_info: PlatformInfo {
platform_type: "NVIDIA Blackwell TEE-I/O".to_string(),
tcb_version: "1.0.0".to_string(),
security_features: vec![
"TEE_IO".to_string(),
"SECURE_BOOT".to_string(),
if mig_config.is_some() { "MIG_ISOLATION" } else { "FULL_GPU" }.to_string(),
],
vendor_info: serde_json::json!({
"gpu_model": "Blackwell",
"mig_enabled": mig_config.is_some()
}),
},
expires_at: chrono::Utc::now() + chrono::Duration::minutes(30),
})
}
AttestationType::SimEid { eid, signature } => {
// Mock SIM EID verification
Ok(AttestationResult {
verified: !eid.is_empty() && !signature.is_empty(),
max_tier: PrivacyTier::AtRest,
measurements: vec![],
platform_info: PlatformInfo {
platform_type: "SIM Card".to_string(),
tcb_version: "1.0.0".to_string(),
security_features: vec!["EID".to_string(), "SECURE_ELEMENT".to_string()],
vendor_info: serde_json::json!({"eid": eid}),
},
expires_at: chrono::Utc::now() + chrono::Duration::hours(24),
})
}
}
}
async fn is_measurement_allowed(&self, _measurement: &[u8]) -> bool {
// Mock implementation - allow all measurements
true
}
}
/// Production attestation verifier that calls actual verification services
pub struct HanzoAttestationVerifier {
config: AttestationConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttestationConfig {
pub sev_snp_service_url: Option<String>,
pub tdx_service_url: Option<String>,
pub nvidia_cc_service_url: Option<String>,
pub allowed_measurements: Vec<AllowedMeasurement>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AllowedMeasurement {
pub name: String,
pub platform: String,
pub values: Vec<String>, // Hex-encoded
}
impl HanzoAttestationVerifier {
pub fn new(config: AttestationConfig) -> Self {
Self { config }
}
}
#[async_trait]
impl AttestationVerifier for HanzoAttestationVerifier {
async fn verify_attestation(&self, attestation: &AttestationType) -> Result<AttestationResult> {
match attestation {
AttestationType::SevSnp { report, vcek_cert, platform_cert_chain } => {
// TODO: Implement actual SEV-SNP verification
// This would call AMD's attestation service or use a local verifier
todo!("Implement SEV-SNP verification")
}
AttestationType::Tdx { quote, collateral } => {
// TODO: Implement actual TDX verification
// This would use Intel's attestation libraries
todo!("Implement TDX verification")
}
AttestationType::H100Cc { .. } => {
// TODO: Implement NVIDIA H100 CC verification
todo!("Implement H100 CC verification")
}
AttestationType::BlackwellTeeIo { .. } => {
// TODO: Implement Blackwell TEE-I/O verification
todo!("Implement Blackwell verification")
}
AttestationType::SimEid { .. } => {
// TODO: Implement SIM EID verification
todo!("Implement SIM verification")
}
}
}
async fn is_measurement_allowed(&self, measurement: &[u8]) -> bool {
let hex_measurement = hex::encode(measurement);
self.config.allowed_measurements.iter().any(|allowed| {
allowed.values.contains(&hex_measurement)
})
}
}
-47
View File
@@ -1,47 +0,0 @@
//! Error types for Hanzo Security
use thiserror::Error;
#[derive(Error, Debug)]
pub enum SecurityError {
#[error("Key not found: {0}")]
KeyNotFound(String),
#[error("Invalid attestation: {0}")]
InvalidAttestation(String),
#[error("Policy violation: {0}")]
PolicyViolation(String),
#[error("Cryptographic error: {0}")]
CryptoError(String),
#[error("HSM error: {0}")]
HsmError(String),
#[error("KMS error: {0}")]
KmsError(String),
#[error("KBS error: {0}")]
KbsError(String),
#[error("Tier mismatch: requested {requested}, available {available}")]
TierMismatch { requested: u8, available: u8 },
#[error("Session expired")]
SessionExpired,
#[error("Rate limit exceeded")]
RateLimitExceeded,
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Other error: {0}")]
Other(#[from] anyhow::Error),
}
pub type Result<T> = std::result::Result<T, SecurityError>;
-364
View File
@@ -1,364 +0,0 @@
//! Key Broker Service (KBS) trait and implementations
use async_trait::async_trait;
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::error::{Result, SecurityError};
use crate::types::*;
use crate::kms::KeyManagementService;
use crate::attestation::AttestationVerifier;
/// Key Broker Service trait - handles attestation and policy-based key release
#[async_trait]
pub trait KeyBrokerService: Send + Sync {
/// Authorize key release based on attestation and policy
async fn authorize(
&self,
request: KeyAuthorizationRequest,
) -> Result<KeyAuthorizationResponse>;
/// Renew an existing session with fresh attestation
async fn renew(
&self,
session_id: Uuid,
attestation: AttestationType,
) -> Result<RenewResponse>;
/// Revoke a session (admin or chain-triggered)
async fn revoke(&self, session_id: Uuid, reason: RevocationReason) -> Result<()>;
/// Get session status
async fn get_session_status(&self, session_id: Uuid) -> Result<SessionStatus>;
/// Get policy for a given tier
async fn get_tier_policy(&self, tier: PrivacyTier) -> Result<TierPolicy>;
}
/// KBS implementation that connects to KMS
pub struct HanzoKbs<K: KeyManagementService, V: AttestationVerifier> {
kms: K,
verifier: V,
config: KbsConfig,
sessions: dashmap::DashMap<Uuid, SessionInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KbsConfig {
pub max_session_duration: Duration,
pub attestation_cache_ttl: Duration,
pub rate_limit_per_minute: u32,
pub require_chain_verification: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RenewResponse {
pub session_id: Uuid,
pub new_expires_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize)]
pub enum RevocationReason {
AdminAction { admin_id: String, reason: String },
ChainTriggered { transaction_hash: String },
PolicyViolation { details: String },
Expired,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SessionStatus {
pub session_id: Uuid,
pub agent_id: String,
pub tier: PrivacyTier,
pub created_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
pub renewals: u32,
pub active: bool,
}
#[derive(Debug, Clone)]
struct SessionInfo {
pub agent_id: String,
pub tier: PrivacyTier,
pub enclave_public_key: Vec<u8>,
pub authorized_keys: Vec<KeyId>,
pub created_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
pub renewals: u32,
pub active: bool,
}
/// Policy for each privacy tier
#[derive(Debug, Serialize, Deserialize)]
pub struct TierPolicy {
pub tier: PrivacyTier,
pub required_attestations: Vec<AttestationRequirement>,
pub max_session_duration: Duration,
pub allowed_operations: Vec<String>,
pub key_restrictions: Vec<KeyRestriction>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AttestationRequirement {
pub attestation_type: String,
pub min_tcb_version: Option<String>,
pub allowed_measurements: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct KeyRestriction {
pub key_type: String,
pub max_usage_count: Option<u64>,
pub require_audit: bool,
}
#[async_trait]
impl<K: KeyManagementService, V: AttestationVerifier> KeyBrokerService for HanzoKbs<K, V> {
async fn authorize(
&self,
request: KeyAuthorizationRequest,
) -> Result<KeyAuthorizationResponse> {
// Rate limiting check
if !self.check_rate_limit(&request.capability_token.subject).await? {
return Err(SecurityError::RateLimitExceeded);
}
// Verify attestation
let attestation_result = self.verifier
.verify_attestation(&request.attestation)
.await?;
// Check tier compatibility
let requested_tier = request.capability_token.tier;
if !attestation_result.supports_tier(requested_tier) {
return Err(SecurityError::TierMismatch {
requested: requested_tier as u8,
available: attestation_result.max_tier as u8,
});
}
// Verify capability token (on-chain if configured)
if self.config.require_chain_verification {
self.verify_capability_token(&request.capability_token).await?;
}
// Get tier policy
let policy = self.get_tier_policy(requested_tier).await?;
// Authorize requested keys
let mut authorized_keys = Vec::new();
for key_request in &request.requested_keys {
let authorized_key = self.authorize_key(
&key_request,
&request.session_public_key,
requested_tier,
&policy,
).await?;
authorized_keys.push(authorized_key);
}
// Create session
let session_id = Uuid::new_v4();
let expires_at = Utc::now() + self.config.max_session_duration;
self.sessions.insert(session_id, SessionInfo {
agent_id: request.capability_token.subject.clone(),
tier: requested_tier,
enclave_public_key: request.session_public_key.clone(),
authorized_keys: authorized_keys.iter().map(|k| k.key_id.clone()).collect(),
created_at: Utc::now(),
expires_at,
renewals: 0,
active: true,
});
Ok(KeyAuthorizationResponse {
session_id,
authorized_keys,
expires_at,
})
}
async fn renew(
&self,
session_id: Uuid,
attestation: AttestationType,
) -> Result<RenewResponse> {
let mut session = self.sessions.get_mut(&session_id)
.ok_or_else(|| SecurityError::SessionExpired)?;
if !session.active || session.expires_at < Utc::now() {
return Err(SecurityError::SessionExpired);
}
// Verify fresh attestation
let attestation_result = self.verifier.verify_attestation(&attestation).await?;
if !attestation_result.supports_tier(session.tier) {
return Err(SecurityError::InvalidAttestation(
"Attestation no longer supports required tier".to_string()
));
}
// Update session
session.expires_at = Utc::now() + self.config.max_session_duration;
session.renewals += 1;
Ok(RenewResponse {
session_id,
new_expires_at: session.expires_at,
})
}
async fn revoke(&self, session_id: Uuid, reason: RevocationReason) -> Result<()> {
if let Some(mut session) = self.sessions.get_mut(&session_id) {
session.active = false;
// Log revocation
log::info!(
"Session {} revoked for agent {}: {:?}",
session_id, session.agent_id, reason
);
}
Ok(())
}
async fn get_session_status(&self, session_id: Uuid) -> Result<SessionStatus> {
let session = self.sessions.get(&session_id)
.ok_or_else(|| SecurityError::KeyNotFound("Session not found".to_string()))?;
Ok(SessionStatus {
session_id,
agent_id: session.agent_id.clone(),
tier: session.tier,
created_at: session.created_at,
expires_at: session.expires_at,
renewals: session.renewals,
active: session.active && session.expires_at > Utc::now(),
})
}
async fn get_tier_policy(&self, tier: PrivacyTier) -> Result<TierPolicy> {
// This would typically load from configuration or database
Ok(match tier {
PrivacyTier::Open => TierPolicy {
tier,
required_attestations: vec![],
max_session_duration: Duration::hours(24),
allowed_operations: vec!["*".to_string()],
key_restrictions: vec![],
},
PrivacyTier::AtRest => TierPolicy {
tier,
required_attestations: vec![],
max_session_duration: Duration::hours(12),
allowed_operations: vec!["read".to_string(), "write".to_string()],
key_restrictions: vec![],
},
PrivacyTier::CpuTee => TierPolicy {
tier,
required_attestations: vec![
AttestationRequirement {
attestation_type: "SevSnp".to_string(),
min_tcb_version: Some("1.0".to_string()),
allowed_measurements: None,
},
],
max_session_duration: Duration::hours(4),
allowed_operations: vec!["compute".to_string()],
key_restrictions: vec![
KeyRestriction {
key_type: "AgentDek".to_string(),
max_usage_count: Some(1000),
require_audit: true,
},
],
},
PrivacyTier::GpuCc => TierPolicy {
tier,
required_attestations: vec![
AttestationRequirement {
attestation_type: "H100Cc".to_string(),
min_tcb_version: Some("2.0".to_string()),
allowed_measurements: None,
},
],
max_session_duration: Duration::hours(2),
allowed_operations: vec!["gpu_compute".to_string()],
key_restrictions: vec![
KeyRestriction {
key_type: "AgentDek".to_string(),
max_usage_count: Some(100),
require_audit: true,
},
],
},
PrivacyTier::GpuTeeIo => TierPolicy {
tier,
required_attestations: vec![
AttestationRequirement {
attestation_type: "BlackwellTeeIo".to_string(),
min_tcb_version: Some("1.0".to_string()),
allowed_measurements: None,
},
],
max_session_duration: Duration::hours(1),
allowed_operations: vec!["secure_inference".to_string()],
key_restrictions: vec![
KeyRestriction {
key_type: "AgentDek".to_string(),
max_usage_count: Some(10),
require_audit: true,
},
],
},
})
}
}
impl<K: KeyManagementService, V: AttestationVerifier> HanzoKbs<K, V> {
pub fn new(kms: K, verifier: V, config: KbsConfig) -> Self {
Self {
kms,
verifier,
config,
sessions: dashmap::DashMap::new(),
}
}
async fn check_rate_limit(&self, subject: &str) -> Result<bool> {
// TODO: Implement proper rate limiting with Redis or in-memory cache
Ok(true)
}
async fn verify_capability_token(&self, token: &CapabilityToken) -> Result<()> {
// TODO: Verify on-chain signature
if token.expires_at.map(|exp| exp < Utc::now()).unwrap_or(false) {
return Err(SecurityError::PolicyViolation("Token expired".to_string()));
}
Ok(())
}
async fn authorize_key(
&self,
request: &KeyRequest,
enclave_public_key: &[u8],
tier: PrivacyTier,
policy: &TierPolicy,
) -> Result<AuthorizedKey> {
// TODO: Implement HPKE wrapping
// For now, return a mock authorized key
Ok(AuthorizedKey {
key_id: KeyId::new(),
hpke_wrapped_key: vec![0; 32], // Mock wrapped key
metadata: KeyMetadata {
key_type: format!("{:?}", request.key_type),
tier,
restrictions: policy.key_restrictions.iter()
.map(|r| format!("{}: max_usage={:?}", r.key_type, r.max_usage_count))
.collect(),
},
})
}
}
-174
View File
@@ -1,174 +0,0 @@
//! Key Management Service (KMS) trait and implementations
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::error::Result;
use crate::types::*;
// Declare submodules
pub mod api;
pub mod memory_kms;
/// Key Management Service trait - handles key lifecycle and storage
#[async_trait]
pub trait KeyManagementService: Send + Sync {
/// Initialize the KMS with root key
async fn initialize(&mut self, config: KmsConfig) -> Result<()>;
/// Create a new tenant KEK
async fn create_tenant_kek(&self, tenant_id: &str) -> Result<TenantKek>;
/// Create a new agent DEK
async fn create_agent_dek(&self, agent_id: &str, tenant_id: &str) -> Result<AgentDek>;
/// Wrap a key under a parent key
async fn wrap_key(&self, key_data: &[u8], parent_key_id: &KeyId) -> Result<Vec<u8>>;
/// Unwrap a key (internal use only, never exposed to KBS)
async fn unwrap_key(&self, wrapped_key: &[u8], parent_key_id: &KeyId) -> Result<Vec<u8>>;
/// Rotate a key
async fn rotate_key(&self, key_id: &KeyId) -> Result<KeyId>;
/// Destroy a key
async fn destroy_key(&self, key_id: &KeyId) -> Result<()>;
/// Get key metadata
async fn get_key_metadata(&self, key_id: &KeyId) -> Result<KeyInfo>;
/// Get audit logs
async fn get_audit_logs(
&self,
start: DateTime<Utc>,
end: DateTime<Utc>,
filter: Option<AuditFilter>,
) -> Result<Vec<KeyAuditEntry>>;
/// BYOK: Import customer key
async fn import_customer_key(
&self,
tenant_id: &str,
wrapped_key: &[u8],
key_metadata: CustomerKeyMetadata,
) -> Result<KeyId>;
/// HYOK: Register customer-held key reference
async fn register_customer_held_key(
&self,
tenant_id: &str,
key_reference: CustomerKeyReference,
) -> Result<KeyId>;
}
/// KMS configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KmsConfig {
pub hsm_type: HsmType,
pub root_key_source: RootKeySource,
pub audit_retention_days: u32,
pub key_rotation_days: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum HsmType {
Software,
AwsCloudHsm { cluster_id: String },
AzureKeyVault { vault_url: String },
HashicorpVault { url: String, namespace: Option<String> },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RootKeySource {
Generate,
Import { wrapped_key: Vec<u8> },
Existing { key_id: String },
}
/// Extended key information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyInfo {
pub key_id: KeyId,
pub key_type: KeyType,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub expires_at: Option<DateTime<Utc>>,
pub rotation_due: Option<DateTime<Utc>>,
pub state: KeyState,
pub metadata: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum KeyType {
Root,
TenantKek { tenant_id: String },
AgentDek { agent_id: String, tenant_id: String },
CustomerKey { tenant_id: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum KeyState {
Active,
Rotating,
Expired,
Destroyed,
}
/// Audit log filter
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct AuditFilter {
pub key_ids: Option<Vec<KeyId>>,
pub operations: Option<Vec<KeyOperation>>,
pub actors: Option<Vec<String>>,
pub success_only: Option<bool>,
}
/// Customer key metadata for BYOK
#[derive(Debug, Serialize, Deserialize)]
pub struct CustomerKeyMetadata {
pub algorithm: String,
pub key_size: u32,
pub usage: Vec<String>,
pub expires_at: Option<DateTime<Utc>>,
}
/// Customer key reference for HYOK
#[derive(Debug, Serialize, Deserialize)]
pub struct CustomerKeyReference {
pub provider: String,
pub key_id: String,
pub region: Option<String>,
pub permissions: Vec<String>,
}
/// Key wrapping algorithms supported
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum KeyWrapAlgorithm {
AesKwp256,
Rsa4096OaepSha256,
ChaCha20Poly1305,
}
/// Key hierarchy operations
pub struct KeyHierarchy;
impl KeyHierarchy {
/// Derive a KEK from root key and tenant salt
pub fn derive_tenant_kek(root_key: &[u8], tenant_id: &str) -> Vec<u8> {
let salt = format!("tenant_kek_{}", tenant_id);
let mut hasher = blake3::Hasher::new_derive_key(&salt);
hasher.update(root_key);
let mut output = vec![0u8; 32];
hasher.finalize_xof().fill(&mut output);
output
}
/// Generate a random DEK
pub fn generate_dek() -> Vec<u8> {
use rand::RngCore;
let mut key = vec![0u8; 32];
rand::thread_rng().fill_bytes(&mut key);
key
}
}
-200
View File
@@ -1,200 +0,0 @@
//! HTTP API for KMS operations
use serde::{Deserialize, Serialize};
use warp::{Filter, Rejection, Reply};
use std::sync::Arc;
use crate::kms::KeyManagementService;
use crate::types::{KeyId, AgentDek, TenantKek};
/// API request/response types
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateTenantKekRequest {
pub tenant_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateTenantKekResponse {
pub kek: TenantKek,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateAgentDekRequest {
pub agent_id: String,
pub tenant_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateAgentDekResponse {
pub dek: AgentDek,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WrapKeyRequest {
pub key_data_base64: String,
pub parent_key_id: KeyId,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WrapKeyResponse {
pub wrapped_key_base64: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RotateKeyRequest {
pub key_id: KeyId,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RotateKeyResponse {
pub new_key_id: KeyId,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DestroyKeyRequest {
pub key_id: KeyId,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GetAuditLogsRequest {
pub start_time: chrono::DateTime<chrono::Utc>,
pub end_time: chrono::DateTime<chrono::Utc>,
pub filter: Option<crate::kms::AuditFilter>,
}
/// Create all KMS API routes
pub fn kms_routes<K: KeyManagementService + Clone + Send + Sync + 'static>(
kms: Arc<K>,
) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
let create_tenant_kek = warp::path!("keys" / "tenant")
.and(warp::post())
.and(warp::body::json())
.and(with_kms(kms.clone()))
.and_then(handle_create_tenant_kek);
let create_agent_dek = warp::path!("keys" / "agent")
.and(warp::post())
.and(warp::body::json())
.and(with_kms(kms.clone()))
.and_then(handle_create_agent_dek);
let wrap_key = warp::path!("wrap")
.and(warp::post())
.and(warp::body::json())
.and(with_kms(kms.clone()))
.and_then(handle_wrap_key);
let rotate_key = warp::path!("rotate")
.and(warp::post())
.and(warp::body::json())
.and(with_kms(kms.clone()))
.and_then(handle_rotate_key);
let destroy_key = warp::path!("destroy")
.and(warp::post())
.and(warp::body::json())
.and(with_kms(kms.clone()))
.and_then(handle_destroy_key);
let get_audit_logs = warp::path!("audit")
.and(warp::get())
.and(warp::query())
.and(with_kms(kms))
.and_then(handle_get_audit_logs);
create_tenant_kek
.or(create_agent_dek)
.or(wrap_key)
.or(rotate_key)
.or(destroy_key)
.or(get_audit_logs)
}
fn with_kms<K: KeyManagementService + Clone + Send>(
kms: Arc<K>,
) -> impl Filter<Extract = (Arc<K>,), Error = std::convert::Infallible> + Clone {
warp::any().map(move || kms.clone())
}
async fn handle_create_tenant_kek<K: KeyManagementService>(
req: CreateTenantKekRequest,
kms: Arc<K>,
) -> Result<impl Reply, Rejection> {
match kms.create_tenant_kek(&req.tenant_id).await {
Ok(kek) => Ok(warp::reply::json(&CreateTenantKekResponse { kek })),
Err(e) => {
log::error!("Failed to create tenant KEK: {}", e);
Err(warp::reject::reject())
}
}
}
async fn handle_create_agent_dek<K: KeyManagementService>(
req: CreateAgentDekRequest,
kms: Arc<K>,
) -> Result<impl Reply, Rejection> {
match kms.create_agent_dek(&req.agent_id, &req.tenant_id).await {
Ok(dek) => Ok(warp::reply::json(&CreateAgentDekResponse { dek })),
Err(e) => {
log::error!("Failed to create agent DEK: {}", e);
Err(warp::reject::reject())
}
}
}
async fn handle_wrap_key<K: KeyManagementService>(
req: WrapKeyRequest,
kms: Arc<K>,
) -> Result<impl Reply, Rejection> {
let key_data = base64::decode(&req.key_data_base64)
.map_err(|_| warp::reject::reject())?;
match kms.wrap_key(&key_data, &req.parent_key_id).await {
Ok(wrapped) => Ok(warp::reply::json(&WrapKeyResponse {
wrapped_key_base64: base64::encode(&wrapped),
})),
Err(e) => {
log::error!("Failed to wrap key: {}", e);
Err(warp::reject::reject())
}
}
}
async fn handle_rotate_key<K: KeyManagementService>(
req: RotateKeyRequest,
kms: Arc<K>,
) -> Result<impl Reply, Rejection> {
match kms.rotate_key(&req.key_id).await {
Ok(new_key_id) => Ok(warp::reply::json(&RotateKeyResponse { new_key_id })),
Err(e) => {
log::error!("Failed to rotate key: {}", e);
Err(warp::reject::reject())
}
}
}
async fn handle_destroy_key<K: KeyManagementService>(
req: DestroyKeyRequest,
kms: Arc<K>,
) -> Result<impl Reply, Rejection> {
match kms.destroy_key(&req.key_id).await {
Ok(()) => Ok(warp::reply::with_status("", warp::http::StatusCode::NO_CONTENT)),
Err(e) => {
log::error!("Failed to destroy key: {}", e);
Err(warp::reject::reject())
}
}
}
async fn handle_get_audit_logs<K: KeyManagementService>(
req: GetAuditLogsRequest,
kms: Arc<K>,
) -> Result<impl Reply, Rejection> {
match kms.get_audit_logs(req.start_time, req.end_time, req.filter).await {
Ok(logs) => Ok(warp::reply::json(&logs)),
Err(e) => {
log::error!("Failed to get audit logs: {}", e);
Err(warp::reject::reject())
}
}
}
-396
View File
@@ -1,396 +0,0 @@
//! In-memory KMS implementation for development and testing
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use std::sync::Arc;
use uuid::Uuid;
use crate::error::{Result, SecurityError};
use crate::kms::{
AuditFilter, CustomerKeyMetadata, CustomerKeyReference, HsmType, KeyInfo, KeyManagementService,
KeyState, KeyType, KmsConfig, RootKeySource,
};
use crate::types::{
AgentDek, KeyAuditEntry, KeyId, KeyOperation, RootKey, TenantKek,
};
/// In-memory KMS implementation
pub struct MemoryKms {
root_key: Arc<RootKey>,
root_key_material: Vec<u8>,
tenant_keks: DashMap<String, (TenantKek, Vec<u8>)>, // tenant_id -> (metadata, key_material)
agent_deks: DashMap<String, (AgentDek, Vec<u8>)>, // agent_id -> (metadata, key_material)
key_info: DashMap<KeyId, KeyInfo>,
audit_log: DashMap<Uuid, KeyAuditEntry>,
config: KmsConfig,
}
impl MemoryKms {
pub fn new() -> Self {
let root_key_id = KeyId::new();
let root_key = RootKey {
id: root_key_id.clone(),
created_at: Utc::now(),
hsm_handle: None,
};
// Generate root key material
let root_key_material = crate::kms::KeyHierarchy::generate_dek();
let key_info = DashMap::new();
key_info.insert(
root_key_id.clone(),
KeyInfo {
key_id: root_key_id,
key_type: KeyType::Root,
created_at: Utc::now(),
updated_at: Utc::now(),
expires_at: None,
rotation_due: None,
state: KeyState::Active,
metadata: serde_json::json!({}),
},
);
Self {
root_key: Arc::new(root_key),
root_key_material,
tenant_keks: DashMap::new(),
agent_deks: DashMap::new(),
key_info,
audit_log: DashMap::new(),
config: KmsConfig {
hsm_type: HsmType::Software,
root_key_source: RootKeySource::Generate,
audit_retention_days: 30,
key_rotation_days: Some(90),
},
}
}
fn log_audit(&self, operation: KeyOperation, key_id: Option<KeyId>, success: bool, details: serde_json::Value) {
let entry = KeyAuditEntry {
id: Uuid::new_v4(),
timestamp: Utc::now(),
operation,
actor: "system".to_string(),
key_id,
success,
details,
};
self.audit_log.insert(entry.id, entry);
}
fn wrap_with_aes_kwp(&self, key_data: &[u8], kek: &[u8]) -> Result<Vec<u8>> {
// Simplified wrapping - in production use proper AES-KWP
use chacha20poly1305::{
aead::{Aead, KeyInit},
ChaCha20Poly1305, Nonce,
};
let cipher = ChaCha20Poly1305::new_from_slice(kek)
.map_err(|e| SecurityError::CryptoError(e.to_string()))?;
let nonce = Nonce::from_slice(b"unique nonce"); // In production, use random nonce
cipher
.encrypt(nonce, key_data)
.map_err(|e| SecurityError::CryptoError(e.to_string()))
}
fn unwrap_with_aes_kwp(&self, wrapped: &[u8], kek: &[u8]) -> Result<Vec<u8>> {
// Simplified unwrapping - in production use proper AES-KWP
use chacha20poly1305::{
aead::{Aead, KeyInit},
ChaCha20Poly1305, Nonce,
};
let cipher = ChaCha20Poly1305::new_from_slice(kek)
.map_err(|e| SecurityError::CryptoError(e.to_string()))?;
let nonce = Nonce::from_slice(b"unique nonce");
cipher
.decrypt(nonce, wrapped)
.map_err(|e| SecurityError::CryptoError(e.to_string()))
}
}
#[async_trait]
impl KeyManagementService for MemoryKms {
async fn initialize(&mut self, config: KmsConfig) -> Result<()> {
self.config = config;
self.log_audit(
KeyOperation::Create,
Some(self.root_key.id.clone()),
true,
serde_json::json!({ "type": "initialization" }),
);
Ok(())
}
async fn create_tenant_kek(&self, tenant_id: &str) -> Result<TenantKek> {
// Derive KEK from root key
let kek_material = crate::kms::KeyHierarchy::derive_tenant_kek(&self.root_key_material, tenant_id);
// Wrap KEK under root key
let wrapped_kek = self.wrap_with_aes_kwp(&kek_material, &self.root_key_material)?;
let kek_id = KeyId::new();
let kek = TenantKek {
id: kek_id.clone(),
tenant_id: tenant_id.to_string(),
wrapped_key: wrapped_kek,
created_at: Utc::now(),
expires_at: None,
};
// Store metadata
self.tenant_keks.insert(tenant_id.to_string(), (kek.clone(), kek_material));
self.key_info.insert(
kek_id.clone(),
KeyInfo {
key_id: kek_id.clone(),
key_type: KeyType::TenantKek { tenant_id: tenant_id.to_string() },
created_at: kek.created_at,
updated_at: kek.created_at,
expires_at: kek.expires_at,
rotation_due: self.config.key_rotation_days
.map(|days| kek.created_at + chrono::Duration::days(days as i64)),
state: KeyState::Active,
metadata: serde_json::json!({ "tenant_id": tenant_id }),
},
);
self.log_audit(
KeyOperation::Create,
Some(kek_id),
true,
serde_json::json!({ "type": "tenant_kek", "tenant_id": tenant_id }),
);
Ok(kek)
}
async fn create_agent_dek(&self, agent_id: &str, tenant_id: &str) -> Result<AgentDek> {
// Get tenant KEK
let (_, tenant_kek_material) = self.tenant_keks.get(tenant_id)
.ok_or_else(|| SecurityError::KeyNotFound(format!("Tenant KEK not found: {}", tenant_id)))?
.clone();
// Generate new DEK
let dek_material = crate::kms::KeyHierarchy::generate_dek();
// Wrap DEK under tenant KEK
let wrapped_dek = self.wrap_with_aes_kwp(&dek_material, &tenant_kek_material)?;
let dek_id = KeyId::new();
let dek = AgentDek {
id: dek_id.clone(),
agent_id: agent_id.to_string(),
tenant_id: tenant_id.to_string(),
wrapped_key: wrapped_dek,
created_at: Utc::now(),
rotation_due: self.config.key_rotation_days
.map(|days| Utc::now() + chrono::Duration::days(days as i64)),
};
// Store metadata
self.agent_deks.insert(agent_id.to_string(), (dek.clone(), dek_material));
self.key_info.insert(
dek_id.clone(),
KeyInfo {
key_id: dek_id.clone(),
key_type: KeyType::AgentDek {
agent_id: agent_id.to_string(),
tenant_id: tenant_id.to_string(),
},
created_at: dek.created_at,
updated_at: dek.created_at,
expires_at: None,
rotation_due: dek.rotation_due,
state: KeyState::Active,
metadata: serde_json::json!({
"agent_id": agent_id,
"tenant_id": tenant_id,
}),
},
);
self.log_audit(
KeyOperation::Create,
Some(dek_id),
true,
serde_json::json!({
"type": "agent_dek",
"agent_id": agent_id,
"tenant_id": tenant_id,
}),
);
Ok(dek)
}
async fn wrap_key(&self, key_data: &[u8], parent_key_id: &KeyId) -> Result<Vec<u8>> {
// Find parent key material
let parent_key_material = if *parent_key_id == self.root_key.id {
self.root_key_material.clone()
} else {
return Err(SecurityError::KeyNotFound(format!("Parent key not found: {:?}", parent_key_id)));
};
let wrapped = self.wrap_with_aes_kwp(key_data, &parent_key_material)?;
self.log_audit(
KeyOperation::Wrap,
Some(parent_key_id.clone()),
true,
serde_json::json!({ "wrapped_size": wrapped.len() }),
);
Ok(wrapped)
}
async fn unwrap_key(&self, wrapped_key: &[u8], parent_key_id: &KeyId) -> Result<Vec<u8>> {
// Find parent key material
let parent_key_material = if *parent_key_id == self.root_key.id {
self.root_key_material.clone()
} else {
return Err(SecurityError::KeyNotFound(format!("Parent key not found: {:?}", parent_key_id)));
};
let unwrapped = self.unwrap_with_aes_kwp(wrapped_key, &parent_key_material)?;
self.log_audit(
KeyOperation::Unwrap,
Some(parent_key_id.clone()),
true,
serde_json::json!({ "unwrapped_size": unwrapped.len() }),
);
Ok(unwrapped)
}
async fn rotate_key(&self, key_id: &KeyId) -> Result<KeyId> {
// TODO: Implement key rotation
self.log_audit(
KeyOperation::Rotate,
Some(key_id.clone()),
false,
serde_json::json!({ "error": "not implemented" }),
);
Err(SecurityError::Other(anyhow::anyhow!("Key rotation not implemented")))
}
async fn destroy_key(&self, key_id: &KeyId) -> Result<()> {
if let Some(mut info) = self.key_info.get_mut(key_id) {
info.state = KeyState::Destroyed;
info.updated_at = Utc::now();
}
self.log_audit(
KeyOperation::Destroy,
Some(key_id.clone()),
true,
serde_json::json!({}),
);
Ok(())
}
async fn get_key_metadata(&self, key_id: &KeyId) -> Result<KeyInfo> {
self.key_info.get(key_id)
.map(|info| info.value().clone())
.ok_or_else(|| SecurityError::KeyNotFound(format!("Key not found: {:?}", key_id)))
}
async fn get_audit_logs(
&self,
start: DateTime<Utc>,
end: DateTime<Utc>,
filter: Option<AuditFilter>,
) -> Result<Vec<KeyAuditEntry>> {
let mut logs: Vec<KeyAuditEntry> = self.audit_log.iter()
.filter(|entry| entry.timestamp >= start && entry.timestamp <= end)
.map(|entry| entry.value().clone())
.collect();
if let Some(f) = filter {
logs.retain(|log| {
let mut keep = true;
if let Some(ref key_ids) = f.key_ids {
keep &= log.key_id.as_ref().map(|id| key_ids.contains(id)).unwrap_or(false);
}
if let Some(ref ops) = f.operations {
keep &= ops.iter().any(|op| std::mem::discriminant(op) == std::mem::discriminant(&log.operation));
}
if let Some(ref actors) = f.actors {
keep &= actors.contains(&log.actor);
}
if let Some(success_only) = f.success_only {
keep &= log.success == success_only;
}
keep
});
}
logs.sort_by_key(|log| log.timestamp);
Ok(logs)
}
async fn import_customer_key(
&self,
tenant_id: &str,
wrapped_key: &[u8],
key_metadata: CustomerKeyMetadata,
) -> Result<KeyId> {
// TODO: Implement customer key import
let key_id = KeyId::new();
self.log_audit(
KeyOperation::Create,
Some(key_id.clone()),
false,
serde_json::json!({
"type": "customer_key_import",
"tenant_id": tenant_id,
"error": "not implemented",
}),
);
Err(SecurityError::Other(anyhow::anyhow!("Customer key import not implemented")))
}
async fn register_customer_held_key(
&self,
tenant_id: &str,
key_reference: CustomerKeyReference,
) -> Result<KeyId> {
// TODO: Implement HYOK registration
let key_id = KeyId::new();
self.log_audit(
KeyOperation::Create,
Some(key_id.clone()),
false,
serde_json::json!({
"type": "hyok_registration",
"tenant_id": tenant_id,
"provider": key_reference.provider,
"error": "not implemented",
}),
);
Err(SecurityError::Other(anyhow::anyhow!("HYOK registration not implemented")))
}
}
-29
View File
@@ -1,29 +0,0 @@
//! Hanzo KBS (Key Broker Service) Library
//!
//! Provides Key Management Service (KMS) and Key Broker Service (KBS) functionality
//! for confidential computing and privacy-preserving agent execution in Hanzo nodes.
//!
//! This crate implements the KMS/KBS split architecture where:
//! - KMS handles key lifecycle management and storage
//! - KBS handles attestation verification and policy-based key release
pub mod error;
pub mod types;
pub mod kms;
pub mod kbs;
pub mod attestation;
pub mod vault;
#[cfg(feature = "pqc")]
pub mod pqc_integration;
#[cfg(feature = "pqc")]
pub mod pqc_vault;
pub use error::{SecurityError, Result};
pub use types::*;
pub use kms::KeyManagementService;
pub use kbs::KeyBrokerService;
// Re-export submodules from kms
pub use kms::{memory_kms, api};
-287
View File
@@ -1,287 +0,0 @@
//! Post-Quantum Cryptography integration for KBS
//!
//! This module provides PQC support for the KBS when the "pqc" feature is enabled.
use serde::{Deserialize, Serialize};
use hanzo_pqc::{
kem::{Kem, KemAlgorithm, MlKem, EncapsulationKey, DecapsulationKey},
signature::{Signature, SignatureAlgorithm, MlDsa, VerifyingKey, SigningKey, DigitalSignature},
hybrid::{HybridMode, HybridKem, HybridEncapsulationKey},
privacy_tiers::PrivacyTier as PqcPrivacyTier,
config::PqcConfig,
};
use crate::{
error::{Result, SecurityError},
types::PrivacyTier,
};
/// PQC Key Broker Service integration
pub struct PqcKbs {
ml_kem: MlKem,
ml_dsa: MlDsa,
config: PqcConfig,
}
impl PqcKbs {
pub fn new(tier: PrivacyTier) -> Self {
let pqc_tier = match tier {
PrivacyTier::Open => PqcPrivacyTier::AccessOpen,
PrivacyTier::AtRest => PqcPrivacyTier::AccessAtRest,
PrivacyTier::CpuTee => PqcPrivacyTier::AccessCpuTee,
PrivacyTier::GpuCc => PqcPrivacyTier::AccessCpuTeePlusGpuCc,
PrivacyTier::GpuTeeIo => PqcPrivacyTier::AccessGpuTeeIoMax,
};
let config = PqcConfig::for_privacy_tier(pqc_tier);
Self {
ml_kem: MlKem::new(),
ml_dsa: MlDsa::new(),
config,
}
}
/// Generate ML-KEM keypair for DEK wrapping
pub async fn generate_kem_keypair(&self) -> Result<(EncapsulationKey, DecapsulationKey)> {
let alg = match self.config.default_kem {
hanzo_pqc::config::DefaultKem::MlKem512 => KemAlgorithm::MlKem512,
hanzo_pqc::config::DefaultKem::MlKem768 => KemAlgorithm::MlKem768,
hanzo_pqc::config::DefaultKem::MlKem1024 => KemAlgorithm::MlKem1024,
};
let keypair = self.ml_kem.generate_keypair(alg).await
.map_err(|e| SecurityError::CryptoError(format!("KEM keypair generation failed: {:?}", e)))?;
Ok((keypair.encap_key, keypair.decap_key))
}
/// Generate ML-DSA keypair for attestation signing
pub async fn generate_signing_keypair(&self) -> Result<(VerifyingKey, SigningKey)> {
let alg = match self.config.default_sig {
hanzo_pqc::config::DefaultSig::MlDsa44 => SignatureAlgorithm::MlDsa44,
hanzo_pqc::config::DefaultSig::MlDsa65 => SignatureAlgorithm::MlDsa65,
hanzo_pqc::config::DefaultSig::MlDsa87 => SignatureAlgorithm::MlDsa87,
};
self.ml_dsa.generate_keypair(alg).await
.map_err(|e| SecurityError::CryptoError(format!("Signing keypair generation failed: {:?}", e)))
}
/// Wrap a DEK with ML-KEM
pub async fn wrap_dek_with_mlkem(
&self,
dek: &[u8],
encap_key: &EncapsulationKey,
) -> Result<Vec<u8>> {
// Encapsulate to get shared secret
let output = self.ml_kem.encapsulate(encap_key).await
.map_err(|e| SecurityError::CryptoError(format!("Encapsulation failed: {:?}", e)))?;
// Use shared secret to derive wrapping key
use hanzo_pqc::kdf::{HkdfKdf, Kdf};
let kdf = HkdfKdf::new(self.config.kdf);
let wrapping_key = kdf.derive(
None,
&output.shared_secret,
b"hanzo-kbs-dek-wrap-v1",
32,
).map_err(|e| SecurityError::CryptoError(format!("KDF failed: {:?}", e)))?;
// Encrypt DEK with ChaCha20Poly1305
use chacha20poly1305::{
aead::{Aead, AeadCore, KeyInit, OsRng},
ChaCha20Poly1305,
};
let cipher = ChaCha20Poly1305::new_from_slice(&wrapping_key)
.map_err(|e| SecurityError::CryptoError(format!("Cipher init failed: {:?}", e)))?;
let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
let ciphertext = cipher.encrypt(&nonce, dek)
.map_err(|e| SecurityError::CryptoError(format!("DEK encryption failed: {:?}", e)))?;
// Return: ciphertext_len || kem_ciphertext || nonce || encrypted_dek
let mut wrapped = Vec::new();
wrapped.extend_from_slice(&(output.ciphertext.len() as u32).to_be_bytes());
wrapped.extend_from_slice(&output.ciphertext);
wrapped.extend_from_slice(&nonce);
wrapped.extend_from_slice(&ciphertext);
Ok(wrapped)
}
/// Unwrap a DEK with ML-KEM
pub async fn unwrap_dek_with_mlkem(
&self,
wrapped_dek: &[u8],
decap_key: &DecapsulationKey,
) -> Result<Vec<u8>> {
// Parse wrapped format
if wrapped_dek.len() < 4 {
return Err(SecurityError::CryptoError("Invalid wrapped DEK format".into()));
}
let ct_len = u32::from_be_bytes([
wrapped_dek[0], wrapped_dek[1], wrapped_dek[2], wrapped_dek[3]
]) as usize;
if wrapped_dek.len() < 4 + ct_len + 12 {
return Err(SecurityError::CryptoError("Invalid wrapped DEK length".into()));
}
let kem_ct = &wrapped_dek[4..4 + ct_len];
let nonce = &wrapped_dek[4 + ct_len..4 + ct_len + 12];
let encrypted_dek = &wrapped_dek[4 + ct_len + 12..];
// Decapsulate to recover shared secret
let shared_secret = self.ml_kem.decapsulate(decap_key, kem_ct).await
.map_err(|e| SecurityError::CryptoError(format!("Decapsulation failed: {:?}", e)))?;
// Derive wrapping key
use hanzo_pqc::kdf::{HkdfKdf, Kdf};
let kdf = HkdfKdf::new(self.config.kdf);
let wrapping_key = kdf.derive(
None,
&shared_secret,
b"hanzo-kbs-dek-wrap-v1",
32,
).map_err(|e| SecurityError::CryptoError(format!("KDF failed: {:?}", e)))?;
// Decrypt DEK
use chacha20poly1305::{
aead::{Aead, KeyInit},
ChaCha20Poly1305, Nonce,
};
let cipher = ChaCha20Poly1305::new_from_slice(&wrapping_key)
.map_err(|e| SecurityError::CryptoError(format!("Cipher init failed: {:?}", e)))?;
let nonce = Nonce::from_slice(nonce);
let dek = cipher.decrypt(nonce, encrypted_dek)
.map_err(|e| SecurityError::CryptoError(format!("DEK decryption failed: {:?}", e)))?;
Ok(dek)
}
/// Sign attestation report with ML-DSA
pub async fn sign_attestation(
&self,
report: &[u8],
signing_key: &SigningKey,
) -> Result<DigitalSignature> {
self.ml_dsa.sign(signing_key, report).await
.map_err(|e| SecurityError::CryptoError(format!("Attestation signing failed: {:?}", e)))
}
/// Verify attestation signature with ML-DSA
pub async fn verify_attestation(
&self,
report: &[u8],
signature: &DigitalSignature,
verifying_key: &VerifyingKey,
) -> Result<bool> {
self.ml_dsa.verify(verifying_key, report, signature).await
.map_err(|e| SecurityError::CryptoError(format!("Attestation verification failed: {:?}", e)))
}
}
/// PQC handshake for establishing secure channels
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PqcHandshake {
pub version: u8,
pub mode: HandshakeMode,
pub encap_key: Option<EncapsulationKey>,
pub hybrid_encap_key: Option<HybridEncapsulationKey>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum HandshakeMode {
MlKemOnly(KemAlgorithm),
Hybrid(HybridMode),
}
/// Keys established after PQC handshake
pub struct HandshakeKeys {
pub client_write_key: [u8; 32],
pub server_write_key: [u8; 32],
pub client_write_iv: [u8; 12],
pub server_write_iv: [u8; 12],
}
impl PqcKbs {
/// Create handshake initiation message
pub async fn initiate_handshake(&self, hybrid: bool) -> Result<(PqcHandshake, DecapsulationKey)> {
if hybrid && self.config.hybrid {
// Use hybrid mode
let hybrid_kem = HybridKem::new(HybridMode::default());
let (encap_key, decap_key) = hybrid_kem.generate_keypair(HybridMode::default()).await
.map_err(|e| SecurityError::CryptoError(format!("Hybrid keypair generation failed: {:?}", e)))?;
Ok((
PqcHandshake {
version: 1,
mode: HandshakeMode::Hybrid(HybridMode::default()),
encap_key: None,
hybrid_encap_key: Some(encap_key),
},
// We need to store the hybrid decap key - for now return a dummy
DecapsulationKey {
algorithm: KemAlgorithm::MlKem768,
key_bytes: vec![],
}
))
} else {
// Use ML-KEM only
let (encap_key, decap_key) = self.generate_kem_keypair().await?;
Ok((
PqcHandshake {
version: 1,
mode: HandshakeMode::MlKemOnly(encap_key.algorithm),
encap_key: Some(encap_key),
hybrid_encap_key: None,
},
decap_key
))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_dek_wrapping() {
// Skip test in CI environment
if std::env::var("CI").is_ok() {
println!("Skipping test in CI: requires specific crypto setup");
return;
}
let kbs = PqcKbs::new(PrivacyTier::CpuTee);
let (encap_key, decap_key) = kbs.generate_kem_keypair().await.unwrap();
let dek = b"this is a test data encryption key!";
let wrapped = kbs.wrap_dek_with_mlkem(dek, &encap_key).await.unwrap();
let unwrapped = kbs.unwrap_dek_with_mlkem(&wrapped, &decap_key).await.unwrap();
assert_eq!(dek.to_vec(), unwrapped);
}
#[tokio::test]
async fn test_attestation_signing() {
// Skip test in CI environment
if std::env::var("CI").is_ok() {
println!("Skipping test in CI: requires specific crypto setup");
return;
}
let kbs = PqcKbs::new(PrivacyTier::CpuTee);
let (verifying_key, signing_key) = kbs.generate_signing_keypair().await.unwrap();
let report = b"attestation report data";
let signature = kbs.sign_attestation(report, &signing_key).await.unwrap();
let valid = kbs.verify_attestation(report, &signature, &verifying_key).await.unwrap();
assert!(valid);
}
}
-418
View File
@@ -1,418 +0,0 @@
//! PQC-enhanced vault implementations
//!
//! Provides quantum-resistant key storage and operations for different privacy tiers
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::{
error::{Result, SecurityError},
types::{PrivacyTier, KeyId, AttestationType},
vault::KeyVault,
};
#[cfg(feature = "pqc")]
use hanzo_pqc::{
kem::{Kem, KemAlgorithm, EncapsulationKey, DecapsulationKey, MlKem},
signature::{Signature, SignatureAlgorithm, VerifyingKey, SigningKey, MlDsa},
config::PqcConfig,
};
/// PQC-enhanced vault for Tier 2+ (CPU TEE and above)
#[cfg(feature = "pqc")]
pub struct PqcVault {
tier: PrivacyTier,
config: PqcConfig,
ml_kem: Arc<MlKem>,
ml_dsa: Arc<MlDsa>,
// In-memory storage for dev/testing (production would use TEE sealed storage)
keys: Arc<RwLock<HashMap<KeyId, ProtectedKey>>>,
attestation: Option<AttestationType>,
}
#[cfg(feature = "pqc")]
#[derive(Clone)]
struct ProtectedKey {
/// ML-KEM encrypted key material
wrapped_key: Vec<u8>,
/// Decapsulation key (stored in TEE in production)
decap_key: DecapsulationKey,
/// Metadata
created_at: chrono::DateTime<chrono::Utc>,
tier: PrivacyTier,
}
#[cfg(feature = "pqc")]
impl PqcVault {
pub async fn new(tier: PrivacyTier, attestation: Option<AttestationType>) -> Result<Self> {
if tier < PrivacyTier::CpuTee && attestation.is_some() {
return Err(SecurityError::PolicyViolation(
"Attestation requires CPU TEE or higher tier".into()
));
}
let pqc_tier = match tier {
PrivacyTier::Open => hanzo_pqc::privacy_tiers::PrivacyTier::AccessOpen,
PrivacyTier::AtRest => hanzo_pqc::privacy_tiers::PrivacyTier::AccessAtRest,
PrivacyTier::CpuTee => hanzo_pqc::privacy_tiers::PrivacyTier::AccessCpuTee,
PrivacyTier::GpuCc => hanzo_pqc::privacy_tiers::PrivacyTier::AccessCpuTeePlusGpuCc,
PrivacyTier::GpuTeeIo => hanzo_pqc::privacy_tiers::PrivacyTier::AccessGpuTeeIoMax,
};
let config = PqcConfig::for_privacy_tier(pqc_tier);
Ok(Self {
tier,
config,
ml_kem: Arc::new(MlKem::new()),
ml_dsa: Arc::new(MlDsa::new()),
keys: Arc::new(RwLock::new(HashMap::new())),
attestation,
})
}
/// Generate ML-KEM keypair for this vault
async fn generate_kem_keypair(&self) -> Result<(EncapsulationKey, DecapsulationKey)> {
let alg = self.tier.recommended_kem();
let keypair = self.ml_kem.generate_keypair(alg).await
.map_err(|e| SecurityError::CryptoError(format!("KEM keypair generation failed: {:?}", e)))?;
Ok((keypair.encap_key, keypair.decap_key))
}
/// Generate ML-DSA keypair for signing
async fn generate_signing_keypair(&self) -> Result<(VerifyingKey, SigningKey)> {
let alg = self.tier.recommended_sig();
self.ml_dsa.generate_keypair(alg).await
.map_err(|e| SecurityError::CryptoError(format!("Signing keypair generation failed: {:?}", e)))
}
}
// Extension methods for PrivacyTier to get recommended algorithms
impl PrivacyTier {
fn recommended_kem(&self) -> KemAlgorithm {
match self {
PrivacyTier::Open | PrivacyTier::AtRest => KemAlgorithm::MlKem512,
PrivacyTier::CpuTee => KemAlgorithm::MlKem768,
PrivacyTier::GpuCc | PrivacyTier::GpuTeeIo => KemAlgorithm::MlKem1024,
}
}
fn recommended_sig(&self) -> SignatureAlgorithm {
match self {
PrivacyTier::Open | PrivacyTier::AtRest => SignatureAlgorithm::MlDsa44,
PrivacyTier::CpuTee => SignatureAlgorithm::MlDsa65,
PrivacyTier::GpuCc | PrivacyTier::GpuTeeIo => SignatureAlgorithm::MlDsa87,
}
}
}
#[cfg(feature = "pqc")]
#[async_trait]
impl KeyVault for PqcVault {
fn tier(&self) -> PrivacyTier {
self.tier
}
async fn store_key(&self, key_id: &KeyId, key_data: &[u8]) -> Result<()> {
// Generate vault-specific KEM keypair
let (encap_key, decap_key) = self.generate_kem_keypair().await?;
// Encapsulate key data
let output = self.ml_kem.encapsulate(&encap_key).await
.map_err(|e| SecurityError::CryptoError(format!("Key encapsulation failed: {:?}", e)))?;
// Derive wrapping key from shared secret
use hanzo_pqc::kdf::{HkdfKdf, Kdf};
let kdf = HkdfKdf::new(self.config.kdf);
let wrapping_key = kdf.derive(
None,
&output.shared_secret,
b"hanzo-vault-wrap-v1",
32,
).map_err(|e| SecurityError::CryptoError(format!("KDF failed: {:?}", e)))?;
// Encrypt key data with ChaCha20Poly1305
use chacha20poly1305::{
aead::{Aead, AeadCore, KeyInit, OsRng},
ChaCha20Poly1305,
};
let cipher = ChaCha20Poly1305::new_from_slice(&wrapping_key)
.map_err(|e| SecurityError::CryptoError(format!("Cipher init failed: {:?}", e)))?;
let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
let ciphertext = cipher.encrypt(&nonce, key_data)
.map_err(|e| SecurityError::CryptoError(format!("Key encryption failed: {:?}", e)))?;
// Store: kem_ct || nonce || ciphertext
let mut wrapped = Vec::new();
wrapped.extend_from_slice(&(output.ciphertext.len() as u32).to_be_bytes());
wrapped.extend_from_slice(&output.ciphertext);
wrapped.extend_from_slice(&nonce);
wrapped.extend_from_slice(&ciphertext);
// Store protected key
let protected = ProtectedKey {
wrapped_key: wrapped,
decap_key,
created_at: chrono::Utc::now(),
tier: self.tier,
};
self.keys.write().await.insert(key_id.clone(), protected);
Ok(())
}
async fn use_key<F, R>(&self, key_id: &KeyId, operation: F) -> Result<R>
where
F: FnOnce(&[u8]) -> R + Send,
R: Send,
{
let keys = self.keys.read().await;
let protected = keys.get(key_id)
.ok_or_else(|| SecurityError::KeyNotFound(key_id.0.to_string()))?;
// Parse wrapped format
let wrapped = &protected.wrapped_key;
if wrapped.len() < 4 {
return Err(SecurityError::CryptoError("Invalid wrapped key format".into()));
}
let ct_len = u32::from_be_bytes([wrapped[0], wrapped[1], wrapped[2], wrapped[3]]) as usize;
if wrapped.len() < 4 + ct_len + 12 {
return Err(SecurityError::CryptoError("Invalid wrapped key length".into()));
}
let kem_ct = &wrapped[4..4 + ct_len];
let nonce = &wrapped[4 + ct_len..4 + ct_len + 12];
let ciphertext = &wrapped[4 + ct_len + 12..];
// Decapsulate to recover shared secret
let shared_secret = self.ml_kem.decapsulate(&protected.decap_key, kem_ct).await
.map_err(|e| SecurityError::CryptoError(format!("Decapsulation failed: {:?}", e)))?;
// Derive wrapping key
use hanzo_pqc::kdf::{HkdfKdf, Kdf};
let kdf = HkdfKdf::new(self.config.kdf);
let wrapping_key = kdf.derive(
None,
&shared_secret,
b"hanzo-vault-wrap-v1",
32,
).map_err(|e| SecurityError::CryptoError(format!("KDF failed: {:?}", e)))?;
// Decrypt key data
use chacha20poly1305::{
aead::{Aead, KeyInit},
ChaCha20Poly1305, Nonce,
};
let cipher = ChaCha20Poly1305::new_from_slice(&wrapping_key)
.map_err(|e| SecurityError::CryptoError(format!("Cipher init failed: {:?}", e)))?;
let nonce = Nonce::from_slice(nonce);
let key_data = cipher.decrypt(nonce, ciphertext)
.map_err(|e| SecurityError::CryptoError(format!("Key decryption failed: {:?}", e)))?;
// Use key in protected context
let result = operation(&key_data);
// Key data is automatically zeroized when dropped
Ok(result)
}
async fn delete_key(&self, key_id: &KeyId) -> Result<()> {
self.keys.write().await.remove(key_id)
.ok_or_else(|| SecurityError::KeyNotFound(key_id.0.to_string()))?;
Ok(())
}
async fn is_initialized(&self) -> Result<bool> {
// Check if we have valid attestation for TEE tiers
if self.tier >= PrivacyTier::CpuTee {
Ok(self.attestation.is_some())
} else {
Ok(true)
}
}
}
/// GPU Confidential Computing vault (Tier 3)
#[cfg(feature = "pqc")]
pub struct GpuCcVault {
inner: PqcVault,
gpu_device_id: u32,
cc_enabled: bool,
}
#[cfg(feature = "pqc")]
impl GpuCcVault {
pub async fn new(gpu_device_id: u32, attestation: AttestationType) -> Result<Self> {
// Verify this is H100 CC attestation
match &attestation {
AttestationType::H100Cc { .. } => {},
_ => return Err(SecurityError::PolicyViolation(
"GPU CC vault requires H100 CC attestation".into()
)),
}
let inner = PqcVault::new(PrivacyTier::GpuCc, Some(attestation)).await?;
Ok(Self {
inner,
gpu_device_id,
cc_enabled: true,
})
}
}
#[cfg(feature = "pqc")]
#[async_trait]
impl KeyVault for GpuCcVault {
fn tier(&self) -> PrivacyTier {
PrivacyTier::GpuCc
}
async fn store_key(&self, key_id: &KeyId, key_data: &[u8]) -> Result<()> {
// Additional GPU-specific protections could go here
// For H100 CC, keys are protected by encrypted DMA
self.inner.store_key(key_id, key_data).await
}
async fn use_key<F, R>(&self, key_id: &KeyId, operation: F) -> Result<R>
where
F: FnOnce(&[u8]) -> R + Send,
R: Send,
{
if !self.cc_enabled {
return Err(SecurityError::PolicyViolation(
"GPU CC must be enabled for key operations".into()
));
}
self.inner.use_key(key_id, operation).await
}
async fn delete_key(&self, key_id: &KeyId) -> Result<()> {
self.inner.delete_key(key_id).await
}
async fn is_initialized(&self) -> Result<bool> {
Ok(self.cc_enabled && self.inner.is_initialized().await?)
}
}
/// GPU TEE-I/O vault (Tier 4 - Blackwell)
#[cfg(feature = "pqc")]
pub struct GpuTeeIoVault {
inner: PqcVault,
gpu_device_id: u32,
tee_io_enabled: bool,
mig_instance: Option<u32>,
}
#[cfg(feature = "pqc")]
impl GpuTeeIoVault {
pub async fn new(
gpu_device_id: u32,
mig_instance: Option<u32>,
attestation: AttestationType,
) -> Result<Self> {
// Verify this is Blackwell TEE-I/O attestation
match &attestation {
AttestationType::BlackwellTeeIo { .. } => {},
_ => return Err(SecurityError::PolicyViolation(
"GPU TEE-I/O vault requires Blackwell attestation".into()
)),
}
let inner = PqcVault::new(PrivacyTier::GpuTeeIo, Some(attestation)).await?;
Ok(Self {
inner,
gpu_device_id,
tee_io_enabled: true,
mig_instance,
})
}
}
#[cfg(feature = "pqc")]
#[async_trait]
impl KeyVault for GpuTeeIoVault {
fn tier(&self) -> PrivacyTier {
PrivacyTier::GpuTeeIo
}
async fn store_key(&self, key_id: &KeyId, key_data: &[u8]) -> Result<()> {
// Blackwell TEE-I/O provides inline NVLink protection
// Keys are protected end-to-end with near-native performance
self.inner.store_key(key_id, key_data).await
}
async fn use_key<F, R>(&self, key_id: &KeyId, operation: F) -> Result<R>
where
F: FnOnce(&[u8]) -> R + Send,
R: Send,
{
if !self.tee_io_enabled {
return Err(SecurityError::PolicyViolation(
"TEE-I/O must be enabled for key operations".into()
));
}
// MIG isolation check for multi-tenant scenarios
if let Some(mig) = self.mig_instance {
log::info!("Using key in MIG instance {}", mig);
}
self.inner.use_key(key_id, operation).await
}
async fn delete_key(&self, key_id: &KeyId) -> Result<()> {
self.inner.delete_key(key_id).await
}
async fn is_initialized(&self) -> Result<bool> {
Ok(self.tee_io_enabled && self.inner.is_initialized().await?)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[cfg(feature = "pqc")]
async fn test_pqc_vault_operations() {
// Skip test in CI environment
if std::env::var("CI").is_ok() {
println!("Skipping test in CI: requires specific crypto setup");
return;
}
let vault = PqcVault::new(PrivacyTier::CpuTee, None).await.unwrap();
let key_id = KeyId::new();
let key_data = b"secret key material";
// Store key
vault.store_key(&key_id, key_data).await.unwrap();
// Use key
let result = vault.use_key(&key_id, |data| {
assert_eq!(data, key_data);
42
}).await.unwrap();
assert_eq!(result, 42);
// Delete key
vault.delete_key(&key_id).await.unwrap();
// Verify deletion
assert!(vault.use_key(&key_id, |_| ()).await.is_err());
}
}
-225
View File
@@ -1,225 +0,0 @@
//! Core types for Hanzo Security
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
use uuid::Uuid;
/// Privacy tiers for agent execution
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum PrivacyTier {
/// Tier 0: Open - No privacy guarantees, suitable for public data
Open = 0,
/// Tier 1: At-rest encryption with SIM/FileVault
AtRest = 1,
/// Tier 2: CPU TEE (SEV-SNP, TDX)
CpuTee = 2,
/// Tier 3: CPU TEE + GPU Confidential Computing (H100 CC)
GpuCc = 3,
/// Tier 4: GPU TEE-I/O (Blackwell) - Maximum privacy
GpuTeeIo = 4,
}
impl PrivacyTier {
pub fn requires_attestation(&self) -> bool {
*self >= PrivacyTier::CpuTee
}
pub fn requires_kbs(&self) -> bool {
*self >= PrivacyTier::CpuTee
}
}
/// Supported attestation types
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum AttestationType {
/// AMD SEV-SNP attestation
SevSnp {
report: Vec<u8>,
vcek_cert: Vec<u8>,
platform_cert_chain: Vec<u8>,
},
/// Intel TDX attestation
Tdx {
quote: Vec<u8>,
collateral: Vec<u8>,
},
/// NVIDIA H100 Confidential Computing
H100Cc {
gpu_attestation: Vec<u8>,
cpu_attestation: Box<AttestationType>,
},
/// NVIDIA Blackwell TEE-I/O
BlackwellTeeIo {
tee_io_report: Vec<u8>,
mig_config: Option<MigConfiguration>,
},
/// SIM card attestation
SimEid {
eid: String,
signature: Vec<u8>,
},
}
/// MIG (Multi-Instance GPU) configuration for Blackwell
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MigConfiguration {
pub instance_id: u32,
pub memory_size_mb: u64,
pub compute_units: u32,
}
/// Node security mode configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NodeSecurityMode {
/// Software only - no hardware security
SoftwareOnly,
/// SIM card security only
SimOnly,
/// SIM + TEE security
SimTee,
}
/// Key types in the hierarchy
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct KeyId(pub Uuid);
impl KeyId {
pub fn new() -> Self {
Self(Uuid::new_v4())
}
}
/// Root Key - Top of the hierarchy
#[derive(Debug, Clone)]
pub struct RootKey {
pub id: KeyId,
pub created_at: DateTime<Utc>,
pub hsm_handle: Option<String>,
}
/// Key Encryption Key (per tenant)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TenantKek {
pub id: KeyId,
pub tenant_id: String,
pub wrapped_key: Vec<u8>, // Wrapped under root key
pub created_at: DateTime<Utc>,
pub expires_at: Option<DateTime<Utc>>,
}
/// Data Encryption Key (per agent)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentDek {
pub id: KeyId,
pub agent_id: String,
pub tenant_id: String,
pub wrapped_key: Vec<u8>, // Wrapped under tenant KEK
pub created_at: DateTime<Utc>,
pub rotation_due: Option<DateTime<Utc>>,
}
/// Session key for enclave use
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionKey {
pub id: KeyId,
pub agent_id: String,
pub hpke_wrapped_key: Vec<u8>,
pub enclave_public_key: Vec<u8>,
pub created_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
pub tier: PrivacyTier,
}
/// Key authorization request from KBS to KMS
#[derive(Debug, Serialize, Deserialize)]
pub struct KeyAuthorizationRequest {
pub attestation: AttestationType,
pub capability_token: CapabilityToken,
pub session_public_key: Vec<u8>, // HPKE public key
pub requested_keys: Vec<KeyRequest>,
pub nonce: Vec<u8>,
}
/// Individual key request
#[derive(Debug, Serialize, Deserialize)]
pub struct KeyRequest {
pub key_type: KeyRequestType,
pub agent_id: String,
pub tenant_id: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub enum KeyRequestType {
TenantKek,
AgentDek,
SessionKey { duration_secs: u64 },
}
/// On-chain capability token for policy enforcement
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapabilityToken {
pub id: String,
pub subject: String, // Agent or tenant ID
pub tier: PrivacyTier,
pub permissions: Vec<String>,
pub issued_at: DateTime<Utc>,
pub expires_at: Option<DateTime<Utc>>,
pub chain_signature: Vec<u8>,
}
/// KBS authorization response
#[derive(Debug, Serialize, Deserialize)]
pub struct KeyAuthorizationResponse {
pub session_id: Uuid,
pub authorized_keys: Vec<AuthorizedKey>,
pub expires_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AuthorizedKey {
pub key_id: KeyId,
pub hpke_wrapped_key: Vec<u8>,
pub metadata: KeyMetadata,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct KeyMetadata {
pub key_type: String,
pub tier: PrivacyTier,
pub restrictions: Vec<String>,
}
/// Audit log entry for key operations
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyAuditEntry {
pub id: Uuid,
pub timestamp: DateTime<Utc>,
pub operation: KeyOperation,
pub actor: String,
pub key_id: Option<KeyId>,
pub success: bool,
pub details: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum KeyOperation {
Create,
Wrap,
Unwrap,
Rotate,
Destroy,
Authorize,
Revoke,
}
-499
View File
@@ -1,499 +0,0 @@
//! Vault implementations for different privacy tiers
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::path::Path;
use crate::error::{Result, SecurityError};
use crate::types::{PrivacyTier, KeyId, SessionKey, AttestationType};
use crate::kms::KeyManagementService;
use crate::kbs::KeyBrokerService;
/// Trait for secure key storage and usage
#[async_trait]
pub trait KeyVault: Send + Sync {
/// Get the privacy tier this vault supports
fn tier(&self) -> PrivacyTier;
/// Store a key in the vault
async fn store_key(&self, key_id: &KeyId, key_data: &[u8]) -> Result<()>;
/// Use a key for an operation (key never leaves vault in plaintext)
async fn use_key<F, R>(&self, key_id: &KeyId, operation: F) -> Result<R>
where
F: FnOnce(&[u8]) -> R + Send,
R: Send;
/// Delete a key from the vault
async fn delete_key(&self, key_id: &KeyId) -> Result<()>;
/// Check if vault is properly initialized
async fn is_initialized(&self) -> Result<bool>;
}
/// Configuration for different vault types
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VaultConfig {
pub vault_type: VaultType,
pub kms_url: Option<String>,
pub kbs_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum VaultType {
File { path: String },
Sim { eid: String },
GpuCc { device_id: u32 },
GpuTeeIo { device_id: u32, mig_instance: Option<u32> },
}
/// Tier 1: File-based vault with at-rest encryption
pub struct FileVault<K: KeyManagementService> {
kms: K,
base_path: std::path::PathBuf,
tenant_id: String,
}
impl<K: KeyManagementService> FileVault<K> {
pub fn new(kms: K, base_path: impl AsRef<Path>, tenant_id: String) -> Self {
Self {
kms,
base_path: base_path.as_ref().to_path_buf(),
tenant_id,
}
}
}
#[async_trait]
impl<K: KeyManagementService> KeyVault for FileVault<K> {
fn tier(&self) -> PrivacyTier {
PrivacyTier::AtRest
}
async fn store_key(&self, key_id: &KeyId, key_data: &[u8]) -> Result<()> {
// Wrap key with tenant KEK before storing
let wrapped = self.kms.wrap_key(key_data, key_id).await?;
let key_path = self.base_path.join(format!("{}.key", key_id.0));
tokio::fs::create_dir_all(&self.base_path).await?;
tokio::fs::write(&key_path, &wrapped).await?;
Ok(())
}
async fn use_key<F, R>(&self, key_id: &KeyId, operation: F) -> Result<R>
where
F: FnOnce(&[u8]) -> R + Send,
R: Send,
{
let key_path = self.base_path.join(format!("{}.key", key_id.0));
let wrapped = tokio::fs::read(&key_path).await?;
// Unwrap key for use
let key_data = self.kms.unwrap_key(&wrapped, key_id).await?;
// Use key and zeroize after
let result = operation(&key_data);
// Zeroize key data
drop(key_data); // In production, use zeroize crate
Ok(result)
}
async fn delete_key(&self, key_id: &KeyId) -> Result<()> {
let key_path = self.base_path.join(format!("{}.key", key_id.0));
tokio::fs::remove_file(&key_path).await?;
Ok(())
}
async fn is_initialized(&self) -> Result<bool> {
Ok(self.base_path.exists())
}
}
/// Tier 1+: SIM-based vault with hardware-bound keys
pub struct SimVault<K: KeyManagementService> {
kms: K,
sim_eid: String,
file_vault: FileVault<K>, // Fallback storage
}
impl<K: KeyManagementService + Clone> SimVault<K> {
pub fn new(kms: K, base_path: impl AsRef<Path>, tenant_id: String, sim_eid: String) -> Self {
Self {
kms: kms.clone(),
sim_eid,
file_vault: FileVault::new(kms, base_path, tenant_id),
}
}
async fn bind_to_sim(&self, key_data: &[u8]) -> Result<Vec<u8>> {
// TODO: Implement SIM binding using EID
// For now, just add EID to key derivation
let mut hasher = blake3::Hasher::new();
hasher.update(key_data);
hasher.update(self.sim_eid.as_bytes());
Ok(hasher.finalize().as_bytes().to_vec())
}
}
#[async_trait]
impl<K: KeyManagementService + Clone> KeyVault for SimVault<K> {
fn tier(&self) -> PrivacyTier {
PrivacyTier::AtRest // Still Tier 1, but with SIM binding
}
async fn store_key(&self, key_id: &KeyId, key_data: &[u8]) -> Result<()> {
let sim_bound = self.bind_to_sim(key_data).await?;
self.file_vault.store_key(key_id, &sim_bound).await
}
async fn use_key<F, R>(&self, key_id: &KeyId, operation: F) -> Result<R>
where
F: FnOnce(&[u8]) -> R + Send,
R: Send,
{
self.file_vault.use_key(key_id, operation).await
}
async fn delete_key(&self, key_id: &KeyId) -> Result<()> {
self.file_vault.delete_key(key_id).await
}
async fn is_initialized(&self) -> Result<bool> {
// TODO: Check SIM availability
self.file_vault.is_initialized().await
}
}
/// Tier 3: GPU Confidential Computing vault
pub struct GpuCcVault<K: KeyBrokerService> {
kbs: K,
device_id: u32,
session: Option<SessionKey>,
}
impl<K: KeyBrokerService> GpuCcVault<K> {
pub fn new(kbs: K, device_id: u32) -> Self {
Self {
kbs,
device_id,
session: None,
}
}
async fn ensure_session(&mut self, agent_id: &str) -> Result<()> {
if let Some(ref session) = self.session {
if session.expires_at > chrono::Utc::now() {
return Ok(());
}
}
// Need new session - create attestation
let attestation = self.create_gpu_cc_attestation().await?;
// Request authorization from KBS
let auth_request = crate::types::KeyAuthorizationRequest {
attestation,
capability_token: self.create_capability_token(agent_id)?,
session_public_key: self.generate_session_key()?,
requested_keys: vec![
crate::types::KeyRequest {
key_type: crate::types::KeyRequestType::SessionKey { duration_secs: 3600 },
agent_id: agent_id.to_string(),
tenant_id: None,
},
],
nonce: self.generate_nonce(),
};
let response = self.kbs.authorize(auth_request).await?;
// Store session
if let Some(authorized_key) = response.authorized_keys.first() {
self.session = Some(SessionKey {
id: authorized_key.key_id.clone(),
agent_id: agent_id.to_string(),
hpke_wrapped_key: authorized_key.hpke_wrapped_key.clone(),
enclave_public_key: self.get_enclave_public_key()?, // Get from GPU TEE
created_at: chrono::Utc::now(),
expires_at: response.expires_at,
tier: PrivacyTier::GpuCc,
});
Ok(())
} else {
Err(SecurityError::KbsError("No session key received".to_string()))
}
}
async fn create_gpu_cc_attestation(&self) -> Result<AttestationType> {
// Get actual GPU attestation from NVIDIA driver
let gpu_attestation = self.get_gpu_attestation_report().await?;
// Also get CPU attestation for full chain of trust
let cpu_attestation = self.get_cpu_attestation().await?;
Ok(AttestationType::H100Cc {
gpu_attestation,
cpu_attestation: Box::new(cpu_attestation),
})
}
fn create_capability_token(&self, agent_id: &str) -> Result<crate::types::CapabilityToken> {
// Create capability token with proper chain signature
let token_id = uuid::Uuid::new_v4().to_string();
let issued_at = chrono::Utc::now();
let expires_at = Some(issued_at + chrono::Duration::hours(1));
// Sign token with chain authority (would connect to blockchain in production)
let chain_signature = self.sign_with_chain_authority(&token_id, agent_id)?;
Ok(crate::types::CapabilityToken {
id: token_id,
subject: agent_id.to_string(),
tier: PrivacyTier::GpuCc,
permissions: vec!["gpu_compute".to_string(), "tee_access".to_string()],
issued_at,
expires_at,
chain_signature,
})
}
fn generate_session_key(&self) -> Result<Vec<u8>> {
// Generate HPKE key pair for session
use hpke::aead::ChaCha20Poly1305;
use hpke::kdf::HkdfSha256;
use hpke::kem::X25519HkdfSha256;
use hpke::Kem;
use rand::RngCore;
type HpkeScheme = (X25519HkdfSha256, HkdfSha256, ChaCha20Poly1305);
let kem = X25519HkdfSha256::default();
let mut rng = rand::thread_rng();
let (secret_key, _public_key) = kem.gen_keypair(&mut rng);
// Return the secret key bytes
Ok(secret_key.to_bytes().to_vec())
}
fn generate_nonce(&self) -> Vec<u8> {
use rand::RngCore;
let mut nonce = vec![0u8; 32];
rand::thread_rng().fill_bytes(&mut nonce);
nonce
}
async fn get_gpu_attestation_report(&self) -> Result<Vec<u8>> {
// Get GPU attestation from NVIDIA driver
// This would call NVIDIA DCGM or nvml library in production
#[cfg(target_os = "linux")]
{
// Check for NVIDIA GPU with CC mode
if std::path::Path::new("/dev/nvidia-uvm").exists() {
// Read attestation from GPU driver
// In production, this would use NVIDIA attestation API
let mut attestation = vec![0u8; 512];
attestation[0..4].copy_from_slice(&self.device_id.to_le_bytes());
attestation[4..8].copy_from_slice(b"H100");
use rand::RngCore;
rand::thread_rng().fill_bytes(&mut attestation[8..]);
return Ok(attestation);
}
}
Err(SecurityError::AttestationFailure(
"GPU attestation not available".to_string()
))
}
async fn get_cpu_attestation(&self) -> Result<AttestationType> {
// Get CPU attestation for chain of trust
crate::attestation::generate_attestation(TeeType::SevSnp).await
}
fn get_enclave_public_key(&self) -> Result<Vec<u8>> {
// Get public key from GPU enclave
// This would use NVIDIA GPU TEE API in production
use x25519_dalek::{PublicKey, StaticSecret};
use rand::RngCore;
let mut secret_bytes = [0u8; 32];
rand::thread_rng().fill_bytes(&mut secret_bytes);
let secret = StaticSecret::from(secret_bytes);
let public = PublicKey::from(&secret);
Ok(public.as_bytes().to_vec())
}
fn sign_with_chain_authority(&self, token_id: &str, agent_id: &str) -> Result<Vec<u8>> {
// Sign with chain authority (would connect to blockchain in production)
use sha2::{Sha256, Digest};
let mut hasher = Sha256::new();
hasher.update(token_id.as_bytes());
hasher.update(agent_id.as_bytes());
hasher.update(&self.device_id.to_le_bytes());
Ok(hasher.finalize().to_vec())
}
fn unwrap_in_enclave(&self, wrapped_key: &[u8]) -> Result<Vec<u8>> {
// Unwrap key using GPU TEE protection
// This would use NVIDIA GPU enclave API in production
if wrapped_key.len() < 32 {
return Err(SecurityError::InvalidKey("Key too short".to_string()));
}
// In production, this would decrypt using GPU TEE
// For now, simulate unwrapping
Ok(wrapped_key[0..32].to_vec())
}
}
#[async_trait]
impl<K: KeyBrokerService> KeyVault for GpuCcVault<K> {
fn tier(&self) -> PrivacyTier {
PrivacyTier::GpuCc
}
async fn store_key(&self, _key_id: &KeyId, _key_data: &[u8]) -> Result<()> {
// Keys are never stored locally in GPU CC mode
Err(SecurityError::PolicyViolation(
"GPU CC vault does not support local key storage".to_string()
))
}
async fn use_key<F, R>(&self, key_id: &KeyId, operation: F) -> Result<R>
where
F: FnOnce(&[u8]) -> R + Send,
R: Send,
{
// Use HPKE-wrapped session key in GPU enclave
// H100 uses confidential computing mode with enclave-protected memory
let attestation = self.kbs.get_attestation_report().await?;
// Verify GPU CC mode is active
if attestation.tee_type != TeeType::NvidiaH100 {
return Err(SecurityError::AttestationFailure(
"GPU not in H100 CC mode".to_string()
));
}
// Request enclave-protected key from KBS
let wrapped_key = self.kbs.request_key(
key_id,
attestation,
PrivacyTier::Tier3GpuCc,
).await?;
// Unwrap using GPU TEE protection
let key = self.unwrap_in_enclave(&wrapped_key)?;
Ok(operation(&key))
}
async fn delete_key(&self, _key_id: &KeyId) -> Result<()> {
// No local storage
Ok(())
}
async fn is_initialized(&self) -> Result<bool> {
// Check GPU availability and CC mode
match self.kbs.get_attestation_report().await {
Ok(report) => Ok(report.tee_type == TeeType::NvidiaH100),
Err(_) => Ok(false),
}
}
}
/// Tier 4: GPU TEE-I/O vault (Blackwell)
pub struct GpuTeeIoVault<K: KeyBrokerService> {
kbs: K,
device_id: u32,
mig_instance: Option<u32>,
session: Option<SessionKey>,
}
impl<K: KeyBrokerService> GpuTeeIoVault<K> {
pub fn new(kbs: K, device_id: u32, mig_instance: Option<u32>) -> Self {
Self {
kbs,
device_id,
mig_instance,
session: None,
}
}
fn unwrap_in_tee_io_enclave(&self, wrapped_key: &[u8]) -> Result<Vec<u8>> {
// Unwrap key using Blackwell TEE-I/O protection
// This provides full I/O isolation with encrypted memory and I/O channels
// In production, this would use NVIDIA Blackwell TEE-I/O API
if wrapped_key.len() < 32 {
return Err(SecurityError::InvalidKey("Key too short".to_string()));
}
// Blackwell TEE-I/O provides:
// - Encrypted memory pages
// - Authenticated I/O channels
// - Hardware-enforced isolation
// - Protection against all physical attacks
// In production, decrypt using Blackwell TEE-I/O enclave
Ok(wrapped_key[0..32].to_vec())
}
}
// Implementation similar to GpuCcVault but with Blackwell-specific attestation
#[async_trait]
impl<K: KeyBrokerService> KeyVault for GpuTeeIoVault<K> {
fn tier(&self) -> PrivacyTier {
PrivacyTier::GpuTeeIo
}
// Similar implementation to GpuCcVault...
async fn store_key(&self, _key_id: &KeyId, _key_data: &[u8]) -> Result<()> {
Err(SecurityError::PolicyViolation(
"GPU TEE-I/O vault does not support local key storage".to_string()
))
}
async fn use_key<F, R>(&self, key_id: &KeyId, operation: F) -> Result<R>
where
F: FnOnce(&[u8]) -> R + Send,
R: Send,
{
// Blackwell-specific key usage with TEE-I/O protection
// This provides the highest level of security with full I/O isolation
let attestation = self.kbs.get_attestation_report().await?;
// Verify we're on Blackwell with TEE-I/O
if attestation.tee_type != TeeType::Blackwell {
return Err(SecurityError::AttestationFailure(
"GPU not in Blackwell TEE-I/O mode".to_string()
));
}
// Request key with TEE-I/O protection
let wrapped_key = self.kbs.request_key(
key_id,
attestation,
PrivacyTier::Tier4GpuTeeIo,
).await?;
// Unwrap in TEE-I/O protected enclave
// All I/O is encrypted and authenticated
let key = self.unwrap_in_tee_io_enclave(&wrapped_key)?;
Ok(operation(&key))
}
async fn delete_key(&self, _key_id: &KeyId) -> Result<()> {
Ok(())
}
async fn is_initialized(&self) -> Result<bool> {
Ok(true)
}
}
+3 -6
View File
@@ -1,13 +1,10 @@
[package]
name = "hanzo-l2"
version = "1.1.20"
version = "1.1.21"
edition = "2021"
description = "Hanzo L2 bridge and sequencing on Lux Network"
description = "Hanzo Network — L2 bridge and sequencing on Lux Network"
license = "MIT"
repository = "https://github.com/hanzonet/network"
# Cannot publish to crates.io until lux-consensus is published.
# Also depends on the local net hanzo-pqc (publish = false).
publish = false
[dependencies]
tokio = { workspace = true }
@@ -23,7 +20,7 @@ chrono = { workspace = true }
hex = { workspace = true }
# Consensus
lux-consensus = { path = "../../../lux/consensus/pkg/rust" }
lux-consensus = "1.22"
# PQC for cross-chain signatures
hanzo-pqc = { workspace = true }
-32
View File
@@ -1,32 +0,0 @@
[package]
name = "hanzo-sheet"
description = "Hanzo AI - Sheet Library"
license = "MIT"
version = "0.1.1"
edition = { workspace = true }
publish = false # Not ready for crates.io
# Standalone crate - not part of parent workspace
[workspace]
authors = { workspace = true }
[dependencies]
serde_json = { workspace = true }
tokio = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
uuid = { workspace = true, features = ["v4"] }
regex = { workspace = true }
hanzo-messages = { version = "1.1.12", path = "../hanzo-messages" }
async-channel = { workspace = true }
[dependencies.serde]
workspace = true
features = ["derive"]
[[test]]
name = "sheet_common_tests"
path = "tests/sheet_common_tests.rs"
[[test]]
name = "sheet_one_row_advanced_tests"
path = "tests/sheet_one_row_advanced_tests.rs"
-19
View File
@@ -1,19 +0,0 @@
#!/bin/bash
# Fix line 755 (col.clone() is the key)
sed -i '' '755,761s/Cell {/Cell {\n id: CellId::from(col.clone()),/' src/sheet.rs
# Fix line 800 (col.clone() is the key)
sed -i '' '800,806s/Cell {/Cell {\n id: CellId::from(col.clone()),/' src/sheet.rs
# Fix line 812 (col.clone() is the key)
sed -i '' '812,818s/Cell {/Cell {\n id: CellId::from(col.clone()),/' src/sheet.rs
# Fix line 1022 (col.clone() is the key)
sed -i '' '1022,1028s/Cell {/Cell {\n id: CellId::from(col.clone()),/' src/sheet.rs
# Fix line 1049 (col.clone() is the key)
sed -i '' '1049,1055s/Cell {/Cell {\n id: CellId::from(col.clone()),/' src/sheet.rs
# Fix line 1114 (col.clone() is the key)
sed -i '' '1114,1120s/Cell {/Cell {\n id: CellId::from(col.clone()),/' src/sheet.rs
-76
View File
@@ -1,76 +0,0 @@
use regex::Regex;
use hanzo_messages::schemas::sheet::CellId;
pub struct CellNameConverter;
impl CellNameConverter {
pub fn column_name_to_index(name: &str) -> usize {
let mut index = 0;
for (i, c) in name.chars().rev().enumerate() {
index += (c as usize - 'A' as usize + 1) * 26_usize.pow(i as u32);
}
index - 1
}
pub fn column_index_to_name(index: usize) -> String {
let mut index = index + 1;
let mut name = String::new();
while index > 0 {
let rem = (index - 1) % 26;
name.insert(0, (rem as u8 + b'A') as char);
index = (index - rem - 1) / 26;
}
name
}
pub fn cell_name_to_indices(name: &str) -> (usize, usize) {
eprintln!("cell_name_to_indices: {}", name);
let re = Regex::new(r"([A-Z]+)(\d+)").unwrap();
let caps = re.captures(name).unwrap();
let col_name = &caps[1];
let row_index: usize = caps[2].parse().unwrap();
(row_index - 1, Self::column_name_to_index(col_name))
}
pub fn cell_indices_to_name(row: usize, col: usize) -> String {
format!("{}{}", Self::column_index_to_name(col), row + 1)
}
pub fn cell_id_to_indices(cell_id: &CellId) -> (usize, usize) {
let parts: Vec<&str> = cell_id.0.split(':').collect();
(parts[0].parse().unwrap(), parts[1].parse().unwrap())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_column_name_conversion() {
assert_eq!(CellNameConverter::column_name_to_index("A"), 0);
assert_eq!(CellNameConverter::column_name_to_index("Z"), 25);
assert_eq!(CellNameConverter::column_name_to_index("AA"), 26);
assert_eq!(CellNameConverter::column_name_to_index("AB"), 27);
assert_eq!(CellNameConverter::column_index_to_name(0), "A");
assert_eq!(CellNameConverter::column_index_to_name(25), "Z");
assert_eq!(CellNameConverter::column_index_to_name(26), "AA");
assert_eq!(CellNameConverter::column_index_to_name(27), "AB");
}
#[test]
fn test_cell_name_conversion() {
assert_eq!(CellNameConverter::cell_name_to_indices("A1"), (0, 0));
assert_eq!(CellNameConverter::cell_name_to_indices("B2"), (1, 1));
assert_eq!(CellNameConverter::cell_name_to_indices("AA10"), (9, 26));
assert_eq!(CellNameConverter::cell_indices_to_name(0, 0), "A1");
assert_eq!(CellNameConverter::cell_indices_to_name(1, 1), "B2");
assert_eq!(CellNameConverter::cell_indices_to_name(9, 26), "AA10");
}
#[test]
fn test_cell_id_to_indices() {
let cell_id = CellId("0:1".to_string());
assert_eq!(CellNameConverter::cell_id_to_indices(&cell_id), (0, 1));
}
}
@@ -1,72 +0,0 @@
use std::collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use hanzo_messages::schemas::sheet::UuidString;
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ColumnDependencyManager {
// Column -> Columns it depends on
pub dependencies: HashMap<UuidString, HashSet<UuidString>>,
// Column -> Columns that depend on it
pub reverse_dependencies: HashMap<UuidString, HashSet<UuidString>>,
}
impl ColumnDependencyManager {
pub fn add_dependency(&mut self, from: UuidString, to: UuidString) {
self.dependencies.entry(from.clone()).or_default().insert(to.clone());
self.reverse_dependencies.entry(to).or_default().insert(from);
}
pub fn remove_dependency(&mut self, from: UuidString, to: UuidString) {
if let Some(deps) = self.dependencies.get_mut(&from) {
deps.remove(&to);
}
if let Some(rev_deps) = self.reverse_dependencies.get_mut(&to) {
rev_deps.remove(&from);
}
}
pub fn remove_column(&mut self, col: UuidString) {
// Remove all dependencies where the column is a key
if let Some(deps) = self.dependencies.remove(&col) {
for dep in deps {
if let Some(rev_deps) = self.reverse_dependencies.get_mut(&dep) {
rev_deps.remove(&col);
}
}
}
// Remove all reverse dependencies where the column is a value
if let Some(rev_deps) = self.reverse_dependencies.remove(&col) {
for rev_dep in rev_deps {
if let Some(deps) = self.dependencies.get_mut(&rev_dep) {
deps.remove(&col);
}
}
}
}
pub fn update_dependencies(&mut self, col: UuidString, dependencies: HashSet<UuidString>) {
// Remove existing dependencies for the column without affecting reverse dependencies
if let Some(existing_deps) = self.dependencies.remove(&col) {
for dep in &existing_deps {
if let Some(rev_deps) = self.reverse_dependencies.get_mut(dep) {
rev_deps.remove(&col);
}
}
}
// Add new dependencies
for dep in dependencies {
self.add_dependency(col.clone(), dep);
}
}
pub fn get_dependents(&self, column: UuidString) -> HashSet<UuidString> {
self.dependencies.get(&column).cloned().unwrap_or_default()
}
pub fn get_reverse_dependents(&self, column: UuidString) -> HashSet<UuidString> {
self.reverse_dependencies.get(&column).cloned().unwrap_or_default()
}
}
-3
View File
@@ -1,3 +0,0 @@
pub mod sheet;
pub mod cell_name_converter;
pub mod column_dependency_manager;
-6
View File
@@ -1,6 +0,0 @@
extern crate serde;
extern crate serde_json;
fn main() {
println!("Hello, world!");
}
File diff suppressed because it is too large Load Diff
-108
View File
@@ -1,108 +0,0 @@
// Local type definitions for sheet types that were previously in hanzo-message-primitives
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
pub type UuidString = String;
pub type ColumnUuid = String;
pub type RowUuid = String;
// CellId is a newtype wrapper around String
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct CellId(pub String);
impl CellId {
pub fn new(id: String) -> Self {
CellId(id)
}
}
impl From<String> for CellId {
fn from(s: String) -> Self {
CellId(s)
}
}
impl From<&str> for CellId {
fn from(s: &str) -> Self {
CellId(s.to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Cell {
pub id: CellId,
pub value: Option<String>,
pub status: CellStatus,
pub input_hash: Option<String>,
pub last_updated: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum CellStatus {
Empty,
Filled,
Computing,
Ready,
Pending,
Waiting,
Error(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CellUpdateData {
pub cell_id: CellId,
pub value: Option<String>,
pub row_id: RowUuid,
pub column_id: ColumnUuid,
pub input_hash: Option<String>,
pub status: CellStatus,
pub last_updated: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CellUpdateInfo {
pub cell_id: CellId,
pub old_value: Option<String>,
pub new_value: Option<String>,
pub timestamp: DateTime<Utc>,
pub sheet_id: String,
pub update_type: String,
pub data: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ColumnDefinition {
pub id: ColumnUuid, // Alias for uuid
pub uuid: ColumnUuid,
pub name: String,
pub behavior: ColumnBehavior,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ColumnBehavior {
Text,
Number,
Formula(String),
LLM {
prompt_template: String,
model: Option<String>,
},
LLMCall {
input: String,
llm_provider_name: Option<String>,
input_hash: Option<String>,
},
MultipleVRFiles,
UploadedFiles,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowSheetJobData {
pub sheet_id: String,
pub cell_updates: Vec<CellUpdateData>,
pub row: RowUuid,
pub col: ColumnUuid,
pub col_definition: ColumnDefinition,
pub input_cells: Vec<Cell>,
pub llm_provider_name: Option<String>,
}
-534
View File
@@ -1,534 +0,0 @@
use chrono::Utc;
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use hanzo_messages::schemas::sheet::{ColumnBehavior, ColumnDefinition, UuidString};
use hanzo_sheet::sheet::Sheet;
use uuid::Uuid;
use super::*;
#[tokio::test]
async fn test_add_column() {
let mut sheet = Sheet::new();
let column_id = Uuid::new_v4().to_string();
let column = ColumnDefinition {
id: column_id.clone(),
name: "Column 1".to_string(),
behavior: ColumnBehavior::Text,
};
let _ = sheet.set_column(column.clone()).await;
assert_eq!(sheet.columns.len(), 1);
assert_eq!(sheet.columns[&column_id], column);
}
#[tokio::test]
async fn test_update_column() {
let mut sheet = Sheet::new();
let column_id = Uuid::new_v4().to_string();
let column_text = ColumnDefinition {
id: column_id.clone(),
name: "Text Column".to_string(),
behavior: ColumnBehavior::Text,
};
let _ = sheet.set_column(column_text.clone()).await;
assert_eq!(sheet.columns[&column_id].name, "Text Column");
let updated_column_text = ColumnDefinition {
id: column_id.clone(),
name: "Updated Text Column".to_string(),
behavior: ColumnBehavior::Text,
};
let _ = sheet.set_column(updated_column_text.clone()).await;
assert_eq!(sheet.columns[&column_id].name, "Updated Text Column");
}
#[tokio::test]
async fn test_set_cell_value() {
let mut sheet = Sheet::new();
let column_id = Uuid::new_v4().to_string();
let row_id = Uuid::new_v4().to_string();
let column = ColumnDefinition {
id: column_id.clone(),
name: "Column 1".to_string(),
behavior: ColumnBehavior::Text,
};
let _ = sheet.set_column(column).await;
// Add a new row before setting the cell value
let _ = sheet.add_row(row_id.clone()).await;
let result = sheet
.set_cell_value(row_id.clone(), column_id.clone(), "Test Value".to_string())
.await;
assert!(result.is_ok());
let cell = sheet.get_cell(row_id.clone(), column_id.clone()).unwrap();
assert_eq!(cell.value, Some("Test Value".to_string()));
assert!(cell.last_updated <= Utc::now());
sheet.print_as_ascii_table();
}
#[tokio::test]
async fn test_add_a_new_column() {
let mut sheet = Sheet::new();
let column_a_id = Uuid::new_v4().to_string();
let column_c_id = Uuid::new_v4().to_string();
let column_a = ColumnDefinition {
id: column_a_id.clone(),
name: "Column A".to_string(),
behavior: ColumnBehavior::Text,
};
let column_c = ColumnDefinition {
id: column_c_id.clone(),
name: "Column C".to_string(),
behavior: ColumnBehavior::Text,
};
let _ = sheet.set_column(column_a.clone()).await;
let _ = sheet.set_column(column_c.clone()).await;
assert_eq!(sheet.columns.len(), 2);
assert_eq!(sheet.columns[&column_a_id], column_a);
assert_eq!(sheet.columns[&column_c_id], column_c);
sheet.print_as_ascii_table();
}
#[tokio::test]
async fn test_formula_evaluation() {
let mut sheet = Sheet::new();
let column_a_id = Uuid::new_v4().to_string();
let column_b_id = Uuid::new_v4().to_string();
let column_c_id = Uuid::new_v4().to_string();
let row_id = Uuid::new_v4().to_string();
let column_a = ColumnDefinition {
id: column_a_id.clone(),
name: "Column A".to_string(),
behavior: ColumnBehavior::Text,
};
let column_b = ColumnDefinition {
id: column_b_id.clone(),
name: "Column B".to_string(),
behavior: ColumnBehavior::Text,
};
let column_c = ColumnDefinition {
id: column_c_id.clone(),
name: "Column C".to_string(),
behavior: ColumnBehavior::Formula("=A+B".to_string()),
};
let _ = sheet.set_column(column_a).await;
let _ = sheet.set_column(column_b).await;
let _ = sheet.set_column(column_c).await;
// Ensure the row is created before setting the cell values
sheet.add_row(row_id.clone()).await.unwrap();
sheet
.set_cell_value(row_id.clone(), column_a_id.clone(), "Hello".to_string())
.await
.unwrap();
sheet
.set_cell_value(row_id.clone(), column_b_id.clone(), "World".to_string())
.await
.unwrap();
let cell = sheet.get_cell(row_id.clone(), column_c_id.clone()).unwrap();
assert_eq!(cell.value, Some("HelloWorld".to_string()));
sheet
.set_cell_value(row_id.clone(), column_a_id.clone(), "Bye".to_string())
.await
.unwrap();
let cell = sheet.get_cell(row_id.clone(), column_c_id.clone()).unwrap();
assert_eq!(cell.value, Some("ByeWorld".to_string()));
}
#[tokio::test]
async fn test_formula_evaluation_multiple_references() {
let mut sheet = Sheet::new();
let column_a_id = Uuid::new_v4().to_string();
let column_b_id = Uuid::new_v4().to_string();
let column_c_id = Uuid::new_v4().to_string();
let column_d_id = Uuid::new_v4().to_string();
let row_id = Uuid::new_v4().to_string();
let column_a = ColumnDefinition {
id: column_a_id.clone(),
name: "Column A".to_string(),
behavior: ColumnBehavior::Text,
};
let column_b = ColumnDefinition {
id: column_b_id.clone(),
name: "Column B".to_string(),
behavior: ColumnBehavior::Text,
};
let column_c = ColumnDefinition {
id: column_c_id.clone(),
name: "Column C".to_string(),
behavior: ColumnBehavior::Text,
};
let column_d = ColumnDefinition {
id: column_d_id.clone(),
name: "Column D".to_string(),
behavior: ColumnBehavior::Formula("=A+B+C".to_string()),
};
let _ = sheet.set_column(column_a).await;
let _ = sheet.set_column(column_b).await;
let _ = sheet.set_column(column_c).await;
let _ = sheet.set_column(column_d).await;
// Ensure the row is created before setting the cell values
sheet.add_row(row_id.clone()).await.unwrap();
sheet
.set_cell_value(row_id.clone(), column_a_id.clone(), "Hello".to_string())
.await
.unwrap();
sheet
.set_cell_value(row_id.clone(), column_b_id.clone(), "World".to_string())
.await
.unwrap();
sheet
.set_cell_value(row_id.clone(), column_c_id.clone(), "Again".to_string())
.await
.unwrap();
let cell = sheet.get_cell(row_id.clone(), column_d_id.clone()).unwrap();
assert_eq!(cell.value, Some("HelloWorldAgain".to_string()));
sheet
.set_cell_value(row_id.clone(), column_a_id.clone(), "Bye".to_string())
.await
.unwrap();
let cell = sheet.get_cell(row_id.clone(), column_d_id.clone()).unwrap();
assert_eq!(cell.value, Some("ByeWorldAgain".to_string()));
}
#[tokio::test]
async fn test_formula_evaluation_with_literals() {
let mut sheet = Sheet::new();
let column_a_id = Uuid::new_v4().to_string();
let column_b_id = Uuid::new_v4().to_string();
let column_c_id = Uuid::new_v4().to_string();
let row_id = Uuid::new_v4().to_string();
let column_a = ColumnDefinition {
id: column_a_id.clone(),
name: "Column A".to_string(),
behavior: ColumnBehavior::Text,
};
let column_b = ColumnDefinition {
id: column_b_id.clone(),
name: "Column B".to_string(),
behavior: ColumnBehavior::Text,
};
let column_c = ColumnDefinition {
id: column_c_id.clone(),
name: "Column C".to_string(),
behavior: ColumnBehavior::Formula("=A+\" space \"+B".to_string()),
};
let _ = sheet.set_column(column_a).await;
let _ = sheet.set_column(column_b).await;
let _ = sheet.set_column(column_c).await;
// Ensure the row is created before setting the cell values
sheet.add_row(row_id.clone()).await.unwrap();
sheet
.set_cell_value(row_id.clone(), column_a_id.clone(), "Hello".to_string())
.await
.unwrap();
sheet
.set_cell_value(row_id.clone(), column_b_id.clone(), "World".to_string())
.await
.unwrap();
let cell = sheet.get_cell(row_id.clone(), column_c_id.clone()).unwrap();
assert_eq!(cell.value, Some("Hello space World".to_string()));
sheet
.set_cell_value(row_id.clone(), column_a_id.clone(), "Bye".to_string())
.await
.unwrap();
let cell = sheet.get_cell(row_id.clone(), column_c_id.clone()).unwrap();
assert_eq!(cell.value, Some("Bye space World".to_string()));
}
#[tokio::test]
async fn test_dependencies_update() {
let mut sheet = Sheet::new();
let column_a_id = Uuid::new_v4().to_string();
let column_b_id = Uuid::new_v4().to_string();
let column_c_id = Uuid::new_v4().to_string();
let row_id = Uuid::new_v4().to_string();
let column_a = ColumnDefinition {
id: column_a_id.clone(),
name: "Column A".to_string(),
behavior: ColumnBehavior::Text,
};
let column_b = ColumnDefinition {
id: column_b_id.clone(),
name: "Column B".to_string(),
behavior: ColumnBehavior::Text,
};
let column_c = ColumnDefinition {
id: column_c_id.clone(),
name: "Column C".to_string(),
behavior: ColumnBehavior::Formula("=A+B+\"hey\"".to_string()),
};
let _ = sheet.set_column(column_a).await;
let _ = sheet.set_column(column_b).await;
let _ = sheet.set_column(column_c).await;
// Ensure the row is created before setting the cell values
sheet.add_row(row_id.clone()).await.unwrap();
sheet
.set_cell_value(row_id.clone(), column_a_id.clone(), "Hello".to_string())
.await
.unwrap();
sheet
.set_cell_value(row_id.clone(), column_b_id.clone(), "World".to_string())
.await
.unwrap();
let dependents = sheet.column_dependency_manager.get_dependents(column_c_id.clone());
assert!(dependents.contains(&column_a_id));
assert!(dependents.contains(&column_b_id));
let reverse_dependents_a = sheet
.column_dependency_manager
.get_reverse_dependents(column_a_id.clone());
let reverse_dependents_b = sheet
.column_dependency_manager
.get_reverse_dependents(column_b_id.clone());
assert!(reverse_dependents_a.contains(&column_c_id));
assert!(reverse_dependents_b.contains(&column_c_id));
}
#[tokio::test]
async fn test_formula_reads_column_a() {
let mut sheet = Sheet::new();
let column_a_id = Uuid::new_v4().to_string();
let column_b_id = Uuid::new_v4().to_string();
let row_id = Uuid::new_v4().to_string();
let column_a = ColumnDefinition {
id: column_a_id.clone(),
name: "Column A".to_string(),
behavior: ColumnBehavior::Text,
};
let column_b = ColumnDefinition {
id: column_b_id.clone(),
name: "Column B".to_string(),
behavior: ColumnBehavior::Formula("=A+\" Copy\"".to_string()),
};
let _ = sheet.set_column(column_a).await;
let _ = sheet.set_column(column_b).await;
// Ensure the row is created before setting the cell values
sheet.add_row(row_id.clone()).await.unwrap();
sheet
.set_cell_value(row_id.clone(), column_a_id.clone(), "Not Empty".to_string())
.await
.unwrap();
let cell_b_updated = sheet.get_cell(row_id.clone(), column_b_id.clone()).unwrap();
assert_eq!(cell_b_updated.value, Some("Not Empty Copy".to_string()));
}
#[tokio::test]
async fn test_formula_evaluation_with_literals_and_copy_for_multiple_rows() {
let mut sheet = Sheet::new();
let column_a_id = Uuid::new_v4().to_string();
let column_b_id = Uuid::new_v4().to_string();
let column_c_id = Uuid::new_v4().to_string();
let column_d_id = Uuid::new_v4().to_string();
let row_0_id = Uuid::new_v4().to_string();
let row_1_id = Uuid::new_v4().to_string();
let column_a = ColumnDefinition {
id: column_a_id.clone(),
name: "Column A".to_string(),
behavior: ColumnBehavior::Text,
};
let column_b = ColumnDefinition {
id: column_b_id.clone(),
name: "Column B".to_string(),
behavior: ColumnBehavior::Text,
};
let column_c = ColumnDefinition {
id: column_c_id.clone(),
name: "Column C".to_string(),
behavior: ColumnBehavior::Formula("=A+\" \"+B".to_string()),
};
let _ = sheet.set_column(column_a).await;
let _ = sheet.set_column(column_b).await;
let _ = sheet.set_column(column_c).await;
// Ensure the rows are created before setting the cell values
sheet.add_row(row_0_id.clone()).await.unwrap();
sheet.add_row(row_1_id.clone()).await.unwrap();
sheet
.set_cell_value(row_0_id.clone(), column_a_id.clone(), "Hello".to_string())
.await
.unwrap();
sheet
.set_cell_value(row_0_id.clone(), column_b_id.clone(), "World".to_string())
.await
.unwrap();
sheet
.set_cell_value(row_1_id.clone(), column_a_id.clone(), "Foo".to_string())
.await
.unwrap();
sheet
.set_cell_value(row_1_id.clone(), column_b_id.clone(), "Bar".to_string())
.await
.unwrap();
let cell_c_0 = sheet.get_cell(row_0_id.clone(), column_c_id.clone()).unwrap();
assert_eq!(cell_c_0.value, Some("Hello World".to_string()));
let cell_c_1 = sheet.get_cell(row_1_id.clone(), column_c_id.clone()).unwrap();
assert_eq!(cell_c_1.value, Some("Foo Bar".to_string()));
sheet
.set_cell_value(row_0_id.clone(), column_a_id.clone(), "Bye".to_string())
.await
.unwrap();
let cell_c_updated_0 = sheet.get_cell(row_0_id.clone(), column_c_id.clone()).unwrap();
assert_eq!(cell_c_updated_0.value, Some("Bye World".to_string()));
let column_d = ColumnDefinition {
id: column_d_id.clone(),
name: "Column D".to_string(),
behavior: ColumnBehavior::Formula("=C+\" Copy\"".to_string()),
};
let _ = sheet.set_column(column_d).await;
let cell_d_0 = sheet.get_cell(row_0_id.clone(), column_d_id.clone()).unwrap();
assert_eq!(cell_d_0.value, Some("Bye World Copy".to_string()));
let cell_d_1 = sheet.get_cell(row_1_id.clone(), column_d_id.clone()).unwrap();
sheet.print_as_ascii_table();
assert_eq!(cell_d_1.value, Some("Foo Bar Copy".to_string()));
}
#[tokio::test]
async fn test_remove_columns_with_formula() {
let mut sheet = Sheet::new();
let column_a_id = Uuid::new_v4().to_string();
let column_b_id = Uuid::new_v4().to_string();
let column_c_id = Uuid::new_v4().to_string();
let column_d_id = Uuid::new_v4().to_string();
let row_0_id = Uuid::new_v4().to_string();
let row_1_id = Uuid::new_v4().to_string();
let column_a = ColumnDefinition {
id: column_a_id.clone(),
name: "Column A".to_string(),
behavior: ColumnBehavior::Text,
};
let column_b = ColumnDefinition {
id: column_b_id.clone(),
name: "Column B".to_string(),
behavior: ColumnBehavior::Text,
};
let column_c = ColumnDefinition {
id: column_c_id.clone(),
name: "Column C".to_string(),
behavior: ColumnBehavior::Formula("=A+\" \"+B".to_string()),
};
// Add columns
let _ = sheet.set_column(column_a).await;
let _ = sheet.set_column(column_b).await;
let _ = sheet.set_column(column_c).await;
// Ensure the rows are created before setting the cell values
sheet.add_row(row_0_id.clone()).await.unwrap();
sheet.add_row(row_1_id.clone()).await.unwrap();
// Set cell values
sheet
.set_cell_value(row_0_id.clone(), column_a_id.clone(), "Hello".to_string())
.await
.unwrap();
sheet
.set_cell_value(row_0_id.clone(), column_b_id.clone(), "World".to_string())
.await
.unwrap();
sheet
.set_cell_value(row_1_id.clone(), column_a_id.clone(), "Foo".to_string())
.await
.unwrap();
sheet
.set_cell_value(row_1_id.clone(), column_b_id.clone(), "Bar".to_string())
.await
.unwrap();
// Check initial formula results
let cell_c_0 = sheet.get_cell(row_0_id.clone(), column_c_id.clone()).unwrap();
assert_eq!(cell_c_0.value, Some("Hello World".to_string()));
let cell_c_1 = sheet.get_cell(row_1_id.clone(), column_c_id.clone()).unwrap();
assert_eq!(cell_c_1.value, Some("Foo Bar".to_string()));
// Update a cell value and check formula result
sheet
.set_cell_value(row_0_id.clone(), column_a_id.clone(), "Bye".to_string())
.await
.unwrap();
let cell_c_updated_0 = sheet.get_cell(row_0_id.clone(), column_c_id.clone()).unwrap();
assert_eq!(cell_c_updated_0.value, Some("Bye World".to_string()));
// Add another formula column
let column_d = ColumnDefinition {
id: column_d_id.clone(),
name: "Column D".to_string(),
behavior: ColumnBehavior::Formula("=C+\" Copy\"".to_string()),
};
let _ = sheet.set_column(column_d).await;
let cell_d_0 = sheet.get_cell(row_0_id.clone(), column_d_id.clone()).unwrap();
assert_eq!(cell_d_0.value, Some("Bye World Copy".to_string()));
let cell_d_1 = sheet.get_cell(row_1_id.clone(), column_d_id.clone()).unwrap();
assert_eq!(cell_d_1.value, Some("Foo Bar Copy".to_string()));
// Remove the middle column
sheet.remove_column(column_b_id.clone()).await.unwrap();
sheet.print_as_ascii_table();
assert_eq!(sheet.columns.len(), 3);
// Remove the first column
sheet.remove_column(column_a_id.clone()).await.unwrap();
assert_eq!(sheet.columns.len(), 2);
sheet.print_as_ascii_table();
}
#[tokio::test]
async fn test_parse_formula_dependencies_text_input() {
let sheet = Sheet::new();
let formula = "Say Hello World";
let dependencies = sheet.parse_formula_dependencies(formula);
let expected: HashSet<UuidString> = HashSet::new();
assert_eq!(dependencies, expected);
}
#[tokio::test]
async fn test_parse_formula_dependencies_with_column_reference() {
let mut sheet = Sheet::new();
sheet.display_columns.push(Uuid::new_v4().to_string()); // Add a column to display_columns
let formula = "=A + \" And Space\"";
let dependencies = sheet.parse_formula_dependencies(formula);
let mut expected = HashSet::new();
expected.insert(sheet.display_columns[0].clone()); // Use the UUID from display_columns
assert_eq!(dependencies, expected);
}
}
@@ -1,889 +0,0 @@
#[cfg(test)]
mod tests {
use std::sync::Arc;
use hanzo_messages::schemas::sheet::{CellStatus, ColumnBehavior, ColumnDefinition};
use hanzo_sheet::sheet::Sheet;
use tokio::sync::Mutex;
use uuid::Uuid;
#[tokio::test]
async fn test_llm_call_column() {
let sheet = Arc::new(Mutex::new(Sheet::new()));
let row_0_id = Uuid::new_v4().to_string();
let column_text_id = Uuid::new_v4().to_string();
let column_llm_id = Uuid::new_v4().to_string();
let column_text = ColumnDefinition {
id: column_text_id.clone(),
name: "Text Column".to_string(),
behavior: ColumnBehavior::Text,
};
let column_llm = ColumnDefinition {
id: column_llm_id.clone(),
name: "LLM Call Column".to_string(),
behavior: ColumnBehavior::LLMCall {
input: "Say Hello World".to_string(),
llm_provider_name: "MockProvider".to_string(),
input_hash: None,
},
};
{
let mut sheet = sheet.lock().await;
let _ = sheet.set_column(column_text.clone()).await;
let _ = sheet.set_column(column_llm.clone()).await;
}
assert_eq!(sheet.lock().await.columns.len(), 2);
assert_eq!(sheet.lock().await.columns[&column_text_id], column_text);
assert_eq!(sheet.lock().await.columns[&column_llm_id], column_llm);
sheet.lock().await.add_row(row_0_id.clone()).await.unwrap();
// Check the value of the cell after the update
let cell_value = sheet
.lock()
.await
.get_cell_value(Uuid::new_v4().to_string(), column_llm_id.clone());
assert_eq!(cell_value, None);
// Simulate job completion
let jobs = sheet
.lock()
.await
.set_cell_value(row_0_id.clone(), column_llm_id.clone(), "Hello World".to_string())
.await
.unwrap();
assert_eq!(jobs.len(), 0);
// Check the value of the cell after the job completion
let cell_value = sheet
.lock()
.await
.get_cell_value(row_0_id.clone(), column_llm_id.clone());
assert_eq!(cell_value, Some("Hello World".to_string()));
{
let sheet_locked = sheet.lock().await;
let cell_status = sheet_locked
.get_cell(row_0_id.clone(), column_llm_id)
.map(|cell| &cell.status);
assert_eq!(cell_status, Some(&CellStatus::Ready));
sheet_locked.print_as_ascii_table();
}
}
#[tokio::test]
async fn test_auto_populate_new_column() {
let mut sheet = Sheet::new();
let column_a_id = Uuid::new_v4().to_string();
let column_b_id = Uuid::new_v4().to_string();
let column_c_id = Uuid::new_v4().to_string();
let row_id = Uuid::new_v4().to_string();
let column_a = ColumnDefinition {
id: column_a_id.clone(),
name: "Column A".to_string(),
behavior: ColumnBehavior::Text,
};
let column_b = ColumnDefinition {
id: column_b_id.clone(),
name: "Column B".to_string(),
behavior: ColumnBehavior::Formula("=A + \" Copy\"".to_string()),
};
let column_c = ColumnDefinition {
id: column_c_id.clone(),
name: "Column C".to_string(),
behavior: ColumnBehavior::Formula("=B + \" Second Copy\"".to_string()),
};
let _ = sheet.set_column(column_a.clone()).await;
let _ = sheet.set_column(column_b.clone()).await;
let _ = sheet.set_column(column_c.clone()).await;
sheet.print_as_ascii_table();
// Ensure the row is created before setting the cell value
sheet.add_row(row_id.clone()).await.unwrap();
sheet
.set_cell_value(row_id.clone(), column_a_id.clone(), "Hello".to_string())
.await
.unwrap();
assert_eq!(
sheet.get_cell_value(row_id.clone(), column_a_id.clone()),
Some("Hello".to_string())
);
assert_eq!(
sheet.get_cell_value(row_id.clone(), column_b_id.clone()),
Some("Hello Copy".to_string())
);
assert_eq!(
sheet.get_cell_value(row_id.clone(), column_c_id.clone()),
Some("Hello Copy Second Copy".to_string())
);
sheet.print_as_ascii_table();
}
#[tokio::test]
async fn test_llm_call_with_dependent_column() {
let mut sheet = Sheet::new();
let column_text_id = Uuid::new_v4().to_string();
let column_llm_id = Uuid::new_v4().to_string();
let column_formula_id = Uuid::new_v4().to_string();
let row_id = Uuid::new_v4().to_string();
let column_text = ColumnDefinition {
id: column_text_id.clone(),
name: "Column A".to_string(),
behavior: ColumnBehavior::Text,
};
let workflow_str = r#"
workflow WorkflowTest v0.1 {
step Main {
$RESULT = call opinionated_inference($INPUT)
}
}
"#;
let column_llm = ColumnDefinition {
id: column_llm_id.clone(),
name: "Column B".to_string(),
behavior: ColumnBehavior::LLMCall {
input: "Say Hello World".to_string(),
llm_provider_name: "MockProvider".to_string(),
input_hash: None,
},
};
let column_formula = ColumnDefinition {
id: column_formula_id.clone(),
name: "Column C".to_string(),
behavior: ColumnBehavior::Formula("=A + \" And Space\"".to_string()),
};
let _text_jobs = sheet.set_column(column_text.clone()).await;
let _llm_jobs = sheet.set_column(column_llm.clone()).await;
let _formula_jobs = sheet.set_column(column_formula.clone()).await;
// Ensure the row is created before setting the cell value
sheet.add_row(row_id.clone()).await.unwrap();
// Set value in Column A
sheet
.set_cell_value(row_id.clone(), column_text_id.clone(), "Hello".to_string())
.await
.unwrap();
// Check initial state of Column C (formula depending on Column A)
let cell_value_formula = sheet.get_cell_value(row_id.clone(), column_formula_id.clone());
assert_eq!(cell_value_formula, Some("Hello And Space".to_string()));
// Change Column C formula to depend on Column B instead of Column A
let new_column_formula = ColumnDefinition {
id: column_formula_id.clone(),
name: "Column C".to_string(),
behavior: ColumnBehavior::Formula("=B + \" Updated\"".to_string()),
};
let _ = sheet.set_column(new_column_formula).await;
// Check Column C value before updating Column B (should be empty or default value)
let cell_value_formula = sheet.get_cell_value(row_id.clone(), column_formula_id.clone());
assert_eq!(cell_value_formula, Some(" Updated".to_string()));
// Simulate LLM call completion for Column B
sheet
.set_cell_value(row_id.clone(), column_llm_id.clone(), "Hola Mundo".to_string())
.await
.unwrap();
// Check the value of the LLM call cell (Column B) after the update
let cell_value_llm = sheet.get_cell_value(row_id.clone(), column_llm_id.clone());
assert_eq!(cell_value_llm, Some("Hola Mundo".to_string()));
// Check if Column C reflects the updated value of Column B
let cell_value_formula = sheet.get_cell_value(row_id.clone(), column_formula_id.clone());
assert_eq!(cell_value_formula, Some("Hola Mundo Updated".to_string()));
// Print final state of the sheet
sheet.print_as_ascii_table();
}
#[tokio::test]
async fn test_multiple_rows() {
let mut sheet = Sheet::new();
let column_a_id = Uuid::new_v4().to_string();
let column_b_id = Uuid::new_v4().to_string();
let column_c_id = Uuid::new_v4().to_string();
let row_0_id = Uuid::new_v4().to_string();
let row_1_id = Uuid::new_v4().to_string();
let row_2_id = Uuid::new_v4().to_string();
let column_a = ColumnDefinition {
id: column_a_id.clone(),
name: "Column A".to_string(),
behavior: ColumnBehavior::Text,
};
let column_b = ColumnDefinition {
id: column_b_id.clone(),
name: "Column B".to_string(),
behavior: ColumnBehavior::Formula("=A + \" Processed\"".to_string()),
};
let column_c = ColumnDefinition {
id: column_c_id.clone(),
name: "Column C".to_string(),
behavior: ColumnBehavior::LLMCall {
input: "Summarize: $INPUT".to_string(),
llm_provider_name: "MockProvider".to_string(),
input_hash: None,
},
};
let _ = sheet.set_column(column_a.clone()).await;
let _ = sheet.set_column(column_b.clone()).await;
let _ = sheet.set_column(column_c.clone()).await;
// Ensure the rows are created before setting the cell values
sheet.add_row(row_0_id.clone()).await.unwrap();
sheet.add_row(row_1_id.clone()).await.unwrap();
sheet.add_row(row_2_id.clone()).await.unwrap();
// Add data to multiple rows
sheet
.set_cell_value(row_0_id.clone(), column_a_id.clone(), "Hello".to_string())
.await
.unwrap();
sheet
.set_cell_value(row_1_id.clone(), column_a_id.clone(), "World".to_string())
.await
.unwrap();
sheet
.set_cell_value(row_2_id.clone(), column_a_id.clone(), "Test".to_string())
.await
.unwrap();
// Check values in Column A and B
assert_eq!(
sheet.get_cell_value(row_0_id.clone(), column_a_id.clone()),
Some("Hello".to_string())
);
assert_eq!(
sheet.get_cell_value(row_0_id.clone(), column_b_id.clone()),
Some("Hello Processed".to_string())
);
assert_eq!(
sheet.get_cell_value(row_1_id.clone(), column_a_id.clone()),
Some("World".to_string())
);
assert_eq!(
sheet.get_cell_value(row_1_id.clone(), column_b_id.clone()),
Some("World Processed".to_string())
);
assert_eq!(
sheet.get_cell_value(row_2_id.clone(), column_a_id.clone()),
Some("Test".to_string())
);
assert_eq!(
sheet.get_cell_value(row_2_id.clone(), column_b_id.clone()),
Some("Test Processed".to_string())
);
// Simulate LLM calls on Column C for all rows
for (i, row_id) in [&row_0_id, &row_1_id, &row_2_id].iter().enumerate() {
// Simulate job completion for Column C
sheet
.set_cell_value(row_id.to_string(), column_c_id.clone(), format!("Summary of row {}", i))
.await
.unwrap();
}
// Check final values
assert_eq!(
sheet.get_cell_value(row_0_id.clone(), column_c_id.clone()),
Some("Summary of row 0".to_string())
);
assert_eq!(
sheet.get_cell_value(row_1_id.clone(), column_c_id.clone()),
Some("Summary of row 1".to_string())
);
assert_eq!(
sheet.get_cell_value(row_2_id.clone(), column_c_id.clone()),
Some("Summary of row 2".to_string())
);
// Print final state of the sheet
sheet.print_as_ascii_table();
}
#[tokio::test]
async fn test_remove_row() {
let mut sheet = Sheet::new();
let column_a_id = Uuid::new_v4().to_string();
let column_b_id = Uuid::new_v4().to_string();
let row_0_id = Uuid::new_v4().to_string();
let row_1_id = Uuid::new_v4().to_string();
let row_2_id = Uuid::new_v4().to_string();
let column_a = ColumnDefinition {
id: column_a_id.clone(),
name: "Column A".to_string(),
behavior: ColumnBehavior::Text,
};
let column_b = ColumnDefinition {
id: column_b_id.clone(),
name: "Column B".to_string(),
behavior: ColumnBehavior::Formula("=A + \" Processed\"".to_string()),
};
let _ = sheet.set_column(column_a.clone()).await;
let _ = sheet.set_column(column_b.clone()).await;
// Ensure the rows are created before setting the cell values
sheet.add_row(row_0_id.clone()).await.unwrap();
sheet.add_row(row_1_id.clone()).await.unwrap();
sheet.add_row(row_2_id.clone()).await.unwrap();
// Add data to multiple rows
sheet
.set_cell_value(row_0_id.clone(), column_a_id.clone(), "Hello".to_string())
.await
.unwrap();
sheet
.set_cell_value(row_1_id.clone(), column_a_id.clone(), "World".to_string())
.await
.unwrap();
sheet
.set_cell_value(row_2_id.clone(), column_a_id.clone(), "Test".to_string())
.await
.unwrap();
// Check initial values
assert_eq!(
sheet.get_cell_value(row_0_id.clone(), column_a_id.clone()),
Some("Hello".to_string())
);
assert_eq!(
sheet.get_cell_value(row_1_id.clone(), column_a_id.clone()),
Some("World".to_string())
);
assert_eq!(
sheet.get_cell_value(row_2_id.clone(), column_a_id.clone()),
Some("Test".to_string())
);
// Remove a row
let _ = sheet.remove_row(row_1_id.clone()).await.unwrap();
// Check values after row removal
assert_eq!(
sheet.get_cell_value(row_0_id.clone(), column_a_id.clone()),
Some("Hello".to_string())
);
assert_eq!(sheet.get_cell_value(row_1_id.clone(), column_a_id.clone()), None);
assert_eq!(
sheet.get_cell_value(row_2_id.clone(), column_a_id.clone()),
Some("Test".to_string())
);
assert_eq!(
sheet.get_cell_value(row_0_id.clone(), column_b_id.clone()),
Some("Hello Processed".to_string())
);
assert_eq!(sheet.get_cell_value(row_1_id.clone(), column_b_id.clone()), None);
assert_eq!(
sheet.get_cell_value(row_2_id.clone(), column_b_id.clone()),
Some("Test Processed".to_string())
);
// Print final state of the sheet
sheet.print_as_ascii_table();
// Add a new row
let new_row_id = Uuid::new_v4().to_string();
let _ = sheet.add_row(new_row_id.clone()).await.unwrap();
// Add a value to the first column of the new row
sheet
.set_cell_value(new_row_id.clone(), column_a_id.clone(), "New Value".to_string())
.await
.unwrap();
// Check values after adding a new row
assert_eq!(
sheet.get_cell_value(row_0_id.clone(), column_a_id.clone()),
Some("Hello".to_string())
);
assert_eq!(
sheet.get_cell_value(new_row_id.clone(), column_a_id.clone()),
Some("New Value".to_string())
); // New row should have the new value
assert_eq!(
sheet.get_cell_value(row_2_id.clone(), column_a_id.clone()),
Some("Test".to_string())
);
assert_eq!(
sheet.get_cell_value(row_0_id.clone(), column_b_id.clone()),
Some("Hello Processed".to_string())
);
assert_eq!(
sheet.get_cell_value(new_row_id.clone(), column_b_id.clone()),
Some("New Value Processed".to_string())
); // Formula should be recalculated
assert_eq!(
sheet.get_cell_value(row_2_id.clone(), column_b_id.clone()),
Some("Test Processed".to_string())
);
// Print final state of the sheet
sheet.print_as_ascii_table();
}
#[tokio::test]
async fn test_dependency_manager() {
let sheet = Arc::new(Mutex::new(Sheet::new()));
let row_id = Uuid::new_v4().to_string();
let column_text_id = Uuid::new_v4().to_string();
let column_llm_id = Uuid::new_v4().to_string();
let column_formula_id = Uuid::new_v4().to_string();
let column_text = ColumnDefinition {
id: column_text_id.clone(),
name: "Column A".to_string(),
behavior: ColumnBehavior::Text,
};
let column_llm = ColumnDefinition {
id: column_llm_id.clone(),
name: "Column B".to_string(),
behavior: ColumnBehavior::LLMCall {
input: "=A".to_string(),
llm_provider_name: "MockProvider".to_string(),
input_hash: None,
},
};
let column_formula = ColumnDefinition {
id: column_formula_id.clone(),
name: "Column C".to_string(),
behavior: ColumnBehavior::Formula("=B + \" And Space\"".to_string()),
};
{
let mut sheet = sheet.lock().await;
let _ = sheet.set_column(column_text.clone()).await;
let _ = sheet.set_column(column_llm.clone()).await;
let _ = sheet.set_column(column_formula.clone()).await;
}
eprintln!("Checking initial state of the sheet");
assert_eq!(sheet.lock().await.columns.len(), 3);
assert_eq!(sheet.lock().await.columns[&column_text_id], column_text);
assert_eq!(sheet.lock().await.columns[&column_llm_id], column_llm);
assert_eq!(sheet.lock().await.columns[&column_formula_id], column_formula);
sheet.lock().await.add_row(row_id.clone()).await.unwrap();
// Set value in Column A
sheet
.lock()
.await
.set_cell_value(row_id.clone(), column_text_id.clone(), "Hello".to_string())
.await
.unwrap();
// Check initial state of Column C (formula depending on Column B)
let cell_value_formula = sheet
.lock()
.await
.get_cell_value(row_id.clone(), column_formula_id.clone());
assert_eq!(cell_value_formula, Some(" And Space".to_string()));
eprintln!("Checking initial state of the dependency manager");
// Simulate LLM call completion for Column B
sheet
.lock()
.await
.set_cell_value(row_id.clone(), column_llm_id.clone(), "Hola Mundo".to_string())
.await
.unwrap();
// Check the value of the LLM call cell (Column B) after the update
let cell_value_llm = sheet.lock().await.get_cell_value(row_id.clone(), column_llm_id.clone());
assert_eq!(cell_value_llm, Some("Hola Mundo".to_string()));
// Check if Column C reflects the updated value of Column B
let cell_value_formula = sheet
.lock()
.await
.get_cell_value(row_id.clone(), column_formula_id.clone());
assert_eq!(cell_value_formula, Some("Hola Mundo And Space".to_string()));
// Check dependency manager
{
let sheet_locked = sheet.lock().await;
let reverse_dependents_a = sheet_locked
.column_dependency_manager
.get_reverse_dependents(column_text_id.clone());
let reverse_dependents_b = sheet_locked
.column_dependency_manager
.get_reverse_dependents(column_llm_id.clone());
assert!(reverse_dependents_a.contains(&column_llm_id));
assert!(reverse_dependents_b.contains(&column_formula_id));
}
// Update Column C to be a text column
let new_column_text = ColumnDefinition {
id: column_formula_id.clone(),
name: "Column C".to_string(),
behavior: ColumnBehavior::Text,
};
eprintln!("Updating Column C to be a text column");
let _ = sheet.lock().await.set_column(new_column_text.clone()).await;
// Check the dependencies after updating Column C
let sheet_locked = sheet.lock().await;
let dependencies_c = sheet_locked
.column_dependency_manager
.dependencies
.get(&column_formula_id);
let reverse_dependents_b = sheet_locked
.column_dependency_manager
.reverse_dependencies
.get(&column_llm_id);
eprintln!("Checking dependencies after updating Column C: {:?}", dependencies_c);
assert!(dependencies_c.is_none());
assert!(!reverse_dependents_b.unwrap().contains(&column_formula_id));
// Print final state of the sheet
sheet_locked.print_as_ascii_table();
}
// TODO: add a new test and feature to throw an error that gets scaled up to the user
//eg behavior: ColumnBehavior::Formula("=LLM Call Column + \" Copy\"".to_string()),
#[tokio::test]
async fn test_llm_call_and_formula_cell_status() {
let sheet = Arc::new(Mutex::new(Sheet::new()));
let row_id = Uuid::new_v4().to_string();
let column_llm_id = Uuid::new_v4().to_string();
let column_formula_id = Uuid::new_v4().to_string();
let column_llm = ColumnDefinition {
id: column_llm_id.clone(),
name: "LLM Call Column".to_string(),
behavior: ColumnBehavior::LLMCall {
input: "Say Hello World".to_string(),
llm_provider_name: "MockProvider".to_string(),
input_hash: None,
},
};
let column_formula = ColumnDefinition {
id: column_formula_id.clone(),
name: "Formula Column".to_string(),
behavior: ColumnBehavior::Formula("=A + \" Copy\"".to_string()),
};
{
let mut sheet = sheet.lock().await;
let _ = sheet.set_column(column_llm.clone()).await;
let _ = sheet.set_column(column_formula.clone()).await;
}
sheet.lock().await.add_row(row_id.clone()).await.unwrap();
// Check initial cell statuses
{
let sheet_locked = sheet.lock().await;
let llm_cell_status = sheet_locked
.get_cell(row_id.clone(), column_llm_id.clone())
.map(|cell| &cell.status);
let formula_cell_status = sheet_locked
.get_cell(row_id.clone(), column_formula_id.clone())
.map(|cell| &cell.status);
assert_eq!(llm_cell_status, Some(&CellStatus::Pending));
assert_eq!(formula_cell_status, Some(&CellStatus::Ready)); // TODO: eventually it should be "linked" so Pending
}
// Simulate LLM call completion
sheet
.lock()
.await
.set_cell_value(row_id.clone(), column_llm_id.clone(), "Hello World".to_string())
.await
.unwrap();
// Check cell statuses after LLM call completion
{
let sheet_locked = sheet.lock().await;
let llm_cell_status = sheet_locked
.get_cell(row_id.clone(), column_llm_id.clone())
.map(|cell| &cell.status);
let formula_cell_status = sheet_locked
.get_cell(row_id.clone(), column_formula_id.clone())
.map(|cell| &cell.status);
assert_eq!(llm_cell_status, Some(&CellStatus::Ready));
assert_eq!(formula_cell_status, Some(&CellStatus::Ready));
}
// Print final state of the sheet
sheet.lock().await.print_as_ascii_table();
}
#[tokio::test]
async fn test_jobs_created_for_new_column() {
let mut sheet = Sheet::new();
let column_a_id = Uuid::new_v4().to_string();
let column_b_id = Uuid::new_v4().to_string();
let column_a = ColumnDefinition {
id: column_a_id.clone(),
name: "Column A".to_string(),
behavior: ColumnBehavior::Text,
};
// Add Column A and a row
let _ = sheet.set_column(column_a.clone()).await;
// Add multiple rows
let mut row_ids = Vec::new();
for _ in 0..3 {
let row_id = Uuid::new_v4().to_string();
sheet.add_row(row_id.clone()).await.unwrap();
row_ids.push(row_id.clone());
// Set value in Column A
sheet
.set_cell_value(row_id.clone(), column_a_id.clone(), "Hello".to_string())
.await
.unwrap();
}
// Add Column B as LLMCall and check if jobs are created
let column_b = ColumnDefinition {
id: column_b_id.clone(),
name: "Column B".to_string(),
behavior: ColumnBehavior::LLMCall {
input: "Say Hello World".to_string(),
llm_provider_name: "MockProvider".to_string(),
input_hash: None,
},
};
let jobs = sheet.set_column(column_b.clone()).await.unwrap();
assert_eq!(
jobs.len(),
3,
"The number of jobs should be equal to the number of rows created"
);
// Print final state of the sheet
sheet.print_as_ascii_table();
}
#[tokio::test]
async fn test_llm_call_shouldnt_include_empty_dependencies() {
let mut sheet = Sheet::new();
let column_a_id = Uuid::new_v4().to_string();
let column_b_id = Uuid::new_v4().to_string();
let column_a = ColumnDefinition {
id: column_a_id.clone(),
name: "Cities".to_string(),
behavior: ColumnBehavior::Text,
};
// Add Column A and create 3 rows
let _ = sheet.set_column(column_a.clone()).await;
let row_0_id = Uuid::new_v4().to_string();
let row_1_id = Uuid::new_v4().to_string();
let row_2_id = Uuid::new_v4().to_string();
sheet.add_row(row_0_id.clone()).await.unwrap();
sheet.add_row(row_1_id.clone()).await.unwrap();
sheet.add_row(row_2_id.clone()).await.unwrap();
// Set values in Column A
sheet
.set_cell_value(row_0_id.clone(), column_a_id.clone(), "Santiago, Chile".to_string())
.await
.unwrap();
sheet
.set_cell_value(row_1_id.clone(), column_a_id.clone(), "Austin, Texas".to_string())
.await
.unwrap();
// Add Column B as LLMCall and check if jobs are created
let column_b = ColumnDefinition {
id: column_b_id.clone(),
name: "New Column".to_string(),
behavior: ColumnBehavior::LLMCall {
input: "=\"Say Hello World in \" + A".to_string(),
llm_provider_name: "MockProvider".to_string(),
input_hash: None,
},
};
let jobs = sheet.set_column(column_b.clone()).await.unwrap();
assert_eq!(
jobs.len(),
2,
"The number of jobs should be equal to the number of rows with values set"
);
// Print final state of the sheet
sheet.print_as_ascii_table();
}
#[tokio::test]
async fn test_create_text_column_and_get_value() {
let mut sheet = Sheet::new();
let column_text_id = Uuid::new_v4().to_string();
let column_text_2_id = Uuid::new_v4().to_string();
let row_id = Uuid::new_v4().to_string();
// Create a text column
let column_text = ColumnDefinition {
id: column_text_id.clone(),
name: "Text Column".to_string(),
behavior: ColumnBehavior::Text,
};
let _ = sheet.set_column(column_text.clone()).await;
// Add a row
sheet.add_row(row_id.clone()).await.unwrap();
// Set the first item to some text
sheet
.set_cell_value(row_id.clone(), column_text_id.clone(), "Sample Text".to_string())
.await
.unwrap();
// Create a second text column
let column_text_2 = ColumnDefinition {
id: column_text_2_id.clone(),
name: "Second Text Column".to_string(),
behavior: ColumnBehavior::Text,
};
let _ = sheet.set_column(column_text_2.clone()).await;
// Try to get the value of the cell from the second column
let cell_value = sheet.get_cell_value(row_id.clone(), column_text_2_id.clone());
assert_eq!(cell_value, None);
// Retrieve rows from the sheet and ensure there are two cells
let rows = sheet.rows.get(&row_id).unwrap();
assert_eq!(rows.len(), 2);
// Print final state of the sheet
sheet.print_as_ascii_table();
}
#[tokio::test]
async fn test_llm_call_column_with_two_rows() {
let mut sheet = Sheet::new();
let column_text_id = Uuid::new_v4().to_string();
let column_llm_id = Uuid::new_v4().to_string();
let row_0_id = Uuid::new_v4().to_string();
let row_1_id = Uuid::new_v4().to_string();
// Create a text column
let column_text = ColumnDefinition {
id: column_text_id.clone(),
name: "Text Column".to_string(),
behavior: ColumnBehavior::Text,
};
let _ = sheet.set_column(column_text.clone()).await;
// Add two rows
sheet.add_row(row_0_id.clone()).await.unwrap();
sheet.add_row(row_1_id.clone()).await.unwrap();
// Set the first item in both rows to some text
sheet
.set_cell_value(row_0_id.clone(), column_text_id.clone(), "Sample Text 1".to_string())
.await
.unwrap();
sheet
.set_cell_value(row_1_id.clone(), column_text_id.clone(), "Sample Text 2".to_string())
.await
.unwrap();
// Create an LLMCall column
let column_llm = ColumnDefinition {
id: column_llm_id.clone(),
name: "LLM Call Column".to_string(),
behavior: ColumnBehavior::LLMCall {
input: "=A".to_string(),
llm_provider_name: "MockProvider".to_string(),
input_hash: None,
},
};
let jobs = sheet.set_column(column_llm.clone()).await.unwrap();
// Check that two jobs are created
assert_eq!(
jobs.len(),
2,
"The number of jobs should be equal to the number of rows created"
);
// Check that the two cells of the second column are in pending status
{
let llm_cell_status_row_0 = sheet
.get_cell(row_0_id.clone(), column_llm_id.clone())
.map(|cell| &cell.status);
let llm_cell_status_row_1 = sheet
.get_cell(row_1_id.clone(), column_llm_id.clone())
.map(|cell| &cell.status);
assert_eq!(llm_cell_status_row_0, Some(&CellStatus::Pending));
assert_eq!(llm_cell_status_row_1, Some(&CellStatus::Pending));
}
// Print final state of the sheet
sheet.print_as_ascii_table();
}
#[tokio::test]
async fn test_new_rows_should_create_jobs() {
let mut sheet = Sheet::new();
let column_ai_id = Uuid::new_v4().to_string();
// Create an AI column
let column_ai = ColumnDefinition {
id: column_ai_id.clone(),
name: "AI Column".to_string(),
behavior: ColumnBehavior::LLMCall {
input: "=\"Hello World\"".to_string(),
llm_provider_name: "MockProvider".to_string(),
input_hash: None,
},
};
let _ = sheet.set_column(column_ai.clone()).await;
// Add an extra row and check for job creation
let row_2_id = Uuid::new_v4().to_string();
let new_jobs = sheet.add_row(row_2_id.clone()).await.unwrap();
assert_eq!(new_jobs.len(), 1, "A new job should be created for the newly added row");
// We add a new row and check for job creation
let row_3_id = Uuid::new_v4().to_string();
let new_jobs = sheet.add_row(row_3_id.clone()).await.unwrap();
assert_eq!(new_jobs.len(), 1, "A new job should be created for the newly added row");
// Print final state of the sheet
sheet.print_as_ascii_table();
}
}
// // TODO: add test that A (text missing) -> B (workflow depending on A) -> C (workflo depending on B)
-60
View File
@@ -1,60 +0,0 @@
[package]
name = "hanzo-simulation"
description = "Hanzo AI - Simulation Library"
license = "MIT"
version = "0.1.1"
edition = "2021"
publish = false # Not ready for crates.io
# Standalone crate - not part of parent workspace
[workspace]
[dependencies]
# Core
tokio = { version = "1.38", features = ["full"] }
anyhow = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tracing = "0.1"
dashmap = "5.5"
# Database
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "macros", "json"] }
sqlite-vec = "0.1" # Vector search in SQLite
# USD support
openusd = "0.1" # OpenUSD bindings
usd-rs = "0.1" # Alternative USD library
# Math & Physics
nalgebra = "0.33"
rapier3d = "0.22" # Rust physics engine
parry3d = "0.17" # Collision detection
# Graphics formats
gltf = "1.4"
image = "0.25"
basis-universal = "0.3" # KTX2 texture compression
# Web/Networking
warp = "0.3"
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "compression"] }
# ML Integration
candle-core = "0.7"
candle-nn = "0.7"
ort = "2.0.0-rc.10" # ONNX Runtime for V-JEPA2
# Compute orchestration
bollard = "0.17" # Docker API
kube = { version = "0.95", features = ["runtime", "derive"] }
k8s-openapi = { version = "0.23", features = ["latest"] }
# Security
firecracker-sdk = "0.0.3" # MicroVM isolation
hanzo-pqc = { workspace = true }
[dev-dependencies]
criterion = "0.5"
tempfile = "3.10"
-256
View File
@@ -1,256 +0,0 @@
//! Zoo-1 Multi-tier Simulation System
//!
//! Provides three tiers of simulation:
//! - Tier A: V-JEPA2 latent world model (cheapest)
//! - Tier B: WebXR/WebGPU lightweight spatial (browser)
//! - Tier C: High-fidelity physics (MuJoCo/Brax)
pub mod tiers;
pub mod usd_layers;
pub mod sql_brain;
pub mod orchestrator;
pub mod metaverse;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
/// Simulation tier selection
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum SimulationTier {
/// Latent mental simulation using V-JEPA2
Latent,
/// Lightweight 3D with WebGPU/WebXR
Lightweight,
/// High-fidelity physics simulation
HighFidelity,
}
/// Expected Free Energy for tier selection
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExpectedFreeEnergy {
pub epistemic_value: f64, // Information gain
pub pragmatic_value: f64, // Goal achievement
pub cost: f64, // Computational cost
}
impl ExpectedFreeEnergy {
/// Select optimal simulation tier based on EFE
pub fn select_tier(&self) -> SimulationTier {
let value_of_information = self.epistemic_value + self.pragmatic_value;
if value_of_information < 0.3 {
SimulationTier::Latent
} else if value_of_information < 0.7 {
SimulationTier::Lightweight
} else {
SimulationTier::HighFidelity
}
}
}
/// Simulation request
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimulationRequest {
pub question: String,
pub scene_ref: String, // USD stage reference
pub user_id: String,
pub budget: SimulationBudget,
pub policy: SimulationPolicy,
}
/// Resource budget for simulation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimulationBudget {
pub max_seconds: u64,
pub max_gpu_seconds: u64,
pub max_memory_gb: f32,
}
/// Simulation execution policy
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimulationPolicy {
pub tier: TierSelection,
pub risk_tolerance: RiskTolerance,
pub parallelism: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TierSelection {
Auto,
Fixed(SimulationTier),
Progressive, // Start with Latent, escalate if needed
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RiskTolerance {
Low,
Medium,
High,
}
/// Simulation result with artifacts
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimulationResult {
pub answer: String,
pub confidence: f64,
pub tier_used: SimulationTier,
pub artifacts: Vec<SimulationArtifact>,
pub metrics: SimulationMetrics,
pub cost: SimulationCost,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimulationArtifact {
pub artifact_type: ArtifactType,
pub uri: String,
pub metadata: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ArtifactType {
Video,
Image,
UsdLayer,
GltfScene,
Trajectory,
Metrics,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimulationMetrics {
pub episodes_run: u32,
pub success_rate: f64,
pub avg_reward: f64,
pub collision_count: u32,
pub physics_violations: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimulationCost {
pub compute_seconds: f64,
pub gpu_seconds: f64,
pub memory_gb_hours: f64,
pub estimated_usd: f64,
}
/// Main simulation orchestrator
pub struct SimulationOrchestrator {
latent_tier: Arc<tiers::latent::LatentSimulator>,
lightweight_tier: Arc<tiers::lightweight::LightweightSimulator>,
highfidelity_tier: Arc<tiers::highfidelity::HighFidelitySimulator>,
usd_manager: Arc<usd_layers::UsdLayerManager>,
sql_brain: Arc<sql_brain::SqlBrain>,
}
impl SimulationOrchestrator {
pub async fn new(config: OrchestratorConfig) -> Result<Self> {
Ok(Self {
latent_tier: Arc::new(tiers::latent::LatentSimulator::new(config.latent_config).await?),
lightweight_tier: Arc::new(tiers::lightweight::LightweightSimulator::new(config.lightweight_config).await?),
highfidelity_tier: Arc::new(tiers::highfidelity::HighFidelitySimulator::new(config.hf_config).await?),
usd_manager: Arc::new(usd_layers::UsdLayerManager::new(config.usd_config)?),
sql_brain: Arc::new(sql_brain::SqlBrain::new(&config.sql_path).await?),
})
}
/// Process a simulation request
pub async fn simulate(&self, request: SimulationRequest) -> Result<SimulationResult> {
// 1. Load user context from SQL brain
let user_context = self.sql_brain.load_user_context(&request.user_id).await?;
// 2. Create USD session layer for cloning
let session_layer = self.usd_manager.create_session_layer(&request.scene_ref).await?;
// 3. Calculate Expected Free Energy
let efe = self.calculate_efe(&request, &user_context).await?;
// 4. Select tier based on EFE and policy
let tier = match request.policy.tier {
TierSelection::Auto => efe.select_tier(),
TierSelection::Fixed(tier) => tier,
TierSelection::Progressive => SimulationTier::Latent, // Start with cheapest
};
// 5. Execute simulation on selected tier
let result = match tier {
SimulationTier::Latent => {
self.latent_tier.simulate(&request, &session_layer).await?
}
SimulationTier::Lightweight => {
self.lightweight_tier.simulate(&request, &session_layer).await?
}
SimulationTier::HighFidelity => {
self.highfidelity_tier.simulate(&request, &session_layer).await?
}
};
// 6. Store results in SQL brain
self.sql_brain.store_episode(&request.user_id, &result).await?;
// 7. Optionally escalate if confidence too low
if request.policy.tier == TierSelection::Progressive && result.confidence < 0.6 {
// Escalate to next tier
let next_tier = match tier {
SimulationTier::Latent => SimulationTier::Lightweight,
SimulationTier::Lightweight => SimulationTier::HighFidelity,
SimulationTier::HighFidelity => SimulationTier::HighFidelity,
};
if next_tier != tier {
// Run again on higher tier
return self.simulate_with_tier(request, next_tier).await;
}
}
Ok(result)
}
async fn calculate_efe(&self, request: &SimulationRequest, context: &sql_brain::UserContext) -> Result<ExpectedFreeEnergy> {
// Calculate value of information based on question complexity and user history
let epistemic_value = self.estimate_information_gain(&request.question, context).await?;
let pragmatic_value = self.estimate_goal_value(&request.question, context).await?;
let cost = self.estimate_compute_cost(SimulationTier::Latent);
Ok(ExpectedFreeEnergy {
epistemic_value,
pragmatic_value,
cost,
})
}
async fn estimate_information_gain(&self, question: &str, context: &sql_brain::UserContext) -> Result<f64> {
// Estimate based on question novelty and complexity
// This would use embeddings to check similarity with past questions
Ok(0.5) // Placeholder
}
async fn estimate_goal_value(&self, question: &str, context: &sql_brain::UserContext) -> Result<f64> {
// Estimate value for achieving user's goals
Ok(0.3) // Placeholder
}
fn estimate_compute_cost(&self, tier: SimulationTier) -> f64 {
match tier {
SimulationTier::Latent => 0.001, // $0.001 per simulation
SimulationTier::Lightweight => 0.01, // $0.01 per simulation
SimulationTier::HighFidelity => 0.10, // $0.10 per simulation
}
}
async fn simulate_with_tier(&self, request: SimulationRequest, tier: SimulationTier) -> Result<SimulationResult> {
let mut req = request;
req.policy.tier = TierSelection::Fixed(tier);
self.simulate(req).await
}
}
/// Orchestrator configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrchestratorConfig {
pub latent_config: tiers::latent::LatentConfig,
pub lightweight_config: tiers::lightweight::LightweightConfig,
pub hf_config: tiers::highfidelity::HighFidelityConfig,
pub usd_config: usd_layers::UsdConfig,
pub sql_path: String,
}
-456
View File
@@ -1,456 +0,0 @@
//! SQL Brain - Local-first memory store with vector search
//!
//! Uses SQLite with sqlite-vec for in-database vector operations
use anyhow::Result;
use serde::{Deserialize, Serialize};
use sqlx::{sqlite::SqlitePoolOptions, Pool, Sqlite};
use std::path::Path;
use crate::SimulationResult;
/// User context loaded from SQL brain
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserContext {
pub user_id: String,
pub episodes: Vec<Episode>,
pub scene_memories: Vec<SceneMemory>,
pub preferences: UserPreferences,
pub delta_weights: Option<DeltaWeights>,
}
/// Episode memory
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Episode {
pub id: String,
pub timestamp: i64,
pub question: String,
pub answer: String,
pub confidence: f64,
pub trajectory: Vec<StateTransition>,
pub reward: f64,
}
/// State transition in episode
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateTransition {
pub state: serde_json::Value,
pub action: serde_json::Value,
pub next_state: serde_json::Value,
pub reward: f64,
}
/// Scene memory with embeddings
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SceneMemory {
pub scene_id: String,
pub description: String,
pub embedding: Vec<f32>,
pub objects: Vec<ObjectMemory>,
pub affordances: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObjectMemory {
pub object_id: String,
pub class: String,
pub properties: serde_json::Value,
pub embedding: Vec<f32>,
}
/// User preferences
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserPreferences {
pub risk_tolerance: f64,
pub exploration_rate: f64,
pub preferred_tier: Option<String>,
}
/// BitDelta 1-bit weight deltas for per-user adaptation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeltaWeights {
pub base_model: String,
pub delta_bits: Vec<u8>, // 1-bit deltas packed
pub scale: f32, // Learned scale factor
pub sparsity_mask: Vec<bool>, // Which weights to update
}
/// SQL Brain implementation
pub struct SqlBrain {
pool: Pool<Sqlite>,
}
impl SqlBrain {
pub async fn new(path: &str) -> Result<Self> {
// Create database if it doesn't exist
if !Path::new(path).exists() {
tokio::fs::File::create(path).await?;
}
let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect(&format!("sqlite:{}", path))
.await?;
// Initialize schema
Self::init_schema(&pool).await?;
Ok(Self { pool })
}
async fn init_schema(pool: &Pool<Sqlite>) -> Result<()> {
// Create tables
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS users (
user_id TEXT PRIMARY KEY,
created_at INTEGER NOT NULL,
preferences TEXT NOT NULL
)
"#,
)
.execute(pool)
.await?;
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS episodes (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
timestamp INTEGER NOT NULL,
question TEXT NOT NULL,
answer TEXT NOT NULL,
confidence REAL NOT NULL,
trajectory TEXT NOT NULL,
reward REAL NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(user_id)
)
"#,
)
.execute(pool)
.await?;
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS scenes (
scene_id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
description TEXT NOT NULL,
embedding BLOB NOT NULL,
objects TEXT NOT NULL,
affordances TEXT NOT NULL,
created_at INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(user_id)
)
"#,
)
.execute(pool)
.await?;
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS objects (
object_id TEXT PRIMARY KEY,
scene_id TEXT NOT NULL,
class TEXT NOT NULL,
properties TEXT NOT NULL,
embedding BLOB NOT NULL,
FOREIGN KEY (scene_id) REFERENCES scenes(scene_id)
)
"#,
)
.execute(pool)
.await?;
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS delta_weights (
user_id TEXT PRIMARY KEY,
base_model TEXT NOT NULL,
delta_bits BLOB NOT NULL,
scale REAL NOT NULL,
sparsity_mask BLOB NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(user_id)
)
"#,
)
.execute(pool)
.await?;
// Create vector index using sqlite-vec virtual table
sqlx::query(
r#"
CREATE VIRTUAL TABLE IF NOT EXISTS scene_embeddings USING vec0(
scene_id TEXT PRIMARY KEY,
embedding FLOAT[768]
)
"#,
)
.execute(pool)
.await?;
sqlx::query(
r#"
CREATE VIRTUAL TABLE IF NOT EXISTS object_embeddings USING vec0(
object_id TEXT PRIMARY KEY,
embedding FLOAT[768]
)
"#,
)
.execute(pool)
.await?;
Ok(())
}
/// Load user context
pub async fn load_user_context(&self, user_id: &str) -> Result<UserContext> {
// Load user preferences
let preferences: UserPreferences = sqlx::query_as!(
UserPreferencesRow,
"SELECT preferences FROM users WHERE user_id = ?",
user_id
)
.fetch_optional(&self.pool)
.await?
.map(|row| serde_json::from_str(&row.preferences).unwrap())
.unwrap_or_default();
// Load recent episodes
let episodes = sqlx::query!(
r#"
SELECT id, timestamp, question, answer, confidence, trajectory, reward
FROM episodes
WHERE user_id = ?
ORDER BY timestamp DESC
LIMIT 100
"#,
user_id
)
.fetch_all(&self.pool)
.await?
.into_iter()
.map(|row| Episode {
id: row.id,
timestamp: row.timestamp,
question: row.question,
answer: row.answer,
confidence: row.confidence,
trajectory: serde_json::from_str(&row.trajectory).unwrap(),
reward: row.reward,
})
.collect();
// Load scene memories
let scene_memories = sqlx::query!(
r#"
SELECT scene_id, description, embedding, objects, affordances
FROM scenes
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT 10
"#,
user_id
)
.fetch_all(&self.pool)
.await?
.into_iter()
.map(|row| SceneMemory {
scene_id: row.scene_id,
description: row.description,
embedding: bincode::deserialize(&row.embedding).unwrap(),
objects: serde_json::from_str(&row.objects).unwrap(),
affordances: serde_json::from_str(&row.affordances).unwrap(),
})
.collect();
// Load delta weights if they exist
let delta_weights = sqlx::query!(
r#"
SELECT base_model, delta_bits, scale, sparsity_mask
FROM delta_weights
WHERE user_id = ?
"#,
user_id
)
.fetch_optional(&self.pool)
.await?
.map(|row| DeltaWeights {
base_model: row.base_model,
delta_bits: row.delta_bits,
scale: row.scale,
sparsity_mask: bincode::deserialize(&row.sparsity_mask).unwrap(),
});
Ok(UserContext {
user_id: user_id.to_string(),
episodes,
scene_memories,
preferences,
delta_weights,
})
}
/// Store simulation episode
pub async fn store_episode(&self, user_id: &str, result: &SimulationResult) -> Result<()> {
let episode_id = format!("ep_{}", uuid::Uuid::new_v4());
let timestamp = chrono::Utc::now().timestamp();
let trajectory = serde_json::to_string(&Vec::<StateTransition>::new())?; // Placeholder
sqlx::query!(
r#"
INSERT INTO episodes (id, user_id, timestamp, question, answer, confidence, trajectory, reward)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
"#,
episode_id,
user_id,
timestamp,
"", // Question from request
result.answer,
result.confidence,
trajectory,
result.metrics.avg_reward
)
.execute(&self.pool)
.await?;
Ok(())
}
/// Vector search for similar scenes
pub async fn search_similar_scenes(
&self,
embedding: &[f32],
limit: usize,
) -> Result<Vec<SceneMemory>> {
// Use sqlite-vec for efficient vector search
let query = format!(
r#"
SELECT s.scene_id, s.description, s.embedding, s.objects, s.affordances
FROM scenes s
JOIN scene_embeddings se ON s.scene_id = se.scene_id
WHERE se.embedding MATCH ?
ORDER BY distance
LIMIT {}
"#,
limit
);
let results = sqlx::query(&query)
.bind(bincode::serialize(embedding)?)
.fetch_all(&self.pool)
.await?;
Ok(results
.into_iter()
.map(|row| SceneMemory {
scene_id: row.get("scene_id"),
description: row.get("description"),
embedding: bincode::deserialize(row.get("embedding")).unwrap(),
objects: serde_json::from_str(row.get("objects")).unwrap(),
affordances: serde_json::from_str(row.get("affordances")).unwrap(),
})
.collect())
}
/// Update BitDelta weights for user
pub async fn update_delta_weights(&self, user_id: &str, delta: DeltaWeights) -> Result<()> {
let timestamp = chrono::Utc::now().timestamp();
let sparsity_mask_bytes = bincode::serialize(&delta.sparsity_mask)?;
sqlx::query!(
r#"
INSERT OR REPLACE INTO delta_weights
(user_id, base_model, delta_bits, scale, sparsity_mask, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
"#,
user_id,
delta.base_model,
delta.delta_bits,
delta.scale,
sparsity_mask_bytes,
timestamp
)
.execute(&self.pool)
.await?;
Ok(())
}
/// Compute BitDelta from recent episodes
pub async fn compute_bitdelta(&self, user_id: &str) -> Result<DeltaWeights> {
// Load recent episodes
let episodes = sqlx::query!(
r#"
SELECT trajectory, reward
FROM episodes
WHERE user_id = ?
ORDER BY timestamp DESC
LIMIT 50
"#,
user_id
)
.fetch_all(&self.pool)
.await?;
// Convert trajectories to training data
let mut training_data = Vec::new();
for episode in episodes {
let trajectory: Vec<StateTransition> = serde_json::from_str(&episode.trajectory)?;
training_data.push((trajectory, episode.reward));
}
// Compute 1-bit deltas (placeholder implementation)
// In reality, this would:
// 1. Fine-tune on the training data
// 2. Compute weight differences
// 3. Quantize to 1-bit with learned scale
// 4. Apply sparsity mask
let delta_weights = DeltaWeights {
base_model: "qwen3-30b-a3b".to_string(),
delta_bits: vec![0xFF; 1024], // Placeholder
scale: 0.01,
sparsity_mask: vec![true; 8192], // Placeholder
};
Ok(delta_weights)
}
}
// Helper structs for SQLx
struct UserPreferencesRow {
preferences: String,
}
impl Default for UserPreferences {
fn default() -> Self {
Self {
risk_tolerance: 0.5,
exploration_rate: 0.1,
preferred_tier: None,
}
}
}
// UUID placeholder
mod uuid {
pub struct Uuid;
impl Uuid {
pub fn new_v4() -> String {
format!("{:x}", rand::random::<u128>())
}
}
}
// Random placeholder
mod rand {
pub fn random<T>() -> T
where
T: Default,
{
T::default()
}
}
-444
View File
@@ -1,444 +0,0 @@
//! Tier A: V-JEPA2 Latent Simulation
//!
//! Cheapest tier using latent world models for mental simulation
use anyhow::Result;
use candle_core::{Device, Tensor};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::{SimulationRequest, SimulationResult, SimulationMetrics, SimulationCost, SimulationArtifact, ArtifactType};
/// Configuration for latent simulator
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LatentConfig {
pub model_path: String,
pub embedding_dim: usize,
pub horizon: usize,
pub num_rollouts: usize,
pub temperature: f32,
}
impl Default for LatentConfig {
fn default() -> Self {
Self {
model_path: "models/v-jepa2-ac.onnx".to_string(),
embedding_dim: 768,
horizon: 50,
num_rollouts: 100,
temperature: 0.7,
}
}
}
/// V-JEPA2-AC latent world model
pub struct VJepa2Model {
encoder: ort::Session,
predictor: ort::Session,
action_head: ort::Session,
device: Device,
}
impl VJepa2Model {
pub async fn load(model_path: &str) -> Result<Self> {
// Load ONNX models for V-JEPA2 components
let encoder = ort::Session::builder()?
.with_model_from_file(format!("{}/encoder.onnx", model_path))?;
let predictor = ort::Session::builder()?
.with_model_from_file(format!("{}/predictor.onnx", model_path))?;
let action_head = ort::Session::builder()?
.with_model_from_file(format!("{}/action_head.onnx", model_path))?;
Ok(Self {
encoder,
predictor,
action_head,
device: Device::Cpu,
})
}
/// Encode scene to latent space
pub async fn encode(&self, scene_state: &SceneState) -> Result<LatentState> {
// Convert scene to tensor
let input_tensor = self.scene_to_tensor(scene_state)?;
// Run encoder
let outputs = self.encoder.run(ort::inputs!["input" => input_tensor]?)?;
let latent = outputs["latent"].try_extract_tensor::<f32>()?;
Ok(LatentState {
embedding: latent.to_vec(),
timestamp: scene_state.timestamp,
})
}
/// Predict future latent states given actions
pub async fn rollout(
&self,
initial_state: &LatentState,
action_sequence: &[Action],
horizon: usize,
) -> Result<Vec<LatentState>> {
let mut states = vec![initial_state.clone()];
let mut current = Tensor::from_slice(
&initial_state.embedding,
&[1, initial_state.embedding.len()],
&self.device,
)?;
for (t, action) in action_sequence.iter().take(horizon).enumerate() {
// Prepare action tensor
let action_tensor = self.action_to_tensor(action)?;
// Run predictor
let outputs = self.predictor.run(ort::inputs![
"state" => current.clone(),
"action" => action_tensor
]?)?;
let next_latent = outputs["next_state"].try_extract_tensor::<f32>()?;
states.push(LatentState {
embedding: next_latent.to_vec(),
timestamp: initial_state.timestamp + t as f64 + 1.0,
});
current = Tensor::from_slice(
&next_latent,
&[1, next_latent.len()],
&self.device,
)?;
}
Ok(states)
}
/// Inverse model: predict action from state transitions
pub async fn inverse_action(
&self,
state_t: &LatentState,
state_t1: &LatentState,
) -> Result<Action> {
let s_t = Tensor::from_slice(
&state_t.embedding,
&[1, state_t.embedding.len()],
&self.device,
)?;
let s_t1 = Tensor::from_slice(
&state_t1.embedding,
&[1, state_t1.embedding.len()],
&self.device,
)?;
let outputs = self.action_head.run(ort::inputs![
"state_t" => s_t,
"state_t1" => s_t1
]?)?;
let action_logits = outputs["action"].try_extract_tensor::<f32>()?;
// Decode action from logits
self.decode_action(action_logits)
}
fn scene_to_tensor(&self, scene: &SceneState) -> Result<Tensor> {
// Convert scene graph to tensor representation
// This would flatten object positions, velocities, etc.
let mut features = Vec::new();
for object in &scene.objects {
features.extend(&object.position);
features.extend(&object.velocity);
features.push(object.mass);
}
Ok(Tensor::from_slice(
&features,
&[1, features.len()],
&self.device,
)?)
}
fn action_to_tensor(&self, action: &Action) -> Result<Tensor> {
let action_vec = match action {
Action::Move(dx, dy, dz) => vec![1.0, *dx, *dy, *dz],
Action::Rotate(rx, ry, rz) => vec![2.0, *rx, *ry, *rz],
Action::Grasp(force) => vec![3.0, *force, 0.0, 0.0],
Action::Release => vec![4.0, 0.0, 0.0, 0.0],
};
Ok(Tensor::from_slice(&action_vec, &[1, 4], &self.device)?)
}
fn decode_action(&self, logits: &[f32]) -> Result<Action> {
// Simple argmax decoding
let action_type = logits[0] as i32;
Ok(match action_type {
1 => Action::Move(logits[1], logits[2], logits[3]),
2 => Action::Rotate(logits[1], logits[2], logits[3]),
3 => Action::Grasp(logits[1]),
_ => Action::Release,
})
}
}
/// Latent state representation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LatentState {
pub embedding: Vec<f32>,
pub timestamp: f64,
}
/// Scene state for encoding
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SceneState {
pub objects: Vec<Object>,
pub timestamp: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Object {
pub id: String,
pub position: [f32; 3],
pub velocity: [f32; 3],
pub mass: f32,
}
/// Action space
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Action {
Move(f32, f32, f32), // dx, dy, dz
Rotate(f32, f32, f32), // rx, ry, rz
Grasp(f32), // force
Release,
}
/// Latent simulator
pub struct LatentSimulator {
config: LatentConfig,
model: Arc<Mutex<VJepa2Model>>,
}
impl LatentSimulator {
pub async fn new(config: LatentConfig) -> Result<Self> {
let model = VJepa2Model::load(&config.model_path).await?;
Ok(Self {
config,
model: Arc::new(Mutex::new(model)),
})
}
pub async fn simulate(
&self,
request: &SimulationRequest,
session_layer: &str,
) -> Result<SimulationResult> {
let start = std::time::Instant::now();
// Parse scene from USD
let scene_state = self.load_scene_state(session_layer).await?;
// Encode to latent space
let model = self.model.lock().await;
let initial_latent = model.encode(&scene_state).await?;
// Generate action sequences for the question
let action_sequences = self.generate_action_hypotheses(&request.question).await?;
// Run rollouts in latent space
let mut trajectories = Vec::new();
let mut success_count = 0;
for actions in action_sequences.iter().take(self.config.num_rollouts) {
let trajectory = model.rollout(
&initial_latent,
actions,
self.config.horizon,
).await?;
// Evaluate trajectory
if self.evaluate_trajectory(&trajectory, &request.question).await? {
success_count += 1;
}
trajectories.push(trajectory);
}
let success_rate = success_count as f64 / self.config.num_rollouts as f64;
let confidence = self.calculate_confidence(success_rate, trajectories.len());
// Generate answer
let answer = self.generate_answer(&request.question, success_rate, &trajectories).await?;
// Create visualization artifact (latent trajectory plot)
let artifact = self.create_latent_visualization(&trajectories).await?;
let elapsed = start.elapsed();
Ok(SimulationResult {
answer,
confidence,
tier_used: crate::SimulationTier::Latent,
artifacts: vec![artifact],
metrics: SimulationMetrics {
episodes_run: self.config.num_rollouts as u32,
success_rate,
avg_reward: 0.0, // Not applicable for latent
collision_count: 0,
physics_violations: 0,
},
cost: SimulationCost {
compute_seconds: elapsed.as_secs_f64(),
gpu_seconds: 0.0, // CPU only for now
memory_gb_hours: 0.001,
estimated_usd: 0.001,
},
})
}
async fn load_scene_state(&self, session_layer: &str) -> Result<SceneState> {
// Load from USD session layer
// This would parse the USD file and extract object states
Ok(SceneState {
objects: vec![
Object {
id: "agent_1".to_string(),
position: [0.0, 0.0, 0.0],
velocity: [0.0, 0.0, 0.0],
mass: 1.0,
},
Object {
id: "agent_2".to_string(),
position: [2.0, 0.0, 0.0],
velocity: [-0.5, 0.0, 0.0],
mass: 1.0,
},
],
timestamp: 0.0,
})
}
async fn generate_action_hypotheses(&self, question: &str) -> Result<Vec<Vec<Action>>> {
// Generate diverse action sequences based on the question
// This would use an LLM or planning algorithm
let mut sequences = Vec::new();
for _ in 0..self.config.num_rollouts {
let mut seq = Vec::new();
for _ in 0..self.config.horizon {
// Random exploration for now
seq.push(Action::Move(
rand::random::<f32>() - 0.5,
rand::random::<f32>() - 0.5,
0.0,
));
}
sequences.push(seq);
}
Ok(sequences)
}
async fn evaluate_trajectory(
&self,
trajectory: &[LatentState],
question: &str,
) -> Result<bool> {
// Evaluate if trajectory answers the question
// This would use a learned evaluator or heuristics
// Simple collision detection in latent space
if question.contains("collision") {
// Check if latent states get too similar (indicating collision)
for window in trajectory.windows(2) {
let dist = self.latent_distance(&window[0], &window[1]);
if dist < 0.1 {
return Ok(true); // Collision detected
}
}
}
Ok(false)
}
fn latent_distance(&self, s1: &LatentState, s2: &LatentState) -> f32 {
// L2 distance in latent space
s1.embedding
.iter()
.zip(&s2.embedding)
.map(|(a, b)| (a - b).powi(2))
.sum::<f32>()
.sqrt()
}
fn calculate_confidence(&self, success_rate: f64, num_samples: usize) -> f64 {
// Confidence based on success rate and sample size
let base_confidence = success_rate;
let sample_factor = (num_samples as f64 / 100.0).min(1.0);
base_confidence * sample_factor
}
async fn generate_answer(
&self,
question: &str,
success_rate: f64,
trajectories: &[Vec<LatentState>],
) -> Result<String> {
if question.contains("collision") {
if success_rate > 0.5 {
Ok(format!(
"Collision likely ({:.1}% probability based on {} rollouts)",
success_rate * 100.0,
trajectories.len()
))
} else {
Ok(format!(
"Collision unlikely ({:.1}% probability based on {} rollouts)",
success_rate * 100.0,
trajectories.len()
))
}
} else {
Ok(format!(
"Simulation complete. Success rate: {:.1}% over {} episodes",
success_rate * 100.0,
trajectories.len()
))
}
}
async fn create_latent_visualization(
&self,
trajectories: &[Vec<LatentState>],
) -> Result<SimulationArtifact> {
// Create a visualization of latent trajectories
// This would generate a plot or animation
Ok(SimulationArtifact {
artifact_type: ArtifactType::Trajectory,
uri: "data:application/json,{}".to_string(), // Placeholder
metadata: serde_json::json!({
"type": "latent_trajectories",
"num_trajectories": trajectories.len(),
"horizon": self.config.horizon,
}),
})
}
}
// Placeholder rand module until we add the crate
mod rand {
pub fn random<T>() -> T
where
T: Default,
{
T::default()
}
}
-18
View File
@@ -1,18 +0,0 @@
[package]
name = "hanzo-sovereign"
version = "1.1.21"
edition = "2021"
description = "Hanzo Network — L1 sovereign blockchain with ML-KEM / ML-DSA primitives"
license = "MIT"
repository = "https://github.com/hanzonet/network"
[dependencies]
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
bincode = "1.3"
sha2 = "0.10"
log = "0.4"
ml-kem = "0.2"
ml-dsa = "0.6"
chacha20poly1305 = "0.10"
rand = "0.8"
-171
View File
@@ -1,171 +0,0 @@
// Hanzo Sovereign Chain with ML-KEM/ML-DSA FIPS standards
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc;
use serde::{Deserialize, Serialize};
/// Hanzo L1 Sovereign Blockchain Node
pub struct HanzoSovereignNode {
chain_id: u64,
node_id: String,
// Transaction pool
mempool: Arc<Mutex<Vec<Transaction>>>,
// Event channels
tx_events: mpsc::Sender<TransactionEvent>,
}
impl HanzoSovereignNode {
/// Initialize Hanzo L1 sovereign chain
pub async fn new(config: SovereignConfig) -> Result<Self, HanzoError> {
// Create event channels
let (tx_events, _) = mpsc::channel(10000);
Ok(Self {
chain_id: config.chain_id,
node_id: config.node_id,
mempool: Arc::new(Mutex::new(Vec::new())),
tx_events,
})
}
/// Start the sovereign chain
pub async fn start(&mut self) -> Result<(), HanzoError> {
log::info!("🚀 Starting Hanzo L1 Sovereign Chain");
log::info!("✅ Hanzo L1 Sovereign Chain started successfully");
log::info!(" Chain ID: {}", self.chain_id);
log::info!(" Node ID: {}", self.node_id);
Ok(())
}
/// Submit transaction to mempool
pub async fn submit_transaction(&mut self, tx: Transaction) -> Result<String, HanzoError> {
let tx_hash = tx.hash();
self.mempool.lock().unwrap().push(tx.clone());
self.tx_events.send(TransactionEvent {
tx_hash: tx_hash.clone(),
tx_type: tx.tx_type(),
status: TransactionStatus::Pending,
}).await?;
log::info!("📨 Transaction submitted: {}", tx_hash);
Ok(tx_hash)
}
/// Get node status
pub async fn get_status(&self) -> NodeStatus {
NodeStatus {
chain_id: self.chain_id,
node_id: self.node_id.clone(),
block_height: 1,
peer_count: 0,
mempool_size: self.mempool.lock().unwrap().len(),
pq_commerce_active: true,
mesh_intelligence_active: true,
lux_bridge_connected: true,
zoo_sequencer_active: true,
}
}
}
// Configuration and data structures
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SovereignConfig {
pub chain_id: u64,
pub node_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Transaction {
pub nonce: u64,
pub from: String,
pub to: String,
pub value: u64,
pub data: Vec<u8>,
pub signature: Option<Vec<u8>>,
}
impl Transaction {
pub fn hash(&self) -> String {
use sha2::{Sha256, Digest};
let mut hasher = Sha256::new();
hasher.update(&self.to_bytes());
format!("0x{:x}", hasher.finalize())
}
pub fn to_bytes(&self) -> Vec<u8> {
bincode::serialize(self).unwrap()
}
pub fn tx_type(&self) -> TransactionType {
if self.data.is_empty() {
TransactionType::Transfer
} else {
TransactionType::Contract
}
}
}
#[derive(Debug, Clone)]
pub struct NodeStatus {
pub chain_id: u64,
pub node_id: String,
pub block_height: u64,
pub peer_count: usize,
pub mempool_size: usize,
pub pq_commerce_active: bool,
pub mesh_intelligence_active: bool,
pub lux_bridge_connected: bool,
pub zoo_sequencer_active: bool,
}
// Event types
#[derive(Debug, Clone)]
pub struct TransactionEvent {
pub tx_hash: String,
pub tx_type: TransactionType,
pub status: TransactionStatus,
}
// Enums
#[derive(Debug, Clone, PartialEq)]
pub enum TransactionType {
Transfer,
Contract,
PQCommerce,
CrossChain,
Intelligence,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TransactionStatus {
Pending,
Confirmed,
Failed,
}
// Error handling
#[derive(Debug)]
pub enum HanzoError {
InvalidTransaction,
NetworkError(String),
}
impl std::error::Error for HanzoError {}
impl std::fmt::Display for HanzoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HanzoError::InvalidTransaction => write!(f, "Invalid transaction"),
HanzoError::NetworkError(msg) => write!(f, "Network error: {}", msg),
}
}
}
impl From<tokio::sync::mpsc::error::SendError<TransactionEvent>> for HanzoError {
fn from(_: tokio::sync::mpsc::error::SendError<TransactionEvent>) -> Self {
HanzoError::NetworkError("Channel send error".to_string())
}
}
-38
View File
@@ -1,38 +0,0 @@
[package]
name = "hanzo-tests"
description = "Hanzo AI - Tests Library"
license = "MIT"
version = "0.1.1"
edition = "2021"
publish = false # Not ready for crates.io
# Standalone crate - not part of parent workspace
[workspace]
[dependencies]
tokio = { version = "1.38", features = ["full"] }
anyhow = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tempfile = "3.10"
criterion = "0.5"
tracing = "0.1"
tracing-subscriber = "0.3"
# Runtime dependencies
hanzo-wasm = {path = "../hanzo-wasm", version = "0.1.1"}
hanzo-tools = { path = "../hanzo-tools" }
hanzo-messages = {path = "../hanzo-messages", version = "1.1.12"}
hanzo-db-sqlite = {path = "../hanzo-db-sqlite", version = "1.1.12"}
# For Docker tests
bollard = "0.17"
# For test assets
include_dir = "0.7"
wat = "1.217"
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
proptest = "1.0"
-307
View File
@@ -1,307 +0,0 @@
//! Unified runtime testing framework for Hanzo Node
//!
//! Tests all execution environments:
//! - Native Rust
//! - WASM
//! - JavaScript/TypeScript (Deno)
//! - Python
//! - Docker containers
//! - Go plugins
//! - MCP servers
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::time::{Duration, Instant};
use tokio::sync::mpsc;
use tracing::{info, warn, error};
pub mod native_tests;
pub mod wasm_tests;
pub mod deno_tests;
pub mod python_tests;
pub mod docker_tests;
pub mod unified_tests;
/// Standard test payload for all runtimes
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestPayload {
pub operation: String,
pub input_a: f64,
pub input_b: f64,
pub expected: f64,
}
/// Test result with performance metrics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestResult {
pub runtime: String,
pub success: bool,
pub result: Option<f64>,
pub execution_time: Duration,
pub memory_usage: Option<u64>,
pub gas_consumed: Option<u64>,
pub error: Option<String>,
}
/// Runtime test trait - all runtimes must implement this
#[async_trait::async_trait]
pub trait RuntimeTest: Send + Sync {
/// Get the runtime name
fn name(&self) -> &str;
/// Execute a simple computation
async fn execute_compute(&self, payload: TestPayload) -> Result<TestResult>;
/// Test file I/O capabilities
async fn test_file_io(&self, content: Vec<u8>) -> Result<Vec<u8>>;
/// Test network capabilities
async fn test_network(&self, url: &str) -> Result<String>;
/// Test crypto operations
async fn test_crypto(&self, data: Vec<u8>) -> Result<Vec<u8>>;
/// Benchmark performance
async fn benchmark(&self) -> Result<BenchmarkResults>;
}
/// Benchmark results for runtime comparison
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkResults {
pub runtime: String,
pub ops_per_second: f64,
pub avg_latency_ms: f64,
pub p99_latency_ms: f64,
pub memory_efficiency: f64,
pub startup_time_ms: f64,
}
/// Unified test orchestrator
pub struct RuntimeOrchestrator {
runtimes: Vec<Box<dyn RuntimeTest>>,
results: Vec<TestResult>,
}
impl RuntimeOrchestrator {
pub fn new() -> Self {
Self {
runtimes: vec![],
results: vec![],
}
}
pub fn add_runtime(&mut self, runtime: Box<dyn RuntimeTest>) {
self.runtimes.push(runtime);
}
/// Run all tests across all runtimes
pub async fn run_all_tests(&mut self) -> Result<TestReport> {
info!("Starting unified runtime tests");
let test_cases = vec![
TestPayload {
operation: "add".to_string(),
input_a: 10.0,
input_b: 20.0,
expected: 30.0,
},
TestPayload {
operation: "multiply".to_string(),
input_a: 5.0,
input_b: 7.0,
expected: 35.0,
},
TestPayload {
operation: "fibonacci".to_string(),
input_a: 10.0,
input_b: 0.0,
expected: 55.0,
},
];
let mut report = TestReport::new();
for runtime in &self.runtimes {
info!("Testing runtime: {}", runtime.name());
for test_case in &test_cases {
let start = Instant::now();
match runtime.execute_compute(test_case.clone()).await {
Ok(result) => {
report.add_result(result);
}
Err(e) => {
error!("Test failed for {}: {}", runtime.name(), e);
report.add_result(TestResult {
runtime: runtime.name().to_string(),
success: false,
result: None,
execution_time: start.elapsed(),
memory_usage: None,
gas_consumed: None,
error: Some(e.to_string()),
});
}
}
}
// Run benchmarks
match runtime.benchmark().await {
Ok(bench) => report.add_benchmark(bench),
Err(e) => warn!("Benchmark failed for {}: {}", runtime.name(), e),
}
}
Ok(report)
}
/// Compare runtime performance
pub async fn compare_performance(&self) -> Result<PerformanceComparison> {
let mut comparison = PerformanceComparison::new();
for runtime in &self.runtimes {
let bench = runtime.benchmark().await?;
comparison.add_runtime_benchmark(bench);
}
comparison.analyze();
Ok(comparison)
}
}
/// Test report with all results
#[derive(Debug, Serialize, Deserialize)]
pub struct TestReport {
pub timestamp: chrono::DateTime<chrono::Utc>,
pub results: Vec<TestResult>,
pub benchmarks: Vec<BenchmarkResults>,
pub summary: TestSummary,
}
impl TestReport {
fn new() -> Self {
Self {
timestamp: chrono::Utc::now(),
results: vec![],
benchmarks: vec![],
summary: TestSummary::default(),
}
}
fn add_result(&mut self, result: TestResult) {
self.results.push(result);
self.update_summary();
}
fn add_benchmark(&mut self, benchmark: BenchmarkResults) {
self.benchmarks.push(benchmark);
}
fn update_summary(&mut self) {
self.summary.total_tests = self.results.len();
self.summary.passed = self.results.iter().filter(|r| r.success).count();
self.summary.failed = self.results.iter().filter(|r| !r.success).count();
if !self.results.is_empty() {
self.summary.avg_execution_time = Duration::from_millis(
(self.results.iter()
.map(|r| r.execution_time.as_millis() as u64)
.sum::<u64>() / self.results.len() as u64) as u64
);
}
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct TestSummary {
pub total_tests: usize,
pub passed: usize,
pub failed: usize,
pub avg_execution_time: Duration,
}
/// Performance comparison across runtimes
#[derive(Debug, Serialize, Deserialize)]
pub struct PerformanceComparison {
pub runtimes: Vec<BenchmarkResults>,
pub fastest_runtime: Option<String>,
pub most_efficient: Option<String>,
pub recommendations: Vec<String>,
}
impl PerformanceComparison {
fn new() -> Self {
Self {
runtimes: vec![],
fastest_runtime: None,
most_efficient: None,
recommendations: vec![],
}
}
fn add_runtime_benchmark(&mut self, bench: BenchmarkResults) {
self.runtimes.push(bench);
}
fn analyze(&mut self) {
if let Some(fastest) = self.runtimes.iter()
.min_by(|a, b| a.avg_latency_ms.partial_cmp(&b.avg_latency_ms).unwrap()) {
self.fastest_runtime = Some(fastest.runtime.clone());
}
if let Some(efficient) = self.runtimes.iter()
.max_by(|a, b| a.memory_efficiency.partial_cmp(&b.memory_efficiency).unwrap()) {
self.most_efficient = Some(efficient.runtime.clone());
}
// Generate recommendations based on analysis
self.generate_recommendations();
}
fn generate_recommendations(&mut self) {
// Analyze and provide recommendations
for runtime in &self.runtimes {
if runtime.ops_per_second > 10000.0 {
self.recommendations.push(
format!("{} is suitable for high-throughput workloads", runtime.runtime)
);
}
if runtime.avg_latency_ms < 1.0 {
self.recommendations.push(
format!("{} is ideal for low-latency requirements", runtime.runtime)
);
}
if runtime.memory_efficiency > 0.9 {
self.recommendations.push(
format!("{} has excellent memory efficiency", runtime.runtime)
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_orchestrator_creation() {
let orchestrator = RuntimeOrchestrator::new();
assert_eq!(orchestrator.runtimes.len(), 0);
}
#[tokio::test]
async fn test_payload_serialization() {
let payload = TestPayload {
operation: "test".to_string(),
input_a: 1.0,
input_b: 2.0,
expected: 3.0,
};
let json = serde_json::to_string(&payload).unwrap();
let decoded: TestPayload = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.operation, payload.operation);
}
}
-231
View File
@@ -1,231 +0,0 @@
//! WASM runtime tests
use crate::{RuntimeTest, TestPayload, TestResult, BenchmarkResults};
use anyhow::Result;
use hanzo_wasm_runtime::{WasmRuntime, WasmConfig};
use std::sync::Arc;
use std::time::{Duration, Instant};
use wat::parse_str;
pub struct WasmRuntimeTest {
runtime: Arc<WasmRuntime>,
}
impl WasmRuntimeTest {
pub async fn new() -> Result<Self> {
let config = WasmConfig {
max_memory_bytes: 128 * 1024 * 1024, // 128MB
max_execution_time: Duration::from_secs(10),
enable_wasi: false,
fuel_limit: Some(1_000_000_000),
};
let runtime = Arc::new(WasmRuntime::new(config)?);
// Load test modules
Self::load_test_modules(&runtime).await?;
Ok(Self { runtime })
}
async fn load_test_modules(runtime: &WasmRuntime) -> Result<()> {
// Simple math module
let math_wat = r#"
(module
(func $add (export "add") (param f64 f64) (result f64)
local.get 0
local.get 1
f64.add
)
(func $multiply (export "multiply") (param f64 f64) (result f64)
local.get 0
local.get 1
f64.mul
)
(func $fibonacci (export "fibonacci") (param i32) (result i32)
(local i32 i32 i32)
local.get 0
i32.const 2
i32.lt_s
if
local.get 0
return
end
i32.const 0
local.set 1
i32.const 1
local.set 2
loop
local.get 1
local.get 2
i32.add
local.set 3
local.get 2
local.set 1
local.get 3
local.set 2
local.get 0
i32.const 1
i32.sub
local.tee 0
i32.const 1
i32.gt_s
br_if 0
end
local.get 3
)
)
"#;
let math_bytes = parse_str(math_wat)?;
runtime.load_module("math".to_string(), math_bytes).await?;
Ok(())
}
}
#[async_trait::async_trait]
impl RuntimeTest for WasmRuntimeTest {
fn name(&self) -> &str {
"WASM"
}
async fn execute_compute(&self, payload: TestPayload) -> Result<TestResult> {
let start = Instant::now();
let fuel_before = 1_000_000_000u64;
let result = match payload.operation.as_str() {
"add" => {
// For now, return a mock result since WASM execution isn't fully implemented
// TODO: Implement proper WASM function calling
Some(payload.input_a + payload.input_b)
}
"multiply" => {
Some(payload.input_a * payload.input_b)
}
"fibonacci" => {
// Calculate fibonacci
let n = payload.input_a as i32;
Some(fibonacci(n) as f64)
}
_ => None,
};
let execution_time = start.elapsed();
let gas_consumed = fuel_before - 900_000_000; // Mock gas consumption
Ok(TestResult {
runtime: self.name().to_string(),
success: result.is_some() && result == Some(payload.expected),
result,
execution_time,
memory_usage: Some(1024 * 1024), // 1MB mock
gas_consumed: Some(gas_consumed),
error: None,
})
}
async fn test_file_io(&self, content: Vec<u8>) -> Result<Vec<u8>> {
// WASM is sandboxed - no direct file I/O
Err(anyhow::anyhow!("File I/O not supported in WASM sandbox"))
}
async fn test_network(&self, _url: &str) -> Result<String> {
// Network access requires host functions
Ok("Network access requires host functions".to_string())
}
async fn test_crypto(&self, data: Vec<u8>) -> Result<Vec<u8>> {
// Simple hash mock - would use WASM crypto module
Ok(data.into_iter().map(|b| b.wrapping_add(1)).collect())
}
async fn benchmark(&self) -> Result<BenchmarkResults> {
let iterations = 1000;
let mut latencies = Vec::new();
let start = Instant::now();
for _ in 0..iterations {
let op_start = Instant::now();
let _ = self.execute_compute(TestPayload {
operation: "add".to_string(),
input_a: 42.0,
input_b: 58.0,
expected: 100.0,
}).await?;
latencies.push(op_start.elapsed().as_millis() as f64);
}
let total_time = start.elapsed();
let ops_per_second = iterations as f64 / total_time.as_secs_f64();
latencies.sort_by(|a, b| a.partial_cmp(b).unwrap());
let avg_latency_ms = latencies.iter().sum::<f64>() / latencies.len() as f64;
let p99_latency_ms = latencies[(latencies.len() as f64 * 0.99) as usize];
Ok(BenchmarkResults {
runtime: self.name().to_string(),
ops_per_second,
avg_latency_ms,
p99_latency_ms,
memory_efficiency: 0.95, // WASM has excellent memory efficiency
startup_time_ms: 10.0, // Fast module loading
})
}
}
fn fibonacci(n: i32) -> i32 {
if n <= 1 {
return n;
}
let mut a = 0;
let mut b = 1;
for _ in 2..=n {
let temp = a + b;
a = b;
b = temp;
}
b
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_wasm_runtime_creation() {
if std::env::var("CI").is_ok() {
println!("Skipping WASM test in CI");
return;
}
let runtime = WasmRuntimeTest::new().await;
assert!(runtime.is_ok());
}
#[tokio::test]
async fn test_wasm_computation() {
if std::env::var("CI").is_ok() {
println!("Skipping WASM test in CI");
return;
}
let runtime = WasmRuntimeTest::new().await.unwrap();
let payload = TestPayload {
operation: "add".to_string(),
input_a: 10.0,
input_b: 20.0,
expected: 30.0,
};
let result = runtime.execute_compute(payload).await.unwrap();
assert!(result.success);
assert_eq!(result.result, Some(30.0));
}
}
+3 -6
View File
@@ -1,14 +1,11 @@
[package]
name = "hanzo-vm"
description = "Lux VM interface for Hanzo L2 - wraps EVM execution"
description = "Hanzo Network — EVM with precompiles for PQ verify, Quasar, AI inference, AI embedding (0x0101/0x0102/0x0201/0x0202)"
license = "MIT"
version.workspace = true
edition.workspace = true
authors.workspace = true
repository = "https://github.com/hanzonet/network"
# Cannot publish to crates.io until hanzo-engine and luxprecompile-sys are published.
# Also depends on the local net hanzo-pqc (publish = false).
publish = false
[features]
default = []
@@ -35,10 +32,10 @@ hanzo-pqc = { workspace = true }
# process-global and populated at startup by `hanzo-node` (or any other
# binary embedding hanzo-vm). If no engine is registered the precompile
# dispatchers revert — there is no in-tree fallback.
hanzo-engine = { path = "../../engine/hanzo-engine" }
hanzo-engine = { version = "0.6.0", path = "../../engine/hanzo-engine" }
# Canonical Go-backed precompile dispatch (Quasar, ML-DSA, etc.)
luxprecompile-sys = { path = "../../../lux/precompile/bindings/rust" }
luxprecompile-sys = "0.1"
# State storage (bundled SQLite for dev; MPT later)
rusqlite = { workspace = true }