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:
Markus Maiwald 2026-01-31 03:21:35 +01:00
parent 8b55df50b5
commit 20c593220c
6 changed files with 407 additions and 0 deletions

View File

@ -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,
}); });

27
membrane-agent/Cargo.toml Normal file
View File

@ -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]

11
membrane-agent/build.rs Normal file
View File

@ -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");
}

10
membrane-agent/src/lib.rs Normal file
View File

@ -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,
};

View File

@ -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(())
}

View File

@ -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);
}
}