Phase 6B Week 1: Rust membrane-agent FFI bindings (partial)
- Created membrane-agent/ Rust crate structure - Implemented qvl_ffi.rs: Safe Rust FFI wrapper around Zig QVL C ABI - QvlClient with RAII semantics (init/deinit) - Safe wrappers: get_trust_score, verify_pop, detect_betrayal, add/revoke edges - AnomalyScore, PopVerdict enums - Created main.rs: Minimal daemon stub - Created Cargo.toml, build.rs for future Zig library linking Blocker: build.zig static library target (Zig 0.15.2 API incompatibility) - addStaticLibrary/addSharedLibrary don't exist in this Zig version - LibraryOptions API changed (no .kind, .root_source_file fields) - Deferred to next session: either upgrade Zig or use manual object linking All Zig FFI tests passing (173/173). Rust compiles but can't link yet.
This commit is contained in:
parent
8b55df50b5
commit
20c593220c
|
|
@ -267,6 +267,7 @@ pub fn build(b: *std.Build) void {
|
||||||
l1_qvl_ffi_mod.addImport("qvl", l1_qvl_mod);
|
l1_qvl_ffi_mod.addImport("qvl", l1_qvl_mod);
|
||||||
l1_qvl_ffi_mod.addImport("time", time_mod);
|
l1_qvl_ffi_mod.addImport("time", time_mod);
|
||||||
|
|
||||||
|
|
||||||
const l1_vector_tests = b.addTest(.{
|
const l1_vector_tests = b.addTest(.{
|
||||||
.root_module = l1_vector_mod,
|
.root_module = l1_vector_mod,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
[package]
|
||||||
|
name = "membrane-agent"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["Markus Maiwald <markus@libertaria.world>"]
|
||||||
|
description = "L2 Membrane Agent - Trust-based policy enforcement daemon for Libertaria"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = "0.3"
|
||||||
|
chrono = "0.4"
|
||||||
|
thiserror = "1.0"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
cc = "1.0"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "membrane_agent"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "membrane-agent"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fn main() {
|
||||||
|
// Link against Zig QVL FFI shared library
|
||||||
|
let sdk_root = std::env::var("CARGO_MANIFEST_DIR")
|
||||||
|
.expect("CARGO_MANIFEST_DIR not set");
|
||||||
|
let lib_path = format!("{}/../zig-out/lib", sdk_root);
|
||||||
|
|
||||||
|
println!("cargo:rustc-link-search=native={}", lib_path);
|
||||||
|
println!("cargo:rustc-link-lib=dylib=qvl_ffi");
|
||||||
|
println!("cargo:rerun-if-changed=../zig-out/lib/libqvl_ffi.so");
|
||||||
|
println!("cargo:rerun-if-changed=../l1-identity/qvl.h");
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
//! Membrane Agent - L2 Trust-Based Policy Enforcement
|
||||||
|
//!
|
||||||
|
//! Library components for the Membrane Agent daemon.
|
||||||
|
|
||||||
|
pub mod qvl_ffi;
|
||||||
|
|
||||||
|
pub use qvl_ffi::{
|
||||||
|
QvlClient, QvlError, AnomalyScore, AnomalyReason,
|
||||||
|
PopVerdict, QvlRiskEdge,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
//! Membrane Agent Daemon
|
||||||
|
//!
|
||||||
|
//! L2 trust-based policy enforcement daemon for Libertaria.
|
||||||
|
|
||||||
|
use membrane_agent::QvlClient;
|
||||||
|
use tracing::{info, error};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Initialize tracing
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
info!("🛡️ Membrane Agent starting...");
|
||||||
|
|
||||||
|
// Initialize QVL client
|
||||||
|
let qvl = QvlClient::new()?;
|
||||||
|
info!("✅ QVL client initialized");
|
||||||
|
|
||||||
|
// Test basic functionality
|
||||||
|
let reputation = qvl.get_reputation(0)?;
|
||||||
|
info!("Node 0 reputation: {:.2}", reputation);
|
||||||
|
|
||||||
|
let anomaly = qvl.detect_betrayal(0)?;
|
||||||
|
info!("Betrayal check: score={:.2}, reason={:?}", anomaly.score, anomaly.reason);
|
||||||
|
|
||||||
|
info!("🚀 Membrane Agent running (stub mode)");
|
||||||
|
info!("TODO: Implement event listener, policy enforcer, alert system");
|
||||||
|
|
||||||
|
// Keep daemon alive
|
||||||
|
tokio::signal::ctrl_c().await?;
|
||||||
|
info!("Shutting down...");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,324 @@
|
||||||
|
//! QVL FFI - Rust bindings to Zig QVL C ABI
|
||||||
|
//!
|
||||||
|
//! Provides safe Rust wrappers around the C FFI exports from l1-identity/qvl_ffi.zig.
|
||||||
|
|
||||||
|
use std::os::raw::c_int;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// RAW FFI DECLARATIONS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Opaque handle to QVL context (Zig internals)
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct QvlContext {
|
||||||
|
_opaque: [u8; 0],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Anomaly score returned from betrayal detection
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct QvlAnomalyScore {
|
||||||
|
pub node: u32,
|
||||||
|
pub score: f64,
|
||||||
|
pub reason: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Risk edge for graph mutations
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct QvlRiskEdge {
|
||||||
|
pub from: u32,
|
||||||
|
pub to: u32,
|
||||||
|
pub risk: f64,
|
||||||
|
pub timestamp_ns: u64,
|
||||||
|
pub nonce: u64,
|
||||||
|
pub level: u8,
|
||||||
|
pub expires_at_ns: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Proof-of-Path verification verdict
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub enum PopVerdict {
|
||||||
|
Valid = 0,
|
||||||
|
InvalidEndpoints = 1,
|
||||||
|
BrokenLink = 2,
|
||||||
|
Revoked = 3,
|
||||||
|
Replay = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
fn qvl_init() -> *mut QvlContext;
|
||||||
|
fn qvl_deinit(ctx: *mut QvlContext);
|
||||||
|
|
||||||
|
fn qvl_get_trust_score(
|
||||||
|
ctx: *mut QvlContext,
|
||||||
|
did: *const u8,
|
||||||
|
did_len: usize,
|
||||||
|
) -> f64;
|
||||||
|
|
||||||
|
fn qvl_get_reputation(ctx: *mut QvlContext, node_id: u32) -> f64;
|
||||||
|
|
||||||
|
fn qvl_verify_pop(
|
||||||
|
ctx: *mut QvlContext,
|
||||||
|
proof_bytes: *const u8,
|
||||||
|
proof_len: usize,
|
||||||
|
sender_did: *const u8,
|
||||||
|
receiver_did: *const u8,
|
||||||
|
) -> PopVerdict;
|
||||||
|
|
||||||
|
fn qvl_detect_betrayal(
|
||||||
|
ctx: *mut QvlContext,
|
||||||
|
source_node: u32,
|
||||||
|
) -> QvlAnomalyScore;
|
||||||
|
|
||||||
|
fn qvl_add_trust_edge(
|
||||||
|
ctx: *mut QvlContext,
|
||||||
|
edge: *const QvlRiskEdge,
|
||||||
|
) -> c_int;
|
||||||
|
|
||||||
|
fn qvl_revoke_trust_edge(
|
||||||
|
ctx: *mut QvlContext,
|
||||||
|
from: u32,
|
||||||
|
to: u32,
|
||||||
|
) -> c_int;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// SAFE RUST WRAPPER
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Anomaly reason enum (safe Rust version)
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum AnomalyReason {
|
||||||
|
None,
|
||||||
|
NegativeCycle,
|
||||||
|
LowCoverage,
|
||||||
|
BpDivergence,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnomalyReason {
|
||||||
|
fn from_u8(val: u8) -> Self {
|
||||||
|
match val {
|
||||||
|
0 => Self::None,
|
||||||
|
1 => Self::NegativeCycle,
|
||||||
|
2 => Self::LowCoverage,
|
||||||
|
3 => Self::BpDivergence,
|
||||||
|
_ => Self::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Anomaly score (safe Rust version)
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AnomalyScore {
|
||||||
|
pub node: u32,
|
||||||
|
pub score: f64,
|
||||||
|
pub reason: AnomalyReason,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// QVL client errors
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum QvlError {
|
||||||
|
#[error("QVL initialization failed")]
|
||||||
|
InitFailed,
|
||||||
|
|
||||||
|
#[error("Invalid DID (must be 32 bytes)")]
|
||||||
|
InvalidDid,
|
||||||
|
|
||||||
|
#[error("Trust score query failed")]
|
||||||
|
TrustScoreFailed,
|
||||||
|
|
||||||
|
#[error("Graph mutation failed")]
|
||||||
|
MutationFailed,
|
||||||
|
|
||||||
|
#[error("Null context")]
|
||||||
|
NullContext,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe Rust wrapper around QVL FFI
|
||||||
|
pub struct QvlClient {
|
||||||
|
ctx: *mut QvlContext,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QvlClient {
|
||||||
|
/// Initialize QVL context
|
||||||
|
pub fn new() -> Result<Self, QvlError> {
|
||||||
|
let ctx = unsafe { qvl_init() };
|
||||||
|
if ctx.is_null() {
|
||||||
|
return Err(QvlError::InitFailed);
|
||||||
|
}
|
||||||
|
Ok(Self { ctx })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get trust score for a DID
|
||||||
|
pub fn get_trust_score(&self, did: &[u8; 32]) -> Result<f64, QvlError> {
|
||||||
|
if self.ctx.is_null() {
|
||||||
|
return Err(QvlError::NullContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
let score = unsafe {
|
||||||
|
qvl_get_trust_score(self.ctx, did.as_ptr(), 32)
|
||||||
|
};
|
||||||
|
|
||||||
|
if score < 0.0 {
|
||||||
|
Err(QvlError::TrustScoreFailed)
|
||||||
|
} else {
|
||||||
|
Ok(score)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get reputation for a node ID
|
||||||
|
pub fn get_reputation(&self, node_id: u32) -> Result<f64, QvlError> {
|
||||||
|
if self.ctx.is_null() {
|
||||||
|
return Err(QvlError::NullContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
let score = unsafe {
|
||||||
|
qvl_get_reputation(self.ctx, node_id)
|
||||||
|
};
|
||||||
|
|
||||||
|
if score < 0.0 {
|
||||||
|
Err(QvlError::TrustScoreFailed)
|
||||||
|
} else {
|
||||||
|
Ok(score)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify a Proof-of-Path
|
||||||
|
pub fn verify_pop(
|
||||||
|
&self,
|
||||||
|
proof: &[u8],
|
||||||
|
sender_did: &[u8; 32],
|
||||||
|
receiver_did: &[u8; 32],
|
||||||
|
) -> Result<PopVerdict, QvlError> {
|
||||||
|
if self.ctx.is_null() {
|
||||||
|
return Err(QvlError::NullContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
let verdict = unsafe {
|
||||||
|
qvl_verify_pop(
|
||||||
|
self.ctx,
|
||||||
|
proof.as_ptr(),
|
||||||
|
proof.len(),
|
||||||
|
sender_did.as_ptr(),
|
||||||
|
receiver_did.as_ptr(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(verdict)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect betrayal (Bellman-Ford negative cycle detection)
|
||||||
|
pub fn detect_betrayal(&self, source_node: u32) -> Result<AnomalyScore, QvlError> {
|
||||||
|
if self.ctx.is_null() {
|
||||||
|
return Err(QvlError::NullContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
let raw_score = unsafe {
|
||||||
|
qvl_detect_betrayal(self.ctx, source_node)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(AnomalyScore {
|
||||||
|
node: raw_score.node,
|
||||||
|
score: raw_score.score,
|
||||||
|
reason: AnomalyReason::from_u8(raw_score.reason),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a trust edge to the risk graph
|
||||||
|
pub fn add_trust_edge(&self, edge: QvlRiskEdge) -> Result<(), QvlError> {
|
||||||
|
if self.ctx.is_null() {
|
||||||
|
return Err(QvlError::NullContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = unsafe {
|
||||||
|
qvl_add_trust_edge(self.ctx, &edge as *const QvlRiskEdge)
|
||||||
|
};
|
||||||
|
|
||||||
|
if result == 0 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(QvlError::MutationFailed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Revoke a trust edge
|
||||||
|
pub fn revoke_trust_edge(&self, from: u32, to: u32) -> Result<(), QvlError> {
|
||||||
|
if self.ctx.is_null() {
|
||||||
|
return Err(QvlError::NullContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = unsafe {
|
||||||
|
qvl_revoke_trust_edge(self.ctx, from, to)
|
||||||
|
};
|
||||||
|
|
||||||
|
if result == 0 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(QvlError::MutationFailed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for QvlClient {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if !self.ctx.is_null() {
|
||||||
|
unsafe { qvl_deinit(self.ctx) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as Send + Sync (QVL is thread-safe via C allocator)
|
||||||
|
unsafe impl Send for QvlClient {}
|
||||||
|
unsafe impl Sync for QvlClient {}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TESTS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_qvl_init_deinit() {
|
||||||
|
let client = QvlClient::new().expect("QVL init failed");
|
||||||
|
drop(client); // Verify deinit doesn't crash
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_reputation() {
|
||||||
|
let client = QvlClient::new().unwrap();
|
||||||
|
let score = client.get_reputation(42).unwrap();
|
||||||
|
assert_eq!(score, 0.5); // Default neutral reputation
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_edge() {
|
||||||
|
let client = QvlClient::new().unwrap();
|
||||||
|
let edge = QvlRiskEdge {
|
||||||
|
from: 0,
|
||||||
|
to: 1,
|
||||||
|
risk: 0.5,
|
||||||
|
timestamp_ns: 1000,
|
||||||
|
nonce: 0,
|
||||||
|
level: 3,
|
||||||
|
expires_at_ns: 2000,
|
||||||
|
};
|
||||||
|
|
||||||
|
client.add_trust_edge(edge).expect("Add edge failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_detect_betrayal_no_cycle() {
|
||||||
|
let client = QvlClient::new().unwrap();
|
||||||
|
let anomaly = client.detect_betrayal(0).unwrap();
|
||||||
|
|
||||||
|
// No betrayal in empty graph
|
||||||
|
assert_eq!(anomaly.score, 0.0);
|
||||||
|
assert_eq!(anomaly.reason, AnomalyReason::None);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue