feat: schema projections, command transactions, quantum-secure encryption

This commit is contained in:
Will Anderson
2026-04-27 18:26:46 -05:00
parent 69410a6908
commit 192528543f
27 changed files with 5460 additions and 4 deletions
+4
View File
@@ -12,6 +12,10 @@ path = "src/main.rs"
[dependencies]
engram-core = { path = "../engram-core" }
engram-sync = { path = "../engram-sync" }
engram-projection = { path = "../engram-projection" }
engram-tx = { path = "../engram-tx" }
engram-crypto = { path = "../engram-crypto" }
sled = "0.34"
axum = { version = "0.7", features = ["json"] }
tokio = { version = "1", features = ["full"] }
tower = "0.4"
+38
View File
@@ -36,7 +36,9 @@ use axum::{
Router,
};
use engram_core::EngramDb;
use engram_projection::registry::ProjectionRegistry;
use engram_sync::{SyncConfig, SyncEngine};
use engram_tx::TransactionEngine;
use mime_guess::from_path;
use rust_embed::RustEmbed;
use tokio::time::interval;
@@ -109,6 +111,18 @@ async fn main() -> anyhow::Result<()> {
let db = EngramDb::open(&PathBuf::from(&db_path))?;
let db = Arc::new(Mutex::new(db));
// Transaction engine (separate sled db alongside the main db)
let tx_log_path = format!("{}-tx-log", db_path);
let tx_log_db = sled::open(&tx_log_path)?;
let tx_engine = Arc::new(Mutex::new(TransactionEngine::new(
db.clone(),
tx_log_db,
Some(uuid::Uuid::new_v4()),
)));
// Projection registry
let projection_registry = Arc::new(Mutex::new(ProjectionRegistry::new()));
info!("Database opened at {}", db_path);
// Sync engine — wrapped in tokio::sync::Mutex so it can be held across .await
@@ -164,6 +178,8 @@ async fn main() -> anyhow::Result<()> {
db: db.clone(),
sync_engine: sync_engine.clone(),
api_key: api_key.clone(),
projection_registry,
tx_engine,
});
// Protected sync/swarm routes (auth middleware applied)
@@ -192,6 +208,26 @@ async fn main() -> anyhow::Result<()> {
.route("/decay", post(routes::core::decay))
.route("/consolidate", post(routes::core::consolidate));
// Projection routes (no auth)
let projection_routes = Router::new()
.route("/projections", post(routes::projection::register_projection))
.route("/projections", get(routes::projection::list_projections))
.route(
"/projections/{name}/schema",
get(routes::projection::get_projection_schema),
)
.route(
"/projections/{name}/query",
post(routes::projection::query_projection),
);
// Transaction routes (no auth — add auth layer if needed)
let tx_routes = Router::new()
.route("/tx/apply", post(routes::tx::tx_apply))
.route("/tx/rollback/{command_id}", post(routes::tx::tx_rollback))
.route("/tx/history", get(routes::tx::tx_history))
.route("/tx/chain/{command_id}", get(routes::tx::tx_causal_chain));
let studio_routes = Router::new()
.route("/", get(serve_studio_index))
.route("/studio", get(serve_studio_index))
@@ -201,6 +237,8 @@ async fn main() -> anyhow::Result<()> {
.merge(studio_routes)
.merge(core_routes)
.merge(sync_routes)
.merge(projection_routes)
.merge(tx_routes)
.layer(CorsLayer::permissive())
.with_state(state);
@@ -1,3 +1,5 @@
pub mod core;
pub mod projection;
pub mod sync;
pub mod swarm;
pub mod tx;
@@ -0,0 +1,121 @@
/// Projection API routes.
///
/// POST /projections — register a projection schema
/// GET /projections — list all registered schemas
/// POST /projections/{name}/query — run activation then project results
/// GET /projections/{name}/schema — get a schema definition
use axum::{
extract::{Path, State},
http::StatusCode,
Json,
};
use engram_projection::{
engine::ProjectionEngine,
schema::{ProjectionResult, ProjectionSchema},
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use uuid::Uuid;
use crate::state::AppState;
// ── POST /projections ─────────────────────────────────────────────────────────
pub async fn register_projection(
State(state): State<Arc<AppState>>,
Json(schema): Json<ProjectionSchema>,
) -> Result<StatusCode, StatusCode> {
let mut reg = state
.projection_registry
.lock()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
reg.upsert(schema)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(StatusCode::CREATED)
}
// ── GET /projections ──────────────────────────────────────────────────────────
#[derive(Serialize)]
pub struct ListProjectionsResponse {
pub schemas: Vec<ProjectionSchema>,
}
pub async fn list_projections(
State(state): State<Arc<AppState>>,
) -> Result<Json<ListProjectionsResponse>, StatusCode> {
let reg = state
.projection_registry
.lock()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let schemas = reg.list().into_iter().cloned().collect();
Ok(Json(ListProjectionsResponse { schemas }))
}
// ── GET /projections/{name}/schema ────────────────────────────────────────────
pub async fn get_projection_schema(
State(state): State<Arc<AppState>>,
Path(name): Path<String>,
) -> Result<Json<ProjectionSchema>, StatusCode> {
let reg = state
.projection_registry
.lock()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
match reg.get(&name) {
Ok(schema) => Ok(Json(schema.clone())),
Err(_) => Err(StatusCode::NOT_FOUND),
}
}
// ── POST /projections/{name}/query ────────────────────────────────────────────
#[derive(Deserialize)]
pub struct ProjectionQueryRequest {
/// Seed node IDs for spreading activation.
pub seeds: Vec<Uuid>,
/// Query embedding for spreading activation.
pub query_embedding: Vec<f32>,
#[serde(default = "default_depth")]
pub max_depth: u8,
#[serde(default = "default_limit")]
pub limit: usize,
}
fn default_depth() -> u8 {
3
}
fn default_limit() -> usize {
20
}
pub async fn query_projection(
State(state): State<Arc<AppState>>,
Path(name): Path<String>,
Json(req): Json<ProjectionQueryRequest>,
) -> Result<Json<ProjectionResult>, StatusCode> {
// Load schema
let schema = {
let reg = state
.projection_registry
.lock()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
match reg.get(&name) {
Ok(s) => s.clone(),
Err(_) => return Err(StatusCode::NOT_FOUND),
}
};
// Run spreading activation
let activated = {
let db = state.db.lock().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
db.activate(&req.seeds, &req.query_embedding, req.max_depth, req.limit)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
};
// Apply projection
let result = ProjectionEngine::project(&schema, &activated)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(result))
}
@@ -0,0 +1,109 @@
/// Transaction API routes.
///
/// POST /tx/apply — apply a command
/// POST /tx/rollback/{id} — roll back a command
/// GET /tx/history?since={ms} — command history
/// GET /tx/chain/{id} — causal chain for a command
use axum::{
extract::{Path, Query, State},
http::StatusCode,
Json,
};
use engram_tx::{Command, CommandResult};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use uuid::Uuid;
use crate::state::AppState;
// ── POST /tx/apply ────────────────────────────────────────────────────────────
pub async fn tx_apply(
State(state): State<Arc<AppState>>,
Json(cmd): Json<Command>,
) -> Result<Json<CommandResult>, StatusCode> {
let mut engine = state
.tx_engine
.lock()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let result = engine
.apply(cmd)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(result))
}
// ── POST /tx/rollback/{command_id} ───────────────────────────────────────────
#[derive(Serialize)]
pub struct RollbackResponse {
pub rollback_command_id: Uuid,
pub status: String,
}
pub async fn tx_rollback(
State(state): State<Arc<AppState>>,
Path(command_id): Path<Uuid>,
) -> Result<Json<RollbackResponse>, StatusCode> {
let mut engine = state
.tx_engine
.lock()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let rb = engine
.rollback(command_id)
.map_err(|e| {
tracing::warn!("rollback failed: {}", e);
StatusCode::BAD_REQUEST
})?;
Ok(Json(RollbackResponse {
rollback_command_id: rb.id,
status: format!("{:?}", rb.status),
}))
}
// ── GET /tx/history ───────────────────────────────────────────────────────────
#[derive(Deserialize)]
pub struct HistoryParams {
pub since: Option<i64>,
}
#[derive(Serialize)]
pub struct HistoryResponse {
pub commands: Vec<Command>,
}
pub async fn tx_history(
State(state): State<Arc<AppState>>,
Query(params): Query<HistoryParams>,
) -> Result<Json<HistoryResponse>, StatusCode> {
let engine = state
.tx_engine
.lock()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let since = params.since.unwrap_or(0);
let commands = engine
.history(since)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(HistoryResponse { commands }))
}
// ── GET /tx/chain/{command_id} ────────────────────────────────────────────────
#[derive(Serialize)]
pub struct CausalChainResponse {
pub chain: Vec<Command>,
}
pub async fn tx_causal_chain(
State(state): State<Arc<AppState>>,
Path(command_id): Path<Uuid>,
) -> Result<Json<CausalChainResponse>, StatusCode> {
let engine = state
.tx_engine
.lock()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let chain = engine
.causal_chain(command_id)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(CausalChainResponse { chain }))
}
+6
View File
@@ -1,6 +1,8 @@
/// Shared application state for all request handlers.
use engram_core::EngramDb;
use engram_projection::registry::ProjectionRegistry;
use engram_sync::SyncEngine;
use engram_tx::TransactionEngine;
use std::sync::{Arc, Mutex};
pub struct AppState {
@@ -10,4 +12,8 @@ pub struct AppState {
pub sync_engine: Arc<tokio::sync::Mutex<SyncEngine>>,
/// API key used to authenticate incoming sync requests
pub api_key: String,
/// Projection registry — named schema views over the activation surface
pub projection_registry: Arc<Mutex<ProjectionRegistry>>,
/// Transaction engine — append-only command log with rollback support
pub tx_engine: Arc<Mutex<TransactionEngine>>,
}