初始化提交
Some checks failed
CI / Check / macos-latest (push) Has been cancelled
CI / Check / ubuntu-latest (push) Has been cancelled
CI / Check / windows-latest (push) Has been cancelled
CI / Test / macos-latest (push) Has been cancelled
CI / Test / ubuntu-latest (push) Has been cancelled
CI / Test / windows-latest (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Secrets Scan (push) Has been cancelled
CI / Install Script Smoke Test (push) Has been cancelled

This commit is contained in:
iven
2026-03-01 16:24:24 +08:00
commit 92e5def702
492 changed files with 211343 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
//! OpenFang Wire Protocol (OFP) — agent-to-agent networking.
//!
//! Provides cross-machine agent discovery, authentication, and communication
//! over TCP connections using a JSON-RPC framed protocol.
//!
//! ## Architecture
//!
//! - **PeerNode**: Local network endpoint that listens for incoming connections
//! - **PeerRegistry**: Tracks known peers and their agents
//! - **WireMessage**: JSON-framed protocol messages
//! - **PeerHandle**: Trait for routing remote messages through the kernel
pub mod message;
pub mod peer;
pub mod registry;
pub use message::{WireMessage, WireRequest, WireResponse};
pub use peer::{PeerConfig, PeerNode};
pub use registry::{PeerEntry, PeerRegistry, RemoteAgent};

View File

@@ -0,0 +1,292 @@
//! Wire protocol message types.
//!
//! All communication between OpenFang peers uses JSON-framed messages
//! over TCP. Each message is prefixed with a 4-byte big-endian length header.
use serde::{Deserialize, Serialize};
/// A wire protocol message (envelope).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WireMessage {
/// Unique message ID.
pub id: String,
/// Message variant.
#[serde(flatten)]
pub kind: WireMessageKind,
}
/// The different kinds of wire messages.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum WireMessageKind {
/// Request from one peer to another.
#[serde(rename = "request")]
Request(WireRequest),
/// Response to a request.
#[serde(rename = "response")]
Response(WireResponse),
/// One-way notification (no response expected).
#[serde(rename = "notification")]
Notification(WireNotification),
}
/// Request messages.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "method")]
pub enum WireRequest {
/// Handshake: exchange peer identity.
#[serde(rename = "handshake")]
Handshake {
/// The peer's unique node ID.
node_id: String,
/// Human-readable node name.
node_name: String,
/// Protocol version.
protocol_version: u32,
/// List of agents available on this peer.
agents: Vec<RemoteAgentInfo>,
/// Random nonce for HMAC authentication.
#[serde(default)]
nonce: String,
/// HMAC-SHA256(shared_secret, nonce + node_id).
#[serde(default)]
auth_hmac: String,
},
/// Discover agents matching a query on the remote peer.
#[serde(rename = "discover")]
Discover {
/// Search query (matches name, tags, description).
query: String,
},
/// Send a message to a specific agent on the remote peer.
#[serde(rename = "agent_message")]
AgentMessage {
/// Target agent ID or name on the remote peer.
agent: String,
/// The message text.
message: String,
/// Optional sender identity.
sender: Option<String>,
},
/// Ping to check if the peer is alive.
#[serde(rename = "ping")]
Ping,
}
/// Response messages.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "method")]
pub enum WireResponse {
/// Handshake acknowledgement.
#[serde(rename = "handshake_ack")]
HandshakeAck {
node_id: String,
node_name: String,
protocol_version: u32,
agents: Vec<RemoteAgentInfo>,
/// Random nonce for HMAC authentication.
#[serde(default)]
nonce: String,
/// HMAC-SHA256(shared_secret, nonce + node_id).
#[serde(default)]
auth_hmac: String,
},
/// Discovery results.
#[serde(rename = "discover_result")]
DiscoverResult { agents: Vec<RemoteAgentInfo> },
/// Agent message response.
#[serde(rename = "agent_response")]
AgentResponse {
/// The agent's response text.
text: String,
},
/// Pong response.
#[serde(rename = "pong")]
Pong {
/// Uptime in seconds.
uptime_secs: u64,
},
/// Error response.
#[serde(rename = "error")]
Error {
/// Error code.
code: i32,
/// Error message.
message: String,
},
}
/// Notification messages (one-way, no response).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "event")]
pub enum WireNotification {
/// An agent was spawned on the peer.
#[serde(rename = "agent_spawned")]
AgentSpawned { agent: RemoteAgentInfo },
/// An agent was terminated on the peer.
#[serde(rename = "agent_terminated")]
AgentTerminated { agent_id: String },
/// Peer is shutting down.
#[serde(rename = "shutting_down")]
ShuttingDown,
}
/// Information about a remote agent.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RemoteAgentInfo {
/// Agent ID (UUID string).
pub id: String,
/// Human-readable name.
pub name: String,
/// Description of what the agent does.
pub description: String,
/// Tags for categorization/discovery.
pub tags: Vec<String>,
/// Available tools.
pub tools: Vec<String>,
/// Current state.
pub state: String,
}
/// Current protocol version.
pub const PROTOCOL_VERSION: u32 = 1;
/// Encode a wire message to bytes (4-byte big-endian length + JSON).
pub fn encode_message(msg: &WireMessage) -> Result<Vec<u8>, serde_json::Error> {
let json = serde_json::to_vec(msg)?;
let len = json.len() as u32;
let mut bytes = Vec::with_capacity(4 + json.len());
bytes.extend_from_slice(&len.to_be_bytes());
bytes.extend_from_slice(&json);
Ok(bytes)
}
/// Decode the length prefix from a 4-byte header.
pub fn decode_length(header: &[u8; 4]) -> u32 {
u32::from_be_bytes(*header)
}
/// Parse a JSON body into a WireMessage.
pub fn decode_message(body: &[u8]) -> Result<WireMessage, serde_json::Error> {
serde_json::from_slice(body)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_decode_roundtrip() {
let msg = WireMessage {
id: "msg-1".to_string(),
kind: WireMessageKind::Request(WireRequest::Ping),
};
let bytes = encode_message(&msg).unwrap();
// First 4 bytes are length
let len = decode_length(&[bytes[0], bytes[1], bytes[2], bytes[3]]);
assert_eq!(len as usize, bytes.len() - 4);
let decoded = decode_message(&bytes[4..]).unwrap();
assert_eq!(decoded.id, "msg-1");
}
#[test]
fn test_handshake_serialization() {
let msg = WireMessage {
id: "hs-1".to_string(),
kind: WireMessageKind::Request(WireRequest::Handshake {
node_id: "node-abc".to_string(),
node_name: "my-kernel".to_string(),
protocol_version: PROTOCOL_VERSION,
agents: vec![RemoteAgentInfo {
id: "agent-1".to_string(),
name: "coder".to_string(),
description: "A coding agent".to_string(),
tags: vec!["code".to_string()],
tools: vec!["file_read".to_string()],
state: "running".to_string(),
}],
nonce: "test-nonce".to_string(),
auth_hmac: "test-hmac".to_string(),
}),
};
let json = serde_json::to_string_pretty(&msg).unwrap();
assert!(json.contains("handshake"));
assert!(json.contains("coder"));
let decoded: WireMessage = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.id, "hs-1");
}
#[test]
fn test_agent_message_serialization() {
let msg = WireMessage {
id: "am-1".to_string(),
kind: WireMessageKind::Request(WireRequest::AgentMessage {
agent: "coder".to_string(),
message: "Write a hello world".to_string(),
sender: Some("orchestrator".to_string()),
}),
};
let bytes = encode_message(&msg).unwrap();
let decoded = decode_message(&bytes[4..]).unwrap();
match decoded.kind {
WireMessageKind::Request(WireRequest::AgentMessage { agent, message, .. }) => {
assert_eq!(agent, "coder");
assert_eq!(message, "Write a hello world");
}
other => panic!("Expected AgentMessage, got {other:?}"),
}
}
#[test]
fn test_error_response() {
let msg = WireMessage {
id: "err-1".to_string(),
kind: WireMessageKind::Response(WireResponse::Error {
code: 404,
message: "Agent not found".to_string(),
}),
};
let json = serde_json::to_string(&msg).unwrap();
let decoded: WireMessage = serde_json::from_str(&json).unwrap();
match decoded.kind {
WireMessageKind::Response(WireResponse::Error { code, message }) => {
assert_eq!(code, 404);
assert_eq!(message, "Agent not found");
}
other => panic!("Expected Error, got {other:?}"),
}
}
#[test]
fn test_notification_serialization() {
let msg = WireMessage {
id: "n-1".to_string(),
kind: WireMessageKind::Notification(WireNotification::AgentSpawned {
agent: RemoteAgentInfo {
id: "a-1".to_string(),
name: "researcher".to_string(),
description: "Research agent".to_string(),
tags: vec![],
tools: vec![],
state: "running".to_string(),
},
}),
};
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("agent_spawned"));
let _: WireMessage = serde_json::from_str(&json).unwrap();
}
#[test]
fn test_discover_request() {
let msg = WireMessage {
id: "d-1".to_string(),
kind: WireMessageKind::Request(WireRequest::Discover {
query: "security".to_string(),
}),
};
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("discover"));
assert!(json.contains("security"));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,351 @@
//! Peer registry — tracks connected peers and their agents.
//!
//! The [`PeerRegistry`] is a thread-safe, concurrent data structure that
//! records all known remote peers, their connection state, and the agents
//! they advertise.
use crate::message::RemoteAgentInfo;
use chrono::{DateTime, Utc};
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::{Arc, RwLock};
/// A tracked remote agent, enriched with the owning peer's identity.
#[derive(Debug, Clone)]
pub struct RemoteAgent {
/// The remote peer that hosts this agent.
pub peer_node_id: String,
/// Agent details from the wire protocol.
pub info: RemoteAgentInfo,
}
/// Connection state of a peer.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PeerState {
/// Handshake completed, fully connected.
Connected,
/// Connection lost but not removed yet (eligible for reconnect).
Disconnected,
}
/// An entry representing a single known peer.
#[derive(Debug, Clone)]
pub struct PeerEntry {
/// Unique node ID of the peer.
pub node_id: String,
/// Human-readable node name.
pub node_name: String,
/// Socket address of the peer.
pub address: SocketAddr,
/// Agents advertised by this peer.
pub agents: Vec<RemoteAgentInfo>,
/// Connection state.
pub state: PeerState,
/// When the peer first connected.
pub connected_at: DateTime<Utc>,
/// Protocol version negotiated during handshake.
pub protocol_version: u32,
}
/// Thread-safe registry of all known peers.
#[derive(Debug, Clone)]
pub struct PeerRegistry {
peers: Arc<RwLock<HashMap<String, PeerEntry>>>,
}
impl PeerRegistry {
/// Create a new empty registry.
pub fn new() -> Self {
Self {
peers: Arc::new(RwLock::new(HashMap::new())),
}
}
/// Register or update a peer after a successful handshake.
pub fn add_peer(&self, entry: PeerEntry) {
let mut peers = self.peers.write().unwrap_or_else(|e| e.into_inner());
peers.insert(entry.node_id.clone(), entry);
}
/// Remove a peer entirely.
pub fn remove_peer(&self, node_id: &str) -> Option<PeerEntry> {
let mut peers = self.peers.write().unwrap_or_else(|e| e.into_inner());
peers.remove(node_id)
}
/// Mark a peer as disconnected (but keep its entry for possible reconnect).
pub fn mark_disconnected(&self, node_id: &str) {
let mut peers = self.peers.write().unwrap_or_else(|e| e.into_inner());
if let Some(entry) = peers.get_mut(node_id) {
entry.state = PeerState::Disconnected;
}
}
/// Mark a peer as connected again.
pub fn mark_connected(&self, node_id: &str) {
let mut peers = self.peers.write().unwrap_or_else(|e| e.into_inner());
if let Some(entry) = peers.get_mut(node_id) {
entry.state = PeerState::Connected;
}
}
/// Get a snapshot of a specific peer.
pub fn get_peer(&self, node_id: &str) -> Option<PeerEntry> {
let peers = self.peers.read().unwrap_or_else(|e| e.into_inner());
peers.get(node_id).cloned()
}
/// Get all connected peers.
pub fn connected_peers(&self) -> Vec<PeerEntry> {
let peers = self.peers.read().unwrap_or_else(|e| e.into_inner());
peers
.values()
.filter(|p| p.state == PeerState::Connected)
.cloned()
.collect()
}
/// Get all peers (connected + disconnected).
pub fn all_peers(&self) -> Vec<PeerEntry> {
let peers = self.peers.read().unwrap_or_else(|e| e.into_inner());
peers.values().cloned().collect()
}
/// Update the agent list for a peer (e.g., after an AgentSpawned notification).
pub fn update_agents(&self, node_id: &str, agents: Vec<RemoteAgentInfo>) {
let mut peers = self.peers.write().unwrap_or_else(|e| e.into_inner());
if let Some(entry) = peers.get_mut(node_id) {
entry.agents = agents;
}
}
/// Add a single agent to a peer's advertised list.
pub fn add_agent(&self, node_id: &str, agent: RemoteAgentInfo) {
let mut peers = self.peers.write().unwrap_or_else(|e| e.into_inner());
if let Some(entry) = peers.get_mut(node_id) {
// Replace if agent with same ID already exists, otherwise push
if let Some(existing) = entry.agents.iter_mut().find(|a| a.id == agent.id) {
*existing = agent;
} else {
entry.agents.push(agent);
}
}
}
/// Remove an agent from a peer's advertised list.
pub fn remove_agent(&self, node_id: &str, agent_id: &str) {
let mut peers = self.peers.write().unwrap_or_else(|e| e.into_inner());
if let Some(entry) = peers.get_mut(node_id) {
entry.agents.retain(|a| a.id != agent_id);
}
}
/// Find all remote agents matching a query (searches name, tags, description).
pub fn find_agents(&self, query: &str) -> Vec<RemoteAgent> {
let query_lower = query.to_lowercase();
let peers = self.peers.read().unwrap_or_else(|e| e.into_inner());
let mut results = Vec::new();
for peer in peers.values() {
if peer.state != PeerState::Connected {
continue;
}
for agent in &peer.agents {
let matches = agent.name.to_lowercase().contains(&query_lower)
|| agent.description.to_lowercase().contains(&query_lower)
|| agent
.tags
.iter()
.any(|t| t.to_lowercase().contains(&query_lower));
if matches {
results.push(RemoteAgent {
peer_node_id: peer.node_id.clone(),
info: agent.clone(),
});
}
}
}
results
}
/// Get all remote agents across all connected peers.
pub fn all_remote_agents(&self) -> Vec<RemoteAgent> {
let peers = self.peers.read().unwrap_or_else(|e| e.into_inner());
let mut results = Vec::new();
for peer in peers.values() {
if peer.state != PeerState::Connected {
continue;
}
for agent in &peer.agents {
results.push(RemoteAgent {
peer_node_id: peer.node_id.clone(),
info: agent.clone(),
});
}
}
results
}
/// Number of connected peers.
pub fn connected_count(&self) -> usize {
let peers = self.peers.read().unwrap_or_else(|e| e.into_inner());
peers
.values()
.filter(|p| p.state == PeerState::Connected)
.count()
}
/// Total number of peers (including disconnected).
pub fn total_count(&self) -> usize {
let peers = self.peers.read().unwrap_or_else(|e| e.into_inner());
peers.len()
}
}
impl Default for PeerRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_peer(node_id: &str, agents: Vec<RemoteAgentInfo>) -> PeerEntry {
PeerEntry {
node_id: node_id.to_string(),
node_name: format!("{node_id}-name"),
address: "127.0.0.1:9000".parse().unwrap(),
agents,
state: PeerState::Connected,
connected_at: Utc::now(),
protocol_version: 1,
}
}
fn make_agent(id: &str, name: &str, tags: &[&str]) -> RemoteAgentInfo {
RemoteAgentInfo {
id: id.to_string(),
name: name.to_string(),
description: format!("{name} agent"),
tags: tags.iter().map(|s| s.to_string()).collect(),
tools: vec![],
state: "running".to_string(),
}
}
#[test]
fn test_add_and_get_peer() {
let registry = PeerRegistry::new();
let peer = make_peer("node-1", vec![make_agent("a1", "coder", &["code"])]);
registry.add_peer(peer);
let retrieved = registry.get_peer("node-1").unwrap();
assert_eq!(retrieved.node_id, "node-1");
assert_eq!(retrieved.agents.len(), 1);
assert_eq!(retrieved.agents[0].name, "coder");
}
#[test]
fn test_remove_peer() {
let registry = PeerRegistry::new();
registry.add_peer(make_peer("node-1", vec![]));
assert_eq!(registry.total_count(), 1);
let removed = registry.remove_peer("node-1");
assert!(removed.is_some());
assert_eq!(registry.total_count(), 0);
}
#[test]
fn test_disconnect_reconnect() {
let registry = PeerRegistry::new();
registry.add_peer(make_peer("node-1", vec![]));
assert_eq!(registry.connected_count(), 1);
registry.mark_disconnected("node-1");
assert_eq!(registry.connected_count(), 0);
assert_eq!(registry.total_count(), 1);
registry.mark_connected("node-1");
assert_eq!(registry.connected_count(), 1);
}
#[test]
fn test_find_agents_by_name() {
let registry = PeerRegistry::new();
registry.add_peer(make_peer(
"node-1",
vec![
make_agent("a1", "coder", &["code"]),
make_agent("a2", "researcher", &["research"]),
],
));
registry.add_peer(make_peer(
"node-2",
vec![make_agent("a3", "code-reviewer", &["code", "review"])],
));
let results = registry.find_agents("code");
assert_eq!(results.len(), 2); // "coder" and "code-reviewer"
}
#[test]
fn test_find_agents_by_tag() {
let registry = PeerRegistry::new();
registry.add_peer(make_peer(
"node-1",
vec![make_agent("a1", "helper", &["security", "audit"])],
));
let results = registry.find_agents("security");
assert_eq!(results.len(), 1);
assert_eq!(results[0].info.name, "helper");
assert_eq!(results[0].peer_node_id, "node-1");
}
#[test]
fn test_find_agents_skips_disconnected() {
let registry = PeerRegistry::new();
registry.add_peer(make_peer(
"node-1",
vec![make_agent("a1", "coder", &["code"])],
));
registry.mark_disconnected("node-1");
let results = registry.find_agents("coder");
assert!(results.is_empty());
}
#[test]
fn test_add_remove_agent() {
let registry = PeerRegistry::new();
registry.add_peer(make_peer("node-1", vec![]));
registry.add_agent("node-1", make_agent("a1", "coder", &[]));
assert_eq!(registry.get_peer("node-1").unwrap().agents.len(), 1);
registry.remove_agent("node-1", "a1");
assert_eq!(registry.get_peer("node-1").unwrap().agents.len(), 0);
}
#[test]
fn test_all_remote_agents() {
let registry = PeerRegistry::new();
registry.add_peer(make_peer("node-1", vec![make_agent("a1", "coder", &[])]));
registry.add_peer(make_peer(
"node-2",
vec![
make_agent("a2", "writer", &[]),
make_agent("a3", "tester", &[]),
],
));
let all = registry.all_remote_agents();
assert_eq!(all.len(), 3);
}
}