use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use tracing::debug;
use clawft_core::bus::MessageBus;
use crate::error::KernelError;
use crate::process::Pid;
static IPC_MSG_COUNTER: AtomicU64 = AtomicU64::new(1);
#[inline]
fn next_ipc_msg_id() -> String {
let id = IPC_MSG_COUNTER.fetch_add(1, Ordering::Relaxed);
format!("ipc-{id}")
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MessageTarget {
Process(Pid),
Topic(String),
Broadcast,
Service(String),
ServiceMethod {
service: String,
method: String,
},
Kernel,
RemoteNode {
node_id: String,
target: Box<MessageTarget>,
},
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MessagePayload {
Text(String),
Json(serde_json::Value),
ToolCall {
name: String,
args: serde_json::Value,
},
ToolResult {
call_id: String,
result: serde_json::Value,
},
Signal(KernelSignal),
Rvf {
segment_type: u8,
data: Vec<u8>,
},
}
impl MessagePayload {
pub fn type_name(&self) -> &'static str {
match self {
MessagePayload::Text(_) => "text",
MessagePayload::Json(_) => "json",
MessagePayload::ToolCall { .. } => "tool_call",
MessagePayload::ToolResult { .. } => "tool_result",
MessagePayload::Signal(_) => "signal",
MessagePayload::Rvf { .. } => "rvf",
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ExitReason {
Normal,
Crash(String),
Killed,
Timeout,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessDown {
pub pid: crate::process::Pid,
pub reason: ExitReason,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum KernelSignal {
Shutdown,
Suspend,
Resume,
Ping,
Pong,
ReloadConfig,
DumpState,
UserDefined(u8),
Kill,
LinkExit {
pid: crate::process::Pid,
reason: ExitReason,
},
MonitorDown(ProcessDown),
ResourceWarning {
resource: String,
current: u64,
limit: u64,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KernelMessage {
pub id: String,
pub from: Pid,
pub target: MessageTarget,
pub payload: MessagePayload,
pub timestamp: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub correlation_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trace_id: Option<String>,
}
impl KernelMessage {
pub fn new(from: Pid, target: MessageTarget, payload: MessagePayload) -> Self {
Self {
id: next_ipc_msg_id(),
from,
target,
payload,
timestamp: Utc::now(),
correlation_id: None,
trace_id: None,
}
}
pub fn with_correlation(
from: Pid,
target: MessageTarget,
payload: MessagePayload,
correlation_id: String,
) -> Self {
Self {
id: next_ipc_msg_id(),
from,
target,
payload,
timestamp: Utc::now(),
correlation_id: Some(correlation_id),
trace_id: None,
}
}
pub fn with_trace(
from: Pid,
target: MessageTarget,
payload: MessagePayload,
trace_id: String,
) -> Self {
Self {
id: next_ipc_msg_id(),
from,
target,
payload,
timestamp: Utc::now(),
correlation_id: None,
trace_id: Some(trace_id),
}
}
pub fn set_trace_id(mut self, trace_id: impl Into<String>) -> Self {
self.trace_id = Some(trace_id.into());
self
}
pub fn ensure_trace_id(mut self) -> Self {
if self.trace_id.is_none() {
self.trace_id = Some(uuid::Uuid::new_v4().to_string());
}
self
}
pub fn text(from: Pid, target: MessageTarget, text: impl Into<String>) -> Self {
Self::new(from, target, MessagePayload::Text(text.into()))
}
pub fn signal(from: Pid, target: MessageTarget, signal: KernelSignal) -> Self {
Self::new(from, target, MessagePayload::Signal(signal))
}
pub fn tool_call(
from: Pid,
target: MessageTarget,
name: impl Into<String>,
args: serde_json::Value,
) -> Self {
Self::new(
from,
target,
MessagePayload::ToolCall {
name: name.into(),
args,
},
)
}
pub fn tool_result(
from: Pid,
target: MessageTarget,
call_id: impl Into<String>,
result: serde_json::Value,
) -> Self {
Self::new(
from,
target,
MessagePayload::ToolResult {
call_id: call_id.into(),
result,
},
)
}
}
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub struct GlobalPid {
pub node_id: String,
pub pid: Pid,
}
impl GlobalPid {
pub fn local(pid: Pid, node_id: &str) -> Self {
Self {
node_id: node_id.to_string(),
pid,
}
}
pub fn is_local(&self, my_node_id: &str) -> bool {
self.node_id == my_node_id
}
}
impl std::fmt::Display for GlobalPid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.node_id, self.pid)
}
}
pub struct KernelIpc {
bus: Arc<MessageBus>,
}
impl KernelIpc {
pub fn new(bus: Arc<MessageBus>) -> Self {
Self { bus }
}
pub fn bus(&self) -> &Arc<MessageBus> {
&self.bus
}
#[cfg(feature = "exochain")]
pub fn send_checked(
&self,
msg: &KernelMessage,
checker: &crate::capability::CapabilityChecker,
chain: Option<&crate::chain::ChainManager>,
) -> Result<(), KernelError> {
if let MessageTarget::Process(to_pid) = &msg.target {
checker.check_ipc_target(msg.from, *to_pid)?;
}
if let Some(cm) = chain {
cm.append(
"ipc",
"ipc.send",
Some(serde_json::json!({
"from": msg.from,
"target": format!("{:?}", msg.target),
"payload_type": msg.payload.type_name(),
"msg_id": msg.id,
})),
);
}
self.send(msg)
}
pub fn send(&self, msg: &KernelMessage) -> Result<(), KernelError> {
debug!(
id = %msg.id,
from = msg.from,
"sending kernel message"
);
let json = serde_json::to_string(msg)
.map_err(|e| KernelError::Ipc(format!("failed to serialize message: {e}")))?;
let inbound = clawft_types::event::InboundMessage {
channel: "kernel-ipc".to_owned(),
sender_id: format!("pid-{}", msg.from),
chat_id: msg.id.clone(),
content: json,
timestamp: msg.timestamp,
media: vec![],
metadata: std::collections::HashMap::new(),
};
self.bus
.publish_inbound(inbound)
.map_err(|e| KernelError::Ipc(format!("bus publish failed: {e}")))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kernel_message_text() {
let msg = KernelMessage::text(0, MessageTarget::Process(1), "hello");
assert_eq!(msg.from, 0);
assert!(matches!(msg.target, MessageTarget::Process(1)));
assert!(matches!(msg.payload, MessagePayload::Text(ref t) if t == "hello"));
}
#[test]
fn kernel_message_signal() {
let msg = KernelMessage::signal(0, MessageTarget::Broadcast, KernelSignal::Shutdown);
assert!(matches!(msg.target, MessageTarget::Broadcast));
assert!(matches!(
msg.payload,
MessagePayload::Signal(KernelSignal::Shutdown)
));
}
#[test]
fn kernel_message_json_payload() {
let payload = MessagePayload::Json(serde_json::json!({"key": "value"}));
let msg = KernelMessage::new(1, MessageTarget::Kernel, payload);
assert!(matches!(msg.payload, MessagePayload::Json(_)));
}
#[test]
fn message_serde_roundtrip() {
let msg = KernelMessage::text(5, MessageTarget::Service("health".into()), "check");
let json = serde_json::to_string(&msg).unwrap();
let restored: KernelMessage = serde_json::from_str(&json).unwrap();
assert_eq!(restored.id, msg.id);
assert_eq!(restored.from, 5);
}
#[tokio::test]
async fn ipc_send() {
let bus = Arc::new(MessageBus::new());
let ipc = KernelIpc::new(bus.clone());
let msg = KernelMessage::text(0, MessageTarget::Process(1), "test");
ipc.send(&msg).unwrap();
let received = bus.consume_inbound().await.unwrap();
assert_eq!(received.channel, "kernel-ipc");
assert_eq!(received.sender_id, "pid-0");
}
#[test]
fn ipc_bus_ref() {
let bus = Arc::new(MessageBus::new());
let ipc = KernelIpc::new(bus.clone());
assert!(Arc::ptr_eq(ipc.bus(), &bus));
}
#[test]
fn message_target_variants() {
let targets = vec![
MessageTarget::Process(1),
MessageTarget::Broadcast,
MessageTarget::Service("test".into()),
MessageTarget::ServiceMethod {
service: "auth".into(),
method: "validate_token".into(),
},
MessageTarget::Kernel,
];
for target in targets {
let json = serde_json::to_string(&target).unwrap();
let _: MessageTarget = serde_json::from_str(&json).unwrap();
}
}
#[test]
fn kernel_signal_variants() {
let signals = vec![
KernelSignal::Shutdown,
KernelSignal::Suspend,
KernelSignal::Resume,
KernelSignal::Ping,
KernelSignal::Pong,
];
for signal in signals {
let json = serde_json::to_string(&signal).unwrap();
let _: KernelSignal = serde_json::from_str(&json).unwrap();
}
}
#[test]
fn expanded_signal_variants_serde() {
let signals = vec![
KernelSignal::ReloadConfig,
KernelSignal::DumpState,
KernelSignal::UserDefined(42),
KernelSignal::Kill,
KernelSignal::LinkExit {
pid: 7,
reason: ExitReason::Crash("boom".into()),
},
KernelSignal::MonitorDown(ProcessDown {
pid: 9,
reason: ExitReason::Normal,
}),
KernelSignal::ResourceWarning {
resource: "memory".into(),
current: 800,
limit: 1000,
},
];
for signal in signals {
let json = serde_json::to_string(&signal).unwrap();
let restored: KernelSignal = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&restored).unwrap();
assert_eq!(json, json2);
}
}
#[test]
fn exit_reason_variants() {
let reasons = vec![
ExitReason::Normal,
ExitReason::Crash("error".into()),
ExitReason::Killed,
ExitReason::Timeout,
];
for reason in reasons {
let json = serde_json::to_string(&reason).unwrap();
let _: ExitReason = serde_json::from_str(&json).unwrap();
}
}
#[test]
fn process_down_serde() {
let pd = ProcessDown {
pid: 42,
reason: ExitReason::Crash("segfault".into()),
};
let json = serde_json::to_string(&pd).unwrap();
let restored: ProcessDown = serde_json::from_str(&json).unwrap();
assert_eq!(restored.pid, 42);
assert!(matches!(restored.reason, ExitReason::Crash(ref s) if s == "segfault"));
}
#[test]
fn message_with_correlation_id() {
let msg = KernelMessage::with_correlation(
1,
MessageTarget::Process(2),
MessagePayload::Text("request".into()),
"req-123".into(),
);
assert_eq!(msg.correlation_id, Some("req-123".into()));
assert_eq!(msg.from, 1);
}
#[test]
fn message_without_correlation_id() {
let msg = KernelMessage::text(1, MessageTarget::Process(2), "hello");
assert!(msg.correlation_id.is_none());
}
#[test]
fn tool_call_message() {
let msg = KernelMessage::tool_call(
1,
MessageTarget::Process(2),
"read_file",
serde_json::json!({"path": "/src/main.rs"}),
);
match &msg.payload {
MessagePayload::ToolCall { name, args } => {
assert_eq!(name, "read_file");
assert_eq!(args["path"], "/src/main.rs");
}
other => panic!("expected ToolCall, got: {other:?}"),
}
}
#[test]
fn tool_result_message() {
let msg = KernelMessage::tool_result(
2,
MessageTarget::Process(1),
"call-123",
serde_json::json!({"content": "file contents"}),
);
match &msg.payload {
MessagePayload::ToolResult { call_id, result } => {
assert_eq!(call_id, "call-123");
assert_eq!(result["content"], "file contents");
}
other => panic!("expected ToolResult, got: {other:?}"),
}
}
#[test]
fn topic_target() {
let msg = KernelMessage::text(1, MessageTarget::Topic("build-status".into()), "done");
assert!(matches!(msg.target, MessageTarget::Topic(ref t) if t == "build-status"));
}
#[test]
fn tool_call_serde_roundtrip() {
let msg = KernelMessage::tool_call(
1,
MessageTarget::Process(2),
"search",
serde_json::json!({"query": "test"}),
);
let json = serde_json::to_string(&msg).unwrap();
let restored: KernelMessage = serde_json::from_str(&json).unwrap();
assert!(matches!(
restored.payload,
MessagePayload::ToolCall { ref name, .. } if name == "search"
));
}
#[test]
fn correlation_id_serde_roundtrip() {
let msg = KernelMessage::with_correlation(
1,
MessageTarget::Process(2),
MessagePayload::Text("req".into()),
"corr-456".into(),
);
let json = serde_json::to_string(&msg).unwrap();
let restored: KernelMessage = serde_json::from_str(&json).unwrap();
assert_eq!(restored.correlation_id, Some("corr-456".into()));
}
#[test]
fn rvf_payload_variant() {
let payload = MessagePayload::Rvf {
segment_type: 0x40,
data: vec![0xCA, 0xFE],
};
let msg = KernelMessage::new(1, MessageTarget::Process(2), payload);
assert_eq!(msg.payload.type_name(), "rvf");
let json = serde_json::to_string(&msg).unwrap();
let restored: KernelMessage = serde_json::from_str(&json).unwrap();
match &restored.payload {
MessagePayload::Rvf { segment_type, data } => {
assert_eq!(*segment_type, 0x40);
assert_eq!(data, &[0xCA, 0xFE]);
}
other => panic!("expected Rvf, got: {other:?}"),
}
}
#[test]
fn payload_type_names() {
assert_eq!(MessagePayload::Text("hi".into()).type_name(), "text");
assert_eq!(
MessagePayload::Json(serde_json::json!(1)).type_name(),
"json"
);
assert_eq!(
MessagePayload::Signal(KernelSignal::Ping).type_name(),
"signal"
);
}
#[test]
fn remote_node_serde_roundtrip() {
let target = MessageTarget::RemoteNode {
node_id: "node-42".into(),
target: Box::new(MessageTarget::Process(7)),
};
let json = serde_json::to_string(&target).unwrap();
let restored: MessageTarget = serde_json::from_str(&json).unwrap();
match restored {
MessageTarget::RemoteNode { node_id, target } => {
assert_eq!(node_id, "node-42");
assert!(matches!(*target, MessageTarget::Process(7)));
}
other => panic!("expected RemoteNode, got: {other:?}"),
}
}
#[test]
fn global_pid_equality() {
let a = GlobalPid::local(1, "node-a");
let b = GlobalPid::local(1, "node-b");
let c = GlobalPid::local(1, "node-a");
assert_ne!(a, b, "same pid on different nodes must not be equal");
assert_eq!(a, c, "same pid on same node must be equal");
}
#[test]
fn global_pid_is_local() {
let gpid = GlobalPid::local(5, "my-node");
assert!(gpid.is_local("my-node"));
assert!(!gpid.is_local("other-node"));
}
#[test]
fn global_pid_display() {
let gpid = GlobalPid::local(42, "alpha");
assert_eq!(gpid.to_string(), "alpha:42");
}
#[test]
fn correlation_id_absent_in_json_when_none() {
let msg = KernelMessage::text(1, MessageTarget::Process(2), "hello");
let json = serde_json::to_string(&msg).unwrap();
assert!(!json.contains("correlation_id"));
}
#[test]
fn trace_id_absent_by_default() {
let msg = KernelMessage::text(1, MessageTarget::Process(2), "hello");
assert!(msg.trace_id.is_none());
}
#[test]
fn trace_id_with_trace() {
let msg = KernelMessage::with_trace(
1,
MessageTarget::Process(2),
MessagePayload::Text("traced".into()),
"trace-abc-123".into(),
);
assert_eq!(msg.trace_id, Some("trace-abc-123".into()));
}
#[test]
fn trace_id_set_builder() {
let msg = KernelMessage::text(1, MessageTarget::Process(2), "hello")
.set_trace_id("my-trace");
assert_eq!(msg.trace_id, Some("my-trace".into()));
}
#[test]
fn trace_id_ensure_generates() {
let msg = KernelMessage::text(1, MessageTarget::Process(2), "hello").ensure_trace_id();
assert!(msg.trace_id.is_some());
assert!(!msg.trace_id.as_ref().unwrap().is_empty());
}
#[test]
fn trace_id_ensure_preserves_existing() {
let msg = KernelMessage::text(1, MessageTarget::Process(2), "hello")
.set_trace_id("existing-trace")
.ensure_trace_id();
assert_eq!(msg.trace_id, Some("existing-trace".into()));
}
#[test]
fn trace_id_serde_roundtrip() {
let msg = KernelMessage::with_trace(
1,
MessageTarget::Process(2),
MessagePayload::Text("traced".into()),
"trace-roundtrip".into(),
);
let json = serde_json::to_string(&msg).unwrap();
let restored: KernelMessage = serde_json::from_str(&json).unwrap();
assert_eq!(restored.trace_id, Some("trace-roundtrip".into()));
}
#[test]
fn trace_id_absent_in_json_when_none() {
let msg = KernelMessage::text(1, MessageTarget::Process(2), "hello");
let json = serde_json::to_string(&msg).unwrap();
assert!(!json.contains("trace_id"));
}
#[test]
fn trace_id_backward_compat_deserialization() {
let json = r#"{"id":"test-id","from":1,"target":{"Process":2},"payload":{"Text":"hello"},"timestamp":"2024-01-01T00:00:00Z"}"#;
let msg: KernelMessage = serde_json::from_str(json).unwrap();
assert!(msg.trace_id.is_none());
assert!(msg.correlation_id.is_none());
}
}