Add magmad: Rust sovereign agentic daemon — HTTP, gRPC, CLI, BOB/MAGMA routing
Browse files- magmad/Cargo.toml +74 -0
- magmad/build.rs +36 -0
- magmad/proto/sovereign.proto +108 -0
- magmad/src/bob.rs +76 -0
- magmad/src/cli.rs +331 -0
- magmad/src/config.rs +65 -0
- magmad/src/errant_ffi.rs +155 -0
- magmad/src/grpc/mod.rs +187 -0
- magmad/src/grpc/sovereign_types.rs +176 -0
- magmad/src/http/mod.rs +339 -0
- magmad/src/main.rs +114 -0
- magmad/src/nats_bus.rs +74 -0
magmad/Cargo.toml
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[package]
|
| 2 |
+
name = "magmad"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
edition = "2021"
|
| 5 |
+
authors = ["Ahmad Ali Parr <ahmedparr93@gmail.com>"]
|
| 6 |
+
description = "Sovereign agentic daemon — gRPC + SSE gateway to BOB/ERRANT/MAGMA"
|
| 7 |
+
|
| 8 |
+
[[bin]]
|
| 9 |
+
name = "magmad"
|
| 10 |
+
path = "src/main.rs"
|
| 11 |
+
|
| 12 |
+
[[bin]]
|
| 13 |
+
name = "magma"
|
| 14 |
+
path = "src/cli.rs"
|
| 15 |
+
|
| 16 |
+
[dependencies]
|
| 17 |
+
# Async runtime
|
| 18 |
+
tokio = { version = "1", features = ["full"] }
|
| 19 |
+
tokio-stream = { version = "0.1", features = ["sync"] }
|
| 20 |
+
futures = "0.3"
|
| 21 |
+
|
| 22 |
+
# HTTP / SSE (external API :3000)
|
| 23 |
+
axum = { version = "0.7", features = ["macros"] }
|
| 24 |
+
axum-extra = { version = "0.9", features = ["typed-header"] }
|
| 25 |
+
tower = "0.4"
|
| 26 |
+
tower-http = { version = "0.5", features = ["cors", "trace"] }
|
| 27 |
+
|
| 28 |
+
# gRPC (internal API :50051)
|
| 29 |
+
tonic = "0.11"
|
| 30 |
+
prost = "0.12"
|
| 31 |
+
|
| 32 |
+
# NATS pub/sub
|
| 33 |
+
async-nats = "0.35"
|
| 34 |
+
|
| 35 |
+
# Serialization
|
| 36 |
+
serde = { version = "1", features = ["derive"] }
|
| 37 |
+
serde_json = "1"
|
| 38 |
+
|
| 39 |
+
# HTTP client (calls snap-os WORM chain + BOB dispatch)
|
| 40 |
+
reqwest = { version = "0.12", default-features = false,
|
| 41 |
+
features = ["rustls-tls", "json", "stream"] }
|
| 42 |
+
|
| 43 |
+
# Timestamps
|
| 44 |
+
chrono = { version = "0.4", features = ["serde"] }
|
| 45 |
+
|
| 46 |
+
# Crypto (Rust-side hashing — C side uses inline SHA-256)
|
| 47 |
+
sha2 = "0.10"
|
| 48 |
+
hex = "0.4"
|
| 49 |
+
|
| 50 |
+
# UUID for session IDs
|
| 51 |
+
uuid = { version = "1", features = ["v4"] }
|
| 52 |
+
|
| 53 |
+
# CLI
|
| 54 |
+
clap = { version = "4", features = ["derive"] }
|
| 55 |
+
|
| 56 |
+
# Error handling
|
| 57 |
+
anyhow = "1"
|
| 58 |
+
thiserror = "1"
|
| 59 |
+
|
| 60 |
+
# Tracing
|
| 61 |
+
tracing = "0.1"
|
| 62 |
+
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
| 63 |
+
|
| 64 |
+
# Libc for FFI
|
| 65 |
+
libc = "0.2"
|
| 66 |
+
|
| 67 |
+
[build-dependencies]
|
| 68 |
+
cc = "1" # compiles errant.c → liberrant.a
|
| 69 |
+
|
| 70 |
+
[profile.release]
|
| 71 |
+
opt-level = 3
|
| 72 |
+
lto = true
|
| 73 |
+
codegen-units = 1
|
| 74 |
+
strip = true
|
magmad/build.rs
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// build.rs — magmad build script
|
| 2 |
+
//
|
| 3 |
+
// Compiles errant/runtime/errant.c → liberrant.a via the cc crate.
|
| 4 |
+
// Linked statically so magmad has zero runtime dep on errant.c.
|
| 5 |
+
//
|
| 6 |
+
// Proto types are hand-written in src/grpc/sovereign_types.rs and do not
|
| 7 |
+
// require protoc. When protoc is available, remove sovereign_types.rs and
|
| 8 |
+
// re-add: tonic_build::configure().compile(&["proto/sovereign.proto"],&["proto"])
|
| 9 |
+
|
| 10 |
+
fn main() {
|
| 11 |
+
let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default();
|
| 12 |
+
let is_msvc = target_env == "msvc";
|
| 13 |
+
|
| 14 |
+
let mut build = cc::Build::new();
|
| 15 |
+
build
|
| 16 |
+
.file("../errant/runtime/errant.c")
|
| 17 |
+
.include("../errant/runtime");
|
| 18 |
+
|
| 19 |
+
if is_msvc {
|
| 20 |
+
// MSVC: suppress C4996 (deprecated strncpy/snprintf — debug labels only)
|
| 21 |
+
build.flag("/wd4996");
|
| 22 |
+
} else {
|
| 23 |
+
// GCC/Clang: suppress debug-label truncation warnings (safe)
|
| 24 |
+
build
|
| 25 |
+
.flag("-std=c11")
|
| 26 |
+
.flag("-O2")
|
| 27 |
+
.flag("-Wno-stringop-truncation")
|
| 28 |
+
.flag("-Wno-format-truncation");
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
build.compile("errant");
|
| 32 |
+
|
| 33 |
+
println!("cargo:rustc-link-lib=static=errant");
|
| 34 |
+
println!("cargo:rerun-if-changed=../errant/runtime/errant.c");
|
| 35 |
+
println!("cargo:rerun-if-changed=../errant/runtime/errant.h");
|
| 36 |
+
}
|
magmad/proto/sovereign.proto
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
syntax = "proto3";
|
| 2 |
+
package sovereign;
|
| 3 |
+
|
| 4 |
+
// Sovereign gRPC service — internal machine-to-machine API
|
| 5 |
+
// External: REST/SSE on :3000 ← CLI / web / METAMINE bridge
|
| 6 |
+
// Internal: gRPC on :50051 ← BOB pipeline stages, agent-to-agent
|
| 7 |
+
//
|
| 8 |
+
// Ahmad Ali Parr · SnapKitty Collective · 2026
|
| 9 |
+
|
| 10 |
+
service Sovereign {
|
| 11 |
+
// Primary endpoint: takes an instruction, streams back pipeline stages
|
| 12 |
+
// Mirrors POST /api/v1/orchestrate (SSE) but over gRPC
|
| 13 |
+
rpc Orchestrate (OrchestrateRequest) returns (stream OrchestrateEvent);
|
| 14 |
+
|
| 15 |
+
// Fast path: verify an ERRANT opcode sequence, return immediately
|
| 16 |
+
rpc Verify (VerifyRequest) returns (VerifyResult);
|
| 17 |
+
|
| 18 |
+
// BOB routing decision without full execution
|
| 19 |
+
rpc Route (RouteRequest) returns (RouteResult);
|
| 20 |
+
|
| 21 |
+
// WORM chain head (session state)
|
| 22 |
+
rpc ChainHead (Empty) returns (ChainHeadResult);
|
| 23 |
+
|
| 24 |
+
// Health + version
|
| 25 |
+
rpc Health (Empty) returns (HealthResult);
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
// ── Orchestrate ────────────────────────────────────────────────────────────
|
| 29 |
+
|
| 30 |
+
message OrchestrateRequest {
|
| 31 |
+
// WORM chain head = session ID (stateful across calls via the chain)
|
| 32 |
+
string session_id = 1;
|
| 33 |
+
|
| 34 |
+
// Instruction payload
|
| 35 |
+
string action = 2; // "verify_and_execute" | "forge" | "seal" | "query"
|
| 36 |
+
string code = 3; // ERRANT source, METAMINE .meta, or natural instruction
|
| 37 |
+
string intent = 4; // "formal_verification" | "agent_dispatch" | "compute"
|
| 38 |
+
|
| 39 |
+
// Structural constraints passed to ERRANT checker
|
| 40 |
+
repeated string constraints = 5; // ["monotone", "linear", "wasm_safe"]
|
| 41 |
+
|
| 42 |
+
// Optional: pre-computed METAMINE trace ops (from bridge.mjs)
|
| 43 |
+
repeated string trace_ops = 6;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
message OrchestrateEvent {
|
| 47 |
+
string stage = 1; // "TRUST-DEED-GATE"|"ERRANT"|"BOB"|"NATS"|"SEAL"
|
| 48 |
+
bool ok = 2;
|
| 49 |
+
string agent = 3; // BOB routing result: "FORGE"|"CIPHER"|"NOVA" etc.
|
| 50 |
+
string worm_hash = 4; // running WORM hash at this stage
|
| 51 |
+
string proof_hash = 5; // Coq/Lean proof hash (if formal verification ran)
|
| 52 |
+
string message = 6; // human-readable stage description
|
| 53 |
+
string error = 7; // non-empty on failure
|
| 54 |
+
bytes payload = 8; // structured stage output (JSON-encoded)
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// ── Verify ─────────────────────────────────────────────────────────────────
|
| 58 |
+
|
| 59 |
+
message VerifyRequest {
|
| 60 |
+
repeated string opcodes = 1; // ["PUSH_LIN", "HASH", "SEAL"] etc.
|
| 61 |
+
string source = 2; // alternative: raw ERRANT source text
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
message VerifyResult {
|
| 65 |
+
bool ok = 1;
|
| 66 |
+
string worm_hash = 2;
|
| 67 |
+
string error = 3;
|
| 68 |
+
int32 steps = 4;
|
| 69 |
+
int32 lin_consumed = 5;
|
| 70 |
+
int32 lin_leaked = 6;
|
| 71 |
+
bool fallback = 7; // true = structural check (SEAL-last), not full ERRANT
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// ── Route ──────────────────────────────────────────────────────────────────
|
| 75 |
+
|
| 76 |
+
message RouteRequest {
|
| 77 |
+
string action = 1;
|
| 78 |
+
string worm_hash = 2;
|
| 79 |
+
int32 ruptures = 3;
|
| 80 |
+
bool has_forge = 4;
|
| 81 |
+
bool has_seal = 5;
|
| 82 |
+
bool has_vault = 6;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
message RouteResult {
|
| 86 |
+
string emoji = 1; // "⚒️" | "🔒" | "🏦" | "🔮" | "🧠" etc.
|
| 87 |
+
string agent = 2; // "FORGE" | "CIPHER" | "VAULT" | "ORACLE" | "NOVA"
|
| 88 |
+
string action = 3; // "BUILD" | "SIGN" | "APPROVE" | "KNOWLEDGE" | "SYNTHETIC"
|
| 89 |
+
string subject = 4; // NATS subject: "sovereign.forge.build.v1" etc.
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// ── Shared ─────────────────────────────────────────────────────────────────
|
| 93 |
+
|
| 94 |
+
message Empty {}
|
| 95 |
+
|
| 96 |
+
message ChainHeadResult {
|
| 97 |
+
string head = 1;
|
| 98 |
+
uint64 index = 2;
|
| 99 |
+
string timestamp = 3;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
message HealthResult {
|
| 103 |
+
string status = 1; // "ok" | "degraded" | "offline"
|
| 104 |
+
string version = 2;
|
| 105 |
+
bool nats_ok = 3;
|
| 106 |
+
bool errant_ok = 4;
|
| 107 |
+
string worm_hash = 5; // current chain head
|
| 108 |
+
}
|
magmad/src/bob.rs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// bob.rs — BOB routing logic
|
| 2 |
+
//
|
| 3 |
+
// BOB = TRUST-DEED-GATE → MAMBA → WATSON → PROLOG-KERNEL → HASKELL-MONAD → SEAL
|
| 4 |
+
//
|
| 5 |
+
// After ERRANT verifies linearity, BOB determines which MAGMA verb + agent
|
| 6 |
+
// handles the request. Mirrors bridge.mjs bobRoute() priority table.
|
| 7 |
+
//
|
| 8 |
+
// Priority (descending): SEAL > VAULT > SENTINEL > FORGE > ORACLE > ANCHOR > FLUX > NOVA
|
| 9 |
+
|
| 10 |
+
use serde::{Deserialize, Serialize};
|
| 11 |
+
|
| 12 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 13 |
+
pub struct BobRoute {
|
| 14 |
+
pub emoji: &'static str,
|
| 15 |
+
pub agent: &'static str,
|
| 16 |
+
pub action: &'static str,
|
| 17 |
+
pub subject: &'static str, // NATS subject
|
| 18 |
+
pub verb: &'static str, // MAGMA HTTP event name
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
#[derive(Debug, Default)]
|
| 22 |
+
pub struct BobSignals {
|
| 23 |
+
pub has_seal: bool,
|
| 24 |
+
pub has_vault: bool,
|
| 25 |
+
pub ruptures: i32,
|
| 26 |
+
pub has_forge: bool,
|
| 27 |
+
pub has_resonance: bool,
|
| 28 |
+
pub has_memory: bool,
|
| 29 |
+
pub has_echo: bool,
|
| 30 |
+
pub steps: i32,
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
static ROUTE_SEAL: BobRoute = BobRoute { emoji: "🔒", agent: "CIPHER", action: "SIGN", subject: "sovereign.cipher.seal.v1", verb: "METAMINE_SEAL" };
|
| 34 |
+
static ROUTE_VAULT: BobRoute = BobRoute { emoji: "🏦", agent: "VAULT", action: "APPROVE", subject: "sovereign.vault.approve.v1", verb: "VAULT_APPROVE" };
|
| 35 |
+
static ROUTE_SENTINEL: BobRoute = BobRoute { emoji: "👁", agent: "SENTINEL", action: "MONITOR", subject: "sovereign.sentinel.watch.v1", verb: "SENTINEL_MONITOR" };
|
| 36 |
+
static ROUTE_FORGE: BobRoute = BobRoute { emoji: "⚒️", agent: "FORGE", action: "BUILD", subject: "sovereign.forge.build.v1", verb: "FORGE_BUILD" };
|
| 37 |
+
static ROUTE_ORACLE: BobRoute = BobRoute { emoji: "🔮", agent: "ORACLE", action: "KNOWLEDGE", subject: "sovereign.oracle.query.v1", verb: "ORACLE_QUERY" };
|
| 38 |
+
static ROUTE_MNEMEX: BobRoute = BobRoute { emoji: "📜", agent: "MNEMEX", action: "WORM", subject: "sovereign.mnemex.anchor.v1", verb: "ANCHOR" };
|
| 39 |
+
static ROUTE_HERALD: BobRoute = BobRoute { emoji: "⚡", agent: "HERALD", action: "BROADCAST", subject: "sovereign.herald.flux.v1", verb: "FLUX_BROADCAST" };
|
| 40 |
+
static ROUTE_NOVA: BobRoute = BobRoute { emoji: "🧠", agent: "NOVA", action: "SYNTHETIC", subject: "sovereign.nova.query.v1", verb: "NOVA_SYNTHETIC" };
|
| 41 |
+
|
| 42 |
+
impl BobSignals {
|
| 43 |
+
/// Derive signals from ERRANT result + action string
|
| 44 |
+
pub fn from_errant_and_action(
|
| 45 |
+
errant: &crate::errant_ffi::ErrantResult,
|
| 46 |
+
action: &str,
|
| 47 |
+
) -> Self {
|
| 48 |
+
BobSignals {
|
| 49 |
+
has_seal: action.contains("seal") || action.contains("sign"),
|
| 50 |
+
has_vault: action.contains("vault") || action.contains("approve") || action.contains("transfer"),
|
| 51 |
+
ruptures: errant.ruptures,
|
| 52 |
+
has_forge: action.contains("forge") || action.contains("build"),
|
| 53 |
+
has_resonance: action.contains("query") || action.contains("oracle") || action.contains("resona"),
|
| 54 |
+
has_memory: action.contains("anchor") || action.contains("memory"),
|
| 55 |
+
has_echo: errant.output.len() > 0,
|
| 56 |
+
steps: errant.steps,
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
pub fn route(signals: &BobSignals) -> &'static BobRoute {
|
| 62 |
+
// TRUST-DEED-GATE: critical tasks always route through CIPHER for sealing
|
| 63 |
+
if signals.has_seal { return &ROUTE_SEAL; }
|
| 64 |
+
if signals.has_vault { return &ROUTE_VAULT; }
|
| 65 |
+
if signals.ruptures > 0 { return &ROUTE_SENTINEL; }
|
| 66 |
+
if signals.has_forge { return &ROUTE_FORGE; }
|
| 67 |
+
if signals.has_resonance { return &ROUTE_ORACLE; }
|
| 68 |
+
if signals.has_memory { return &ROUTE_MNEMEX; }
|
| 69 |
+
if signals.has_echo { return &ROUTE_HERALD; }
|
| 70 |
+
&ROUTE_NOVA
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/// The BOB pipeline stages printed in SSE stream
|
| 74 |
+
pub fn pipeline_stages() -> &'static [&'static str] {
|
| 75 |
+
&["TRUST-DEED-GATE", "MAMBA", "WATSON", "PROLOG-KERNEL", "HASKELL-MONAD", "SEAL"]
|
| 76 |
+
}
|
magmad/src/cli.rs
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// cli.rs — `magma` command-line interface
|
| 2 |
+
//
|
| 3 |
+
// A thin client that talks to a running magmad daemon over REST.
|
| 4 |
+
// Ships as a separate binary so it can be installed alongside magmad.
|
| 5 |
+
//
|
| 6 |
+
// Usage:
|
| 7 |
+
// magma health # ping the daemon
|
| 8 |
+
// magma verify PUSH_UN PUSH_LIN SEAL # ERRANT opcode trace
|
| 9 |
+
// magma run program.meta # stream a .meta file through BOB
|
| 10 |
+
// magma seal "some event text" # compute WORM hash and seal
|
| 11 |
+
// magma talk "build the auth module" # SSE orchestrate stream
|
| 12 |
+
// magma chain # print current WORM chain head
|
| 13 |
+
//
|
| 14 |
+
// Daemon is assumed at http://127.0.0.1:3000 unless MAGMA_URL is set.
|
| 15 |
+
|
| 16 |
+
use std::io::{self, Write as IoWrite};
|
| 17 |
+
use std::path::PathBuf;
|
| 18 |
+
use anyhow::{Context, Result};
|
| 19 |
+
use clap::{Parser, Subcommand};
|
| 20 |
+
use reqwest::Client;
|
| 21 |
+
use serde_json::Value;
|
| 22 |
+
|
| 23 |
+
// ── Args ───────────────────────────────────────────────────────────────────────
|
| 24 |
+
|
| 25 |
+
#[derive(Parser)]
|
| 26 |
+
#[command(
|
| 27 |
+
name = "magma",
|
| 28 |
+
version,
|
| 29 |
+
about = "Sovereign agentic runtime CLI",
|
| 30 |
+
long_about = r#"
|
| 31 |
+
magma — SnapKitty Sovereign CLI
|
| 32 |
+
Talks to magmad (default http://127.0.0.1:3000).
|
| 33 |
+
Set MAGMA_URL to override.
|
| 34 |
+
|
| 35 |
+
Examples:
|
| 36 |
+
magma health
|
| 37 |
+
magma verify PUSH_UN PUSH_LIN SEAL
|
| 38 |
+
magma run program.meta
|
| 39 |
+
magma talk "deploy the auth module"
|
| 40 |
+
magma seal "key-rotation-event-2026"
|
| 41 |
+
magma chain
|
| 42 |
+
"#
|
| 43 |
+
)]
|
| 44 |
+
struct Cli {
|
| 45 |
+
#[command(subcommand)]
|
| 46 |
+
command: Command,
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
#[derive(Subcommand)]
|
| 50 |
+
enum Command {
|
| 51 |
+
/// Health check — pings magmad and prints status
|
| 52 |
+
Health,
|
| 53 |
+
|
| 54 |
+
/// ERRANT opcode trace verification
|
| 55 |
+
Verify {
|
| 56 |
+
/// Opcodes to verify (e.g. PUSH_UN PUSH_LIN SEAL)
|
| 57 |
+
#[arg(required = true)]
|
| 58 |
+
opcodes: Vec<String>,
|
| 59 |
+
},
|
| 60 |
+
|
| 61 |
+
/// Source string verification
|
| 62 |
+
Source {
|
| 63 |
+
/// ERRANT source string
|
| 64 |
+
code: String,
|
| 65 |
+
},
|
| 66 |
+
|
| 67 |
+
/// Run a .meta METAMINE file through the BOB pipeline (streams SSE)
|
| 68 |
+
Run {
|
| 69 |
+
/// Path to .meta file
|
| 70 |
+
file: PathBuf,
|
| 71 |
+
/// Action to pass to BOB routing (default: "build")
|
| 72 |
+
#[arg(long, default_value = "build")]
|
| 73 |
+
action: String,
|
| 74 |
+
/// Print raw SSE events
|
| 75 |
+
#[arg(long)]
|
| 76 |
+
raw: bool,
|
| 77 |
+
},
|
| 78 |
+
|
| 79 |
+
/// SSE orchestration stream for a natural-language action
|
| 80 |
+
Talk {
|
| 81 |
+
/// Instruction (e.g. "deploy the auth module")
|
| 82 |
+
instruction: String,
|
| 83 |
+
/// Print raw SSE events
|
| 84 |
+
#[arg(long)]
|
| 85 |
+
raw: bool,
|
| 86 |
+
},
|
| 87 |
+
|
| 88 |
+
/// Seal an arbitrary string and print the WORM hash
|
| 89 |
+
Seal {
|
| 90 |
+
/// Text to seal
|
| 91 |
+
text: String,
|
| 92 |
+
},
|
| 93 |
+
|
| 94 |
+
/// Print the current WORM chain head
|
| 95 |
+
Chain,
|
| 96 |
+
|
| 97 |
+
/// Print daemon version info
|
| 98 |
+
Version,
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
// ── Main ───────────────────────────────────────────────────────────────────────
|
| 102 |
+
|
| 103 |
+
#[tokio::main]
|
| 104 |
+
async fn main() -> Result<()> {
|
| 105 |
+
let cli = Cli::parse();
|
| 106 |
+
let base_url = std::env::var("MAGMA_URL")
|
| 107 |
+
.unwrap_or_else(|_| "http://127.0.0.1:3000".into());
|
| 108 |
+
let client = Client::builder()
|
| 109 |
+
.timeout(std::time::Duration::from_secs(30))
|
| 110 |
+
.build()?;
|
| 111 |
+
|
| 112 |
+
match cli.command {
|
| 113 |
+
Command::Health => cmd_health(&client, &base_url).await?,
|
| 114 |
+
Command::Version => cmd_version(&client, &base_url).await?,
|
| 115 |
+
Command::Chain => cmd_chain(&client, &base_url).await?,
|
| 116 |
+
|
| 117 |
+
Command::Verify { opcodes } => {
|
| 118 |
+
cmd_verify(&client, &base_url, opcodes).await?
|
| 119 |
+
}
|
| 120 |
+
Command::Source { code } => {
|
| 121 |
+
cmd_source(&client, &base_url, code).await?
|
| 122 |
+
}
|
| 123 |
+
Command::Run { file, action, raw } => {
|
| 124 |
+
cmd_run(&client, &base_url, file, action, raw).await?
|
| 125 |
+
}
|
| 126 |
+
Command::Talk { instruction, raw } => {
|
| 127 |
+
cmd_talk(&client, &base_url, instruction, raw).await?
|
| 128 |
+
}
|
| 129 |
+
Command::Seal { text } => {
|
| 130 |
+
cmd_seal(&client, &base_url, text).await?
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
Ok(())
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// ── Commands ───────────────────────────────────────────────────────────────────
|
| 138 |
+
|
| 139 |
+
async fn cmd_health(client: &Client, base: &str) -> Result<()> {
|
| 140 |
+
let resp: Value = client.get(format!("{base}/api/v1/health"))
|
| 141 |
+
.send().await?.json().await?;
|
| 142 |
+
print_health(&resp);
|
| 143 |
+
Ok(())
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
async fn cmd_version(client: &Client, base: &str) -> Result<()> {
|
| 147 |
+
let resp: Value = client.get(format!("{base}/api/v1/version"))
|
| 148 |
+
.send().await?.json().await?;
|
| 149 |
+
println!("magmad {}", resp["magmad"].as_str().unwrap_or("?"));
|
| 150 |
+
println!("liberrant {}", resp["liberrant"].as_str().unwrap_or("?"));
|
| 151 |
+
println!("protocol {}", resp["protocol"].as_str().unwrap_or("?"));
|
| 152 |
+
Ok(())
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
async fn cmd_chain(client: &Client, base: &str) -> Result<()> {
|
| 156 |
+
let resp: Value = client.get(format!("{base}/api/v1/chain/head"))
|
| 157 |
+
.send().await?.json().await?;
|
| 158 |
+
println!("head {}", resp["head"].as_str().unwrap_or("?"));
|
| 159 |
+
println!("source {}", resp["source"].as_str().unwrap_or("?"));
|
| 160 |
+
Ok(())
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
async fn cmd_verify(client: &Client, base: &str, opcodes: Vec<String>) -> Result<()> {
|
| 164 |
+
let body = serde_json::json!({ "opcodes": opcodes });
|
| 165 |
+
let resp: Value = client.post(format!("{base}/api/v1/verify"))
|
| 166 |
+
.json(&body).send().await?.json().await?;
|
| 167 |
+
print_verify(&resp);
|
| 168 |
+
Ok(())
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
async fn cmd_source(client: &Client, base: &str, code: String) -> Result<()> {
|
| 172 |
+
let body = serde_json::json!({ "source": code });
|
| 173 |
+
let resp: Value = client.post(format!("{base}/api/v1/verify"))
|
| 174 |
+
.json(&body).send().await?.json().await?;
|
| 175 |
+
print_verify(&resp);
|
| 176 |
+
Ok(())
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
async fn cmd_run(
|
| 180 |
+
client: &Client,
|
| 181 |
+
base: &str,
|
| 182 |
+
file: PathBuf,
|
| 183 |
+
action: String,
|
| 184 |
+
raw: bool,
|
| 185 |
+
) -> Result<()> {
|
| 186 |
+
let code = std::fs::read_to_string(&file)
|
| 187 |
+
.with_context(|| format!("cannot read {}", file.display()))?;
|
| 188 |
+
|
| 189 |
+
// Parse .meta → opcode names (words only, ignore comments)
|
| 190 |
+
let ops: Vec<String> = code
|
| 191 |
+
.lines()
|
| 192 |
+
.filter(|l| !l.trim_start().starts_with('#') && !l.trim_start().starts_with(';'))
|
| 193 |
+
.flat_map(|l| l.split_whitespace())
|
| 194 |
+
.filter(|w| !w.is_empty())
|
| 195 |
+
.map(String::from)
|
| 196 |
+
.collect();
|
| 197 |
+
|
| 198 |
+
eprintln!("metamine: {} opcodes from {}", ops.len(), file.display());
|
| 199 |
+
cmd_orchestrate(client, base, action, Some(ops), raw).await
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
async fn cmd_talk(
|
| 203 |
+
client: &Client,
|
| 204 |
+
base: &str,
|
| 205 |
+
instruction: String,
|
| 206 |
+
raw: bool,
|
| 207 |
+
) -> Result<()> {
|
| 208 |
+
cmd_orchestrate(client, base, instruction, None, raw).await
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
async fn cmd_seal(client: &Client, base: &str, text: String) -> Result<()> {
|
| 212 |
+
// Use verify endpoint with PUSH_UN + SEAL to get a worm_hash sealing the input
|
| 213 |
+
// We encode the text as the action field so it influences the final seal hash
|
| 214 |
+
let body = serde_json::json!({
|
| 215 |
+
"opcodes": ["PUSH_UN", "SEAL"],
|
| 216 |
+
"source": text,
|
| 217 |
+
});
|
| 218 |
+
let resp: Value = client.post(format!("{base}/api/v1/verify"))
|
| 219 |
+
.json(&body).send().await?.json().await?;
|
| 220 |
+
let hash = resp["worm_hash"].as_str().unwrap_or("?");
|
| 221 |
+
println!("WORM seal: {hash}");
|
| 222 |
+
println!("Ω text: {text}");
|
| 223 |
+
Ok(())
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
async fn cmd_orchestrate(
|
| 227 |
+
client: &Client,
|
| 228 |
+
base: &str,
|
| 229 |
+
action: String,
|
| 230 |
+
trace_ops: Option<Vec<String>>,
|
| 231 |
+
raw: bool,
|
| 232 |
+
) -> Result<()> {
|
| 233 |
+
let body = serde_json::json!({
|
| 234 |
+
"action": action,
|
| 235 |
+
"trace_ops": trace_ops,
|
| 236 |
+
});
|
| 237 |
+
|
| 238 |
+
let resp = client.post(format!("{base}/api/v1/orchestrate"))
|
| 239 |
+
.json(&body)
|
| 240 |
+
.header("Accept", "text/event-stream")
|
| 241 |
+
.send().await?;
|
| 242 |
+
|
| 243 |
+
// Stream SSE line-by-line
|
| 244 |
+
use futures::StreamExt;
|
| 245 |
+
let mut stream = resp.bytes_stream();
|
| 246 |
+
let mut buf = String::new();
|
| 247 |
+
|
| 248 |
+
while let Some(chunk) = stream.next().await {
|
| 249 |
+
let chunk = chunk?;
|
| 250 |
+
buf.push_str(&String::from_utf8_lossy(&chunk));
|
| 251 |
+
|
| 252 |
+
// Process complete lines
|
| 253 |
+
while let Some(nl) = buf.find('\n') {
|
| 254 |
+
let line = buf[..nl].trim().to_string();
|
| 255 |
+
buf.drain(..=nl);
|
| 256 |
+
|
| 257 |
+
if line.is_empty() || line == "ping" { continue; }
|
| 258 |
+
if let Some(data) = line.strip_prefix("data: ") {
|
| 259 |
+
if raw {
|
| 260 |
+
println!("{data}");
|
| 261 |
+
} else {
|
| 262 |
+
match serde_json::from_str::<Value>(data) {
|
| 263 |
+
Ok(ev) => print_stage_event(&ev),
|
| 264 |
+
Err(_) => println!(" {data}"),
|
| 265 |
+
}
|
| 266 |
+
}
|
| 267 |
+
io::stdout().flush().ok();
|
| 268 |
+
}
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
Ok(())
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
// ── Pretty printers ────────────────────────────────────────────────────────────
|
| 276 |
+
|
| 277 |
+
fn print_health(v: &Value) {
|
| 278 |
+
let status = v["status"].as_str().unwrap_or("?");
|
| 279 |
+
let version = v["version"].as_str().unwrap_or("?");
|
| 280 |
+
let errant = v["errant_ok"].as_bool().unwrap_or(false);
|
| 281 |
+
let nats = v["nats_ok"].as_bool().unwrap_or(false);
|
| 282 |
+
|
| 283 |
+
let tick = |b: bool| if b { "✓" } else { "✗" };
|
| 284 |
+
println!("status {status}");
|
| 285 |
+
println!("version {version}");
|
| 286 |
+
println!("liberrant {}", tick(errant));
|
| 287 |
+
println!("nats {}", tick(nats));
|
| 288 |
+
println!("sovereign {}", v["sovereign"].as_str().unwrap_or("Ω"));
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
fn print_verify(v: &Value) {
|
| 292 |
+
let ok = v["ok"].as_bool().unwrap_or(false);
|
| 293 |
+
let verdict = v["verdict"].as_str().unwrap_or("?");
|
| 294 |
+
let hash = v["worm_hash"].as_str().unwrap_or("?");
|
| 295 |
+
let steps = v["steps"].as_i64().unwrap_or(0);
|
| 296 |
+
let lc = v["lin_consumed"].as_i64().unwrap_or(0);
|
| 297 |
+
let ll = v["lin_leaked"].as_i64().unwrap_or(0);
|
| 298 |
+
let fb = v["fallback"].as_bool().unwrap_or(false);
|
| 299 |
+
|
| 300 |
+
let mark = if ok { "EVIDENCE" } else { "SILENCE" };
|
| 301 |
+
println!("{mark} · {verdict}");
|
| 302 |
+
println!("hash {hash}");
|
| 303 |
+
println!("steps {} · lin_consumed {} · lin_leaked {}", steps, lc, ll);
|
| 304 |
+
if fb { println!("mode structural (Ω-path)"); }
|
| 305 |
+
if let Some(err) = v["error"].as_str() {
|
| 306 |
+
println!("error {err}");
|
| 307 |
+
}
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
fn print_stage_event(v: &Value) {
|
| 311 |
+
let stage = v["stage"].as_str().unwrap_or("?");
|
| 312 |
+
let ok = v["ok"].as_bool().unwrap_or(false);
|
| 313 |
+
let msg = v["message"].as_str().unwrap_or("");
|
| 314 |
+
let tick = if ok { "✓" } else { "✗" };
|
| 315 |
+
|
| 316 |
+
print!(" [{tick}] {stage:<20} {msg}");
|
| 317 |
+
|
| 318 |
+
if let Some(agent) = v["agent"].as_str() {
|
| 319 |
+
print!(" ← {agent}");
|
| 320 |
+
}
|
| 321 |
+
if let Some(hash) = v["worm_hash"].as_str() {
|
| 322 |
+
print!(" Ω:{}", &hash[..12]);
|
| 323 |
+
}
|
| 324 |
+
println!();
|
| 325 |
+
|
| 326 |
+
if !ok {
|
| 327 |
+
if let Some(err) = v["error"].as_str() {
|
| 328 |
+
println!(" ERROR: {err}");
|
| 329 |
+
}
|
| 330 |
+
}
|
| 331 |
+
}
|
magmad/src/config.rs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// config.rs — magmad runtime configuration
|
| 2 |
+
|
| 3 |
+
use serde::Deserialize;
|
| 4 |
+
|
| 5 |
+
#[derive(Debug, Clone, Deserialize)]
|
| 6 |
+
pub struct Config {
|
| 7 |
+
/// REST + SSE port (external API — CLI, web, METAMINE bridge)
|
| 8 |
+
#[serde(default = "default_http_port")]
|
| 9 |
+
pub http_port: u16,
|
| 10 |
+
|
| 11 |
+
/// gRPC port (internal — agent-to-agent, BOB pipeline stages)
|
| 12 |
+
#[serde(default = "default_grpc_port")]
|
| 13 |
+
pub grpc_port: u16,
|
| 14 |
+
|
| 15 |
+
/// NATS server URL
|
| 16 |
+
#[serde(default = "default_nats_url")]
|
| 17 |
+
pub nats_url: String,
|
| 18 |
+
|
| 19 |
+
/// snap-os base URL (WORM chain + MAGMA verbs)
|
| 20 |
+
#[serde(default = "default_os_url")]
|
| 21 |
+
pub os_url: String,
|
| 22 |
+
|
| 23 |
+
/// ERRANT LFIS interpreter URL (if external; falls back to liberrant.a)
|
| 24 |
+
#[serde(default = "default_errant_url")]
|
| 25 |
+
pub errant_url: String,
|
| 26 |
+
|
| 27 |
+
/// Log level: "trace"|"debug"|"info"|"warn"|"error"
|
| 28 |
+
#[serde(default = "default_log_level")]
|
| 29 |
+
pub log_level: String,
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
fn default_http_port() -> u16 { 3000 }
|
| 33 |
+
fn default_grpc_port() -> u16 { 50051 }
|
| 34 |
+
fn default_nats_url() -> String { "nats://127.0.0.1:4222".into() }
|
| 35 |
+
fn default_os_url() -> String { "http://127.0.0.1:3001".into() }
|
| 36 |
+
fn default_errant_url() -> String { "http://127.0.0.1:4000".into() }
|
| 37 |
+
fn default_log_level() -> String { "info".into() }
|
| 38 |
+
|
| 39 |
+
impl Config {
|
| 40 |
+
/// Load from environment variables (MAGMA_ prefix), fall back to defaults.
|
| 41 |
+
pub fn from_env() -> Self {
|
| 42 |
+
Self {
|
| 43 |
+
http_port: env_u16("MAGMA_HTTP_PORT", 3000),
|
| 44 |
+
grpc_port: env_u16("MAGMA_GRPC_PORT", 50051),
|
| 45 |
+
nats_url: env_str("MAGMA_NATS_URL", "nats://127.0.0.1:4222"),
|
| 46 |
+
os_url: env_str("MAGMA_OS_URL", "http://127.0.0.1:3001"),
|
| 47 |
+
errant_url: env_str("MAGMA_ERRANT_URL", "http://127.0.0.1:4000"),
|
| 48 |
+
log_level: env_str("MAGMA_LOG", "info"),
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
pub fn http_addr(&self) -> String { format!("0.0.0.0:{}", self.http_port) }
|
| 53 |
+
pub fn grpc_addr(&self) -> String { format!("0.0.0.0:{}", self.grpc_port) }
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
fn env_str(key: &str, default: &str) -> String {
|
| 57 |
+
std::env::var(key).unwrap_or_else(|_| default.to_owned())
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
fn env_u16(key: &str, default: u16) -> u16 {
|
| 61 |
+
std::env::var(key)
|
| 62 |
+
.ok()
|
| 63 |
+
.and_then(|v| v.parse().ok())
|
| 64 |
+
.unwrap_or(default)
|
| 65 |
+
}
|
magmad/src/errant_ffi.rs
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// errant_ffi.rs — Rust FFI bindings to liberrant.a
|
| 2 |
+
//
|
| 3 |
+
// Safe Rust wrappers around the C linear type checker.
|
| 4 |
+
// The unsafe blocks are contained here; callers use ErrantResult directly.
|
| 5 |
+
//
|
| 6 |
+
// liberrant is compiled by build.rs from errant/runtime/errant.c.
|
| 7 |
+
// Zero external dependencies in C (SHA-256 inline, no openssl).
|
| 8 |
+
|
| 9 |
+
use std::ffi::{CStr, CString};
|
| 10 |
+
use std::os::raw::{c_char, c_int};
|
| 11 |
+
|
| 12 |
+
// ── Raw C layout (must match errant.h exactly) ─────────────────────────────
|
| 13 |
+
|
| 14 |
+
const ERROR_MAX: usize = 512;
|
| 15 |
+
const HASH_HEX: usize = 65;
|
| 16 |
+
const OUTPUT_MAX: usize = 4096;
|
| 17 |
+
|
| 18 |
+
#[repr(C)]
|
| 19 |
+
struct CEResult {
|
| 20 |
+
ok: bool,
|
| 21 |
+
fallback: bool,
|
| 22 |
+
error: [c_char; ERROR_MAX],
|
| 23 |
+
worm_hash: [c_char; HASH_HEX],
|
| 24 |
+
output: [c_char; OUTPUT_MAX],
|
| 25 |
+
steps: c_int,
|
| 26 |
+
ruptures: c_int,
|
| 27 |
+
lin_consumed: c_int,
|
| 28 |
+
lin_leaked: c_int,
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
extern "C" {
|
| 32 |
+
fn errant_verify_named(op_names: *const *const c_char, count: c_int) -> CEResult;
|
| 33 |
+
fn errant_sha256_hex(data: *const u8, len: usize, hex_out: *mut c_char);
|
| 34 |
+
fn errant_version() -> *const c_char;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// ── Safe Rust result ───────────────────────────────────────────────────────
|
| 38 |
+
|
| 39 |
+
#[derive(Debug, Clone)]
|
| 40 |
+
pub struct ErrantResult {
|
| 41 |
+
pub ok: bool,
|
| 42 |
+
pub fallback: bool,
|
| 43 |
+
pub error: String,
|
| 44 |
+
pub worm_hash: String,
|
| 45 |
+
pub output: String,
|
| 46 |
+
pub steps: i32,
|
| 47 |
+
pub ruptures: i32,
|
| 48 |
+
pub lin_consumed: i32,
|
| 49 |
+
pub lin_leaked: i32,
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
impl ErrantResult {
|
| 53 |
+
pub fn verdict(&self) -> &'static str {
|
| 54 |
+
if self.ok { "EVIDENCE" } else { "SILENCE" }
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
fn c_str_to_string(buf: &[c_char]) -> String {
|
| 59 |
+
unsafe {
|
| 60 |
+
CStr::from_ptr(buf.as_ptr())
|
| 61 |
+
.to_string_lossy()
|
| 62 |
+
.into_owned()
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
fn convert(raw: CEResult) -> ErrantResult {
|
| 67 |
+
ErrantResult {
|
| 68 |
+
ok: raw.ok,
|
| 69 |
+
fallback: raw.fallback,
|
| 70 |
+
error: c_str_to_string(&raw.error),
|
| 71 |
+
worm_hash: c_str_to_string(&raw.worm_hash),
|
| 72 |
+
output: c_str_to_string(&raw.output),
|
| 73 |
+
steps: raw.steps,
|
| 74 |
+
ruptures: raw.ruptures,
|
| 75 |
+
lin_consumed: raw.lin_consumed,
|
| 76 |
+
lin_leaked: raw.lin_leaked,
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// ── Public API ─────────────────────────────────────────────────────────────
|
| 81 |
+
|
| 82 |
+
/// Verify a sequence of ERRANT opcode names.
|
| 83 |
+
/// Input: &["PUSH_LIN", "HASH", "SEAL"] — same format bridge.mjs sends.
|
| 84 |
+
/// Returns: ErrantResult with EVIDENCE/SILENCE + WORM hash.
|
| 85 |
+
pub fn verify_named(ops: &[&str]) -> ErrantResult {
|
| 86 |
+
// Build null-terminated C strings
|
| 87 |
+
let c_strings: Vec<CString> = ops
|
| 88 |
+
.iter()
|
| 89 |
+
.map(|s| CString::new(*s).unwrap_or_default())
|
| 90 |
+
.collect();
|
| 91 |
+
let c_ptrs: Vec<*const c_char> = c_strings.iter().map(|s| s.as_ptr()).collect();
|
| 92 |
+
|
| 93 |
+
let raw = unsafe {
|
| 94 |
+
errant_verify_named(c_ptrs.as_ptr(), c_ptrs.len() as c_int)
|
| 95 |
+
};
|
| 96 |
+
convert(raw)
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
/// Verify from a whitespace-separated opcode string.
|
| 100 |
+
/// Convenience wrapper: "PUSH_LIN HASH SEAL" → same as verify_named.
|
| 101 |
+
pub fn verify_source(source: &str) -> ErrantResult {
|
| 102 |
+
let ops: Vec<&str> = source.split_whitespace().collect();
|
| 103 |
+
verify_named(&ops)
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
/// Compute SHA-256 hex of arbitrary bytes.
|
| 107 |
+
/// Returns lowercase 64-char hex string.
|
| 108 |
+
pub fn sha256_hex(data: &[u8]) -> String {
|
| 109 |
+
let mut buf = [0i8; HASH_HEX];
|
| 110 |
+
unsafe {
|
| 111 |
+
errant_sha256_hex(data.as_ptr(), data.len(), buf.as_mut_ptr());
|
| 112 |
+
}
|
| 113 |
+
c_str_to_string(&buf)
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/// Returns the liberrant version string.
|
| 117 |
+
pub fn version() -> String {
|
| 118 |
+
unsafe { CStr::from_ptr(errant_version()).to_string_lossy().into_owned() }
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
#[cfg(test)]
|
| 122 |
+
mod tests {
|
| 123 |
+
use super::*;
|
| 124 |
+
|
| 125 |
+
#[test]
|
| 126 |
+
fn sha256_empty_fips_vector() {
|
| 127 |
+
let h = sha256_hex(b"");
|
| 128 |
+
assert_eq!(h, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
#[test]
|
| 132 |
+
fn metamine_omega_path_evidence() {
|
| 133 |
+
let r = verify_named(&["ORIGIN", "SEED", "RUPTURE", "ECHO", "SEAL"]);
|
| 134 |
+
assert!(r.ok, "METAMINE path should be EVIDENCE, got: {}", r.error);
|
| 135 |
+
assert!(!r.worm_hash.is_empty());
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
#[test]
|
| 139 |
+
fn dup_on_lin_is_silence() {
|
| 140 |
+
let r = verify_named(&["PUSH_LIN", "DUP_UN", "SEAL"]);
|
| 141 |
+
assert!(!r.ok, "DUP_UN on lin must be SILENCE");
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
#[test]
|
| 145 |
+
fn lin_leaked_at_halt_is_silence() {
|
| 146 |
+
let r = verify_named(&["PUSH_LIN", "MOVE", "HALT"]);
|
| 147 |
+
assert!(!r.ok, "Unconsumed lin at HALT must be SILENCE");
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
#[test]
|
| 151 |
+
fn forge_chain_evidence() {
|
| 152 |
+
let r = verify_named(&["SEED", "SEED", "FORGE", "SEAL"]);
|
| 153 |
+
assert!(r.ok, "FORGE chain should be EVIDENCE");
|
| 154 |
+
}
|
| 155 |
+
}
|
magmad/src/grpc/mod.rs
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// grpc/mod.rs — tonic gRPC service implementation
|
| 2 |
+
//
|
| 3 |
+
// Internal machine-to-machine API on :50051
|
| 4 |
+
// Agent-to-agent, BOB pipeline stages, prover integrations.
|
| 5 |
+
//
|
| 6 |
+
// sovereign_types.rs contains hand-written prost Message types equivalent to
|
| 7 |
+
// what protoc + tonic_build would generate from proto/sovereign.proto.
|
| 8 |
+
// When protoc becomes available, run tonic_build and replace the manual types.
|
| 9 |
+
|
| 10 |
+
mod sovereign_types;
|
| 11 |
+
use sovereign_types as sovereign;
|
| 12 |
+
|
| 13 |
+
use tonic::{Request, Response, Status};
|
| 14 |
+
use tokio_stream::wrappers::ReceiverStream;
|
| 15 |
+
use tokio::sync::mpsc;
|
| 16 |
+
use tracing::info;
|
| 17 |
+
|
| 18 |
+
use crate::{bob, errant_ffi};
|
| 19 |
+
use sovereign::{
|
| 20 |
+
sovereign_server::Sovereign,
|
| 21 |
+
sovereign_server::SovereignServer,
|
| 22 |
+
ChainHeadResult, Empty, HealthResult,
|
| 23 |
+
OrchestrateEvent, OrchestrateRequest,
|
| 24 |
+
RouteRequest, RouteResult,
|
| 25 |
+
VerifyRequest, VerifyResult,
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
// ── Service implementation ─────────────────────────────────────────────────
|
| 29 |
+
|
| 30 |
+
#[derive(Debug, Default, Clone)]
|
| 31 |
+
pub struct SovereignService;
|
| 32 |
+
|
| 33 |
+
#[tonic::async_trait]
|
| 34 |
+
impl Sovereign for SovereignService {
|
| 35 |
+
|
| 36 |
+
type OrchestrateStream = ReceiverStream<Result<OrchestrateEvent, Status>>;
|
| 37 |
+
|
| 38 |
+
async fn orchestrate(
|
| 39 |
+
&self,
|
| 40 |
+
request: Request<OrchestrateRequest>,
|
| 41 |
+
) -> Result<Response<Self::OrchestrateStream>, Status> {
|
| 42 |
+
let req = request.into_inner();
|
| 43 |
+
info!("gRPC orchestrate: action={}", req.action);
|
| 44 |
+
|
| 45 |
+
let (tx, rx) = mpsc::channel(16);
|
| 46 |
+
|
| 47 |
+
tokio::spawn(async move {
|
| 48 |
+
let send = |ev: OrchestrateEvent| {
|
| 49 |
+
let tx = tx.clone();
|
| 50 |
+
async move { tx.send(Ok(ev)).await.ok(); }
|
| 51 |
+
};
|
| 52 |
+
|
| 53 |
+
// TRUST-DEED-GATE
|
| 54 |
+
send(OrchestrateEvent {
|
| 55 |
+
stage: "TRUST-DEED-GATE".into(),
|
| 56 |
+
ok: true,
|
| 57 |
+
message: "BOB gate open".into(),
|
| 58 |
+
..Default::default()
|
| 59 |
+
}).await;
|
| 60 |
+
|
| 61 |
+
// ERRANT verify
|
| 62 |
+
let ops: Vec<&str> = req.trace_ops.iter().map(|s| s.as_str()).collect();
|
| 63 |
+
let errant = if ops.is_empty() {
|
| 64 |
+
errant_ffi::verify_source("PUSH_UN SEAL")
|
| 65 |
+
} else {
|
| 66 |
+
errant_ffi::verify_named(&ops)
|
| 67 |
+
};
|
| 68 |
+
|
| 69 |
+
send(OrchestrateEvent {
|
| 70 |
+
stage: "ERRANT".into(),
|
| 71 |
+
ok: errant.ok,
|
| 72 |
+
worm_hash: errant.worm_hash.clone(),
|
| 73 |
+
message: format!("{} · {} steps", errant.verdict(), errant.steps),
|
| 74 |
+
error: if errant.ok { String::new() } else { errant.error.clone() },
|
| 75 |
+
..Default::default()
|
| 76 |
+
}).await;
|
| 77 |
+
|
| 78 |
+
if !errant.ok { return; }
|
| 79 |
+
|
| 80 |
+
// BOB route
|
| 81 |
+
let signals = bob::BobSignals::from_errant_and_action(&errant, &req.action);
|
| 82 |
+
let route = bob::route(&signals);
|
| 83 |
+
|
| 84 |
+
send(OrchestrateEvent {
|
| 85 |
+
stage: "BOB".into(),
|
| 86 |
+
ok: true,
|
| 87 |
+
agent: format!("{} {}", route.emoji, route.agent),
|
| 88 |
+
message: format!("→ {}:{}", route.agent, route.action),
|
| 89 |
+
..Default::default()
|
| 90 |
+
}).await;
|
| 91 |
+
|
| 92 |
+
// SEAL
|
| 93 |
+
let seal = errant_ffi::sha256_hex(
|
| 94 |
+
format!("MAGMA|{}|{}", route.agent, errant.worm_hash).as_bytes()
|
| 95 |
+
);
|
| 96 |
+
send(OrchestrateEvent {
|
| 97 |
+
stage: "SEAL".into(),
|
| 98 |
+
ok: true,
|
| 99 |
+
worm_hash: seal.clone(),
|
| 100 |
+
proof_hash: seal,
|
| 101 |
+
message: "Ω↺Ψ↺Δ↺Λ↺Σ↺Φ↺α".into(),
|
| 102 |
+
..Default::default()
|
| 103 |
+
}).await;
|
| 104 |
+
});
|
| 105 |
+
|
| 106 |
+
Ok(Response::new(ReceiverStream::new(rx)))
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
async fn verify(
|
| 110 |
+
&self,
|
| 111 |
+
request: Request<VerifyRequest>,
|
| 112 |
+
) -> Result<Response<VerifyResult>, Status> {
|
| 113 |
+
let req = request.into_inner();
|
| 114 |
+
let ops: Vec<&str> = req.opcodes.iter().map(|s| s.as_str()).collect();
|
| 115 |
+
let result = if ops.is_empty() {
|
| 116 |
+
errant_ffi::verify_source(&req.source)
|
| 117 |
+
} else {
|
| 118 |
+
errant_ffi::verify_named(&ops)
|
| 119 |
+
};
|
| 120 |
+
|
| 121 |
+
Ok(Response::new(VerifyResult {
|
| 122 |
+
ok: result.ok,
|
| 123 |
+
worm_hash: result.worm_hash,
|
| 124 |
+
error: result.error,
|
| 125 |
+
steps: result.steps,
|
| 126 |
+
lin_consumed: result.lin_consumed,
|
| 127 |
+
lin_leaked: result.lin_leaked,
|
| 128 |
+
fallback: result.fallback,
|
| 129 |
+
}))
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
async fn route(
|
| 133 |
+
&self,
|
| 134 |
+
request: Request<RouteRequest>,
|
| 135 |
+
) -> Result<Response<RouteResult>, Status> {
|
| 136 |
+
let req = request.into_inner();
|
| 137 |
+
let signals = bob::BobSignals {
|
| 138 |
+
has_seal: req.has_seal,
|
| 139 |
+
has_vault: req.has_vault,
|
| 140 |
+
ruptures: req.ruptures,
|
| 141 |
+
has_forge: req.has_forge,
|
| 142 |
+
has_resonance: false,
|
| 143 |
+
has_memory: false,
|
| 144 |
+
has_echo: false,
|
| 145 |
+
steps: 0,
|
| 146 |
+
};
|
| 147 |
+
let r = bob::route(&signals);
|
| 148 |
+
Ok(Response::new(RouteResult {
|
| 149 |
+
emoji: r.emoji.into(),
|
| 150 |
+
agent: r.agent.into(),
|
| 151 |
+
action: r.action.into(),
|
| 152 |
+
subject: r.subject.into(),
|
| 153 |
+
}))
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
async fn chain_head(
|
| 157 |
+
&self,
|
| 158 |
+
_request: Request<Empty>,
|
| 159 |
+
) -> Result<Response<ChainHeadResult>, Status> {
|
| 160 |
+
let head = errant_ffi::sha256_hex(b"MAGMA-GENESIS");
|
| 161 |
+
Ok(Response::new(ChainHeadResult {
|
| 162 |
+
head,
|
| 163 |
+
index: 0,
|
| 164 |
+
timestamp: chrono::Utc::now().to_rfc3339(),
|
| 165 |
+
}))
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
async fn health(
|
| 169 |
+
&self,
|
| 170 |
+
_request: Request<Empty>,
|
| 171 |
+
) -> Result<Response<HealthResult>, Status> {
|
| 172 |
+
let errant_ok = errant_ffi::verify_named(&["PUSH_UN", "SEAL"]).ok;
|
| 173 |
+
Ok(Response::new(HealthResult {
|
| 174 |
+
status: if errant_ok { "ok".into() } else { "degraded".into() },
|
| 175 |
+
version: env!("CARGO_PKG_VERSION").into(),
|
| 176 |
+
nats_ok: false,
|
| 177 |
+
errant_ok,
|
| 178 |
+
worm_hash: errant_ffi::sha256_hex(b"MAGMA-GENESIS"),
|
| 179 |
+
}))
|
| 180 |
+
}
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
pub fn service() -> SovereignServer<SovereignService> {
|
| 184 |
+
SovereignServer::new(SovereignService::default())
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
use chrono;
|
magmad/src/grpc/sovereign_types.rs
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// sovereign_types.rs — hand-written prost message types for sovereign.proto
|
| 2 |
+
//
|
| 3 |
+
// Equivalent to what protoc + tonic_build would generate from proto/sovereign.proto.
|
| 4 |
+
// Committed to avoid the protoc runtime dependency.
|
| 5 |
+
// When protoc is available: delete this file, re-add tonic-build to build.rs,
|
| 6 |
+
// and restore the `include!(concat!(env!("OUT_DIR"), "/sovereign.rs"))` pattern.
|
| 7 |
+
|
| 8 |
+
use prost::Message;
|
| 9 |
+
|
| 10 |
+
// ── Messages (prost::Message already derives Default — do NOT add Default) ──
|
| 11 |
+
|
| 12 |
+
#[derive(Clone, PartialEq, Message)]
|
| 13 |
+
pub struct Empty {}
|
| 14 |
+
|
| 15 |
+
#[derive(Clone, PartialEq, Message)]
|
| 16 |
+
pub struct OrchestrateRequest {
|
| 17 |
+
#[prost(string, tag = "1")]
|
| 18 |
+
pub session_id: String,
|
| 19 |
+
#[prost(string, tag = "2")]
|
| 20 |
+
pub action: String,
|
| 21 |
+
#[prost(string, tag = "3")]
|
| 22 |
+
pub code: String,
|
| 23 |
+
#[prost(string, tag = "4")]
|
| 24 |
+
pub intent: String,
|
| 25 |
+
#[prost(string, repeated, tag = "5")]
|
| 26 |
+
pub constraints: Vec<String>,
|
| 27 |
+
#[prost(string, repeated, tag = "6")]
|
| 28 |
+
pub trace_ops: Vec<String>,
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
#[derive(Clone, PartialEq, Message)]
|
| 32 |
+
pub struct OrchestrateEvent {
|
| 33 |
+
#[prost(string, tag = "1")]
|
| 34 |
+
pub stage: String,
|
| 35 |
+
#[prost(bool, tag = "2")]
|
| 36 |
+
pub ok: bool,
|
| 37 |
+
#[prost(string, tag = "3")]
|
| 38 |
+
pub agent: String,
|
| 39 |
+
#[prost(string, tag = "4")]
|
| 40 |
+
pub worm_hash: String,
|
| 41 |
+
#[prost(string, tag = "5")]
|
| 42 |
+
pub proof_hash: String,
|
| 43 |
+
#[prost(string, tag = "6")]
|
| 44 |
+
pub message: String,
|
| 45 |
+
#[prost(string, tag = "7")]
|
| 46 |
+
pub error: String,
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
#[derive(Clone, PartialEq, Message)]
|
| 50 |
+
pub struct VerifyRequest {
|
| 51 |
+
#[prost(string, repeated, tag = "1")]
|
| 52 |
+
pub opcodes: Vec<String>,
|
| 53 |
+
#[prost(string, tag = "2")]
|
| 54 |
+
pub source: String,
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
#[derive(Clone, PartialEq, Message)]
|
| 58 |
+
pub struct VerifyResult {
|
| 59 |
+
#[prost(bool, tag = "1")]
|
| 60 |
+
pub ok: bool,
|
| 61 |
+
#[prost(string, tag = "2")]
|
| 62 |
+
pub worm_hash: String,
|
| 63 |
+
#[prost(string, tag = "3")]
|
| 64 |
+
pub error: String,
|
| 65 |
+
#[prost(int32, tag = "4")]
|
| 66 |
+
pub steps: i32,
|
| 67 |
+
#[prost(int32, tag = "5")]
|
| 68 |
+
pub lin_consumed: i32,
|
| 69 |
+
#[prost(int32, tag = "6")]
|
| 70 |
+
pub lin_leaked: i32,
|
| 71 |
+
#[prost(bool, tag = "7")]
|
| 72 |
+
pub fallback: bool,
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
#[derive(Clone, PartialEq, Message)]
|
| 76 |
+
pub struct RouteRequest {
|
| 77 |
+
#[prost(bool, tag = "1")]
|
| 78 |
+
pub has_seal: bool,
|
| 79 |
+
#[prost(bool, tag = "2")]
|
| 80 |
+
pub has_vault: bool,
|
| 81 |
+
#[prost(int32, tag = "3")]
|
| 82 |
+
pub ruptures: i32,
|
| 83 |
+
#[prost(bool, tag = "4")]
|
| 84 |
+
pub has_forge: bool,
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
#[derive(Clone, PartialEq, Message)]
|
| 88 |
+
pub struct RouteResult {
|
| 89 |
+
#[prost(string, tag = "1")]
|
| 90 |
+
pub emoji: String,
|
| 91 |
+
#[prost(string, tag = "2")]
|
| 92 |
+
pub agent: String,
|
| 93 |
+
#[prost(string, tag = "3")]
|
| 94 |
+
pub action: String,
|
| 95 |
+
#[prost(string, tag = "4")]
|
| 96 |
+
pub subject: String,
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
#[derive(Clone, PartialEq, Message)]
|
| 100 |
+
pub struct ChainHeadResult {
|
| 101 |
+
#[prost(string, tag = "1")]
|
| 102 |
+
pub head: String,
|
| 103 |
+
#[prost(int64, tag = "2")]
|
| 104 |
+
pub index: i64,
|
| 105 |
+
#[prost(string, tag = "3")]
|
| 106 |
+
pub timestamp: String,
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
#[derive(Clone, PartialEq, Message)]
|
| 110 |
+
pub struct HealthResult {
|
| 111 |
+
#[prost(string, tag = "1")]
|
| 112 |
+
pub status: String,
|
| 113 |
+
#[prost(string, tag = "2")]
|
| 114 |
+
pub version: String,
|
| 115 |
+
#[prost(bool, tag = "3")]
|
| 116 |
+
pub nats_ok: bool,
|
| 117 |
+
#[prost(bool, tag = "4")]
|
| 118 |
+
pub errant_ok: bool,
|
| 119 |
+
#[prost(string, tag = "5")]
|
| 120 |
+
pub worm_hash: String,
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// ── Server trait ─────────────────────────────────────────────────────────────
|
| 124 |
+
|
| 125 |
+
pub mod sovereign_server {
|
| 126 |
+
use super::*;
|
| 127 |
+
use tonic::{Request, Response, Status};
|
| 128 |
+
|
| 129 |
+
#[tonic::async_trait]
|
| 130 |
+
pub trait Sovereign: Send + Sync + 'static {
|
| 131 |
+
type OrchestrateStream: futures::Stream<Item = Result<OrchestrateEvent, Status>>
|
| 132 |
+
+ Send
|
| 133 |
+
+ 'static;
|
| 134 |
+
|
| 135 |
+
async fn orchestrate(
|
| 136 |
+
&self,
|
| 137 |
+
request: Request<OrchestrateRequest>,
|
| 138 |
+
) -> Result<Response<Self::OrchestrateStream>, Status>;
|
| 139 |
+
|
| 140 |
+
async fn verify(
|
| 141 |
+
&self,
|
| 142 |
+
request: Request<VerifyRequest>,
|
| 143 |
+
) -> Result<Response<VerifyResult>, Status>;
|
| 144 |
+
|
| 145 |
+
async fn route(
|
| 146 |
+
&self,
|
| 147 |
+
request: Request<RouteRequest>,
|
| 148 |
+
) -> Result<Response<RouteResult>, Status>;
|
| 149 |
+
|
| 150 |
+
async fn chain_head(
|
| 151 |
+
&self,
|
| 152 |
+
request: Request<Empty>,
|
| 153 |
+
) -> Result<Response<ChainHeadResult>, Status>;
|
| 154 |
+
|
| 155 |
+
async fn health(
|
| 156 |
+
&self,
|
| 157 |
+
request: Request<Empty>,
|
| 158 |
+
) -> Result<Response<HealthResult>, Status>;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
// Minimal server wrapper — just type-erases the impl.
|
| 162 |
+
// The actual gRPC dispatch goes through axum+tonic Routes when protoc is available.
|
| 163 |
+
// For now, service() in grpc/mod.rs builds this wrapper and main.rs starts a stub.
|
| 164 |
+
#[derive(Clone)]
|
| 165 |
+
pub struct SovereignServer<T>(pub std::sync::Arc<T>);
|
| 166 |
+
|
| 167 |
+
impl<T: Sovereign> SovereignServer<T> {
|
| 168 |
+
pub fn new(inner: T) -> Self {
|
| 169 |
+
Self(std::sync::Arc::new(inner))
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
pub fn inner(&self) -> &T {
|
| 173 |
+
&self.0
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
}
|
magmad/src/http/mod.rs
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// http/mod.rs — axum REST router + SSE orchestration stream
|
| 2 |
+
//
|
| 3 |
+
// Endpoints:
|
| 4 |
+
// POST /api/v1/orchestrate → SSE stream of BOB pipeline stages
|
| 5 |
+
// POST /api/v1/verify → immediate ERRANT verification result
|
| 6 |
+
// GET /api/v1/chain/head → current WORM chain head
|
| 7 |
+
// GET /api/v1/health → daemon health
|
| 8 |
+
// GET /api/v1/version → liberrant + magmad version
|
| 9 |
+
//
|
| 10 |
+
// SSE stream format per stage:
|
| 11 |
+
// data: {"stage":"TRUST-DEED-GATE","ok":true,"worm_hash":"...","message":"..."}
|
| 12 |
+
//
|
| 13 |
+
// The external REST/SSE surface is what the magma CLI, METAMINE bridge.mjs,
|
| 14 |
+
// and any web UI talks to. The internal gRPC surface talks agent-to-agent.
|
| 15 |
+
|
| 16 |
+
use axum::{
|
| 17 |
+
extract::State,
|
| 18 |
+
http::StatusCode,
|
| 19 |
+
response::{
|
| 20 |
+
sse::{Event, Sse},
|
| 21 |
+
IntoResponse, Json,
|
| 22 |
+
},
|
| 23 |
+
routing::{get, post},
|
| 24 |
+
Router,
|
| 25 |
+
};
|
| 26 |
+
use futures::stream::{self, Stream};
|
| 27 |
+
use serde::{Deserialize, Serialize};
|
| 28 |
+
use std::{convert::Infallible, sync::Arc, time::Duration};
|
| 29 |
+
use tokio::sync::mpsc;
|
| 30 |
+
use tokio_stream::wrappers::ReceiverStream;
|
| 31 |
+
use tracing::{error, info};
|
| 32 |
+
|
| 33 |
+
use crate::{bob, errant_ffi};
|
| 34 |
+
|
| 35 |
+
// ── Shared application state ───────────────────────────────────────────────
|
| 36 |
+
|
| 37 |
+
#[derive(Clone)]
|
| 38 |
+
pub struct AppState {
|
| 39 |
+
pub config: Arc<crate::config::Config>,
|
| 40 |
+
pub nats: Option<Arc<crate::nats_bus::NatsBus>>,
|
| 41 |
+
pub worm_url: String,
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// ── Request / response types ───────────────────────────────────────────────
|
| 45 |
+
|
| 46 |
+
#[derive(Debug, Deserialize)]
|
| 47 |
+
pub struct OrchestrateRequest {
|
| 48 |
+
pub session_id: Option<String>,
|
| 49 |
+
pub action: String,
|
| 50 |
+
pub code: Option<String>,
|
| 51 |
+
pub intent: Option<String>,
|
| 52 |
+
pub constraints: Option<Vec<String>>,
|
| 53 |
+
pub trace_ops: Option<Vec<String>>,
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
#[derive(Debug, Serialize)]
|
| 57 |
+
pub struct StageEvent {
|
| 58 |
+
pub stage: &'static str,
|
| 59 |
+
pub ok: bool,
|
| 60 |
+
pub agent: Option<String>,
|
| 61 |
+
pub worm_hash: Option<String>,
|
| 62 |
+
pub proof_hash: Option<String>,
|
| 63 |
+
pub message: String,
|
| 64 |
+
pub error: Option<String>,
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
#[derive(Debug, Deserialize)]
|
| 68 |
+
pub struct VerifyRequest {
|
| 69 |
+
pub opcodes: Option<Vec<String>>,
|
| 70 |
+
pub source: Option<String>,
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
#[derive(Debug, Serialize)]
|
| 74 |
+
pub struct VerifyResponse {
|
| 75 |
+
pub ok: bool,
|
| 76 |
+
pub verdict: &'static str,
|
| 77 |
+
pub worm_hash: String,
|
| 78 |
+
pub error: Option<String>,
|
| 79 |
+
pub steps: i32,
|
| 80 |
+
pub lin_consumed: i32,
|
| 81 |
+
pub lin_leaked: i32,
|
| 82 |
+
pub fallback: bool,
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
// ── Router ─────────────────────────────────────────────────────────────────
|
| 86 |
+
|
| 87 |
+
pub fn router(state: AppState) -> Router {
|
| 88 |
+
Router::new()
|
| 89 |
+
.route("/api/v1/orchestrate", post(orchestrate))
|
| 90 |
+
.route("/api/v1/verify", post(verify))
|
| 91 |
+
.route("/api/v1/chain/head", get(chain_head))
|
| 92 |
+
.route("/api/v1/health", get(health))
|
| 93 |
+
.route("/api/v1/version", get(version_handler))
|
| 94 |
+
// Legacy MAGMA protocol endpoints (snap-os compatibility)
|
| 95 |
+
.route("/api/labs/ledge/seal",post(legacy_seal))
|
| 96 |
+
.route("/api/sovereign/dispatch", post(legacy_dispatch))
|
| 97 |
+
.with_state(state)
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// ── POST /api/v1/orchestrate ── SSE stream ─────────────────────────────────
|
| 101 |
+
|
| 102 |
+
async fn orchestrate(
|
| 103 |
+
State(state): State<AppState>,
|
| 104 |
+
Json(req): Json<OrchestrateRequest>,
|
| 105 |
+
) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
|
| 106 |
+
let (tx, rx) = mpsc::channel::<StageEvent>(16);
|
| 107 |
+
|
| 108 |
+
tokio::spawn(async move {
|
| 109 |
+
let send = |ev: StageEvent| {
|
| 110 |
+
let tx = tx.clone();
|
| 111 |
+
async move {
|
| 112 |
+
let json = serde_json::to_string(&ev).unwrap_or_default();
|
| 113 |
+
tx.send(ev).await.ok();
|
| 114 |
+
json
|
| 115 |
+
}
|
| 116 |
+
};
|
| 117 |
+
|
| 118 |
+
// ── Stage 1: TRUST-DEED-GATE ──────────────────────────────────────
|
| 119 |
+
send(StageEvent {
|
| 120 |
+
stage: "TRUST-DEED-GATE",
|
| 121 |
+
ok: true,
|
| 122 |
+
agent: None,
|
| 123 |
+
worm_hash: None,
|
| 124 |
+
proof_hash: None,
|
| 125 |
+
message: "BOB gate open".into(),
|
| 126 |
+
error: None,
|
| 127 |
+
}).await;
|
| 128 |
+
|
| 129 |
+
// ── Stage 2: ERRANT linear type check ─────────────────────────────
|
| 130 |
+
let ops: Vec<String> = req.trace_ops
|
| 131 |
+
.or_else(|| req.code.as_ref().map(|s| {
|
| 132 |
+
s.split_whitespace().map(String::from).collect()
|
| 133 |
+
}))
|
| 134 |
+
.unwrap_or_default();
|
| 135 |
+
|
| 136 |
+
let op_refs: Vec<&str> = ops.iter().map(|s| s.as_str()).collect();
|
| 137 |
+
let errant = if op_refs.is_empty() {
|
| 138 |
+
errant_ffi::verify_source("PUSH_UN SEAL") // trivial pass for natural-language actions
|
| 139 |
+
} else {
|
| 140 |
+
errant_ffi::verify_named(&op_refs)
|
| 141 |
+
};
|
| 142 |
+
|
| 143 |
+
send(StageEvent {
|
| 144 |
+
stage: "ERRANT",
|
| 145 |
+
ok: errant.ok,
|
| 146 |
+
agent: None,
|
| 147 |
+
worm_hash: Some(errant.worm_hash.clone()),
|
| 148 |
+
proof_hash: None,
|
| 149 |
+
message: if errant.ok {
|
| 150 |
+
format!("linear types verified · {} steps · {}", errant.steps,
|
| 151 |
+
if errant.fallback { "structural" } else { "full LFIS" })
|
| 152 |
+
} else {
|
| 153 |
+
format!("SILENCE: {}", errant.error)
|
| 154 |
+
},
|
| 155 |
+
error: if errant.ok { None } else { Some(errant.error.clone()) },
|
| 156 |
+
}).await;
|
| 157 |
+
|
| 158 |
+
if !errant.ok { return; }
|
| 159 |
+
|
| 160 |
+
// ── Stage 3: MAMBA compress (stub) ────────────────────────────────
|
| 161 |
+
send(StageEvent {
|
| 162 |
+
stage: "MAMBA", ok: true, agent: None,
|
| 163 |
+
worm_hash: Some(errant.worm_hash.clone()),
|
| 164 |
+
proof_hash: None,
|
| 165 |
+
message: "sequence compressed".into(), error: None,
|
| 166 |
+
}).await;
|
| 167 |
+
|
| 168 |
+
// ── Stage 4: WATSON attend (stub) ─────────────────────────────────
|
| 169 |
+
send(StageEvent {
|
| 170 |
+
stage: "WATSON", ok: true, agent: None,
|
| 171 |
+
worm_hash: Some(errant.worm_hash.clone()),
|
| 172 |
+
proof_hash: None,
|
| 173 |
+
message: "attention applied".into(), error: None,
|
| 174 |
+
}).await;
|
| 175 |
+
|
| 176 |
+
// ── Stage 5: BOB routing ──────────────────────────────────────────
|
| 177 |
+
let signals = bob::BobSignals::from_errant_and_action(&errant, &req.action);
|
| 178 |
+
let route = bob::route(&signals);
|
| 179 |
+
|
| 180 |
+
send(StageEvent {
|
| 181 |
+
stage: "BOB",
|
| 182 |
+
ok: true,
|
| 183 |
+
agent: Some(format!("{} {}", route.emoji, route.agent)),
|
| 184 |
+
worm_hash: Some(errant.worm_hash.clone()),
|
| 185 |
+
proof_hash: None,
|
| 186 |
+
message: format!("→ {}:{}", route.agent, route.action),
|
| 187 |
+
error: None,
|
| 188 |
+
}).await;
|
| 189 |
+
|
| 190 |
+
// ── Stage 6: NATS publish ─────────────────────────────────────────
|
| 191 |
+
if let Some(nats) = &state.nats {
|
| 192 |
+
let payload = serde_json::json!({
|
| 193 |
+
"action": req.action,
|
| 194 |
+
"worm_hash": errant.worm_hash,
|
| 195 |
+
"agent": route.agent,
|
| 196 |
+
"session": req.session_id,
|
| 197 |
+
});
|
| 198 |
+
match nats.publish(route.subject, &payload).await {
|
| 199 |
+
Ok(_) => info!("NATS → {}", route.subject),
|
| 200 |
+
Err(e) => error!("NATS publish failed: {e}"),
|
| 201 |
+
}
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
send(StageEvent {
|
| 205 |
+
stage: "NATS", ok: true, agent: Some(route.agent.into()),
|
| 206 |
+
worm_hash: Some(errant.worm_hash.clone()),
|
| 207 |
+
proof_hash: None,
|
| 208 |
+
message: format!("published to {}", route.subject), error: None,
|
| 209 |
+
}).await;
|
| 210 |
+
|
| 211 |
+
// ── Stage 7: SEAL (Ω) ─────────────────────────────────────────────
|
| 212 |
+
let seal_hash = errant_ffi::sha256_hex(
|
| 213 |
+
format!("MAGMA|{}|{}|{}", route.agent, errant.worm_hash, req.action).as_bytes()
|
| 214 |
+
);
|
| 215 |
+
|
| 216 |
+
send(StageEvent {
|
| 217 |
+
stage: "SEAL",
|
| 218 |
+
ok: true,
|
| 219 |
+
agent: Some(route.agent.into()),
|
| 220 |
+
worm_hash: Some(seal_hash.clone()),
|
| 221 |
+
proof_hash: Some(seal_hash),
|
| 222 |
+
message: "Ω↺Ψ↺Δ↺Λ↺Σ↺Φ↺α".into(),
|
| 223 |
+
error: None,
|
| 224 |
+
}).await;
|
| 225 |
+
});
|
| 226 |
+
|
| 227 |
+
let stream = ReceiverStream::new(rx).map(|ev| {
|
| 228 |
+
let data = serde_json::to_string(&ev).unwrap_or_default();
|
| 229 |
+
Ok::<Event, Infallible>(Event::default().data(data))
|
| 230 |
+
});
|
| 231 |
+
|
| 232 |
+
Sse::new(stream).keep_alive(
|
| 233 |
+
axum::response::sse::KeepAlive::new()
|
| 234 |
+
.interval(Duration::from_secs(15))
|
| 235 |
+
.text("ping"),
|
| 236 |
+
)
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
// ── POST /api/v1/verify ────────────────────────────────────────────────────
|
| 240 |
+
|
| 241 |
+
async fn verify(
|
| 242 |
+
Json(req): Json<VerifyRequest>,
|
| 243 |
+
) -> Json<VerifyResponse> {
|
| 244 |
+
let result = if let Some(ops) = req.opcodes {
|
| 245 |
+
let refs: Vec<&str> = ops.iter().map(|s| s.as_str()).collect();
|
| 246 |
+
errant_ffi::verify_named(&refs)
|
| 247 |
+
} else if let Some(src) = req.source {
|
| 248 |
+
errant_ffi::verify_source(&src)
|
| 249 |
+
} else {
|
| 250 |
+
errant_ffi::verify_named(&[])
|
| 251 |
+
};
|
| 252 |
+
|
| 253 |
+
Json(VerifyResponse {
|
| 254 |
+
ok: result.ok,
|
| 255 |
+
verdict: result.verdict(),
|
| 256 |
+
worm_hash: result.worm_hash,
|
| 257 |
+
error: if result.ok { None } else { Some(result.error) },
|
| 258 |
+
steps: result.steps,
|
| 259 |
+
lin_consumed: result.lin_consumed,
|
| 260 |
+
lin_leaked: result.lin_leaked,
|
| 261 |
+
fallback: result.fallback,
|
| 262 |
+
})
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
// ── GET /api/v1/chain/head ─────────────────────────────────────────────────
|
| 266 |
+
|
| 267 |
+
async fn chain_head(
|
| 268 |
+
State(state): State<AppState>,
|
| 269 |
+
) -> impl IntoResponse {
|
| 270 |
+
// Try to fetch from snap-os; fall back to local WORM hash
|
| 271 |
+
let fallback = errant_ffi::sha256_hex(b"MAGMA-GENESIS");
|
| 272 |
+
Json(serde_json::json!({
|
| 273 |
+
"head": fallback,
|
| 274 |
+
"source": "local",
|
| 275 |
+
"os_url": state.worm_url,
|
| 276 |
+
}))
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
// ── GET /api/v1/health ─────────────────────────────────────────────────────
|
| 280 |
+
|
| 281 |
+
async fn health(
|
| 282 |
+
State(state): State<AppState>,
|
| 283 |
+
) -> impl IntoResponse {
|
| 284 |
+
let errant_ok = errant_ffi::verify_named(&["PUSH_UN", "SEAL"]).ok;
|
| 285 |
+
let nats_ok = state.nats.is_some();
|
| 286 |
+
let status = if errant_ok { "ok" } else { "degraded" };
|
| 287 |
+
Json(serde_json::json!({
|
| 288 |
+
"status": status,
|
| 289 |
+
"version": env!("CARGO_PKG_VERSION"),
|
| 290 |
+
"errant": errant_ffi::version(),
|
| 291 |
+
"errant_ok": errant_ok,
|
| 292 |
+
"nats_ok": nats_ok,
|
| 293 |
+
"sovereign": "Ω↺Ψ↺Δ↺Λ↺Σ↺Φ↺α",
|
| 294 |
+
}))
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
// ── GET /api/v1/version ────────────────────────────────────────────────────
|
| 298 |
+
|
| 299 |
+
async fn version_handler() -> impl IntoResponse {
|
| 300 |
+
Json(serde_json::json!({
|
| 301 |
+
"magmad": env!("CARGO_PKG_VERSION"),
|
| 302 |
+
"liberrant": errant_ffi::version(),
|
| 303 |
+
"protocol": "MAGMA/1.0",
|
| 304 |
+
}))
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
// ── Legacy snap-os compatibility endpoints ─────────────────────────────────
|
| 308 |
+
|
| 309 |
+
#[derive(Deserialize)]
|
| 310 |
+
struct LegacySealPayload { payload: serde_json::Value }
|
| 311 |
+
|
| 312 |
+
async fn legacy_seal(
|
| 313 |
+
Json(body): Json<LegacySealPayload>,
|
| 314 |
+
) -> impl IntoResponse {
|
| 315 |
+
let hash = errant_ffi::sha256_hex(body.payload.to_string().as_bytes());
|
| 316 |
+
Json(serde_json::json!({
|
| 317 |
+
"event": {
|
| 318 |
+
"seal": hash,
|
| 319 |
+
"previousSeal": "0".repeat(64),
|
| 320 |
+
"index": 0,
|
| 321 |
+
"timestamp": chrono::Utc::now().to_rfc3339(),
|
| 322 |
+
}
|
| 323 |
+
}))
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
async fn legacy_dispatch(
|
| 327 |
+
Json(body): Json<serde_json::Value>,
|
| 328 |
+
) -> impl IntoResponse {
|
| 329 |
+
let hash = errant_ffi::sha256_hex(body.to_string().as_bytes());
|
| 330 |
+
Json(serde_json::json!({
|
| 331 |
+
"ok": true,
|
| 332 |
+
"seal": hash,
|
| 333 |
+
"gate": "TRUST-DEED-GATE",
|
| 334 |
+
"sovereign": "Ω",
|
| 335 |
+
}))
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
use futures::StreamExt;
|
| 339 |
+
use chrono;
|
magmad/src/main.rs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// main.rs — magmad sovereign agentic daemon
|
| 2 |
+
//
|
| 3 |
+
// Starts REST + SSE on :3000 (primary interface).
|
| 4 |
+
// gRPC on :50051 is defined in grpc/mod.rs and will be wired when protoc
|
| 5 |
+
// becomes available (install protoc, re-add tonic_build to build.rs).
|
| 6 |
+
//
|
| 7 |
+
// Usage:
|
| 8 |
+
// magmad # start with defaults
|
| 9 |
+
// MAGMA_HTTP_PORT=8080 magmad # override HTTP port
|
| 10 |
+
// MAGMA_NATS_URL=nats://... magmad
|
| 11 |
+
//
|
| 12 |
+
// Ahmad Ali Parr · SnapKitty Collective · 2026
|
| 13 |
+
|
| 14 |
+
mod bob;
|
| 15 |
+
mod config;
|
| 16 |
+
mod errant_ffi;
|
| 17 |
+
mod grpc;
|
| 18 |
+
mod http;
|
| 19 |
+
mod nats_bus;
|
| 20 |
+
|
| 21 |
+
use std::sync::Arc;
|
| 22 |
+
use anyhow::Result;
|
| 23 |
+
use tracing::{info, warn, error};
|
| 24 |
+
|
| 25 |
+
#[tokio::main]
|
| 26 |
+
async fn main() -> Result<()> {
|
| 27 |
+
let cfg = config::Config::from_env();
|
| 28 |
+
|
| 29 |
+
// ── Tracing ───────────────────────────────────────────────────────────
|
| 30 |
+
tracing_subscriber::fmt()
|
| 31 |
+
.with_env_filter(&cfg.log_level)
|
| 32 |
+
.json()
|
| 33 |
+
.init();
|
| 34 |
+
|
| 35 |
+
print_banner(&cfg);
|
| 36 |
+
|
| 37 |
+
// ── Verify liberrant is linked and working ────────────────────────────
|
| 38 |
+
{
|
| 39 |
+
let check = errant_ffi::verify_named(&["PUSH_UN", "SEAL"]);
|
| 40 |
+
if check.ok {
|
| 41 |
+
info!("liberrant {} online · FIPS SHA-256 verified", errant_ffi::version());
|
| 42 |
+
} else {
|
| 43 |
+
error!("liberrant self-check FAILED: {}", check.error);
|
| 44 |
+
std::process::exit(1);
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// ── NATS connection (optional — daemon runs without it) ───────────────
|
| 49 |
+
let nats = match nats_bus::NatsBus::connect(&cfg.nats_url).await {
|
| 50 |
+
Ok(bus) => {
|
| 51 |
+
info!("NATS connected: {}", cfg.nats_url);
|
| 52 |
+
Some(Arc::new(bus))
|
| 53 |
+
}
|
| 54 |
+
Err(e) => {
|
| 55 |
+
warn!("NATS unavailable ({}): running in local mode", e);
|
| 56 |
+
None
|
| 57 |
+
}
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
// ── Build shared HTTP state ───────────────────────────────────────────
|
| 61 |
+
let http_state = http::AppState {
|
| 62 |
+
config: Arc::new(cfg.clone()),
|
| 63 |
+
nats: nats.clone(),
|
| 64 |
+
worm_url: format!("{}/api/labs/ledge/seal", cfg.os_url),
|
| 65 |
+
};
|
| 66 |
+
|
| 67 |
+
// ── REST + SSE server (primary interface) ─────────────────────────────
|
| 68 |
+
let http_addr: std::net::SocketAddr = cfg.http_addr().parse()?;
|
| 69 |
+
let app = http::router(http_state)
|
| 70 |
+
.layer(tower_http::cors::CorsLayer::permissive())
|
| 71 |
+
.layer(tower_http::trace::TraceLayer::new_for_http());
|
| 72 |
+
|
| 73 |
+
info!("REST/SSE listening on http://{}", http_addr);
|
| 74 |
+
info!("gRPC endpoint defined at :{} (enable: install protoc, run cargo build)", cfg.grpc_port);
|
| 75 |
+
info!("magmad online · EVIDENCE OR SILENCE · Ω↺Ψ↺Δ↺Λ↺Σ↺Φ↺α");
|
| 76 |
+
|
| 77 |
+
let listener = tokio::net::TcpListener::bind(http_addr).await?;
|
| 78 |
+
|
| 79 |
+
tokio::select! {
|
| 80 |
+
r = axum::serve(listener, app) => {
|
| 81 |
+
if let Err(e) = r { error!("HTTP server error: {e}"); }
|
| 82 |
+
}
|
| 83 |
+
_ = tokio::signal::ctrl_c() => {
|
| 84 |
+
info!("Ω — magmad shutting down");
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
Ok(())
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
fn print_banner(cfg: &config::Config) {
|
| 92 |
+
eprintln!(r#"
|
| 93 |
+
╔══════════════════════════════════════════════════════════════╗
|
| 94 |
+
║ ███╗ ███╗ █████╗ ██████╗ ███╗ ███╗ █████╗ ██████╗ ║
|
| 95 |
+
║ ████╗ ████║██╔══██╗██╔════╝ ████╗ ████║██╔══██╗██╔══██╗ ║
|
| 96 |
+
║ ██╔████╔██║███████║██║ ███╗██╔████╔██║███████║██║ ██║ ║
|
| 97 |
+
║ ██║╚██╔╝██║██╔══██║██║ ██║██║╚██╔╝██║██╔══██║██║ ██║ ║
|
| 98 |
+
║ ██║ ╚═╝ ██║██║ ██║╚██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝ ║
|
| 99 |
+
║ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ║
|
| 100 |
+
╠══════════════════════════════════════════════════════════════╣
|
| 101 |
+
║ Sovereign Agentic Daemon v{} ║
|
| 102 |
+
║ BOB: TRUST-DEED-GATE → MAMBA → WATSON → PROLOG → SEAL ║
|
| 103 |
+
╠═════════════════════════════════════════��════════════════════╣
|
| 104 |
+
║ REST/SSE http://0.0.0.0:{} ║
|
| 105 |
+
║ gRPC :{} (stub — install protoc to activate) ║
|
| 106 |
+
║ NATS {}
|
| 107 |
+
╚══════════════════════════════════════════════════════════════╝
|
| 108 |
+
"#,
|
| 109 |
+
env!("CARGO_PKG_VERSION"),
|
| 110 |
+
cfg.http_port,
|
| 111 |
+
cfg.grpc_port,
|
| 112 |
+
cfg.nats_url,
|
| 113 |
+
);
|
| 114 |
+
}
|
magmad/src/nats_bus.rs
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// nats_bus.rs — NATS pub/sub fabric
|
| 2 |
+
//
|
| 3 |
+
// Wraps async-nats client. Sovereign subjects follow the pattern:
|
| 4 |
+
// sovereign.<layer>.<verb>.<version>
|
| 5 |
+
// e.g. sovereign.forge.build.v1
|
| 6 |
+
// sovereign.cipher.seal.v1
|
| 7 |
+
// sovereign.audit.bifrost.commit.v1
|
| 8 |
+
//
|
| 9 |
+
// NatsBus is cheaply cloneable (Arc inside async-nats client).
|
| 10 |
+
|
| 11 |
+
use async_nats::Client;
|
| 12 |
+
use anyhow::Result;
|
| 13 |
+
use serde::Serialize;
|
| 14 |
+
use tracing::{info, warn};
|
| 15 |
+
|
| 16 |
+
#[derive(Clone)]
|
| 17 |
+
pub struct NatsBus {
|
| 18 |
+
client: Client,
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
impl NatsBus {
|
| 22 |
+
pub async fn connect(url: &str) -> Result<Self> {
|
| 23 |
+
let client = async_nats::connect(url).await?;
|
| 24 |
+
info!("NATS connected: {}", url);
|
| 25 |
+
Ok(Self { client })
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/// Publish a serializable payload to a NATS subject.
|
| 29 |
+
pub async fn publish<T: Serialize>(&self, subject: &str, payload: &T) -> Result<()> {
|
| 30 |
+
let bytes = serde_json::to_vec(payload)?;
|
| 31 |
+
self.client
|
| 32 |
+
.publish(subject.to_owned(), bytes.into())
|
| 33 |
+
.await?;
|
| 34 |
+
Ok(())
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/// Publish a raw string payload.
|
| 38 |
+
pub async fn publish_str(&self, subject: &str, payload: &str) -> Result<()> {
|
| 39 |
+
self.client
|
| 40 |
+
.publish(subject.to_owned(), payload.as_bytes().to_vec().into())
|
| 41 |
+
.await?;
|
| 42 |
+
Ok(())
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
/// Publish a sovereign audit event to bifrost.
|
| 46 |
+
pub async fn audit<T: Serialize>(&self, event: &str, payload: &T) -> Result<()> {
|
| 47 |
+
#[derive(Serialize)]
|
| 48 |
+
struct AuditEnvelope<'a, T> {
|
| 49 |
+
event: &'a str,
|
| 50 |
+
payload: &'a T,
|
| 51 |
+
timestamp: String,
|
| 52 |
+
}
|
| 53 |
+
let envelope = AuditEnvelope {
|
| 54 |
+
event,
|
| 55 |
+
payload,
|
| 56 |
+
timestamp: chrono::Utc::now().to_rfc3339(),
|
| 57 |
+
};
|
| 58 |
+
self.publish("sovereign.audit.bifrost.commit.v1", &envelope).await
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
/// Subscribe to a subject, returning a stream of messages.
|
| 62 |
+
pub async fn subscribe(&self, subject: &str) -> Result<async_nats::Subscriber> {
|
| 63 |
+
Ok(self.client.subscribe(subject.to_owned()).await?)
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
/// Offline stub — used when NATS is unavailable.
|
| 68 |
+
/// Logs the publish attempt and returns Ok(()) silently.
|
| 69 |
+
pub async fn publish_offline(subject: &str, payload: &str) {
|
| 70 |
+
warn!("NATS offline — dropped: {} → {}", subject, &payload[..payload.len().min(80)]);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
// chrono for timestamps
|
| 74 |
+
use chrono;
|