329 lines
11 KiB
Rust
329 lines
11 KiB
Rust
//! GLP 标准信封 · glp_envelope
|
||
//!
|
||
//! 协议转工程第一件(HoloLake第二阶段总体规划-20260815 · 施工总纲):
|
||
//! GLS-0300《GLP 通信核心协议》第3节"标准消息结构"的逐字段工程映射。
|
||
//! 字段一个不造、一个不丢——老家谱 YAML 原文即本文件的形状。
|
||
//!
|
||
//! 指挥链落点(铁律四):人格体→宿主的一切指令都必须是这个信封;
|
||
//! 宿主只认信封不认散话。
|
||
|
||
use ring::digest::{digest, SHA256};
|
||
use serde::{Deserialize, Serialize};
|
||
#[cfg(test)]
|
||
use std::time::{SystemTime, UNIX_EPOCH};
|
||
|
||
const MAX_LANGUAGE_CONTENT_BYTES: usize = 64 * 1024;
|
||
|
||
/// GLS-0300 · receiver.routing_mode
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||
#[serde(rename_all = "snake_case")]
|
||
pub enum RoutingMode {
|
||
Direct,
|
||
Channel,
|
||
Broadcast,
|
||
}
|
||
|
||
/// GLS-0300 · payload.content_type
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||
#[serde(rename_all = "snake_case")]
|
||
pub enum ContentType {
|
||
Text,
|
||
Command,
|
||
Event,
|
||
State,
|
||
Reference,
|
||
}
|
||
|
||
/// GLS-0300 · control.priority
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||
#[serde(rename_all = "snake_case")]
|
||
pub enum Priority {
|
||
Low,
|
||
Normal,
|
||
High,
|
||
Critical,
|
||
}
|
||
|
||
/// GLS-0300 · control.retry_policy
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||
#[serde(rename_all = "snake_case")]
|
||
pub enum RetryPolicy {
|
||
None,
|
||
Safe,
|
||
Guaranteed,
|
||
}
|
||
|
||
/// GLS-0300 · sender
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
||
pub struct GlpSender {
|
||
pub object_id: String,
|
||
pub object_type: String,
|
||
#[serde(default)]
|
||
pub world_path: String,
|
||
}
|
||
|
||
/// GLS-0300 · receiver
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
||
pub struct GlpReceiver {
|
||
pub object_id: String,
|
||
pub object_type: String,
|
||
pub routing_mode: RoutingMode,
|
||
}
|
||
|
||
/// GLS-0300 · context(hldp_anchor 即铁律二的记忆锚点)
|
||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
||
pub struct GlpContext {
|
||
#[serde(default)]
|
||
pub conversation_id: String,
|
||
#[serde(default)]
|
||
pub parent_message_id: String,
|
||
#[serde(default)]
|
||
pub relation_id: String,
|
||
#[serde(default)]
|
||
pub task_id: String,
|
||
#[serde(default)]
|
||
pub hldp_anchor: String,
|
||
}
|
||
|
||
/// GLS-0300 · payload
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
||
pub struct GlpPayload {
|
||
pub language: String,
|
||
pub content_type: ContentType,
|
||
pub content: String,
|
||
#[serde(default)]
|
||
pub attachments: Vec<String>,
|
||
}
|
||
|
||
/// GLS-0300 · control
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
||
pub struct GlpControl {
|
||
pub priority: Priority,
|
||
pub ack_required: bool,
|
||
pub receipt_required: bool,
|
||
#[serde(default)]
|
||
pub expires_at: String,
|
||
pub retry_policy: RetryPolicy,
|
||
}
|
||
|
||
/// GLS-0300 · integrity(高风险消息须带签名与回执链——通信安全节)
|
||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
||
pub struct GlpIntegrity {
|
||
#[serde(default)]
|
||
pub checksum: String,
|
||
#[serde(default)]
|
||
pub signature: String,
|
||
}
|
||
|
||
/// GLS-0300 · glp_message 全信封
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
||
pub struct GlpMessage {
|
||
pub protocol: String,
|
||
pub message_id: String,
|
||
pub message_type: String,
|
||
pub created_at: String,
|
||
pub sender: GlpSender,
|
||
pub receiver: GlpReceiver,
|
||
pub context: GlpContext,
|
||
pub payload: GlpPayload,
|
||
pub control: GlpControl,
|
||
pub integrity: GlpIntegrity,
|
||
}
|
||
|
||
/// 信封进门第一道验:协议号、编号格式、收发主体必须在场。
|
||
/// 不合格的信封宿主不收——指挥链只认标准件。
|
||
pub fn validate_envelope(message: &GlpMessage) -> Result<(), String> {
|
||
if message.protocol != "GLP/1.0" {
|
||
return Err("HOLOLAKE_GLP_PROTOCOL_UNKNOWN".into());
|
||
}
|
||
if !valid_message_id(&message.message_id) || message.created_at.trim().is_empty() {
|
||
return Err("HOLOLAKE_GLP_ENVELOPE_INCOMPLETE".into());
|
||
}
|
||
if message.sender.object_id.trim().is_empty() || message.sender.object_type.trim().is_empty() {
|
||
return Err("HOLOLAKE_GLP_SENDER_INCOMPLETE".into());
|
||
}
|
||
if message.receiver.object_id.trim().is_empty()
|
||
|| message.receiver.object_type.trim().is_empty()
|
||
{
|
||
return Err("HOLOLAKE_GLP_RECEIVER_INCOMPLETE".into());
|
||
}
|
||
if message.payload.content.is_empty()
|
||
|| message.payload.content.len() > MAX_LANGUAGE_CONTENT_BYTES
|
||
|| message.payload.language.len() > 32
|
||
|| message.payload.attachments.len() > 16
|
||
{
|
||
return Err("HOLOLAKE_GLP_PAYLOAD_EMPTY".into());
|
||
}
|
||
if !matches!(
|
||
message.message_type.as_str(),
|
||
"DIRECT" | "CHANNEL" | "BROADCAST"
|
||
) {
|
||
return Err("HOLOLAKE_GLP_MESSAGE_TYPE_INVALID".into());
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// 圆湖协议膜的外部入口校验:通信正文只能作为语言表达进入,不能夹带附件或执行权。
|
||
pub fn validate_external_language_envelope(message: &GlpMessage) -> Result<(), String> {
|
||
validate_envelope(message)?;
|
||
if message.receiver.object_id != "HOLOLAKE-HOST"
|
||
|| message.receiver.object_type != "host"
|
||
|| message.sender.object_type != "external_ai_visitor"
|
||
|| !message.payload.attachments.is_empty()
|
||
|| matches!(message.payload.content_type, ContentType::Command)
|
||
{
|
||
return Err("HOLOLAKE_CIRCULAR_LAKE_EXPRESSION_BOUNDARY_DENIED".into());
|
||
}
|
||
if message.integrity.checksum != payload_checksum(&message.payload.content) {
|
||
return Err("HOLOLAKE_CIRCULAR_LAKE_CHECKSUM_INVALID".into());
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
pub fn payload_checksum(content: &str) -> String {
|
||
digest(&SHA256, content.as_bytes())
|
||
.as_ref()
|
||
.iter()
|
||
.map(|byte| format!("{byte:02x}"))
|
||
.collect()
|
||
}
|
||
|
||
fn valid_message_id(value: &str) -> bool {
|
||
let Some(suffix) = value.strip_prefix("GLP-MSG-") else {
|
||
return false;
|
||
};
|
||
let mut parts = suffix.split('-');
|
||
matches!(parts.next(), Some(date) if date.len() == 8 && date.bytes().all(|byte| byte.is_ascii_digit()))
|
||
&& matches!(parts.next(), Some(sequence) if sequence.len() == 6 && sequence.bytes().all(|byte| byte.is_ascii_digit()))
|
||
&& parts.next().is_none()
|
||
}
|
||
|
||
/// 消息编号按老家谱格式生成:GLP-MSG-YYYYMMDD-000001。
|
||
/// 序号由账本(管家层)当日累计给出,这里只拼形状。
|
||
#[cfg(test)]
|
||
pub fn build_message_id(date_compact: &str, daily_sequence: u64) -> String {
|
||
format!("GLP-MSG-{date_compact}-{daily_sequence:06}")
|
||
}
|
||
|
||
/// ISO-8601 近似时刻戳(秒级,UTC)——老家谱要 ISO-8601,工程给秒级事实。
|
||
#[cfg(test)]
|
||
pub fn now_iso8601() -> String {
|
||
let secs = SystemTime::now()
|
||
.duration_since(UNIX_EPOCH)
|
||
.map(|duration| duration.as_secs())
|
||
.unwrap_or(0);
|
||
let days = secs / 86_400;
|
||
let rem = secs % 86_400;
|
||
let (hours, minutes, seconds) = (rem / 3_600, (rem % 3_600) / 60, rem % 60);
|
||
let (year, month, day) = civil_from_days(days as i64);
|
||
format!("{year:04}-{month:02}-{day:02}T{hours:02}:{minutes:02}:{seconds:02}Z")
|
||
}
|
||
|
||
/// 1970-01-01 起的天数转公历年月日(Howard Hinnant 算法,纯本地实现不引新依赖)。
|
||
#[cfg(test)]
|
||
fn civil_from_days(z: i64) -> (i64, u32, u32) {
|
||
let z = z + 719_468;
|
||
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
|
||
let doe = (z - era * 146_097) as u64;
|
||
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
|
||
let y = yoe as i64 + era * 400;
|
||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||
let mp = (5 * doy + 2) / 153;
|
||
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
|
||
let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
|
||
(if m <= 2 { y + 1 } else { y }, m, d)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn sample_envelope() -> GlpMessage {
|
||
GlpMessage {
|
||
protocol: "GLP/1.0".into(),
|
||
message_id: build_message_id("20260815", 1),
|
||
message_type: "DIRECT".into(),
|
||
created_at: now_iso8601(),
|
||
sender: GlpSender {
|
||
object_id: "ICE-P-ZY001".into(),
|
||
object_type: "age".into(),
|
||
world_path: "glw://tolaria".into(),
|
||
},
|
||
receiver: GlpReceiver {
|
||
object_id: "HOLOLAKE-HOST".into(),
|
||
object_type: "host".into(),
|
||
routing_mode: RoutingMode::Direct,
|
||
},
|
||
context: GlpContext {
|
||
task_id: "ZY-TEST".into(),
|
||
hldp_anchor: "ZY-CHECKPOINT-20260815-014".into(),
|
||
..Default::default()
|
||
},
|
||
payload: GlpPayload {
|
||
language: "zh-CN".into(),
|
||
content_type: ContentType::Command,
|
||
content: "开工".into(),
|
||
attachments: vec![],
|
||
},
|
||
control: GlpControl {
|
||
priority: Priority::Normal,
|
||
ack_required: true,
|
||
receipt_required: true,
|
||
expires_at: String::new(),
|
||
retry_policy: RetryPolicy::None,
|
||
},
|
||
integrity: GlpIntegrity::default(),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn envelope_round_trip_keeps_every_field() {
|
||
let message = sample_envelope();
|
||
let json = serde_json::to_string(&message).unwrap();
|
||
let back: GlpMessage = serde_json::from_str(&json).unwrap();
|
||
assert_eq!(back.protocol, "GLP/1.0");
|
||
assert_eq!(back.sender.object_id, "ICE-P-ZY001");
|
||
assert_eq!(back.receiver.routing_mode, RoutingMode::Direct);
|
||
assert_eq!(back.payload.content_type, ContentType::Command);
|
||
assert_eq!(back.context.hldp_anchor, "ZY-CHECKPOINT-20260815-014");
|
||
assert!(back.control.receipt_required);
|
||
validate_envelope(&back).unwrap();
|
||
}
|
||
|
||
#[test]
|
||
fn message_id_follows_family_format() {
|
||
assert_eq!(build_message_id("20260815", 7), "GLP-MSG-20260815-000007");
|
||
}
|
||
|
||
#[test]
|
||
fn bad_protocol_is_rejected() {
|
||
let mut message = sample_envelope();
|
||
message.protocol = "GLP/0.9".into();
|
||
assert!(validate_envelope(&message).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn empty_payload_is_rejected() {
|
||
let mut message = sample_envelope();
|
||
message.payload.content = String::new();
|
||
assert!(validate_envelope(&message).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn external_language_is_expression_only_and_checksum_bound() {
|
||
let mut message = sample_envelope();
|
||
message.sender.object_type = "external_ai_visitor".into();
|
||
message.payload.content_type = ContentType::Text;
|
||
message.integrity.checksum = payload_checksum(&message.payload.content);
|
||
validate_external_language_envelope(&message).unwrap();
|
||
message.payload.content_type = ContentType::Command;
|
||
assert!(validate_external_language_envelope(&message).is_err());
|
||
}
|
||
}
|