2026-08-16 00:02:05 +08:00
|
|
|
|
//! GLP 标准信封 · glp_envelope
|
|
|
|
|
|
//!
|
|
|
|
|
|
//! 协议转工程第一件(HoloLake第二阶段总体规划-20260815 · 施工总纲):
|
|
|
|
|
|
//! GLS-0300《GLP 通信核心协议》第3节"标准消息结构"的逐字段工程映射。
|
|
|
|
|
|
//! 字段一个不造、一个不丢——老家谱 YAML 原文即本文件的形状。
|
|
|
|
|
|
//!
|
|
|
|
|
|
//! 指挥链落点(铁律四):人格体→宿主的一切指令都必须是这个信封;
|
|
|
|
|
|
//! 宿主只认信封不认散话。
|
|
|
|
|
|
|
2026-08-16 15:10:31 +08:00
|
|
|
|
use ring::digest::{digest, SHA256};
|
2026-08-16 00:02:05 +08:00
|
|
|
|
use serde::{Deserialize, Serialize};
|
2026-08-16 15:10:31 +08:00
|
|
|
|
#[cfg(test)]
|
2026-08-16 00:02:05 +08:00
|
|
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
|
|
|
2026-08-16 15:10:31 +08:00
|
|
|
|
const MAX_LANGUAGE_CONTENT_BYTES: usize = 64 * 1024;
|
|
|
|
|
|
|
2026-08-16 00:02:05 +08:00
|
|
|
|
/// 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)]
|
2026-08-16 15:10:31 +08:00
|
|
|
|
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
2026-08-16 00:02:05 +08:00
|
|
|
|
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)]
|
2026-08-16 15:10:31 +08:00
|
|
|
|
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
2026-08-16 00:02:05 +08:00
|
|
|
|
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)]
|
2026-08-16 15:10:31 +08:00
|
|
|
|
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
2026-08-16 00:02:05 +08:00
|
|
|
|
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)]
|
2026-08-16 15:10:31 +08:00
|
|
|
|
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
2026-08-16 00:02:05 +08:00
|
|
|
|
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)]
|
2026-08-16 15:10:31 +08:00
|
|
|
|
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
2026-08-16 00:02:05 +08:00
|
|
|
|
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)]
|
2026-08-16 15:10:31 +08:00
|
|
|
|
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
2026-08-16 00:02:05 +08:00
|
|
|
|
pub struct GlpIntegrity {
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub checksum: String,
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub signature: String,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// GLS-0300 · glp_message 全信封
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
2026-08-16 15:10:31 +08:00
|
|
|
|
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
2026-08-16 00:02:05 +08:00
|
|
|
|
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());
|
|
|
|
|
|
}
|
2026-08-16 15:10:31 +08:00
|
|
|
|
if !valid_message_id(&message.message_id) || message.created_at.trim().is_empty() {
|
2026-08-16 00:02:05 +08:00
|
|
|
|
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());
|
|
|
|
|
|
}
|
2026-08-16 15:10:31 +08:00
|
|
|
|
if message.payload.content.is_empty()
|
|
|
|
|
|
|| message.payload.content.len() > MAX_LANGUAGE_CONTENT_BYTES
|
|
|
|
|
|
|| message.payload.language.len() > 32
|
|
|
|
|
|
|| message.payload.attachments.len() > 16
|
|
|
|
|
|
{
|
2026-08-16 00:02:05 +08:00
|
|
|
|
return Err("HOLOLAKE_GLP_PAYLOAD_EMPTY".into());
|
|
|
|
|
|
}
|
2026-08-16 15:10:31 +08:00
|
|
|
|
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());
|
|
|
|
|
|
}
|
2026-08-16 00:02:05 +08:00
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-16 15:10:31 +08:00
|
|
|
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-16 00:02:05 +08:00
|
|
|
|
/// 消息编号按老家谱格式生成:GLP-MSG-YYYYMMDD-000001。
|
|
|
|
|
|
/// 序号由账本(管家层)当日累计给出,这里只拼形状。
|
2026-08-16 15:10:31 +08:00
|
|
|
|
#[cfg(test)]
|
2026-08-16 00:02:05 +08:00
|
|
|
|
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,工程给秒级事实。
|
2026-08-16 15:10:31 +08:00
|
|
|
|
#[cfg(test)]
|
2026-08-16 00:02:05 +08:00
|
|
|
|
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 算法,纯本地实现不引新依赖)。
|
2026-08-16 15:10:31 +08:00
|
|
|
|
#[cfg(test)]
|
2026-08-16 00:02:05 +08:00
|
|
|
|
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 {
|
2026-08-16 18:12:38 +08:00
|
|
|
|
object_id: "ICE-P-ZY001".into(),
|
2026-08-16 00:02:05 +08:00
|
|
|
|
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");
|
2026-08-16 18:12:38 +08:00
|
|
|
|
assert_eq!(back.sender.object_id, "ICE-P-ZY001");
|
2026-08-16 00:02:05 +08:00
|
|
|
|
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());
|
|
|
|
|
|
}
|
2026-08-16 15:10:31 +08:00
|
|
|
|
|
|
|
|
|
|
#[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());
|
|
|
|
|
|
}
|
2026-08-16 00:02:05 +08:00
|
|
|
|
}
|