feat: card standalone — primitiva de identidad soberana (hoja, autocontenida)
Tarjeta de Presentación canónica: identidad arje + flujos tipados brahman, content-addressed. Hoja pura sin deps internas — la base sobre la que se montan red (chasqui/minga) y escritorio. cargo check pasa (3 crates). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
*.pdb
|
||||
Generated
+3933
File diff suppressed because it is too large
Load Diff
+432
@@ -0,0 +1,432 @@
|
||||
# Cargo.toml raíz STANDALONE de card — front-door sobre Llimphi.
|
||||
# Solo el código de card; Llimphi y lo fundacional por git-dep del monorepo gioser.git.
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"shared/card/card-core",
|
||||
"shared/card/card-net",
|
||||
"shared/card/card-wit",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.80"
|
||||
license = "MIT"
|
||||
authors = ["Sergio <gerencia@jlsoltech.com>"]
|
||||
publish = false
|
||||
repository = "https://gitea.gioser.net/sergio/card"
|
||||
|
||||
[workspace.dependencies]
|
||||
|
||||
# === Registro de apps / menú global ===
|
||||
app-bus = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
# === Serialización ===
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
lsp-types = "0.97"
|
||||
serde-big-array = "0.5"
|
||||
postcard = { version = "1", features = ["use-std"] }
|
||||
toml = "0.8"
|
||||
ron = "0.8"
|
||||
bincode = "1"
|
||||
base64 = "0.22"
|
||||
|
||||
# === Errores ===
|
||||
thiserror = "2" # bump uniforme; arje (era 1) puede requerir ajustes menores
|
||||
anyhow = "1"
|
||||
|
||||
# === Async ===
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["compat"] }
|
||||
async-trait = "0.1"
|
||||
futures = "0.3"
|
||||
|
||||
# === Observabilidad ===
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||
|
||||
# === Linux primitives (arje) ===
|
||||
nix = { version = "0.29", features = ["signal", "process", "sched", "mount", "fs", "socket", "net", "user"] }
|
||||
libc = "0.2"
|
||||
|
||||
# === IDs / Hash / Crypto ===
|
||||
ulid = { version = "1", features = ["serde"] }
|
||||
uuid = { version = "1", features = ["v4", "rng-getrandom"] }
|
||||
sha2 = "0.10"
|
||||
blake3 = "1.5"
|
||||
ed25519-dalek = "2"
|
||||
aes-gcm = "0.10"
|
||||
chacha20poly1305 = "0.10"
|
||||
argon2 = "0.5"
|
||||
rand = "0.8"
|
||||
|
||||
# === WASM (arje) ===
|
||||
# wasmi 1.0: unifica la versión con renaser (su kernel ya corre 1.0), para
|
||||
# que el ABI WASM del host sea idéntico en Linux y en bare-metal.
|
||||
wasmi = "1.0"
|
||||
wat = "1"
|
||||
|
||||
# === Storage / DB ===
|
||||
sled = "0.34"
|
||||
rusqlite = { version = "0.31", features = ["bundled", "blob"] }
|
||||
|
||||
# === Ingesta de documentos (iniy-ingest: PDF / EPUB) ===
|
||||
pdf-extract = "0.7"
|
||||
epub = "2.1"
|
||||
|
||||
# === Bulk import Wikipedia (iniy-wiki dump) ===
|
||||
bzip2 = "0.4"
|
||||
|
||||
# === Compresión (minga multi-bundle) ===
|
||||
zstd = "0.13"
|
||||
|
||||
# === HTTP server (iniy-server) ===
|
||||
axum = "0.7"
|
||||
tower = "0.5"
|
||||
|
||||
# === ANN sobre embeddings (iniy nli --ann) ===
|
||||
instant-distance = "0.6"
|
||||
|
||||
# === P2P (minga) ===
|
||||
libp2p = { version = "0.56", features = ["tokio", "tcp", "noise", "yamux", "macros", "kad", "identify", "relay", "dcutr", "autonat", "mdns"] }
|
||||
libp2p-stream = "=0.4.0-alpha"
|
||||
libp2p-allow-block-list = "0.6"
|
||||
|
||||
# === SSH (ssh, sandokan RemoteEngine, matilda) ===
|
||||
russh = "0.54"
|
||||
|
||||
# === Math determinista cross-platform (dominium) ===
|
||||
libm = "0.2"
|
||||
|
||||
# === SMF (takiy-midi) ===
|
||||
# midly: parser/emitter SMF tipo 0/1, no_std-friendly, sin allocs en hot path.
|
||||
midly = "0.5"
|
||||
|
||||
# === Code parsing (minga) ===
|
||||
arboard = "3"
|
||||
ropey = "1.6"
|
||||
tree-sitter = "0.24"
|
||||
tree-sitter-rust = "0.23"
|
||||
tree-sitter-python = "0.23"
|
||||
tree-sitter-typescript = "0.23"
|
||||
tree-sitter-javascript = "0.23"
|
||||
tree-sitter-go = "0.23"
|
||||
|
||||
# === FS notify ===
|
||||
notify = "6.1"
|
||||
|
||||
# === Grafos (iniy, nakui-core ya lo usa directo en 0.6) ===
|
||||
petgraph = "0.6"
|
||||
|
||||
# === Image decoding (nahual-image-viewer-llimphi) ===
|
||||
# default-features = false: nos quedamos con PNG + JPEG + WebP (lossless).
|
||||
# tullpu-render exporta a las tres; AVIF/TIFF/… los habilitamos si una app
|
||||
# los pide específicamente.
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp"] }
|
||||
|
||||
# === FUSE (minga-vfs) ===
|
||||
# default-features = false: prescinde de pkg-config/libfuse-dev en build.
|
||||
# El montaje pasa a ser Rust puro (vía el helper `fusermount3` en runtime).
|
||||
fuser = { version = "0.15", default-features = false }
|
||||
|
||||
# === CLI / auth (minga) ===
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
rpassword = "7"
|
||||
|
||||
# === PAM (auth-core) ===
|
||||
pam = "0.8"
|
||||
|
||||
# === D-Bus (arje compat) ===
|
||||
zbus = { version = "4", default-features = false, features = ["tokio"] }
|
||||
|
||||
# === Tests ===
|
||||
tempfile = "3"
|
||||
|
||||
# === Llimphi (motor gráfico soberano) ===
|
||||
# wgpu sobre Vulkan/Metal/DX12, winit para ventana en dev Linux.
|
||||
# raw-window-handle 0.6 alinea winit 0.30 con wgpu 24.
|
||||
# vello 0.5 = rasterizador vectorial sobre wgpu 24.
|
||||
# taffy 0.9 = motor Flexbox/Grid puro Rust (ya pulled por transitivos, lo alineamos).
|
||||
# parley 0.2 = shaping/layout de texto compatible con peniko 0.4 (que vello 0.5 expone).
|
||||
wgpu = "24"
|
||||
winit = "0.30"
|
||||
raw-window-handle = "0.6"
|
||||
pollster = "0.4"
|
||||
vello = "0.5"
|
||||
taffy = "0.9"
|
||||
# parley = shaping completo (bidi, ligatures, fallback CJK/emoji vía fontique, line break).
|
||||
parley = "0.4"
|
||||
# Bucle Elm (input→update→view→layout→raster→present). Lo consumen las apps.
|
||||
llimphi-ui = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
# Paleta semántica compartida por las apps y los widgets.
|
||||
llimphi-theme = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
# Tweens y helpers de animación sobre el bucle Elm.
|
||||
llimphi-motion = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
# Iconos vectoriales (BezPath en grid 24×24) compartidos por todas las apps.
|
||||
llimphi-icons = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
# Widgets reusables sobre llimphi-ui — uno por crate.
|
||||
llimphi-widget-app-header = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-banner = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-button = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-card = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-clipboard = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-context-menu = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-edit-menu = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-menubar = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-list = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-grid = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-slider = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-scroll = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-splitter = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-stat-card = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-tabs = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-module-command-palette = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-module-diff-viewer = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-module-fif = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-module-file-picker = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-module-bookmarks = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-module-mini-map = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-module-shuma-term = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-module-symbol-outline = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-plugin-host = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-theme-switcher = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-text-area = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-text-editor-core = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-text-editor = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-text-editor-lsp = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-text-input = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-tiled = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-nodegraph = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-tree = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-navigator = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
# Sello vectorial wawa (rombo + W implícita + Merkle Core).
|
||||
llimphi-widget-wawa-mark = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
# Widgets de elegancia transversal (tooltip, spinner, progress, toast,
|
||||
# modal, empty, status-bar, shortcuts-help, splash).
|
||||
llimphi-widget-tooltip = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-spinner = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-progress = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-toast = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-modal = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-empty = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-status-bar = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-shortcuts-help = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-timeline = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-splash = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
# Controles de formulario y signaling (switch, segmented, breadcrumb,
|
||||
# badge, avatar, skeleton, field).
|
||||
llimphi-widget-switch = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-segmented = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-dock-rail = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-breadcrumb = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-badge = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-avatar = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-skeleton = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-field = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
# Firma visual transversal (gradient sutil + hairline accent).
|
||||
llimphi-widget-panel = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-widget-panes = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
llimphi-workspace = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
# Abstracción Selector — host (paths) + wawa (khipus).
|
||||
llimphi-module-selector = { git = "https://gitea.gioser.net/sergio/llimphi.git" }
|
||||
|
||||
# === Filesystem helpers ===
|
||||
directories = "5"
|
||||
|
||||
# === Diff line-based (llimphi-module-diff-viewer) ===
|
||||
# `similar` es la crate de facto: implementa Myers + Patience + LCS,
|
||||
# expone `TextDiff` con ChangeTag por línea (Equal/Insert/Delete),
|
||||
# zero deps fuera de std. La 2.x es estable hace años.
|
||||
similar = "2"
|
||||
|
||||
# === Fuzzy matching (shuma-history) ===
|
||||
# nucleo-matcher = mismo matcher que helix-editor: rápido, Unicode-correct,
|
||||
# bonus por prefijos, ranking estable. La versión 0.3 expone el API simple
|
||||
# que necesitamos (Matcher + Pattern + score).
|
||||
nucleo-matcher = "0.3"
|
||||
|
||||
# === Transporte autenticado (shuma-link) ===
|
||||
# snow = framework Noise pure-rust. Lo usamos en modo Noise_XK (cliente
|
||||
# conoce la pubkey del servidor, server descubre la del cliente y la
|
||||
# valida contra una allowlist). ChaCha20-Poly1305 + X25519 + BLAKE2s.
|
||||
# La versión 0.9 viene pinneada por libp2p, así nos alineamos.
|
||||
snow = "0.9"
|
||||
hex = "0.4"
|
||||
|
||||
# === PTY + emulador de terminal (shuma-exec, módulos REPL) ===
|
||||
# portable-pty aloja un PTY cross-platform; lo usamos para los
|
||||
# comandos TUI tipo vim/htop/less que necesitan un terminal de verdad.
|
||||
# vt100 parsea la secuencia de bytes que el PTY emite (ANSI + cursor
|
||||
# movement + erase + screen state) y mantiene un buffer de pantalla
|
||||
# renderizable como grid.
|
||||
portable-pty = "0.9"
|
||||
vt100 = "0.16"
|
||||
|
||||
# === WASM web (gioser) ===
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
js-sys = "0.3"
|
||||
web-sys = "0.3"
|
||||
glam = "0.30"
|
||||
|
||||
# === Markdown (pluma) ===
|
||||
pulldown-cmark = { version = "0.12", default-features = false, features = ["html"] }
|
||||
|
||||
# === Archivos comprimidos (nahual archive viewer) ===
|
||||
# Sólo listamos el directorio central (nombres/tamaños); no descomprimimos,
|
||||
# por eso default-features=false alcanza para ZIP. Para tar.gz sí
|
||||
# descomprimimos en streaming con flate2 (ya declarado arriba), saltando
|
||||
# los datos de cada entrada — sólo leemos headers.
|
||||
zip = { version = "2.4", default-features = false }
|
||||
tar = { version = "0.4", default-features = false }
|
||||
|
||||
# === Fuentes (nahual font viewer) ===
|
||||
# Parseo de TTF/OTF/TTC y extracción de contornos de glifo a paths.
|
||||
ttf-parser = "0.25"
|
||||
|
||||
# ============================================================
|
||||
# Intra-workspace deps de nahual (referenciadas por workspace = true)
|
||||
# ============================================================
|
||||
nahual-text-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
nahual-image-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
nahual-thumb-core = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
nahual-gallery-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
nahual-video-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
nahual-card-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
nahual-audio-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
nahual-tree-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
nahual-hex-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
nahual-table-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
nahual-markdown-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
nahual-archive-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
nahual-font-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
nahual-map-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
nahual-geo-core = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
nahual-viewer-core = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
nahual-file-explorer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
|
||||
# ============================================================
|
||||
# Intra-workspace deps de pineal (módulo de gráficos)
|
||||
# ============================================================
|
||||
pineal-core = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
pineal-render = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
pineal-cartesian = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
pineal-stream = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
pineal-mesh = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
pineal-financial = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
pineal-polar = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
pineal-heatmap = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
pineal-treemap = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
pineal-flow = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
pineal-phosphor = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
pineal-export = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
pineal-hexbin = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
pineal-contour = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
pineal-bars = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
pineal = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
|
||||
# ============================================================
|
||||
# Intra-workspace deps de iniy (laboratorio semántico de creencias)
|
||||
# ============================================================
|
||||
iniy-core = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
iniy-ingest = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
iniy-extract = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
iniy-nli = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
iniy-nli-llm = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
iniy-graph = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
iniy-store = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
|
||||
# === auto: declarados por crates internos faltantes ===
|
||||
cosmos-coords = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
cosmos-core = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
cosmos-ephemeris = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
cosmos-time = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
cosmos-wcs = { git = "https://gitea.gioser.net/sergio/gioser.git" }
|
||||
|
||||
# === auto: externas de eternal ===
|
||||
celestial-eop-data = { version = "0.1"}
|
||||
approx = "0.5"
|
||||
byteorder = "1.5"
|
||||
cc = "1.0"
|
||||
chrono = "0.4"
|
||||
crc32fast = "1.4"
|
||||
criterion = "0.5"
|
||||
csv = "1.4"
|
||||
flate2 = "1.0"
|
||||
glob = "0.3"
|
||||
indicatif = "0.18"
|
||||
lz4_flex = "0.11"
|
||||
memmap2 = "0.9"
|
||||
mockito = "1.0"
|
||||
ndarray = "0.15"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.19"
|
||||
parking_lot = "0.12"
|
||||
png = "0.18"
|
||||
proptest = "1.4"
|
||||
quick-xml = "0.31"
|
||||
rayon = "1.8"
|
||||
regex = "1.11"
|
||||
reqwest = "0.12"
|
||||
tiff = "0.11"
|
||||
wide = "0.7"
|
||||
wiremock = "0.6"
|
||||
|
||||
# === i18n (rimay-localize) ===
|
||||
fluent-bundle = "0.15"
|
||||
unic-langid = { version = "0.9", features = ["macros"] }
|
||||
sys-locale = "0.3"
|
||||
|
||||
# === Servo (puriy-engine) ===
|
||||
# Crates publicados de Servo embebibles individualmente. html5ever/markup5ever
|
||||
# ya entran via ammonia→surrealdb→nakui, así que alineamos versión para no
|
||||
# duplicar el árbol. markup5ever_rcdom es el DOM Rc-based simple (suficiente
|
||||
# para Fase 2: parsear y renderizar, sin scripting). cssparser es el tokenizer
|
||||
# CSS de Stylo, sirve para inline styles. ureq = HTTP síncrono minimalista,
|
||||
# evita pull de tokio en el engine.
|
||||
html5ever = "0.39"
|
||||
markup5ever = "0.39"
|
||||
markup5ever_rcdom = "0.39"
|
||||
cssparser = "0.35"
|
||||
url = "2"
|
||||
ureq = { version = "2", default-features = false, features = ["tls"] }
|
||||
|
||||
# === takiy-synth (SoundFont MIDI) ===
|
||||
# rustysynth = sintetizador SF2 puro Rust, MIT. Reemplaza el oscilador
|
||||
# feo de takiy-synth por muestras reales (FluidR3, GeneralUser GS, etc).
|
||||
rustysynth = "1.3"
|
||||
|
||||
# === takiy-playback (audio device output) ===
|
||||
# cpal = backend de audio cross-platform (ALSA/PulseAudio/Pipewire en
|
||||
# Linux, WASAPI en Windows, CoreAudio en macOS). Lo usamos sólo para
|
||||
# abrir el device default y empujar muestras f32 — nada de mezclado
|
||||
# ni efectos en el callback.
|
||||
cpal = "0.15"
|
||||
|
||||
# === media-source-wav (decoder PCM en disco) ===
|
||||
# hound = lector/escritor WAV puro-Rust, sin deps nativas. Soporta PCM
|
||||
# entero (8/16/24/32) y float (32). Suficiente para abrir samples y
|
||||
# stems de prueba sin meter ffmpeg/symphonia.
|
||||
hound = "3.5"
|
||||
|
||||
# === media-source-{mp3,flac,vorbis} (decoders vía symphonia) ===
|
||||
# symphonia es una colección de decoders puro-Rust mantenida. `mp3` cubre
|
||||
# media-source-mp3; `flac` (decoder + demuxer FLAC nativo) cubre
|
||||
# media-source-flac (lossless); `vorbis` + `ogg` (codec + demuxer Ogg)
|
||||
# cubren media-source-vorbis (lossy clásico, libre de patentes). Sin aac:
|
||||
# ese tier patentado entra por shared/foreign-av.
|
||||
symphonia = { version = "0.5", default-features = false, features = ["mp3", "flac", "vorbis", "ogg"] }
|
||||
|
||||
# === media-source-opus (decoder Opus NATIVO puro-Rust) ===
|
||||
# Opus es el formato de audio nativo de gioser (par del video AV1). ogg
|
||||
# demuxea las páginas Ogg; opus-wave es un port puro-Rust de libopus
|
||||
# (SILK+CELT, sin C ni FFI) — par del rav1d del lado video.
|
||||
ogg = "0.9"
|
||||
opus-wave = "3"
|
||||
|
||||
# === media-source-webm (demux nativo Matroska/WebM) ===
|
||||
# matroska-demuxer es un demuxer puro-Rust de MKV/WebM (EBML). Saca los
|
||||
# paquetes de los tracks V_AV1 y A_OPUS para alimentar a media-source-av1
|
||||
# y media-source-opus — un .webm AV1+Opus se reproduce 100% nativo.
|
||||
matroska-demuxer = "0.7"
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Sergio
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,18 @@
|
||||
# card
|
||||
|
||||
> The canonical **presentation card** — sovereign identity + typed flows, in Rust.
|
||||
|
||||
`card` is the suite's identity primitive: a content-addressed *Tarjeta de Presentación* that binds an `arje` identity to typed `brahman` flows. It is the leaf that the networking and desktop layers build on — peers, channels and capabilities are all expressed in terms of cards.
|
||||
|
||||
## Crates
|
||||
|
||||
- **`card-core`** — the canonical card type (identity + typed flows), content-addressed.
|
||||
- **`card-wit`** — WIT-typed flow descriptions (component-model interfaces).
|
||||
|
||||
## How dependencies work
|
||||
|
||||
A true leaf: `card` has **no internal dependencies** — only crates.io. It compiles standalone and is git-depended by the rest of the suite (networking via `chasqui`/`minga`, the desktop, etc.).
|
||||
|
||||
## License
|
||||
|
||||
MIT.
|
||||
@@ -0,0 +1,36 @@
|
||||
# card — identidad agnóstica de transporte
|
||||
|
||||
Contrato de identidad y membresía **independiente del transporte**: claves
|
||||
Ed25519, `EspinaId` (hash de la clave pública), firmas y handshake de membresía
|
||||
de una "espina" (red privada). El mismo contrato lo implementan dos transportes
|
||||
distintos: `card-net` (libp2p) y `wawa-akasha` (protocolo propio de wawa).
|
||||
|
||||
## Subcrates
|
||||
|
||||
- **`card-core`** — el contrato agnóstico real: par Ed25519 → `EspinaId`,
|
||||
`Card` firmada, `handshake` de membresía (un miembro presenta su tarjeta
|
||||
firmada; el anfitrión la verifica contra la raíz de confianza de la espina).
|
||||
Sólo bytes firmados — no sabe de libp2p ni Akasha.
|
||||
- **`card-net`** — espina dorsal P2P sobre **libp2p**: discovery (mDNS +
|
||||
Kademlia DHT), gossipsub, NAT traversal (Circuit Relay v2 + DCUtR + AutoNAT).
|
||||
Implementa el contrato de `card-core` sobre libp2p. Lo consume `khipu`.
|
||||
- **`card-wit`** — **[DORMIDO]** binding WIT/wasm del contrato. Se reactiva
|
||||
cuando `card` cruce a apps WASM; hoy el contrato real es `card-core`.
|
||||
|
||||
## Estado (2026-05-31)
|
||||
|
||||
### Hecho
|
||||
- `card-core`: identidad Ed25519 + `EspinaId` + handshake de membresía (contrato
|
||||
agnóstico declarado como la fuente de verdad).
|
||||
- `card-net`: discovery (mDNS+DHT), gossipsub y NAT traversal completo
|
||||
(Relay v2 + DCUtR + AutoNAT); discovery de personas por `DhtKey::Persona`.
|
||||
|
||||
### Pendiente
|
||||
- `card-wit`: dormido — binding WASM pendiente de reactivación.
|
||||
- Espejo del contrato sobre `wawa-akasha` (transporte wawa) aún por cablear.
|
||||
- Endurecer revocación / rotación de membresía de espina.
|
||||
|
||||
## Lugar en el repo
|
||||
|
||||
`shared/card` — contrato de identidad. `card-net` lo lleva a libp2p (khipu);
|
||||
`agora` cubre firma/confianza de más alto nivel.
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "card-core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
description = "Brahman — Tarjeta de Presentación canónica (identidad arje + flujos tipados brahman)."
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
ulid = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
postcard = { workspace = true }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "card-net"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
description = "Brahman — capa de transporte P2P compartida (libp2p TCP+Noise+Yamux+Kad+Identify+Stream). Cualquier protocolo (handshake brahman, sync minga, futuros) puede registrar su StreamProtocol y abrir/aceptar streams sobre la malla común."
|
||||
|
||||
[dependencies]
|
||||
futures = { workspace = true }
|
||||
libp2p = { workspace = true }
|
||||
libp2p-stream = { workspace = true }
|
||||
libp2p-allow-block-list = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
blake3 = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true }
|
||||
tokio-util = { workspace = true }
|
||||
@@ -0,0 +1,167 @@
|
||||
//! Claves namespaced del DHT compartido — la **primitiva unificadora** de
|
||||
//! Brahman (Capa 1).
|
||||
//!
|
||||
//! El ecosistema brahman corre UN solo Kademlia (en este crate, `card-net`).
|
||||
//! Para que distintos dominios — código indexado (minga), Cards
|
||||
//! (card-discovery), Personas (ágora), servicios — coexistan sin colisión,
|
||||
//! cada clave lleva un byte de `kind` como prefijo. La representación en wire
|
||||
//! es de longitud fija: `[kind_tag] ++ blake3(id)` = 33 bytes.
|
||||
//!
|
||||
//! Vive en `card-net` (y no en un dominio concreto) precisamente porque es el
|
||||
//! namespace COMÚN: minga, agora y card-discovery la comparten. `minga-dht`
|
||||
//! la re-exporta por compatibilidad histórica.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Tipo de registro — el namespace de una clave en el DHT.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum RecordKind {
|
||||
/// Bloque de código indexado (minga).
|
||||
Code,
|
||||
/// `Card` brahman (módulo, ente, etc.).
|
||||
Card,
|
||||
/// `Persona` de ágora (identidad humana federada).
|
||||
Persona,
|
||||
/// Endpoint de servicio.
|
||||
Service,
|
||||
/// Dominio definido por el consumidor.
|
||||
Custom(u8),
|
||||
}
|
||||
|
||||
impl RecordKind {
|
||||
/// Byte de etiqueta. `Custom(n)` ocupa `0x80 | n` (top bit) para no
|
||||
/// chocar nunca con los kinds estándar (`0x00..`).
|
||||
pub fn tag(&self) -> u8 {
|
||||
match self {
|
||||
RecordKind::Code => 0x01,
|
||||
RecordKind::Card => 0x02,
|
||||
RecordKind::Persona => 0x03,
|
||||
RecordKind::Service => 0x04,
|
||||
RecordKind::Custom(n) => 0x80 | (n & 0x7f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Longitud fija de la clave en wire: 1 byte de kind + 32 de hash.
|
||||
pub const DHT_KEY_LEN: usize = 33;
|
||||
|
||||
/// Clave de DHT namespaced. Dos formas de construcción:
|
||||
///
|
||||
/// - Con un `id` legible (`new`, `code`, `card`, `persona`) — el wire
|
||||
/// hashea el id con blake3. Útil cuando el consumidor publica
|
||||
/// identidades simbólicas (nombres de módulos, slugs de personas).
|
||||
/// - Con `for_hash` — el wire usa los 32 bytes del hash directamente.
|
||||
/// Útil cuando el id YA es un blake3 (como en minga, que indexa
|
||||
/// contenido por su α-hash, o ágora, cuyo `IdentityId` ya es
|
||||
/// `blake3(pubkey)`) — evita una segunda pasada de blake3.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DhtKey {
|
||||
kind: RecordKind,
|
||||
/// Representación canónica de los 32 bytes que forman la "id" en wire.
|
||||
/// Si se construyó por nombre simbólico, son `blake3(id_string)`.
|
||||
/// Si se construyó por hash directo, son el hash tal cual.
|
||||
body: [u8; 32],
|
||||
/// `id` legible — para `Display`. `None` si se construyó por hash.
|
||||
label: Option<String>,
|
||||
}
|
||||
|
||||
impl DhtKey {
|
||||
pub fn new(kind: RecordKind, id: impl Into<String>) -> Self {
|
||||
let id = id.into();
|
||||
let body = *blake3::hash(id.as_bytes()).as_bytes();
|
||||
Self {
|
||||
kind,
|
||||
body,
|
||||
label: Some(id),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clave para un bloque de código (id legible).
|
||||
pub fn code(id: impl Into<String>) -> Self {
|
||||
Self::new(RecordKind::Code, id)
|
||||
}
|
||||
|
||||
/// Clave para una Card.
|
||||
pub fn card(id: impl Into<String>) -> Self {
|
||||
Self::new(RecordKind::Card, id)
|
||||
}
|
||||
|
||||
/// Clave para una Persona.
|
||||
pub fn persona(id: impl Into<String>) -> Self {
|
||||
Self::new(RecordKind::Persona, id)
|
||||
}
|
||||
|
||||
/// Clave a partir de un hash ya computado (32 bytes). El wire usa
|
||||
/// esos bytes directamente, **sin re-hashear**.
|
||||
pub fn for_hash(kind: RecordKind, hash: [u8; 32]) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
body: hash,
|
||||
label: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> RecordKind {
|
||||
self.kind
|
||||
}
|
||||
|
||||
pub fn id(&self) -> Option<&str> {
|
||||
self.label.as_deref()
|
||||
}
|
||||
|
||||
/// Representación en wire: `[kind_tag] ++ body`, 33 bytes.
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(DHT_KEY_LEN);
|
||||
out.push(self.kind.tag());
|
||||
out.extend_from_slice(&self.body);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn wire_key_has_fixed_length() {
|
||||
assert_eq!(DhtKey::card("modulo-x").to_bytes().len(), DHT_KEY_LEN);
|
||||
assert_eq!(DhtKey::code("fn-hash").to_bytes().len(), DHT_KEY_LEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_id_different_kind_does_not_collide() {
|
||||
let a = DhtKey::card("foo").to_bytes();
|
||||
let b = DhtKey::code("foo").to_bytes();
|
||||
let c = DhtKey::persona("foo").to_bytes();
|
||||
assert_ne!(a, b);
|
||||
assert_ne!(b, c);
|
||||
assert_ne!(a, c);
|
||||
// El hash del id es el mismo; sólo difiere el byte de kind.
|
||||
assert_eq!(a[1..], b[1..]);
|
||||
assert_ne!(a[0], b[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_kind_and_id_is_stable() {
|
||||
assert_eq!(DhtKey::card("x").to_bytes(), DhtKey::card("x").to_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_hash_no_rehashea() {
|
||||
// for_hash debe usar los bytes tal cual: body = hash, no blake3(hash).
|
||||
let h = [7u8; 32];
|
||||
let k = DhtKey::for_hash(RecordKind::Persona, h);
|
||||
let wire = k.to_bytes();
|
||||
assert_eq!(wire[0], RecordKind::Persona.tag());
|
||||
assert_eq!(&wire[1..], &h);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_kind_never_collides_with_standard() {
|
||||
for std in [RecordKind::Code, RecordKind::Card, RecordKind::Persona, RecordKind::Service] {
|
||||
for n in 0..=127u8 {
|
||||
assert_ne!(std.tag(), RecordKind::Custom(n).tag());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
//! `brahman-net` — capa P2P compartida de la red Brahman.
|
||||
//!
|
||||
//! Provee un nodo libp2p genérico que cualquier protocolo de la
|
||||
//! familia (handshake brahman remoto, sync minga, futuros) puede
|
||||
//! reusar. La idea: una sola malla, múltiples sub-protocolos
|
||||
//! multiplexados por `StreamProtocol`.
|
||||
//!
|
||||
//! ## Stack
|
||||
//!
|
||||
//! - **TCP + Noise + Yamux**: transporte autenticado y multiplexado.
|
||||
//! - **`stream::Behaviour`**: streams bidireccionales por
|
||||
//! `StreamProtocol`. Cada protocolo (`/brahman/handshake/1.0.0`,
|
||||
//! `/minga/sync/1.0.0`, …) se registra independientemente vía el
|
||||
//! `stream::Control` que `BrahmanNet` expone.
|
||||
//! - **`kad::Behaviour<MemoryStore>`**: Kademlia DHT en modo Server
|
||||
//! para discovery (peers cercanos + content providers).
|
||||
//! - **`identify::Behaviour`**: cada peer anuncia sus listen-addrs
|
||||
//! reales; las inyectamos automáticamente al routing table de Kad.
|
||||
//!
|
||||
//! ## Modelo
|
||||
//!
|
||||
//! El swarm corre en una task tokio dedicada. La interfaz pública son:
|
||||
//! 1. **Comandos** (canal mpsc): `dial`, `listen`, `add_dht_peer`,
|
||||
//! `find_closest_peers`, `start_providing`, `find_providers`.
|
||||
//! 2. **`stream::Control`** (acceso directo): para abrir/aceptar
|
||||
//! streams de un protocolo concreto. Cada protocolo se ocupa de
|
||||
//! su propia lógica sobre el stream resultante.
|
||||
//!
|
||||
//! La separación entre comandos y control permite que la lógica de
|
||||
//! red (DHT, dial, listen) y la lógica de protocolos (handshake/sync)
|
||||
//! evolucionen independientes — el protocolo no necesita conocer al
|
||||
//! swarm, sólo pide streams.
|
||||
//!
|
||||
//! ## Identidad
|
||||
//!
|
||||
//! Por defecto se genera una keypair Ed25519 efímera. Para identidad
|
||||
//! persistente (la misma `peer_id` across reboots), pasar la keypair
|
||||
//! con [`BrahmanNet::with_keypair`]. Esa misma keypair puede ser la
|
||||
//! base para firmas de Cards (cuando se implemente trust remoto).
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(rust_2018_idioms)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::StreamExt;
|
||||
use libp2p::{
|
||||
autonat, dcutr, identify, identity, kad, mdns, noise, relay,
|
||||
swarm::{behaviour::toggle::Toggle, NetworkBehaviour, SwarmEvent},
|
||||
tcp, yamux, Swarm, SwarmBuilder,
|
||||
};
|
||||
use libp2p_allow_block_list::{self as allow_block_list, BlockedPeers};
|
||||
use libp2p_stream as stream;
|
||||
use tokio::sync::{mpsc, oneshot, Mutex};
|
||||
|
||||
pub mod key;
|
||||
pub use key::{DhtKey, RecordKind, DHT_KEY_LEN};
|
||||
|
||||
pub use libp2p::{
|
||||
identity::{Keypair, PublicKey},
|
||||
multiaddr::Protocol,
|
||||
Multiaddr, PeerId, Stream, StreamProtocol,
|
||||
};
|
||||
pub use libp2p_stream::OpenStreamError;
|
||||
|
||||
const IDENTIFY_PROTOCOL: &str = "/brahman-net/0.1.0";
|
||||
const IDLE_CONNECTION_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
#[derive(NetworkBehaviour)]
|
||||
struct BrahmanBehaviour {
|
||||
/// Block-list a nivel de swarm: peers en este behaviour son
|
||||
/// rechazados ANTES del handshake Noise. Más eficiente que
|
||||
/// rechazar al nivel del handshake brahman (ahorra round-trip
|
||||
/// TCP+Noise por intento denegado). Sincronizado con la
|
||||
/// `PeerPolicy.deny` vía `block_peer`/`unblock_peer` exposed
|
||||
/// en `BrahmanNet`.
|
||||
block_list: allow_block_list::Behaviour<BlockedPeers>,
|
||||
stream: stream::Behaviour,
|
||||
kad: kad::Behaviour<kad::store::MemoryStore>,
|
||||
identify: identify::Behaviour,
|
||||
/// Relay server (Circuit Relay v2): un nodo alcanzable presta su
|
||||
/// conexión para que dos pares detrás de NAT se contacten. Pasivo —
|
||||
/// sólo actúa ante reservas/solicitudes de circuito.
|
||||
relay: relay::Behaviour,
|
||||
/// Relay client: permite reservar un circuito en un relay y ser
|
||||
/// alcanzable vía `…/p2p/<relay>/p2p-circuit/p2p/<self>`.
|
||||
relay_client: relay::client::Behaviour,
|
||||
/// DCUtR: tras conectar por relay, intenta promover a conexión
|
||||
/// directa (hole-punching) coordinando por el circuito.
|
||||
dcutr: dcutr::Behaviour,
|
||||
/// AutoNAT: confirma qué direcciones externas son realmente alcanzables
|
||||
/// pidiéndole a otros peers que nos disquen de vuelta. Sustituye el
|
||||
/// "confiar a ciegas en el observed_addr de identify" por direcciones
|
||||
/// verificadas — sólo esas se anuncian (y entran en reservas de relay).
|
||||
autonat: autonat::Behaviour,
|
||||
/// mDNS: descubrimiento de pares en la MISMA LAN sin bootstrap ni IP
|
||||
/// conocida (multicast 224.0.0.251). Los pares descubiertos se inyectan
|
||||
/// a la routing table de Kad (igual que las listen-addrs de identify), así
|
||||
/// el DHT y los protocolos de stream funcionan zero-config en LAN.
|
||||
/// `Toggle`: si el socket multicast no se puede abrir (sandbox, red sin
|
||||
/// multicast), queda deshabilitado y el nodo sigue andando igual.
|
||||
mdns: Toggle<mdns::tokio::Behaviour>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum NodeError {
|
||||
#[error("transport build failed: {0}")]
|
||||
Build(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Command {
|
||||
Dial(Multiaddr),
|
||||
Listen(Multiaddr),
|
||||
AddDhtPeer(PeerId, Multiaddr),
|
||||
FindClosestPeers(PeerId, oneshot::Sender<Vec<DiscoveredPeer>>),
|
||||
StartProviding(Vec<u8>),
|
||||
StopProviding(Vec<u8>),
|
||||
GetProviders(Vec<u8>, oneshot::Sender<Vec<PeerId>>),
|
||||
BlockPeer(PeerId),
|
||||
UnblockPeer(PeerId),
|
||||
}
|
||||
|
||||
/// Peer descubierto vía DHT: identidad + direcciones conocidas.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DiscoveredPeer {
|
||||
pub peer_id: PeerId,
|
||||
pub addrs: Vec<Multiaddr>,
|
||||
}
|
||||
|
||||
/// Nodo Brahman en la malla P2P. Maneja el swarm libp2p y expone
|
||||
/// API uniforme para listen/dial/DHT/streams.
|
||||
pub struct BrahmanNet {
|
||||
/// Identidad libp2p de este nodo. Estable mientras viva la
|
||||
/// keypair (efímera por default; persistente si pasaste una
|
||||
/// vía [`with_keypair`]).
|
||||
pub peer_id: PeerId,
|
||||
/// Keypair compartida (Arc para compartir con consumers que
|
||||
/// necesitan firmar mensajes con la misma identidad — p. ej.
|
||||
/// `card_handshake::network::connect_libp2p` que firma el
|
||||
/// Hello). NO se expone públicamente; usar [`Self::keypair`].
|
||||
keypair: Arc<Keypair>,
|
||||
cmd_tx: mpsc::UnboundedSender<Command>,
|
||||
listen_rx: Mutex<mpsc::UnboundedReceiver<Multiaddr>>,
|
||||
/// Control para abrir y aceptar streams. Cada protocolo
|
||||
/// (handshake brahman, sync minga, etc.) llama
|
||||
/// `control.accept(StreamProtocol::new("/foo/1.0.0"))` para
|
||||
/// recibir streams entrantes, o `control.open_stream(peer, proto)`
|
||||
/// para abrirlos. Multiplexado y demultiplexado lo hace libp2p.
|
||||
pub control: stream::Control,
|
||||
}
|
||||
|
||||
impl BrahmanNet {
|
||||
/// Crea un nodo con keypair Ed25519 generada al vuelo (peer_id
|
||||
/// efímero — cambia en cada arranque).
|
||||
pub fn new() -> Result<Self, NodeError> {
|
||||
Self::with_keypair(identity::Keypair::generate_ed25519())
|
||||
}
|
||||
|
||||
/// Crea un nodo con una keypair libp2p específica. Usá esto para
|
||||
/// `peer_id` estable (por ejemplo si tu identidad se persiste a
|
||||
/// disco, o si la derivás de la identidad criptográfica del
|
||||
/// módulo).
|
||||
///
|
||||
/// Sólo Ed25519 se soporta — la `keypair` se duplica internamente
|
||||
/// vía clone del `ed25519::Keypair` para que tanto el swarm
|
||||
/// (Noise auth) como el caller (firma de Cards) compartan la
|
||||
/// misma identidad sin la fricción de que `identity::Keypair` no
|
||||
/// implemente `Clone`.
|
||||
pub fn with_keypair(keypair: identity::Keypair) -> Result<Self, NodeError> {
|
||||
let ed_kp = keypair
|
||||
.try_into_ed25519()
|
||||
.map_err(|_| NodeError::Build("brahman-net sólo soporta keypairs Ed25519".into()))?;
|
||||
let kp_for_swarm = identity::Keypair::from(ed_kp.clone());
|
||||
let kp_for_storage = Arc::new(identity::Keypair::from(ed_kp));
|
||||
let peer_id = kp_for_swarm.public().to_peer_id();
|
||||
|
||||
let mut swarm: Swarm<BrahmanBehaviour> = SwarmBuilder::with_existing_identity(kp_for_swarm)
|
||||
.with_tokio()
|
||||
.with_tcp(
|
||||
tcp::Config::default(),
|
||||
noise::Config::new,
|
||||
yamux::Config::default,
|
||||
)
|
||||
.map_err(|e| NodeError::Build(format!("{e}")))?
|
||||
// Inyecta el transporte de relay-client: habilita marcar y
|
||||
// escuchar direcciones `…/p2p-circuit`. Provee el
|
||||
// `relay_client` behaviour al closure de abajo.
|
||||
.with_relay_client(noise::Config::new, yamux::Config::default)
|
||||
.map_err(|e| NodeError::Build(format!("{e}")))?
|
||||
.with_behaviour(|key, relay_client| {
|
||||
let local = key.public().to_peer_id();
|
||||
let mut kad =
|
||||
kad::Behaviour::new(local, kad::store::MemoryStore::new(local));
|
||||
// Modo Server: respondemos a queries del DHT. Auto
|
||||
// requiere detectar reachability; para entornos
|
||||
// controlados (localhost, redes privadas) Server es
|
||||
// lo correcto.
|
||||
kad.set_mode(Some(kad::Mode::Server));
|
||||
let identify = identify::Behaviour::new(
|
||||
identify::Config::new(IDENTIFY_PROTOCOL.to_string(), key.public())
|
||||
.with_agent_version(format!("brahman-net/{}", env!("CARGO_PKG_VERSION"))),
|
||||
);
|
||||
// mDNS resiliente: si el socket multicast no abre, seguimos sin
|
||||
// descubrimiento LAN en vez de fallar el nodo entero.
|
||||
let depurar = std::env::var("BRAHMAN_DEBUG").is_ok();
|
||||
let mdns = match mdns::tokio::Behaviour::new(mdns::Config::default(), local) {
|
||||
Ok(b) => {
|
||||
if depurar {
|
||||
eprintln!("brahman: mDNS ACTIVO (descubrimiento LAN)");
|
||||
}
|
||||
Toggle::from(Some(b))
|
||||
}
|
||||
Err(e) => {
|
||||
if depurar {
|
||||
eprintln!("brahman: mDNS DESACTIVADO ({e})");
|
||||
}
|
||||
Toggle::from(None)
|
||||
}
|
||||
};
|
||||
BrahmanBehaviour {
|
||||
block_list: allow_block_list::Behaviour::default(),
|
||||
stream: stream::Behaviour::new(),
|
||||
kad,
|
||||
identify,
|
||||
relay: relay::Behaviour::new(local, Default::default()),
|
||||
relay_client,
|
||||
dcutr: dcutr::Behaviour::new(local),
|
||||
mdns,
|
||||
autonat: autonat::Behaviour::new(
|
||||
local,
|
||||
autonat::Config {
|
||||
// Confirmamos también IPs privadas/loopback: la
|
||||
// malla Brahman vive tanto en LAN como en WAN, no
|
||||
// sólo en IPs globales (default `true` las
|
||||
// ignoraría y nada se confirmaría en LAN).
|
||||
only_global_ips: false,
|
||||
// Sondeo pronto tras arrancar (default ~15 s es
|
||||
// demasiado para el flujo de reservas de relay).
|
||||
boot_delay: Duration::from_secs(2),
|
||||
retry_interval: Duration::from_secs(5),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
}
|
||||
})
|
||||
.map_err(|e| NodeError::Build(format!("{e}")))?
|
||||
.with_swarm_config(|c| c.with_idle_connection_timeout(IDLE_CONNECTION_TIMEOUT))
|
||||
.build();
|
||||
|
||||
let control = swarm.behaviour().stream.new_control();
|
||||
|
||||
let (cmd_tx, mut cmd_rx) = mpsc::unbounded_channel::<Command>();
|
||||
let (listen_tx, listen_rx) = mpsc::unbounded_channel::<Multiaddr>();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut pending_finds: HashMap<
|
||||
kad::QueryId,
|
||||
oneshot::Sender<Vec<DiscoveredPeer>>,
|
||||
> = HashMap::new();
|
||||
let mut pending_providers: HashMap<
|
||||
kad::QueryId,
|
||||
(Vec<PeerId>, oneshot::Sender<Vec<PeerId>>),
|
||||
> = HashMap::new();
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
Some(cmd) = cmd_rx.recv() => {
|
||||
match cmd {
|
||||
Command::Dial(addr) => {
|
||||
let _ = swarm.dial(addr);
|
||||
}
|
||||
Command::Listen(addr) => {
|
||||
let _ = swarm.listen_on(addr);
|
||||
}
|
||||
Command::AddDhtPeer(peer, addr) => {
|
||||
swarm.behaviour_mut().kad.add_address(&peer, addr);
|
||||
}
|
||||
Command::FindClosestPeers(target, tx) => {
|
||||
let qid = swarm.behaviour_mut().kad.get_closest_peers(target);
|
||||
pending_finds.insert(qid, tx);
|
||||
}
|
||||
Command::StartProviding(key) => {
|
||||
// Best-effort: si falla (sin peers cercanos para
|
||||
// replicar), seguirá viviendo en el local store
|
||||
// y se servirá vía get_providers de quien tenga
|
||||
// conexión con nosotros.
|
||||
let _ = swarm.behaviour_mut().kad.start_providing(key.into());
|
||||
}
|
||||
Command::StopProviding(key) => {
|
||||
// Quitamos el record local del provider store.
|
||||
// Los peers cercanos eventualmente expiran su
|
||||
// copia replicada por TTL natural (~24h en
|
||||
// libp2p kad default); para retiro inmediato
|
||||
// habría que enviar un republish con sentinel,
|
||||
// pero kad no expone esa primitiva. Aceptable
|
||||
// para el caso "el provider local desapareció":
|
||||
// queries que pasen por nosotros dejan de
|
||||
// listarnos al instante.
|
||||
swarm.behaviour_mut().kad.stop_providing(&key.into());
|
||||
}
|
||||
Command::GetProviders(key, tx) => {
|
||||
let qid = swarm.behaviour_mut().kad.get_providers(key.into());
|
||||
pending_providers.insert(qid, (Vec::new(), tx));
|
||||
}
|
||||
Command::BlockPeer(peer) => {
|
||||
swarm.behaviour_mut().block_list.block_peer(peer);
|
||||
}
|
||||
Command::UnblockPeer(peer) => {
|
||||
swarm.behaviour_mut().block_list.unblock_peer(peer);
|
||||
}
|
||||
}
|
||||
}
|
||||
event = swarm.select_next_some() => {
|
||||
match event {
|
||||
SwarmEvent::NewListenAddr { address, .. } => {
|
||||
let _ = listen_tx.send(address);
|
||||
}
|
||||
// Identify nos dice las listen-addrs reales del
|
||||
// peer. Las inyectamos a Kad para poblar el
|
||||
// routing table sin necesidad de add_dht_peer
|
||||
// manual — la propagación pasa a ser automática.
|
||||
SwarmEvent::Behaviour(BrahmanBehaviourEvent::Identify(
|
||||
identify::Event::Received { peer_id, info, .. }
|
||||
)) => {
|
||||
// El observed_addr que nos reporta identify se
|
||||
// emite como CANDIDATO a externa; AutoNAT lo
|
||||
// prueba pidiendo dial-backs y, si es
|
||||
// alcanzable, dispara StatusChanged(Public) —
|
||||
// ahí recién lo confirmamos (abajo). Las
|
||||
// listen-addrs del peer pueblan la routing
|
||||
// table de Kad.
|
||||
for addr in info.listen_addrs {
|
||||
swarm.behaviour_mut().kad.add_address(&peer_id, addr);
|
||||
}
|
||||
}
|
||||
// mDNS descubrió pares en la LAN: inyectamos sus
|
||||
// direcciones a Kad (igual que identify). Con eso el
|
||||
// DHT y los streams funcionan sin bootstrap manual.
|
||||
SwarmEvent::Behaviour(BrahmanBehaviourEvent::Mdns(
|
||||
mdns::Event::Discovered(list)
|
||||
)) => {
|
||||
for (peer_id, addr) in list {
|
||||
if std::env::var("BRAHMAN_DEBUG").is_ok() {
|
||||
eprintln!("brahman: mDNS descubrió {peer_id} en {addr}");
|
||||
}
|
||||
swarm.behaviour_mut().kad.add_address(&peer_id, addr);
|
||||
}
|
||||
}
|
||||
// AutoNAT confirmó (vía dial-back de otros peers)
|
||||
// que somos alcanzables en `addr`: recién entonces
|
||||
// la anunciamos como externa — la usa el relay
|
||||
// server en las reservas y la ven los pares.
|
||||
SwarmEvent::Behaviour(BrahmanBehaviourEvent::Autonat(
|
||||
autonat::Event::StatusChanged { new: autonat::NatStatus::Public(addr), .. }
|
||||
)) => {
|
||||
swarm.add_external_address(addr);
|
||||
}
|
||||
SwarmEvent::Behaviour(BrahmanBehaviourEvent::Kad(
|
||||
kad::Event::OutboundQueryProgressed { id, result, step, .. }
|
||||
)) => {
|
||||
match result {
|
||||
kad::QueryResult::GetClosestPeers(Ok(ok)) if step.last => {
|
||||
if let Some(tx) = pending_finds.remove(&id) {
|
||||
let infos = ok.peers.into_iter()
|
||||
.map(|p| DiscoveredPeer {
|
||||
peer_id: p.peer_id,
|
||||
addrs: p.addrs,
|
||||
})
|
||||
.collect();
|
||||
let _ = tx.send(infos);
|
||||
}
|
||||
}
|
||||
kad::QueryResult::GetClosestPeers(Err(_)) if step.last => {
|
||||
if let Some(tx) = pending_finds.remove(&id) {
|
||||
let _ = tx.send(Vec::new());
|
||||
}
|
||||
}
|
||||
kad::QueryResult::GetProviders(Ok(ok)) => {
|
||||
if let Some((collected, _)) =
|
||||
pending_providers.get_mut(&id)
|
||||
{
|
||||
if let kad::GetProvidersOk::FoundProviders {
|
||||
providers, ..
|
||||
} = ok
|
||||
{
|
||||
for p in providers {
|
||||
if !collected.contains(&p) {
|
||||
collected.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if step.last {
|
||||
if let Some((providers, tx)) =
|
||||
pending_providers.remove(&id)
|
||||
{
|
||||
let _ = tx.send(providers);
|
||||
}
|
||||
}
|
||||
}
|
||||
kad::QueryResult::GetProviders(Err(_)) if step.last => {
|
||||
if let Some((providers, tx)) =
|
||||
pending_providers.remove(&id)
|
||||
{
|
||||
let _ = tx.send(providers);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
peer_id,
|
||||
keypair: kp_for_storage,
|
||||
cmd_tx,
|
||||
listen_rx: Mutex::new(listen_rx),
|
||||
control,
|
||||
})
|
||||
}
|
||||
|
||||
/// Acceso a la keypair de identidad del nodo. Usar para firmar
|
||||
/// payloads que viajan asociados al `peer_id` (handshake brahman
|
||||
/// firmado, futuros sub-protocolos con autenticación). El `Arc`
|
||||
/// permite compartir sin copia — la keypair libp2p no es `Clone`.
|
||||
pub fn keypair(&self) -> Arc<Keypair> {
|
||||
self.keypair.clone()
|
||||
}
|
||||
|
||||
/// Bloquea conexiones desde/hacia `peer` a nivel del swarm.
|
||||
/// Conexiones existentes se cierran y nuevos intentos son
|
||||
/// rechazados ANTES del Noise handshake — más eficiente que
|
||||
/// rechazar al nivel del handshake brahman (ahorra round-trip
|
||||
/// TCP+Noise por intento). Idempotente.
|
||||
pub fn block_peer(&self, peer: PeerId) {
|
||||
let _ = self.cmd_tx.send(Command::BlockPeer(peer));
|
||||
}
|
||||
|
||||
/// Quita a `peer` de la block-list del swarm. Conexiones futuras
|
||||
/// son aceptadas con normalidad. Idempotente.
|
||||
pub fn unblock_peer(&self, peer: PeerId) {
|
||||
let _ = self.cmd_tx.send(Command::UnblockPeer(peer));
|
||||
}
|
||||
|
||||
/// Empieza a escuchar en `addr`. Bloquea hasta que el listener
|
||||
/// publique su dirección real (Multiaddr resuelta — útil cuando
|
||||
/// pediste `/ip4/0.0.0.0/tcp/0` y querés saber qué puerto te tocó).
|
||||
pub async fn listen(&self, addr: Multiaddr) -> Multiaddr {
|
||||
self.cmd_tx
|
||||
.send(Command::Listen(addr))
|
||||
.expect("swarm task alive");
|
||||
let mut rx = self.listen_rx.lock().await;
|
||||
rx.recv().await.expect("listen address arrives")
|
||||
}
|
||||
|
||||
/// Inicia conexión con un peer en `addr`. No-op si ya hay
|
||||
/// conexión. Best-effort — fallos se loggean al swarm pero no se
|
||||
/// propagan al caller (consistente con libp2p).
|
||||
pub fn dial(&self, addr: Multiaddr) {
|
||||
let _ = self.cmd_tx.send(Command::Dial(addr));
|
||||
}
|
||||
|
||||
/// Añade un peer al routing table de Kademlia. Punto de entrada
|
||||
/// para bootstrap: tras esto, el nodo puede dirigir queries DHT
|
||||
/// a través de este peer.
|
||||
pub fn add_dht_peer(&self, peer: PeerId, addr: Multiaddr) {
|
||||
let _ = self.cmd_tx.send(Command::AddDhtPeer(peer, addr));
|
||||
}
|
||||
|
||||
/// Consulta el DHT por los peers más cercanos al `target` PeerId.
|
||||
/// Devuelve la lista resuelta (vacía si la query falla o si no
|
||||
/// hay peers conocidos). Bloquea hasta que la query completa.
|
||||
pub async fn find_closest_peers(&self, target: PeerId) -> Vec<DiscoveredPeer> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let _ = self
|
||||
.cmd_tx
|
||||
.send(Command::FindClosestPeers(target, tx));
|
||||
rx.await.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Anuncia en el DHT que este peer tiene el contenido identificado
|
||||
/// por `key`. Otros peers pueden luego descubrirlo vía
|
||||
/// [`find_providers`](Self::find_providers). Best-effort: si la
|
||||
/// replicación falla inicialmente, el record vive en el store
|
||||
/// local hasta que llegue conexión.
|
||||
pub fn start_providing(&self, key: &[u8]) {
|
||||
let _ = self.cmd_tx.send(Command::StartProviding(key.to_vec()));
|
||||
}
|
||||
|
||||
/// Retira el anuncio previo de [`start_providing`] para `key`.
|
||||
/// El record local se borra al instante (queries que lleguen a
|
||||
/// nosotros dejan de listarnos). Los records replicados en peers
|
||||
/// remotos viven hasta su TTL — kad no expone primitiva para
|
||||
/// retracción inmediata cross-peer. Aceptable: simétrico al
|
||||
/// caso "el provider apareció" (también propagación eventual).
|
||||
pub fn stop_providing(&self, key: &[u8]) {
|
||||
let _ = self.cmd_tx.send(Command::StopProviding(key.to_vec()));
|
||||
}
|
||||
|
||||
/// Consulta el DHT por peers que han anunciado proveer `key`.
|
||||
/// Devuelve la lista de `PeerId`s que se reportan como providers.
|
||||
/// Lista vacía si nadie anuncia.
|
||||
pub async fn find_providers(&self, key: &[u8]) -> Vec<PeerId> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let _ = self
|
||||
.cmd_tx
|
||||
.send(Command::GetProviders(key.to_vec(), tx));
|
||||
rx.await.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "card-wit"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
description = "Brahman — extractor opcional: parsea contratos WIT y devuelve `WitInterface` listo para acoplar a una `Card`."
|
||||
|
||||
[dependencies]
|
||||
card-core = { path = "../card-core" }
|
||||
wit-parser = "0.230"
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[[example]]
|
||||
name = "brahman-wit-info"
|
||||
path = "examples/brahman-wit-info.rs"
|
||||
@@ -0,0 +1,34 @@
|
||||
# brahman-card-wit
|
||||
|
||||
> **DORMIDO (2026-05-30).** Capa 3 de Brahman. Ver `/BRAHMAN.md`.
|
||||
|
||||
Parser opcional de contratos WIT (`.wit` texto → `Vec<card_core::WitInterface>`, uno por `world`),
|
||||
sobre `wit-parser` (sin `wasm-tools`/`wit-component`).
|
||||
|
||||
## Estado: relegado, no borrado
|
||||
|
||||
La visión original de Brahman incluía **módulos agnósticos descritos por interfaz WIT** (eventualmente WASM).
|
||||
Esa capa **nunca se ejecutó**:
|
||||
|
||||
- No existe ningún archivo `.wit` en el workspace.
|
||||
- Ningún crate de producción depende de este crate — sólo `examples/brahman-wit-info.rs` y la
|
||||
**dev-dependency** de `card-sidecar`.
|
||||
|
||||
Se conserva (funciona, 210 LOC, reversible) por si algún día aparecen `.wit` reales. **No asumir que está
|
||||
en ninguna ruta de build.**
|
||||
|
||||
## El contrato agnóstico real y vigente
|
||||
|
||||
```
|
||||
shared/card (formato Card) + card-handshake (handshake nativo Rust) + DhtKey (namespacing en la DHT)
|
||||
```
|
||||
|
||||
El tipo de metadata `WitInterface` vive en **`card-core`** (no aquí) y **sí** lo usa el broker
|
||||
(`chasqui-broker`) para matching estructural — es metadata opcional viva. Lo único dormido es *este parser*
|
||||
de archivos `.wit` inexistentes.
|
||||
|
||||
## Si se decide revivir WIT
|
||||
|
||||
Requeriría: (1) que el Init lea `<modulo>/wit/protocol.wit` en el descubrimiento y construya
|
||||
`ResolvedCard::from_conscious(card, wit)`; (2) que algún módulo de producción publique un `.wit`.
|
||||
Hasta entonces, este crate es tooling latente (`cargo run -p card-wit --example brahman-wit-info -- <archivo.wit>`).
|
||||
@@ -0,0 +1,45 @@
|
||||
//! `brahman-wit-info` — inspecciona un archivo WIT y lista sus worlds.
|
||||
//!
|
||||
//! Uso:
|
||||
//! ```sh
|
||||
//! cargo run -p brahman-card-wit --example brahman-wit-info -- shared_wit/protocol.wit
|
||||
//! ```
|
||||
|
||||
use std::process::ExitCode;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let path = match std::env::args().nth(1) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
eprintln!("uso: brahman-wit-info <ruta.wit>");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
|
||||
let worlds = match card_wit::parse_wit_file(&path) {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
eprintln!("error parseando {path}: {e}");
|
||||
return ExitCode::from(1);
|
||||
}
|
||||
};
|
||||
|
||||
if worlds.is_empty() {
|
||||
println!("(ningún world declarado)");
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
println!("{} world(s):", worlds.len());
|
||||
for w in &worlds {
|
||||
println!();
|
||||
println!(" package: {}", w.package);
|
||||
println!(" world: {}", w.world);
|
||||
if !w.imports.is_empty() {
|
||||
println!(" imports: {}", w.imports.join(", "));
|
||||
}
|
||||
if !w.exports.is_empty() {
|
||||
println!(" exports: {}", w.exports.join(", "));
|
||||
}
|
||||
}
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
//! `brahman-card-wit` — extractor de contratos WIT.
|
||||
//!
|
||||
//! **DORMIDO (2026-05-30).** No existe ningún archivo `.wit` en el workspace
|
||||
//! y ningún crate de producción depende de éste (sólo `examples/` y la
|
||||
//! dev-dependency de `card-sidecar`). Es la "Capa 3" de Brahman (ver `/BRAHMAN.md`):
|
||||
//! la idea de módulos agnósticos descritos por interfaz WIT nunca se ejecutó.
|
||||
//! El **contrato agnóstico real y vigente** es `shared/card` (formato `Card`) +
|
||||
//! el handshake nativo Rust de `card-handshake` + el namespacing de `DhtKey`.
|
||||
//! Se conserva como herramienta opcional por si algún día aparecen `.wit`
|
||||
//! reales; no asumir que está en ninguna ruta de build.
|
||||
//!
|
||||
//! Nota: el tipo de metadata [`WitInterface`] vive en `card-core` y SÍ lo usa
|
||||
//! el broker para matching estructural — eso es metadata opcional viva. Lo
|
||||
//! dormido es este *parser* de archivos `.wit` inexistentes.
|
||||
//!
|
||||
//! Crate **opcional** (no es dep de `brahman-card`). Parsea texto WIT
|
||||
//! mediante [`wit-parser`] y devuelve una lista de [`WitInterface`]
|
||||
//! (uno por `world`) lista para acoplarse a una [`card_core::Card`]
|
||||
//! cuando se construye una [`card_core::ResolvedCard`].
|
||||
//!
|
||||
//! Casos de uso (hipotéticos hasta que existan `.wit`):
|
||||
//!
|
||||
//! - El Init lee `<modulo>/wit/protocol.wit` durante el descubrimiento
|
||||
//! y lo combina con la Card del módulo para obtener una
|
||||
//! `ResolvedCard::from_conscious(card, wit)`.
|
||||
//! - Tooling (`brahman-wit-info`) inspecciona un `.wit` y muestra
|
||||
//! sus mundos, exports e imports.
|
||||
//!
|
||||
//! No depende de `wasm-tools`/`wit-component` — sólo del parser texto.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(rust_2018_idioms)]
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use card_core::WitInterface;
|
||||
use thiserror::Error;
|
||||
use wit_parser::{Resolve, WorldKey};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum WitError {
|
||||
#[error("parse: {0}")]
|
||||
Parse(String),
|
||||
#[error("E/S: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
/// Parsea WIT desde una string. Devuelve un `WitInterface` por cada
|
||||
/// `world` declarado.
|
||||
pub fn parse_wit(source: &str) -> Result<Vec<WitInterface>, WitError> {
|
||||
parse_with_path(source, Path::new("inline.wit"))
|
||||
}
|
||||
|
||||
/// Parsea WIT desde un archivo. Útil para `module/wit/protocol.wit`.
|
||||
pub fn parse_wit_file(path: impl AsRef<Path>) -> Result<Vec<WitInterface>, WitError> {
|
||||
let p = path.as_ref();
|
||||
let source = std::fs::read_to_string(p)?;
|
||||
parse_with_path(&source, p)
|
||||
}
|
||||
|
||||
fn parse_with_path(source: &str, path: &Path) -> Result<Vec<WitInterface>, WitError> {
|
||||
let mut resolve = Resolve::new();
|
||||
let path_buf: PathBuf = path.to_path_buf();
|
||||
resolve
|
||||
.push_str(&path_buf, source)
|
||||
.map_err(|e| WitError::Parse(e.to_string()))?;
|
||||
|
||||
let mut out = Vec::new();
|
||||
for (_pkg_id, pkg) in resolve.packages.iter() {
|
||||
let pkg_name = pkg.name.to_string();
|
||||
for (_name, &world_id) in &pkg.worlds {
|
||||
let world = &resolve.worlds[world_id];
|
||||
let exports = collect_keys(world.exports.iter().map(|(k, _)| k), &resolve);
|
||||
let imports = collect_keys(world.imports.iter().map(|(k, _)| k), &resolve);
|
||||
out.push(WitInterface {
|
||||
package: pkg_name.clone(),
|
||||
world: world.name.clone(),
|
||||
exports,
|
||||
imports,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn collect_keys<'a, I>(keys: I, resolve: &Resolve) -> Vec<String>
|
||||
where
|
||||
I: Iterator<Item = &'a WorldKey>,
|
||||
{
|
||||
keys.map(|k| match k {
|
||||
WorldKey::Name(n) => n.clone(),
|
||||
WorldKey::Interface(id) => resolve.interfaces[*id]
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("<interface#{}>", id.index())),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const SAMPLE: &str = r#"
|
||||
package brahman:test@0.1.0;
|
||||
|
||||
interface handshake {
|
||||
hello: func() -> result<_, string>;
|
||||
}
|
||||
|
||||
interface lifecycle {
|
||||
report: func();
|
||||
}
|
||||
|
||||
world module {
|
||||
import handshake;
|
||||
import lifecycle;
|
||||
export run: func() -> result<_, string>;
|
||||
}
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn parses_inline_wit() {
|
||||
let worlds = parse_wit(SAMPLE).unwrap();
|
||||
assert_eq!(worlds.len(), 1, "esperaba un único world");
|
||||
let w = &worlds[0];
|
||||
assert!(w.package.starts_with("brahman:test"));
|
||||
assert_eq!(w.world, "module");
|
||||
assert!(
|
||||
w.imports.iter().any(|i| i == "handshake"),
|
||||
"imports={:?}",
|
||||
w.imports
|
||||
);
|
||||
assert!(
|
||||
w.imports.iter().any(|i| i == "lifecycle"),
|
||||
"imports={:?}",
|
||||
w.imports
|
||||
);
|
||||
assert!(
|
||||
w.exports.iter().any(|e| e == "run"),
|
||||
"exports={:?}",
|
||||
w.exports
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_shared_protocol() {
|
||||
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../../shared_wit/protocol.wit");
|
||||
let worlds = parse_wit_file(path).unwrap();
|
||||
assert!(
|
||||
worlds.iter().any(|w| w.world == "module"),
|
||||
"no encontró world 'module' en {:?}",
|
||||
worlds.iter().map(|w| &w.world).collect::<Vec<_>>()
|
||||
);
|
||||
assert!(
|
||||
worlds.iter().any(|w| w.world == "admin-host"),
|
||||
"no encontró world 'admin-host'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_error_on_garbage() {
|
||||
let bad = "this is not wit at all { } } ;;;;";
|
||||
assert!(matches!(parse_wit(bad), Err(WitError::Parse(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_world_handled() {
|
||||
let src = r#"
|
||||
package brahman:empty@0.1.0;
|
||||
world hollow {}
|
||||
"#;
|
||||
let worlds = parse_wit(src).unwrap();
|
||||
assert_eq!(worlds.len(), 1);
|
||||
assert!(worlds[0].exports.is_empty());
|
||||
assert!(worlds[0].imports.is_empty());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user