machined: MachineSigner — server signs as the machine without holding the key
All machine-seed consumers now resolve identity machined-first with a loud legacy fallback, and none of them generate. Federation envelopes are signed through MachineSigner with domain separation (souveraine-machined:v1:federation-envelope:...) so the wire format is identical whichever tier holds the key; verify reconstructs the same frame. Unsignable events are dropped loudly, never sent unsigned. identity show/sign are load-only now — reading an identity must not mint one. Round-trip + tamper tests on the envelope.
This commit is contained in:
parent
5039163a52
commit
116562a615
10 changed files with 238 additions and 67 deletions
|
|
@ -14,3 +14,4 @@
|
|||
|
||||
pub mod client;
|
||||
pub mod protocol;
|
||||
pub mod signer;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ pub const MAX_REQUEST_BYTES: u64 = 64 * 1024;
|
|||
/// Version tag baked into every signature's domain separation.
|
||||
pub const SIGNING_CONTEXT: &str = "souveraine-machined:v1";
|
||||
|
||||
/// Domain for federation transport envelopes (`SignedEvent`).
|
||||
pub const DOMAIN_FEDERATION_ENVELOPE: &str = "federation-envelope";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "op", rename_all = "snake_case")]
|
||||
pub enum Request {
|
||||
|
|
|
|||
102
src/machined/signer.rs
Normal file
102
src/machined/signer.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
//! MachineSigner — how server code signs as the machine without knowing
|
||||
//! which tier holds the key.
|
||||
//!
|
||||
//! Daemon mode asks souveraine-machined over its socket; the private key
|
||||
//! never enters this process. Legacy mode holds the old user-tier seed
|
||||
//! directly — transitional, loud at resolve time, and it signs the exact
|
||||
//! same domain-separated bytes the daemon does, so the wire format is
|
||||
//! identical either way. Verifiers reconstruct
|
||||
//! `protocol::signing_bytes(domain, payload)` and never care where the key
|
||||
//! lived.
|
||||
//!
|
||||
//! Neither mode generates identity. A machine with no seed is an error with
|
||||
//! provisioning instructions, not a fresh key.
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use super::{client, protocol};
|
||||
use crate::core::identity::SeedId;
|
||||
|
||||
pub enum MachineSigner {
|
||||
/// souveraine-machined answers on its socket; we hold only the pubkey.
|
||||
Daemon { pubkey_hex: String },
|
||||
/// Transitional: the legacy user-tier seed, held in-process.
|
||||
Legacy(Arc<SeedId>),
|
||||
}
|
||||
|
||||
impl MachineSigner {
|
||||
/// Resolve the machine identity: system tier first, legacy user seed as
|
||||
/// a loud fallback. Blocking (one local socket round-trip / file read) —
|
||||
/// call at startup, not per-event.
|
||||
pub fn resolve(base: &Path) -> Result<Self> {
|
||||
match client::pubkey() {
|
||||
Ok((pubkey_hex, glyph)) => {
|
||||
info!("machine identity via souveraine-machined: {glyph} ({pubkey_hex})");
|
||||
return Ok(Self::Daemon { pubkey_hex });
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"souveraine-machined unavailable ({e:#}); falling back to legacy \
|
||||
user-tier machine seed — provision the system tier with \
|
||||
`sudo souveraine machine init`"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let legacy_dir = SeedId::default_dir(base);
|
||||
let seed = SeedId::load(&legacy_dir).with_context(|| {
|
||||
format!(
|
||||
"no machine identity: souveraine-machined is not running and no legacy \
|
||||
seed exists at {} — provision one with `sudo souveraine machine init --fresh`",
|
||||
legacy_dir.display()
|
||||
)
|
||||
})?;
|
||||
Ok(Self::Legacy(Arc::new(seed)))
|
||||
}
|
||||
|
||||
pub fn pubkey_hex(&self) -> String {
|
||||
match self {
|
||||
Self::Daemon { pubkey_hex } => pubkey_hex.clone(),
|
||||
Self::Legacy(seed) => seed.public_key_hex(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Hex Ed25519 signature over `protocol::signing_bytes(domain, payload)`.
|
||||
/// Daemon mode is a blocking local socket round-trip (microseconds); a
|
||||
/// dead daemon mid-run is an error the caller must treat as fail-closed —
|
||||
/// never send unsigned.
|
||||
pub fn sign(&self, domain: &str, payload: &[u8]) -> Result<String> {
|
||||
match self {
|
||||
Self::Daemon { .. } => client::sign(domain, payload),
|
||||
Self::Legacy(seed) => {
|
||||
let bytes = protocol::signing_bytes(domain, payload);
|
||||
Ok(hex::encode(seed.sign(&bytes).to_bytes()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn legacy_signature_covers_domain_separated_bytes() {
|
||||
let seed = SeedId::generate();
|
||||
let pubkey = seed.public_key_bytes();
|
||||
let signer = MachineSigner::Legacy(Arc::new(seed));
|
||||
|
||||
let sig_hex = signer.sign("test-domain", b"payload").unwrap();
|
||||
let sig_bytes: [u8; 64] = hex::decode(sig_hex).unwrap().try_into().unwrap();
|
||||
let signature = ed25519_dalek::Signature::from_bytes(&sig_bytes);
|
||||
|
||||
let framed = protocol::signing_bytes("test-domain", b"payload");
|
||||
assert!(SeedId::verify_with_pubkey(&pubkey, &framed, &signature));
|
||||
// Raw payload must NOT verify — domain separation is load-bearing.
|
||||
assert!(!SeedId::verify_with_pubkey(&pubkey, b"payload", &signature));
|
||||
}
|
||||
}
|
||||
|
|
@ -629,7 +629,8 @@ async fn run_identity(
|
|||
|
||||
match action {
|
||||
IdentityAction::Show => {
|
||||
let seed = SeedId::load_or_generate(&seed_dir)?;
|
||||
// load, never generate: showing an identity must not mint one.
|
||||
let seed = SeedId::load(&seed_dir)?;
|
||||
if json {
|
||||
let mut obj = serde_json::json!({
|
||||
"public_key": seed.public_key_hex(),
|
||||
|
|
@ -661,7 +662,9 @@ async fn run_identity(
|
|||
println!("Generated seed identity: {} (glyph: {})", seed.public_key_hex(), seed.glyph());
|
||||
}
|
||||
IdentityAction::Sign { message } => {
|
||||
let seed = SeedId::load_or_generate(&seed_dir)?;
|
||||
// load, never generate: signing with a key that didn't exist a
|
||||
// moment ago proves nothing.
|
||||
let seed = SeedId::load(&seed_dir)?;
|
||||
let sig = seed.sign(message.as_bytes());
|
||||
let sig_hex = hex::encode(sig.to_bytes());
|
||||
if json {
|
||||
|
|
|
|||
|
|
@ -56,15 +56,15 @@ impl AgentInventory {
|
|||
}
|
||||
|
||||
/// Load the instance-level seed identity. This is the owner identity for
|
||||
/// all agents created by this Souveraine instance. Falls back to None
|
||||
/// silently — agents created without an owner can still be managed via
|
||||
/// per-agent tokens.
|
||||
/// all agents created by this Souveraine instance. Falls back to None —
|
||||
/// agents created without an owner can still be managed via per-agent
|
||||
/// tokens. Resolution goes machined-first with a loud legacy fallback and
|
||||
/// never generates.
|
||||
fn load_instance_seed_id(souveraine_root: &Path) -> Option<String> {
|
||||
let seed_dir = crate::core::identity::SeedId::default_dir(souveraine_root);
|
||||
match crate::core::identity::SeedId::load_or_generate(&seed_dir) {
|
||||
Ok(seed) => Some(seed.public_key_hex()),
|
||||
match crate::machined::client::machine_pubkey_with_fallback(souveraine_root) {
|
||||
Ok((pubkey, _source)) => Some(pubkey),
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "instance seed not available — agent ownership disabled");
|
||||
tracing::warn!("machine identity not available — agent ownership disabled: {e:#}");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ use futures::SinkExt;
|
|||
use tokio_tungstenite::tungstenite::Message;
|
||||
|
||||
use crate::core::config::{FederationRole, PeerConfig};
|
||||
use crate::core::identity::SeedId;
|
||||
use crate::core::nervous::{EventBus, SensorEvent};
|
||||
use crate::machined::signer::MachineSigner;
|
||||
|
||||
use super::types::SignedEvent;
|
||||
|
||||
|
|
@ -19,16 +19,16 @@ use super::types::SignedEvent;
|
|||
/// they arrive on this instance's own `/v1/federation/events` endpoint.
|
||||
pub struct FederationBridge {
|
||||
event_bus: EventBus,
|
||||
seed: Arc<SeedId>,
|
||||
signer: Arc<MachineSigner>,
|
||||
role: FederationRole,
|
||||
peers: Vec<PeerConfig>,
|
||||
}
|
||||
|
||||
impl FederationBridge {
|
||||
pub fn new(event_bus: EventBus, seed: Arc<SeedId>, role: FederationRole) -> Self {
|
||||
pub fn new(event_bus: EventBus, signer: Arc<MachineSigner>, role: FederationRole) -> Self {
|
||||
Self {
|
||||
event_bus,
|
||||
seed,
|
||||
signer,
|
||||
role,
|
||||
peers: Vec::new(),
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ impl FederationBridge {
|
|||
tokio::spawn(peer_outbound_task(
|
||||
peer,
|
||||
self.event_bus.clone(),
|
||||
self.seed.clone(),
|
||||
self.signer.clone(),
|
||||
self.role,
|
||||
));
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ impl FederationBridge {
|
|||
async fn peer_outbound_task(
|
||||
peer: PeerConfig,
|
||||
event_bus: EventBus,
|
||||
seed: Arc<SeedId>,
|
||||
signer: Arc<MachineSigner>,
|
||||
role: FederationRole,
|
||||
) {
|
||||
let endpoint = federation_endpoint(&peer.url);
|
||||
|
|
@ -80,15 +80,24 @@ async fn peer_outbound_task(
|
|||
payload: Some(serde_json::json!({
|
||||
"federation_url": endpoint,
|
||||
"label": None::<String>,
|
||||
"pubkey": seed.public_key_hex(),
|
||||
"pubkey": signer.pubkey_hex(),
|
||||
"role": role.as_str(),
|
||||
})),
|
||||
seed_id: None,
|
||||
reply_to: None,
|
||||
};
|
||||
let signed = SignedEvent::sign(&announce, &seed);
|
||||
if let Ok(json) = serde_json::to_string(&signed) {
|
||||
let _ = ws.send(Message::Text(json)).await;
|
||||
// Fail-closed: an unsignable announce is not sent. The peer
|
||||
// would reject an unsigned frame anyway; say why here.
|
||||
match SignedEvent::sign(&announce, &signer) {
|
||||
Ok(signed) => {
|
||||
if let Ok(json) = serde_json::to_string(&signed) {
|
||||
let _ = ws.send(Message::Text(json)).await;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(peer = %endpoint, error = %e,
|
||||
"federation: cannot sign announce — machine signer unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
let mut rx = event_bus.subscribe();
|
||||
|
|
@ -109,7 +118,14 @@ async fn peer_outbound_task(
|
|||
{
|
||||
continue;
|
||||
}
|
||||
let signed = SignedEvent::sign(&event, &seed);
|
||||
let signed = match SignedEvent::sign(&event, &signer) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
tracing::error!(peer = %endpoint, error = %e,
|
||||
"federation: cannot sign event — dropped, never sent unsigned");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let json = match serde_json::to_string(&signed) {
|
||||
Ok(j) => j,
|
||||
Err(_) => continue,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ use serde::{Deserialize, Serialize};
|
|||
use crate::core::config::PeerConfig;
|
||||
use crate::core::identity::SeedId;
|
||||
use crate::core::nervous::SensorEvent;
|
||||
use crate::machined::protocol::{signing_bytes, DOMAIN_FEDERATION_ENVELOPE};
|
||||
use crate::machined::signer::MachineSigner;
|
||||
|
||||
/// Return whether an inbound transport signer is explicitly trusted by this
|
||||
/// instance's federation configuration. A valid signature only establishes
|
||||
|
|
@ -17,12 +19,15 @@ pub fn signer_is_trusted(peers: &[PeerConfig], signer_pubkey_hex: &str) -> bool
|
|||
}
|
||||
|
||||
/// A [`SensorEvent`] carried between instances, Ed25519-signed by its origin.
|
||||
/// The signature covers the canonical JSON of `event` as it was at the
|
||||
/// sender — verified before the event is allowed onto the local bus.
|
||||
/// The signature covers the domain-separated frame
|
||||
/// `signing_bytes(DOMAIN_FEDERATION_ENVELOPE, canonical_json(event))` — so an
|
||||
/// envelope signature can never be replayed as any other payload class the
|
||||
/// machine key signs, and the wire format is identical whether the key lives
|
||||
/// in souveraine-machined or a legacy user-tier seed.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SignedEvent {
|
||||
pub event: SensorEvent,
|
||||
/// Hex-encoded Ed25519 signature over `serde_json::to_vec(&event)`.
|
||||
/// Hex-encoded Ed25519 signature over the domain-separated frame.
|
||||
pub signature_hex: String,
|
||||
/// Hex-encoded Ed25519 public key of the signing instance.
|
||||
pub signer_pubkey_hex: String,
|
||||
|
|
@ -30,14 +35,16 @@ pub struct SignedEvent {
|
|||
|
||||
impl SignedEvent {
|
||||
/// Wrap and sign a locally-originated event for transmission to a peer.
|
||||
pub fn sign(event: &SensorEvent, seed: &SeedId) -> Self {
|
||||
let bytes = serde_json::to_vec(event).unwrap_or_default();
|
||||
let signature = seed.sign(&bytes);
|
||||
Self {
|
||||
/// Fails if the signer cannot produce a signature (daemon died mid-run);
|
||||
/// callers must treat that as fail-closed — never send unsigned.
|
||||
pub fn sign(event: &SensorEvent, signer: &MachineSigner) -> anyhow::Result<Self> {
|
||||
let canonical = serde_json::to_vec(event)?;
|
||||
let signature_hex = signer.sign(DOMAIN_FEDERATION_ENVELOPE, &canonical)?;
|
||||
Ok(Self {
|
||||
event: event.clone(),
|
||||
signature_hex: hex::encode(signature.to_bytes()),
|
||||
signer_pubkey_hex: seed.public_key_hex(),
|
||||
}
|
||||
signature_hex,
|
||||
signer_pubkey_hex: signer.pubkey_hex(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Verify the signature. On success, returns the inner event with its
|
||||
|
|
@ -49,8 +56,9 @@ impl SignedEvent {
|
|||
let sig_bytes = decode_array::<64>(&self.signature_hex)?;
|
||||
let signature = ed25519_dalek::Signature::from_bytes(&sig_bytes);
|
||||
let canonical = serde_json::to_vec(&self.event).ok()?;
|
||||
let framed = signing_bytes(DOMAIN_FEDERATION_ENVELOPE, &canonical);
|
||||
|
||||
if SeedId::verify_with_pubkey(&pubkey, &canonical, &signature) {
|
||||
if SeedId::verify_with_pubkey(&pubkey, &framed, &signature) {
|
||||
let mut event = self.event.clone();
|
||||
event.seed_id = Some(self.signer_pubkey_hex.clone());
|
||||
Some(event)
|
||||
|
|
@ -68,8 +76,37 @@ fn decode_array<const N: usize>(hex_str: &str) -> Option<[u8; N]> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::signer_is_trusted;
|
||||
use super::{signer_is_trusted, MachineSigner, SignedEvent};
|
||||
use crate::core::config::PeerConfig;
|
||||
use crate::core::identity::SeedId;
|
||||
use crate::core::nervous::SensorEvent;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn envelope_round_trips_and_stamps_signer() {
|
||||
let seed = SeedId::generate();
|
||||
let pubkey_hex = seed.public_key_hex();
|
||||
let signer = MachineSigner::Legacy(Arc::new(seed));
|
||||
let event = SensorEvent {
|
||||
sensor_name: "test".into(),
|
||||
timestamp: chrono::Utc::now(),
|
||||
event_type: "unit".into(),
|
||||
target: None,
|
||||
urgency: 0.1,
|
||||
payload: None,
|
||||
seed_id: None,
|
||||
reply_to: None,
|
||||
};
|
||||
|
||||
let envelope = SignedEvent::sign(&event, &signer).unwrap();
|
||||
let verified = envelope.verify().expect("envelope must verify");
|
||||
assert_eq!(verified.seed_id.as_deref(), Some(pubkey_hex.as_str()));
|
||||
|
||||
// Tampering with the event breaks the signature.
|
||||
let mut forged = envelope.clone();
|
||||
forged.event.event_type = "forged".into();
|
||||
assert!(forged.verify().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_peer_key_is_required_for_trust() {
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ use axum::{
|
|||
};
|
||||
|
||||
use crate::core::config::FederationConfig;
|
||||
use crate::core::identity::{verify_summon, SeedId};
|
||||
use crate::core::identity::verify_summon;
|
||||
use crate::core::nervous::{EventBus, SensorEvent};
|
||||
use crate::machined::signer::MachineSigner;
|
||||
use crate::server::federation::{FederationBridge, SignedEvent};
|
||||
|
||||
/// Shared state for the lite route set — only what the minimal handlers and
|
||||
|
|
@ -61,8 +62,10 @@ impl LiteListener {
|
|||
/// bridge to configured peers, and spawn the summon-wake watcher.
|
||||
pub fn new(config: &FederationConfig, base: PathBuf) -> anyhow::Result<Arc<Self>> {
|
||||
let event_bus = EventBus::default();
|
||||
let seed = SeedId::load_or_generate(&SeedId::default_dir(&base))?;
|
||||
let local_seed_id = seed.public_key_hex();
|
||||
// machined first, legacy seed loudly; never generates. A listener
|
||||
// with no identity cannot exist — it has nothing to answer as.
|
||||
let signer = Arc::new(MachineSigner::resolve(&base)?);
|
||||
let local_seed_id = signer.pubkey_hex();
|
||||
|
||||
let listener = Arc::new(Self {
|
||||
event_bus: event_bus.clone(),
|
||||
|
|
@ -74,7 +77,7 @@ impl LiteListener {
|
|||
|
||||
// Outbound federation bridge — so this instance can also reach peers.
|
||||
if !config.peers.is_empty() {
|
||||
let mut bridge = FederationBridge::new(event_bus.clone(), Arc::new(seed), config.role);
|
||||
let mut bridge = FederationBridge::new(event_bus.clone(), signer, config.role);
|
||||
for peer in &config.peers {
|
||||
bridge.add_peer(peer.clone());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
use crate::bridge::{build_registry, ProviderRegistry};
|
||||
use crate::core::compact::{CompactionEngine, DefaultCompactionEngine, UtcClock};
|
||||
use crate::core::config::ConsciousnessConfig;
|
||||
use crate::core::identity::SeedId;
|
||||
use crate::server::gitea_memory::GiteaMemory;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -66,6 +65,10 @@ pub struct SouveraineServer {
|
|||
/// This instance's Ed25519 public key hex — used to filter self-announcements
|
||||
/// from the device registry. Loaded at construction; None if seed unavailable.
|
||||
pub local_seed_id: Option<String>,
|
||||
/// The machine signer behind `local_seed_id` — machined daemon or legacy
|
||||
/// seed. Resolved once at construction; the federation bridge signs
|
||||
/// envelopes through it.
|
||||
pub machine_signer: Option<Arc<crate::machined::signer::MachineSigner>>,
|
||||
/// Per-conversation turn backchannel. Holds the cancel token for the
|
||||
/// turn in flight and a persistent interjection queue, so any surface
|
||||
/// can interrupt (`POST /v1/conversations/:id/cancel`) or slip a note
|
||||
|
|
@ -273,19 +276,25 @@ impl SouveraineServer {
|
|||
gitea_url: std::env::var("SOUVERAINE_GITEA_URL").ok(),
|
||||
};
|
||||
|
||||
// ── Device registry ──
|
||||
// ── Machine identity ──
|
||||
// Resolved once: souveraine-machined (system tier) first, legacy
|
||||
// user-tier seed as a loud fallback. Never generates — a box with no
|
||||
// identity runs federation-less with a warning, it does not silently
|
||||
// mint a key.
|
||||
let souveraine_base = dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join(".souveraine");
|
||||
let local_seed_id = match crate::core::identity::SeedId::load_or_generate(
|
||||
&crate::core::identity::SeedId::default_dir(&souveraine_base),
|
||||
) {
|
||||
Ok(seed) => {
|
||||
let pubkey = seed.public_key_hex();
|
||||
Some(pubkey)
|
||||
let machine_signer = match crate::machined::signer::MachineSigner::resolve(&souveraine_base) {
|
||||
Ok(signer) => Some(Arc::new(signer)),
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"no machine identity — device registry, summon handling, and \
|
||||
federation are disabled: {e:#}"
|
||||
);
|
||||
None
|
||||
}
|
||||
Err(_) => None,
|
||||
};
|
||||
let local_seed_id = machine_signer.as_ref().map(|s| s.pubkey_hex());
|
||||
let sb_for_device_reg = souveraine_base.clone();
|
||||
let local_role = config.federation.role;
|
||||
let device_registry = local_seed_id.clone().map(|seed_id| {
|
||||
|
|
@ -312,19 +321,13 @@ impl SouveraineServer {
|
|||
});
|
||||
|
||||
// ── Summon handler ──
|
||||
let sb_for_seed = souveraine_base.clone();
|
||||
let local_seed: Option<Arc<SeedId>> = local_seed_id.as_ref().and_then(|_| {
|
||||
crate::core::identity::SeedId::load_or_generate(
|
||||
&crate::core::identity::SeedId::default_dir(&sb_for_seed),
|
||||
).ok().map(Arc::new)
|
||||
});
|
||||
let summon_handler = match (&local_seed_id, &local_seed) {
|
||||
(Some(seed_id), Some(seed)) => {
|
||||
let summon_handler = match (&local_seed_id, &machine_signer) {
|
||||
(Some(seed_id), Some(signer)) => {
|
||||
let handler = Arc::new(
|
||||
summon_handler::SummonHandler::new(
|
||||
seed_id.clone(),
|
||||
event_bus.clone(),
|
||||
seed.clone(),
|
||||
signer.clone(),
|
||||
souveraine_base.clone(),
|
||||
config.federation.auto_wake,
|
||||
),
|
||||
|
|
@ -351,6 +354,7 @@ impl SouveraineServer {
|
|||
device_registry,
|
||||
summon_handler,
|
||||
local_seed_id,
|
||||
machine_signer,
|
||||
turn_signals: Arc::new(dashmap::DashMap::new()),
|
||||
sensorium: Arc::new(Mutex::new(crate::core::sensorium::SensoriumCoordinator::new())),
|
||||
surface_conversations: Arc::new(StdMutex::new(HashMap::new())),
|
||||
|
|
@ -392,14 +396,11 @@ impl SouveraineServer {
|
|||
let fed = self.app_config.read().await.federation.clone();
|
||||
if fed.enabled && !fed.peers.is_empty() {
|
||||
let peer_count = fed.peers.len();
|
||||
let base = dirs::home_dir().unwrap_or_default().join(".souveraine");
|
||||
match crate::core::identity::SeedId::load_or_generate(
|
||||
&crate::core::identity::SeedId::default_dir(&base),
|
||||
) {
|
||||
Ok(seed) => {
|
||||
match &self.machine_signer {
|
||||
Some(signer) => {
|
||||
let mut bridge = federation::FederationBridge::new(
|
||||
self.event_bus.clone(),
|
||||
Arc::new(seed),
|
||||
signer.clone(),
|
||||
fed.role,
|
||||
);
|
||||
for peer in fed.peers {
|
||||
|
|
@ -408,8 +409,11 @@ impl SouveraineServer {
|
|||
bridge.run();
|
||||
tracing::info!("federation bridge started ({peer_count} peer(s))");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("federation: failed to load seed identity: {}", e);
|
||||
None => {
|
||||
tracing::error!(
|
||||
"federation: enabled in config but no machine identity is \
|
||||
available — bridge not started"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ use dashmap::DashMap;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::core::identity::SeedId;
|
||||
use crate::core::nervous::handler::TurnInjector;
|
||||
use crate::machined::signer::MachineSigner;
|
||||
use crate::core::nervous::{EventBus, SensorEvent};
|
||||
|
||||
const RESPONSE_TIMEOUT_SECS: u64 = 60;
|
||||
|
|
@ -51,8 +51,10 @@ pub struct SummonHandler {
|
|||
local_seed_id: String,
|
||||
/// The nervous system bus.
|
||||
event_bus: EventBus,
|
||||
/// Instance seed for signing outbound requests.
|
||||
seed: Arc<SeedId>,
|
||||
/// Machine signer for signing outbound requests. Not yet exercised —
|
||||
/// held so the reply-signing path lands on the signer, never a raw key.
|
||||
#[allow(dead_code)]
|
||||
signer: Arc<MachineSigner>,
|
||||
/// Base path for agent memory — used to access inbox files.
|
||||
souveraine_base: std::path::PathBuf,
|
||||
/// When true, an inbound summon wakes the target agent with a background
|
||||
|
|
@ -69,7 +71,7 @@ impl SummonHandler {
|
|||
pub fn new(
|
||||
local_seed_id: String,
|
||||
event_bus: EventBus,
|
||||
seed: Arc<SeedId>,
|
||||
signer: Arc<MachineSigner>,
|
||||
souveraine_base: std::path::PathBuf,
|
||||
auto_wake: bool,
|
||||
) -> Self {
|
||||
|
|
@ -78,7 +80,7 @@ impl SummonHandler {
|
|||
inbound: DashMap::new(),
|
||||
local_seed_id,
|
||||
event_bus,
|
||||
seed,
|
||||
signer,
|
||||
souveraine_base,
|
||||
auto_wake,
|
||||
injector: OnceLock::new(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue