Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ members = [
"thrum-core",
"thrumd",
"hum-paths",
"hum-identity",
"hum-thrum",
"hum-mcp",
"ids",
"config",
"codegen",
Expand Down
173 changes: 4 additions & 169 deletions ensemble/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,7 @@ use anyhow::Result;
use async_trait::async_trait;
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use parking_lot::RwLock;
use rand::RngCore;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use tokio::sync::{broadcast, mpsc};
use tokio::task::JoinSet;

Expand Down Expand Up @@ -89,173 +87,10 @@ const HANDSHAKE_SKEW_MS: i64 = 60_000;
pub type Tone = serde_json::Value;

// ── Identity ───────────────────────────────────────────────────────────────

/// Universal identity primitive for everything addressable on the
/// hum wire: humds, worker bees, forager bees, future kinds. 32-byte
/// SHA-256 of a public key (Ed25519); wire form is `<prefix>_<hex>`
/// where prefix discriminates the role.
///
/// Short form keeps the prefix: `humd_a4f2b8c19d3e` (12 hex chars
/// after the underscore). Long form is the full 64-hex tail. Both
/// parse via [`Hid::from_str`].
///
/// Prefixes locked in v0:
/// - `humd_` — daemon
/// - `wbee_` — worker bee
/// - `fbee_` — forager bee
///
/// Stable per install (each binary persists its own key). The hex is
/// content-addressable — no registry, no central naming.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Hid {
pub prefix: HidPrefix,
pub bytes: [u8; 32],
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HidPrefix {
Humd,
Wbee,
Fbee,
}

impl HidPrefix {
pub fn as_str(self) -> &'static str {
match self {
HidPrefix::Humd => "humd",
HidPrefix::Wbee => "wbee",
HidPrefix::Fbee => "fbee",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"humd" => Some(HidPrefix::Humd),
"wbee" => Some(HidPrefix::Wbee),
"fbee" => Some(HidPrefix::Fbee),
_ => None,
}
}
}

impl Hid {
/// Hash a pubkey to its content-addressable bytes; tag with the
/// role prefix.
pub fn from_pubkey(prefix: HidPrefix, pubkey: &[u8]) -> Self {
let mut h = Sha256::new();
h.update(pubkey);
let digest = h.finalize();
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&digest[..32]);
Self { prefix, bytes }
}

/// Mint a random hid for the given role. Tests / pre-crypto only.
pub fn random(prefix: HidPrefix) -> Self {
let mut bytes = [0u8; 32];
rand::thread_rng().fill_bytes(&mut bytes);
Self { prefix, bytes }
}

/// Convenience: random hid tagged as a humd. Matches the legacy
/// `Hid::random_humd()` shape; preferred call sites use
/// [`Hid::random`] directly.
pub fn random_humd() -> Self { Self::random(HidPrefix::Humd) }

pub fn as_bytes(&self) -> &[u8; 32] { &self.bytes }

/// Full wire form: `<prefix>_<64 hex>`.
pub fn to_hex(&self) -> String {
format!("{}_{}", self.prefix.as_str(), hex::encode(self.bytes))
}

/// Log-friendly short form: `<prefix>_<12 hex>`.
pub fn short(&self) -> String {
format!("{}_{}", self.prefix.as_str(), hex::encode(&self.bytes[..6]))
}

/// Parse `<prefix>_<hex>`. Accepts either the 12-char short or
/// 64-char full form. Also accepts a bare 64-hex string as a
/// humd-prefixed legacy value so old peers.json keeps loading.
pub fn from_hex(s: &str) -> Result<Self, HidParseError> {
if let Some((p, h)) = s.split_once('_') {
let prefix = HidPrefix::parse(p).ok_or(HidParseError::UnknownPrefix)?;
let tail = hex::decode(h).map_err(|_| HidParseError::BadHex)?;
if tail.len() == 32 {
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&tail);
return Ok(Hid { prefix, bytes });
}
// 12-char short form: 6 bytes; widen with zeros so the
// short can still round-trip through serializers (used in
// logs + sigils, not for security-bearing addresses).
if tail.len() == 6 {
let mut bytes = [0u8; 32];
bytes[..6].copy_from_slice(&tail);
return Ok(Hid { prefix, bytes });
}
return Err(HidParseError::WrongLength(tail.len()));
}
// Legacy fallback: bare 64-hex was the daemon's old wire form.
// Auto-prefix as `humd_` so older peers.json keeps parsing.
let bytes_vec = hex::decode(s).map_err(|_| HidParseError::BadHex)?;
if bytes_vec.len() != 32 {
return Err(HidParseError::WrongLength(bytes_vec.len()));
}
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&bytes_vec);
Ok(Hid { prefix: HidPrefix::Humd, bytes })
}
}

#[derive(Debug)]
pub enum HidParseError {
UnknownPrefix,
BadHex,
WrongLength(usize),
}

impl fmt::Display for HidParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HidParseError::UnknownPrefix => write!(f, "unknown hid prefix"),
HidParseError::BadHex => write!(f, "bad hex"),
HidParseError::WrongLength(n) => write!(f, "wrong hid length ({n} bytes)"),
}
}
}

impl std::error::Error for HidParseError {}

impl fmt::Display for Hid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_hex())
}
}

impl Serialize for Hid {
fn serialize<S>(&self, ser: S) -> Result<S::Ok, S::Error>
where S: serde::Serializer {
ser.serialize_str(&self.to_hex())
}
}

impl<'de> Deserialize<'de> for Hid {
fn deserialize<D>(de: D) -> Result<Self, D::Error>
where D: serde::Deserializer<'de> {
let s = String::deserialize(de)?;
Hid::from_hex(&s).map_err(serde::de::Error::custom)
}
}

impl From<[u8; 32]> for Hid {
/// Legacy: bare 32-byte construction defaults to `humd_` prefix.
/// Migrate to `Hid { prefix, bytes }` directly when the role is
/// known.
fn from(bytes: [u8; 32]) -> Self {
Self { prefix: HidPrefix::Humd, bytes }
}
}

/// Content-addressable identity, moved to `ids`. Re-exported here for
/// back-compat so existing `ensemble::Hid` / `ensemble::HidPrefix` call
/// sites keep compiling.
pub use ids::{Hid, HidPrefix, HidParseError};
/// Ed25519 signing key for a humd. The pubkey's SHA-256 is the
/// [`Hid`] — identity is content-addressable, no separate registry.
///
Expand Down
3 changes: 3 additions & 0 deletions hives/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ description = "Shared building blocks for nests — regex Classifier for the dro

[dependencies]
hum-paths = { path = "../../hum-paths" }
hum-identity = { path = "../../hum-identity" }
hum-thrum = { path = "../../hum-thrum" }
hum-mcp = { path = "../../hum-mcp" }
drone = { path = "../../drone" }
ensemble = { path = "../../ensemble" }
mcp = { path = "../../mcp" }
Expand Down
Loading