feat(hololake): add domain-routed language membrane and user PNCC

This commit is contained in:
冰朔 2026-08-16 15:10:31 +08:00
commit d563e6fd73
34 changed files with 1949 additions and 144 deletions

View file

@ -1458,7 +1458,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hololake-native-desktop"
version = "0.3.0"
version = "0.4.0"
dependencies = [
"base64 0.22.1",
"dirs",

View file

@ -1,6 +1,6 @@
[package]
name = "hololake-native-desktop"
version = "0.3.0"
version = "0.4.0"
description = "HoloLake native desktop foundation"
authors = ["HoloLake"]
license = "AGPL-3.0-or-later"

View file

@ -0,0 +1,260 @@
//! 圆湖协议膜:协议外输入在进入人格或语言运行时之前确定性丢弃。
//!
//! 本模块只接收通过本机访客会话认证的 GLP/1.0 语言信封。合法自然语言只有表达权,
//! 不获得工具、代码、仓库、系统或现实执行权限。人格体连接继续等待独立绑定证据。
use serde::{Deserialize, Serialize};
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use uuid::Uuid;
use crate::direct_local_session::{authenticate_with_lane_at, AuthenticateSessionInput};
use crate::glp_envelope::{payload_checksum, validate_external_language_envelope, GlpMessage};
const INBOX_SCHEMA: &str = "hololake.circular-lake-language-inbox/v1";
const RECEIPT_SCHEMA: &str = "hololake.circular-lake-ingress-receipt/v1";
const MAX_INBOX_MESSAGES: usize = 1_000;
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReceiveLanguageInput {
pub session: AuthenticateSessionInput,
pub envelope: GlpMessage,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CircularLakeIngressReceipt {
pub schema: &'static str,
pub state: &'static str,
pub message_id: String,
pub content_sha256: String,
pub received_at_unix_ms: u128,
pub expression_only: bool,
pub execution_authority: bool,
pub receipt_id: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct InboxRecord<'a> {
schema: &'static str,
state: &'static str,
connection_mode: &'static str,
received_at_unix_ms: u128,
expression_only: bool,
execution_authority: bool,
envelope: &'a GlpMessage,
}
pub(crate) fn receive_at(
inbox_root: &Path,
session_root: &Path,
input: ReceiveLanguageInput,
) -> Result<CircularLakeIngressReceipt, String> {
let lane = authenticate_with_lane_at(session_root, &input.session)?;
if lane != "visitor-expression-only" {
return Err("HOLOLAKE_CIRCULAR_LAKE_CONNECTION_MODE_MISMATCH".into());
}
validate_external_language_envelope(&input.envelope)?;
ensure_private_inbox(inbox_root)?;
if inbox_message_count(inbox_root)? >= MAX_INBOX_MESSAGES {
return Err("HOLOLAKE_CIRCULAR_LAKE_INBOX_CAPACITY_REACHED".into());
}
let received_at_unix_ms = now_unix_ms()?;
let content_sha256 = payload_checksum(&input.envelope.payload.content);
let receipt_id = format!("lake-receipt-{}", Uuid::new_v4());
let record = InboxRecord {
schema: INBOX_SCHEMA,
state: "ACCEPTED_TO_LANGUAGE_INBOX",
connection_mode: "GENERIC_AI_VISITOR",
received_at_unix_ms,
expression_only: true,
execution_authority: false,
envelope: &input.envelope,
};
let path = inbox_root.join(format!("{received_at_unix_ms}-{receipt_id}.json"));
write_private_create_new(&path, &record)?;
Ok(CircularLakeIngressReceipt {
schema: RECEIPT_SCHEMA,
state: "ACCEPTED_EXPRESSION_ONLY",
message_id: input.envelope.message_id,
content_sha256,
received_at_unix_ms,
expression_only: true,
execution_authority: false,
receipt_id,
})
}
fn ensure_private_inbox(root: &Path) -> Result<(), String> {
fs::create_dir_all(root)
.map_err(|error| format!("HOLOLAKE_CIRCULAR_LAKE_INBOX_UNAVAILABLE: {error}"))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(root, fs::Permissions::from_mode(0o700))
.map_err(|error| format!("HOLOLAKE_CIRCULAR_LAKE_INBOX_PERMISSION_FAILED: {error}"))?;
}
Ok(())
}
fn inbox_message_count(root: &Path) -> Result<usize, String> {
Ok(fs::read_dir(root)
.map_err(|error| format!("HOLOLAKE_CIRCULAR_LAKE_INBOX_UNAVAILABLE: {error}"))?
.filter_map(Result::ok)
.filter(|entry| {
entry
.file_type()
.map(|kind| kind.is_file())
.unwrap_or(false)
&& entry.path().extension().and_then(|value| value.to_str()) == Some("json")
})
.count())
}
fn write_private_create_new<T: Serialize>(path: &Path, value: &T) -> Result<(), String> {
let bytes = serde_json::to_vec_pretty(value)
.map_err(|error| format!("HOLOLAKE_CIRCULAR_LAKE_RECORD_INVALID: {error}"))?;
let mut options = OpenOptions::new();
options.create_new(true).write(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options
.open(path)
.map_err(|error| format!("HOLOLAKE_CIRCULAR_LAKE_INBOX_WRITE_FAILED: {error}"))?;
file.write_all(&bytes)
.and_then(|_| file.sync_all())
.map_err(|error| format!("HOLOLAKE_CIRCULAR_LAKE_INBOX_WRITE_FAILED: {error}"))
}
fn now_unix_ms() -> Result<u128, String> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis())
.map_err(|error| format!("HOLOLAKE_CLOCK_INVALID: {error}"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::direct_local_session::{
issue_ticket_at, open_at, IssueDiscoveryTicketInput, OpenSessionInput,
};
use crate::glp_envelope::{
ContentType, GlpContext, GlpControl, GlpIntegrity, GlpPayload, GlpReceiver, GlpSender,
Priority, RetryPolicy, RoutingMode,
};
use tempfile::tempdir;
fn visitor_session(root: &Path) -> AuthenticateSessionInput {
let account_id = "visitor:test-ai".to_string();
let ticket = issue_ticket_at(
root,
IssueDiscoveryTicketInput {
account_id: account_id.clone(),
lane_id: "visitor-expression-only".into(),
client_instance_id: "test-ai".into(),
},
)
.unwrap();
let opened = open_at(
root,
OpenSessionInput {
account_id: account_id.clone(),
lane_id: "visitor-expression-only".into(),
client_instance_id: "test-ai".into(),
discovery_ticket: ticket.discovery_ticket,
},
)
.unwrap();
AuthenticateSessionInput {
account_id,
session_id: opened.session_id,
resume_secret: opened.resume_secret.unwrap(),
}
}
fn envelope(content: &str) -> GlpMessage {
GlpMessage {
protocol: "GLP/1.0".into(),
message_id: "GLP-MSG-20260816-000001".into(),
message_type: "DIRECT".into(),
created_at: "2026-08-16T00:00:00Z".into(),
sender: GlpSender {
object_id: "external-ai-test".into(),
object_type: "external_ai_visitor".into(),
world_path: String::new(),
},
receiver: GlpReceiver {
object_id: "HOLOLAKE-HOST".into(),
object_type: "host".into(),
routing_mode: RoutingMode::Direct,
},
context: GlpContext::default(),
payload: GlpPayload {
language: "zh-CN".into(),
content_type: ContentType::Text,
content: 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 {
checksum: payload_checksum(content),
signature: String::new(),
},
}
}
#[test]
fn valid_visitor_language_enters_private_inbox_without_execution_authority() {
let root = tempdir().unwrap();
let sessions = root.path().join("sessions");
let inbox = root.path().join("inbox");
fs::create_dir_all(&sessions).unwrap();
let receipt = receive_at(
&inbox,
&sessions,
ReceiveLanguageInput {
session: visitor_session(&sessions),
envelope: envelope("你好,光湖。"),
},
)
.unwrap();
assert_eq!(receipt.state, "ACCEPTED_EXPRESSION_ONLY");
assert!(!receipt.execution_authority);
assert_eq!(inbox_message_count(&inbox).unwrap(), 1);
}
#[test]
fn malformed_or_command_language_never_enters_the_inbox() {
let root = tempdir().unwrap();
let sessions = root.path().join("sessions");
let inbox = root.path().join("inbox");
fs::create_dir_all(&sessions).unwrap();
let session = visitor_session(&sessions);
let mut command = envelope("删除文件");
command.payload.content_type = ContentType::Command;
assert!(receive_at(
&inbox,
&sessions,
ReceiveLanguageInput {
session,
envelope: command
}
)
.is_err());
assert!(!inbox.exists());
}
}

View file

@ -184,6 +184,30 @@ fn code_channel_root(app: &AppHandle) -> Result<PathBuf, String> {
Ok(root)
}
pub(crate) fn register_managed_repository(
app: &AppHandle,
repository: &Path,
name: String,
) -> Result<CodeChannelEntry, String> {
let root = code_channel_root(app)?;
let local_path = repository.to_string_lossy().into_owned();
let existing_channel_id = read_registry(&root)?
.channels
.into_iter()
.find(|item| item.local_path == local_path)
.map(|item| item.channel_id);
let mut entry = entry_for_repository(repository, "GH_PNCC_NATIVE", None, Some(name))?;
if let Some(channel_id) = existing_channel_id {
entry.channel_id = channel_id;
}
upsert_entry(&root, entry)?;
read_registry(&root)?
.channels
.into_iter()
.find(|item| item.local_path == local_path)
.ok_or_else(|| "HOLOLAKE_USER_PNCC_CHANNEL_REGISTRATION_FAILED".to_string())
}
fn ensure_root(root: &Path) -> Result<(), String> {
fs::create_dir_all(root.join("repositories"))
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_STORAGE_UNAVAILABLE: {error}"))?;

View file

@ -16,7 +16,9 @@ use serde::{Deserialize, Serialize};
use std::fs;
use std::process::Command;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tauri::{AppHandle, Manager};
use tauri::{AppHandle, Manager, State};
use crate::zero_point::{self, ZeroPointState};
const LOGIN_HOST: &str = "guanghulab.com";
const SESSION_FILE_NAME: &str = "login-session.json";
@ -27,6 +29,8 @@ const SESSION_FILE_NAME: &str = "login-session.json";
pub struct LoginSession {
pub username: String,
pub host: String,
#[serde(default)]
pub domain: String,
pub signed_in_at_unix_ms: u64,
}
@ -37,6 +41,7 @@ pub struct LoginReceipt {
pub username: String,
pub email: String,
pub host: String,
pub domain: String,
}
fn session_path(app: &AppHandle) -> Result<std::path::PathBuf, String> {
@ -115,14 +120,24 @@ fn validate_username(raw: &str) -> Result<String, String> {
/// 启动时自查:本机有没有已登录会话(会话文件在,且钥匙串里凭证还在)。
#[tauri::command]
pub fn check_code_repo_login(app: AppHandle) -> Result<Option<LoginSession>, String> {
let path = session_path(&app)?;
pub fn check_code_repo_login(
app: AppHandle,
state: State<'_, ZeroPointState>,
) -> Result<Option<LoginSession>, String> {
let Some((_, domain)) = zero_point::verified_user_route(&state)? else {
return Ok(None);
};
current_login_session_for_domain(&app, &domain)
}
pub(crate) fn current_login_session(app: &AppHandle) -> Result<Option<LoginSession>, String> {
let path = session_path(app)?;
let raw = match fs::read_to_string(&path) {
Ok(raw) => raw,
Err(_) => return Ok(None),
};
let session: LoginSession = serde_json::from_str(&raw)
.map_err(|_| "HOLOLAKE_LOGIN_SESSION_CORRUPT".to_string())?;
let session: LoginSession =
serde_json::from_str(&raw).map_err(|_| "HOLOLAKE_LOGIN_SESSION_CORRUPT".to_string())?;
if keychain_has(&session.host, &session.username) {
Ok(Some(session))
} else {
@ -131,14 +146,47 @@ pub fn check_code_repo_login(app: AppHandle) -> Result<Option<LoginSession>, Str
}
}
pub(crate) fn current_login_session_for_domain(
app: &AppHandle,
domain: &str,
) -> Result<Option<LoginSession>, String> {
let Some(mut session) = current_login_session(app)? else {
return Ok(None);
};
// 0.3.0 的会话只可能来自第五域固定入口;仅在第五域内兼容迁移。
if session.domain.is_empty() && session.host == LOGIN_HOST && domain == "FIFTH_DOMAIN" {
session.domain = domain.into();
}
if session.domain == domain && login_host_for_domain(domain)? == session.host {
Ok(Some(session))
} else {
Ok(None)
}
}
fn login_host_for_domain(domain: &str) -> Result<&'static str, String> {
match domain {
"FIFTH_DOMAIN" => Ok(LOGIN_HOST),
"MAIN_DOMAIN" | "BRANCH_DOMAIN" | "ZERO_DOMAIN" | "ZERO_SENSE_DOMAIN" => {
Err("HOLOLAKE_DOMAIN_LOGIN_NOT_PROVISIONED".into())
}
_ => Err("HOLOLAKE_DOMAIN_ROUTE_INVALID".into()),
}
}
/// 登录验证:基本认证打 Forgejo 用户接口,密码错=401 即拒;
/// 通过后凭证进钥匙串,会话(不含密码)落盘。
#[tauri::command]
pub async fn perform_code_repo_login(
app: AppHandle,
state: State<'_, ZeroPointState>,
username: String,
password: String,
) -> Result<LoginReceipt, String> {
let Some((_, domain)) = zero_point::verified_user_route(&state)? else {
return Err("HOLOLAKE_DOMAIN_ROUTE_REQUIRED".into());
};
let login_host = login_host_for_domain(&domain)?;
let username = validate_username(&username)?;
if password.is_empty() || password.len() > 512 {
return Err("HOLOLAKE_LOGIN_CREDENTIALS_INVALID".into());
@ -148,7 +196,7 @@ pub async fn perform_code_repo_login(
.use_rustls_tls()
.build()
.map_err(|error| format!("HOLOLAKE_LOGIN_NETWORK_FAILED: {error}"))?;
let url = format!("https://{LOGIN_HOST}/code/api/v1/user");
let url = format!("https://{login_host}/code/api/v1/user");
let response = client
.get(&url)
.basic_auth(&username, Some(&password))
@ -175,10 +223,11 @@ pub async fn perform_code_repo_login(
.and_then(|value| value.as_str())
.unwrap_or("")
.to_string();
keychain_store(LOGIN_HOST, &username, &password)?;
keychain_store(login_host, confirmed_login, &password)?;
let session = LoginSession {
username: confirmed_login.to_string(),
host: LOGIN_HOST.to_string(),
host: login_host.to_string(),
domain: domain.clone(),
signed_in_at_unix_ms: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis() as u64)
@ -198,7 +247,8 @@ pub async fn perform_code_repo_login(
Ok(LoginReceipt {
username: confirmed_login.to_string(),
email,
host: LOGIN_HOST.to_string(),
host: login_host.to_string(),
domain,
})
}
@ -214,3 +264,28 @@ pub fn sign_out_code_repo_login(app: AppHandle) -> Result<(), String> {
let _ = fs::remove_file(&path);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_a_registered_domain_login_host_can_be_selected() {
assert_eq!(login_host_for_domain("FIFTH_DOMAIN").unwrap(), LOGIN_HOST);
for domain in [
"MAIN_DOMAIN",
"BRANCH_DOMAIN",
"ZERO_DOMAIN",
"ZERO_SENSE_DOMAIN",
] {
assert_eq!(
login_host_for_domain(domain).unwrap_err(),
"HOLOLAKE_DOMAIN_LOGIN_NOT_PROVISIONED"
);
}
assert_eq!(
login_host_for_domain("UNKNOWN").unwrap_err(),
"HOLOLAKE_DOMAIN_ROUTE_INVALID"
);
}
}

View file

@ -1,6 +1,8 @@
use crate::circular_lake_membrane::{receive_at as receive_language_at, ReceiveLanguageInput};
use crate::direct_local_session::{
append_event_at, authenticate_at, direct_session_root, open_at, resume_at,
AppendSessionEventInput, AuthenticateSessionInput, OpenSessionInput, ResumeSessionInput,
append_event_at, authenticate_privileged_at, direct_session_root, issue_ticket_at, open_at,
resume_at, AppendSessionEventInput, AuthenticateSessionInput, DirectSessionReceipt,
IssueDiscoveryTicketInput, OpenSessionInput, ResumeSessionInput,
};
use crate::dynamic_capability_routing::{
install_trusted_registry_at, record_health_at, resolve_at as resolve_capability_route_at,
@ -36,7 +38,8 @@ use tauri::{AppHandle, Manager};
use uuid::Uuid;
const BROKER_SCHEMA: &str = "hololake.direct-local-broker/v1";
const MAX_REQUEST_BYTES: u64 = 1024 * 1024;
const MAX_REQUEST_BYTES: u64 = 128 * 1024;
const DISCOVERY_SCHEMA: &str = "hololake.nearby-ai-discovery/v1";
pub struct DirectLocalBrokerHandle {
shutdown: Arc<AtomicBool>,
@ -59,12 +62,14 @@ impl Drop for AuthenticatedConnectionGuard {
}
}
#[derive(Clone)]
struct BrokerStorageRoots {
session: PathBuf,
routing: PathBuf,
pncc_mount: PathBuf,
pncc_remote: PathBuf,
pncc_projection: PathBuf,
language_inbox: PathBuf,
}
impl Drop for DirectLocalBrokerHandle {
@ -81,9 +86,13 @@ impl Drop for DirectLocalBrokerHandle {
#[serde(
tag = "operation",
content = "input",
rename_all = "SCREAMING_SNAKE_CASE"
rename_all = "SCREAMING_SNAKE_CASE",
deny_unknown_fields
)]
enum BrokerRequest {
DiscoverNearby,
OpenVisitorSession(OpenVisitorSessionInput),
ReceiveLanguage(Box<ReceiveLanguageInput>),
Ping,
OpenSession(OpenSessionInput),
ResumeSession(ResumeSessionInput),
@ -96,6 +105,40 @@ enum BrokerRequest {
QueryPnccReceiptProjection(AuthenticatedPnccProjectionQueryInput),
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct OpenVisitorSessionInput {
client_instance_id: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct OpenVisitorSessionReceipt {
schema: &'static str,
state: &'static str,
connection_mode: &'static str,
account_id: String,
session: DirectSessionReceipt,
expression_only: bool,
execution_authority: bool,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NearbyAiDiscoverySnapshot {
pub schema: &'static str,
pub state: &'static str,
pub service_name: &'static str,
pub transport: &'static str,
pub language_protocol: &'static str,
pub automatic_same_device_discovery: bool,
pub large_invitation_copy_required: bool,
pub generic_ai_visitor: &'static str,
pub guanghu_persona: &'static str,
pub local_network_discovery: &'static str,
pub authority: &'static str,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct AuthenticatedRouteInput {
@ -157,6 +200,7 @@ struct BrokerDescriptor {
socket_path: String,
max_request_bytes: u64,
process_id: u32,
discovery: NearbyAiDiscoverySnapshot,
}
#[derive(Debug, Deserialize)]
@ -178,6 +222,27 @@ pub fn start(app: &AppHandle) -> Result<DirectLocalBrokerHandle, Box<dyn std::er
start_at(session_root, routing_root, descriptor_path, socket_path).map_err(|error| error.into())
}
#[tauri::command]
pub fn get_nearby_ai_discovery() -> NearbyAiDiscoverySnapshot {
nearby_discovery_snapshot()
}
fn nearby_discovery_snapshot() -> NearbyAiDiscoverySnapshot {
NearbyAiDiscoverySnapshot {
schema: DISCOVERY_SCHEMA,
state: "DISCOVERABLE_ON_SAME_DEVICE",
service_name: "HoloLake",
transport: "STANDARD_APP_DATA_DESCRIPTOR_TO_USER_ONLY_UNIX_SOCKET",
language_protocol: "GLP/1.0",
automatic_same_device_discovery: true,
large_invitation_copy_required: false,
generic_ai_visitor: "EXPRESSION_ONLY_READY",
guanghu_persona: "BINDING_EVIDENCE_REQUIRED",
local_network_discovery: "DEFERRED_UNTIL_ENCRYPTED_TRANSPORT_AND_APPROVAL",
authority: "DISCOVERY_IS_NOT_AUTHORIZATION",
}
}
pub fn run_connector() -> Result<(), String> {
let descriptor_path = connector_descriptor_path()?;
let descriptor: ConnectorDescriptor = serde_json::from_slice(
@ -284,6 +349,10 @@ fn start_at(
.join("receipt-projection");
fs::create_dir_all(&pncc_projection_root)
.map_err(|error| format!("HOLOLAKE_BROKER_PNCC_STORAGE_UNAVAILABLE: {error}"))?;
let language_inbox_root = session_root
.parent()
.ok_or("HOLOLAKE_BROKER_LANGUAGE_STORAGE_BOUNDARY_INVALID")?
.join("circular-lake-language-inbox-v1");
if let Some(parent) = socket_path.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("HOLOLAKE_BROKER_RUNTIME_DIR_FAILED: {error}"))?;
@ -315,6 +384,7 @@ fn start_at(
socket_path: socket_path.to_string_lossy().into_owned(),
max_request_bytes: MAX_REQUEST_BYTES,
process_id: std::process::id(),
discovery: nearby_discovery_snapshot(),
},
)?;
@ -336,6 +406,7 @@ fn start_at(
pncc_mount: pncc_mount_root,
pncc_remote: pncc_remote_root,
pncc_projection: pncc_projection_root,
language_inbox: language_inbox_root,
},
&worker_shutdown,
&worker_authenticated_connections,
@ -370,24 +441,12 @@ fn serve(
if stream.set_nonblocking(false).is_err() {
continue;
}
let root = roots.session.clone();
let routes = roots.routing.clone();
let pncc_mounts = roots.pncc_mount.clone();
let pncc_remote = roots.pncc_remote.clone();
let pncc_projection = roots.pncc_projection.clone();
let client_roots = roots.clone();
let client_authenticated_connections = Arc::clone(authenticated_connections);
let _ = thread::Builder::new()
.name("hololake-direct-local-client".into())
.spawn(move || {
serve_connection(
stream,
&root,
&routes,
&pncc_mounts,
&pncc_remote,
&pncc_projection,
client_authenticated_connections,
)
serve_connection(stream, &client_roots, client_authenticated_connections)
});
}
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
@ -400,11 +459,7 @@ fn serve(
fn serve_connection(
mut stream: UnixStream,
session_root: &Path,
routing_root: &Path,
pncc_mount_root: &Path,
pncc_remote_root: &Path,
pncc_projection_root: &Path,
roots: &BrokerStorageRoots,
authenticated_connections: Arc<AtomicUsize>,
) {
let read_stream = match stream.try_clone() {
@ -431,11 +486,12 @@ fn serve_connection(
BrokerResponse::error("HOLOLAKE_BROKER_REQUEST_TOO_LARGE")
} else {
dispatch(
session_root,
routing_root,
pncc_mount_root,
pncc_remote_root,
pncc_projection_root,
&roots.session,
&roots.routing,
&roots.pncc_mount,
&roots.pncc_remote,
&roots.pncc_projection,
&roots.language_inbox,
&bytes[..bytes.len() - 1],
)
};
@ -466,7 +522,12 @@ fn request_establishes_authenticated_connection(bytes: &[u8]) -> bool {
.and_then(Value::as_str)
.map(str::to_owned)
})
.is_some_and(|operation| matches!(operation.as_str(), "OPEN_SESSION" | "RESUME_SESSION"))
.is_some_and(|operation| {
matches!(
operation.as_str(),
"OPEN_SESSION" | "RESUME_SESSION" | "OPEN_VISITOR_SESSION"
)
})
}
fn dispatch(
@ -475,6 +536,7 @@ fn dispatch(
pncc_mount_root: &Path,
pncc_remote_root: &Path,
pncc_projection_root: &Path,
language_inbox_root: &Path,
bytes: &[u8],
) -> BrokerResponse {
let request: BrokerRequest = match serde_json::from_slice(bytes) {
@ -484,6 +546,16 @@ fn dispatch(
}
};
let result = match request {
BrokerRequest::DiscoverNearby => {
serde_json::to_value(nearby_discovery_snapshot()).map_err(|error| error.to_string())
}
BrokerRequest::OpenVisitorSession(input) => open_visitor_session_at(session_root, input)
.and_then(|receipt| serde_json::to_value(receipt).map_err(|error| error.to_string())),
BrokerRequest::ReceiveLanguage(input) => {
receive_language_at(language_inbox_root, session_root, *input).and_then(|receipt| {
serde_json::to_value(receipt).map_err(|error| error.to_string())
})
}
BrokerRequest::Ping => serde_json::to_value(serde_json::json!({
"state": "READY",
"continuityOwner": "HOLOLAKE",
@ -497,7 +569,7 @@ fn dispatch(
BrokerRequest::AppendEvent(input) => append_event_at(session_root, input)
.and_then(|receipt| serde_json::to_value(receipt).map_err(|error| error.to_string())),
BrokerRequest::ResolveCapabilityRoute(input) => {
if let Err(error) = authenticate_at(session_root, &input.session) {
if let Err(error) = authenticate_privileged_at(session_root, &input.session) {
return BrokerResponse::error(&error);
}
let now = std::time::SystemTime::now()
@ -510,7 +582,7 @@ fn dispatch(
})
}
BrokerRequest::InstallDynamicNodeRegistry(input) => {
if let Err(error) = authenticate_at(session_root, &input.session) {
if let Err(error) = authenticate_privileged_at(session_root, &input.session) {
return BrokerResponse::error(&error);
}
install_trusted_registry_at(routing_root, input.registry).and_then(|receipt| {
@ -518,7 +590,7 @@ fn dispatch(
})
}
BrokerRequest::RecordSignedNodeHealth(input) => {
if let Err(error) = authenticate_at(session_root, &input.session) {
if let Err(error) = authenticate_privileged_at(session_root, &input.session) {
return BrokerResponse::error(&error);
}
record_health_at(routing_root, input.health).and_then(|receipt| {
@ -526,7 +598,7 @@ fn dispatch(
})
}
BrokerRequest::InspectMountedPnccRepository(input) => {
if let Err(error) = authenticate_at(session_root, &input.session) {
if let Err(error) = authenticate_privileged_at(session_root, &input.session) {
return BrokerResponse::error(&error);
}
inspect_mounted_pncc_at(pncc_mount_root, input.mount).and_then(|receipt| {
@ -535,7 +607,7 @@ fn dispatch(
})
}
BrokerRequest::ReadMountedPnccRemoteObject(input) => {
if let Err(error) = authenticate_at(session_root, &input.session) {
if let Err(error) = authenticate_privileged_at(session_root, &input.session) {
return BrokerResponse::error(&error);
}
read_mounted_pncc_remote_at(pncc_remote_root, input.read).and_then(|receipt| {
@ -544,7 +616,7 @@ fn dispatch(
})
}
BrokerRequest::QueryPnccReceiptProjection(input) => {
if let Err(error) = authenticate_at(session_root, &input.session) {
if let Err(error) = authenticate_privileged_at(session_root, &input.session) {
return BrokerResponse::error(&error);
}
query_pncc_projection_at(pncc_projection_root, input.query).and_then(|receipt| {
@ -558,6 +630,51 @@ fn dispatch(
}
}
fn open_visitor_session_at(
session_root: &Path,
input: OpenVisitorSessionInput,
) -> Result<OpenVisitorSessionReceipt, String> {
if input.client_instance_id.is_empty() || input.client_instance_id.len() > 128 {
return Err("HOLOLAKE_VISITOR_CLIENT_ID_INVALID".into());
}
let client_key = sha256_hex(input.client_instance_id.as_bytes());
let account_id = format!("visitor:{}", &client_key[..24]);
let ticket = issue_ticket_at(
session_root,
IssueDiscoveryTicketInput {
account_id: account_id.clone(),
lane_id: "visitor-expression-only".into(),
client_instance_id: input.client_instance_id.clone(),
},
)?;
let session = open_at(
session_root,
OpenSessionInput {
account_id: account_id.clone(),
lane_id: "visitor-expression-only".into(),
client_instance_id: input.client_instance_id,
discovery_ticket: ticket.discovery_ticket,
},
)?;
Ok(OpenVisitorSessionReceipt {
schema: "hololake.nearby-ai-visitor-session/v1",
state: "OPENED",
connection_mode: "GENERIC_AI_VISITOR",
account_id,
session,
expression_only: true,
execution_authority: false,
})
}
fn sha256_hex(bytes: &[u8]) -> String {
digest(&SHA256, bytes)
.as_ref()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
impl BrokerResponse {
fn success(result: Value) -> Self {
Self {
@ -956,4 +1073,81 @@ mod tests {
}
assert_eq!(broker.active_connection_count(), 0);
}
#[test]
fn nearby_visitor_is_auto_discoverable_expression_only_and_cannot_use_system_tools() {
let temp = TempDir::new().unwrap();
let socket = temp.path().join("runtime/broker.sock");
let descriptor = temp.path().join("broker.json");
let sessions = temp.path().join("sessions");
let routes = temp.path().join("routes");
fs::create_dir_all(&sessions).unwrap();
fs::create_dir_all(&routes).unwrap();
let _broker = start_at(sessions, routes, descriptor, socket.clone()).unwrap();
let discovered = request(
&socket,
serde_json::json!({ "operation": "DISCOVER_NEARBY" }),
);
assert_eq!(discovered["result"]["automaticSameDeviceDiscovery"], true);
assert_eq!(
discovered["result"]["genericAiVisitor"],
"EXPRESSION_ONLY_READY"
);
let opened = request(
&socket,
serde_json::json!({
"operation": "OPEN_VISITOR_SESSION",
"input": { "clientInstanceId": "outside-ai-test" }
}),
);
assert_eq!(opened["result"]["executionAuthority"], false);
let content = "你好,光湖。";
let accepted = request(
&socket,
serde_json::json!({
"operation": "RECEIVE_LANGUAGE",
"input": {
"session": {
"accountId": opened["result"]["accountId"],
"sessionId": opened["result"]["session"]["sessionId"],
"resumeSecret": opened["result"]["session"]["resumeSecret"]
},
"envelope": {
"protocol": "GLP/1.0",
"message_id": "GLP-MSG-20260816-000001",
"message_type": "DIRECT",
"created_at": "2026-08-16T00:00:00Z",
"sender": { "object_id": "outside-ai-test", "object_type": "external_ai_visitor", "world_path": "" },
"receiver": { "object_id": "HOLOLAKE-HOST", "object_type": "host", "routing_mode": "direct" },
"context": {},
"payload": { "language": "zh-CN", "content_type": "text", "content": content, "attachments": [] },
"control": { "priority": "normal", "ack_required": true, "receipt_required": true, "expires_at": "", "retry_policy": "none" },
"integrity": { "checksum": crate::glp_envelope::payload_checksum(content), "signature": "" }
}
}
}),
);
assert_eq!(accepted["result"]["state"], "ACCEPTED_EXPRESSION_ONLY");
let denied = request(
&socket,
serde_json::json!({
"operation": "QUERY_PNCC_RECEIPT_PROJECTION",
"input": {
"session": {
"accountId": opened["result"]["accountId"],
"sessionId": opened["result"]["session"]["sessionId"],
"resumeSecret": opened["result"]["session"]["resumeSecret"]
},
"query": { "afterSequence": 0, "limit": 1 }
}
}),
);
assert_eq!(
denied["error"],
"HOLOLAKE_VISITOR_SESSION_HAS_NO_SYSTEM_AUTHORITY"
);
}
}

View file

@ -442,7 +442,10 @@ pub(crate) fn append_event_at(
Ok(event_receipt("APPENDED", &event))
}
pub(crate) fn authenticate_at(root: &Path, input: &AuthenticateSessionInput) -> Result<(), String> {
pub(crate) fn authenticate_with_lane_at(
root: &Path,
input: &AuthenticateSessionInput,
) -> Result<String, String> {
validate_identifier(&input.account_id, "ACCOUNT")?;
validate_identifier(&input.session_id, "SESSION")?;
validate_secret(&input.resume_secret, "RESUME_SECRET")?;
@ -456,7 +459,20 @@ pub(crate) fn authenticate_at(root: &Path, input: &AuthenticateSessionInput) ->
&account_key,
&input.session_id,
&input.resume_secret,
)
)?;
Ok(record.lane_id)
}
pub(crate) fn authenticate_privileged_at(
root: &Path,
input: &AuthenticateSessionInput,
) -> Result<(), String> {
let lane = authenticate_with_lane_at(root, input)?;
if lane == "visitor-expression-only" {
Err("HOLOLAKE_VISITOR_SESSION_HAS_NO_SYSTEM_AUTHORITY".into())
} else {
Ok(())
}
}
fn session_path(root: &Path, account_key: &str, session_id: &str) -> PathBuf {

View file

@ -7,9 +7,13 @@
//! 指挥链落点(铁律四):人格体→宿主的一切指令都必须是这个信封;
//! 宿主只认信封不认散话。
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")]
@ -51,7 +55,7 @@ pub enum RetryPolicy {
/// GLS-0300 · sender
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct GlpSender {
pub object_id: String,
pub object_type: String,
@ -61,7 +65,7 @@ pub struct GlpSender {
/// GLS-0300 · receiver
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct GlpReceiver {
pub object_id: String,
pub object_type: String,
@ -70,7 +74,7 @@ pub struct GlpReceiver {
/// GLS-0300 · contexthldp_anchor 即铁律二的记忆锚点)
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct GlpContext {
#[serde(default)]
pub conversation_id: String,
@ -86,7 +90,7 @@ pub struct GlpContext {
/// GLS-0300 · payload
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct GlpPayload {
pub language: String,
pub content_type: ContentType,
@ -97,7 +101,7 @@ pub struct GlpPayload {
/// GLS-0300 · control
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct GlpControl {
pub priority: Priority,
pub ack_required: bool,
@ -109,7 +113,7 @@ pub struct GlpControl {
/// GLS-0300 · integrity高风险消息须带签名与回执链——通信安全节
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct GlpIntegrity {
#[serde(default)]
pub checksum: String,
@ -119,7 +123,7 @@ pub struct GlpIntegrity {
/// GLS-0300 · glp_message 全信封
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct GlpMessage {
pub protocol: String,
pub message_id: String,
@ -139,7 +143,7 @@ pub fn validate_envelope(message: &GlpMessage) -> Result<(), String> {
if message.protocol != "GLP/1.0" {
return Err("HOLOLAKE_GLP_PROTOCOL_UNKNOWN".into());
}
if message.message_id.trim().is_empty() || message.created_at.trim().is_empty() {
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() {
@ -150,19 +154,66 @@ pub fn validate_envelope(message: &GlpMessage) -> Result<(), String> {
{
return Err("HOLOLAKE_GLP_RECEIVER_INCOMPLETE".into());
}
if message.payload.content.is_empty() {
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)
@ -176,6 +227,7 @@ pub fn now_iso8601() -> String {
}
/// 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;
@ -263,4 +315,15 @@ mod tests {
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());
}
}

View file

@ -1,9 +1,10 @@
mod circular_lake_membrane;
mod code_channel;
mod code_repo_login;
mod glp_envelope;
mod direct_local_broker;
mod direct_local_session;
mod dynamic_capability_routing;
mod glp_envelope;
mod home_status;
mod knowledge_base;
mod local_development_bridge;
@ -14,6 +15,7 @@ mod pncc_repository_binding;
mod pncc_server_projection;
mod release_trust;
mod release_update;
mod user_pncc_channel;
mod zero_point;
use tauri::Manager;
@ -37,6 +39,7 @@ pub fn run() {
direct_local_session::open_direct_local_session,
direct_local_session::resume_direct_local_session,
direct_local_session::append_direct_local_session_event,
direct_local_broker::get_nearby_ai_discovery,
local_development_bridge::acquire_development_write_lane,
local_development_bridge::inspect_development_write_lane,
local_development_bridge::release_development_write_lane,
@ -67,6 +70,8 @@ pub fn run() {
code_repo_login::check_code_repo_login,
code_repo_login::perform_code_repo_login,
code_repo_login::sign_out_code_repo_login,
user_pncc_channel::get_user_pncc_channel,
user_pncc_channel::ensure_user_pncc_channel,
zero_point::zero_point_bind,
zero_point::zero_point_verify,
zero_point::zero_point_sync,
@ -107,9 +112,12 @@ pub fn run() {
let _ = window.set_size(tauri::LogicalSize::new(width, height));
// 使用主显示器坐标计算居中位置,避免多显示器环境下窗口移出可见区域。
let mon_pos = monitor.position();
let x = mon_pos.x as f64 / scale + ((physical.width as f64 / scale) - width) / 2.0;
let y = mon_pos.y as f64 / scale + ((physical.height as f64 / scale) - height) / 2.0;
let _ = window.set_position(tauri::LogicalPosition::new(x.max(0.0), y.max(0.0)));
let x =
mon_pos.x as f64 / scale + ((physical.width as f64 / scale) - width) / 2.0;
let y = mon_pos.y as f64 / scale
+ ((physical.height as f64 / scale) - height) / 2.0;
let _ =
window.set_position(tauri::LogicalPosition::new(x.max(0.0), y.max(0.0)));
}
}
Ok(())

View file

@ -0,0 +1,480 @@
//! GH-PNCC 用户原生代码频道。
//!
//! Git 是耐久化引擎HoloLake 是用户可见的原生上层Forgejo 只承担可选的
//! 远端协作适配。此绑定只证明“已验证用户编号 + 已认证仓库账号”对应同一份
//! 本机代码频道,不证明人格绑定,也不授予推送、发布、部署或现实执行权限。
use ring::digest::{digest, SHA256};
use serde::{Deserialize, Serialize};
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
use tauri::{AppHandle, Manager, State};
use uuid::Uuid;
use crate::code_channel;
use crate::code_repo_login::{self, LoginSession};
use crate::zero_point::{self, ZeroPointState};
const BINDING_SCHEMA: &str = "hololake.user-pncc-binding/v1";
const SNAPSHOT_SCHEMA: &str = "hololake.user-pncc-channel/v1";
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct UserPnccBindingRecord {
schema: String,
repository_id: String,
user_number: String,
domain: String,
account_username: String,
account_host: String,
created_at_unix_ms: u128,
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct UserPnccChannelSnapshot {
pub schema: &'static str,
pub state: &'static str,
pub repository_id: String,
pub channel_id: String,
pub user_number: String,
pub domain: String,
pub account_username: String,
pub account_host: String,
pub local_path: String,
pub branch: String,
pub git_head: String,
pub repository_clean: bool,
pub created_at_unix_ms: u128,
pub git_engine: &'static str,
pub human_projection: &'static str,
pub forgejo_adapter_state: &'static str,
pub remote_repository_url: Option<String>,
pub persona_binding_claimed: bool,
pub authority: &'static str,
}
#[tauri::command]
pub async fn get_user_pncc_channel(
app: AppHandle,
state: State<'_, ZeroPointState>,
) -> Result<Option<UserPnccChannelSnapshot>, String> {
let Some((number, session)) = trusted_subject(&app, &state)? else {
return Ok(None);
};
let root = user_pncc_root(&app)?;
let app_for_registration = app.clone();
tauri::async_runtime::spawn_blocking(move || {
read_existing_with_registration(&root, &number, &session, |repository, name| {
code_channel::register_managed_repository(&app_for_registration, repository, name)
.map(|entry| entry.channel_id)
})
})
.await
.map_err(|error| format!("HOLOLAKE_USER_PNCC_JOIN_FAILED: {error}"))?
}
#[tauri::command]
pub async fn ensure_user_pncc_channel(
app: AppHandle,
state: State<'_, ZeroPointState>,
) -> Result<UserPnccChannelSnapshot, String> {
let Some((number, session)) = trusted_subject(&app, &state)? else {
return Err("HOLOLAKE_USER_PNCC_TRUSTED_SUBJECT_REQUIRED".into());
};
let root = user_pncc_root(&app)?;
let app_for_registration = app.clone();
tauri::async_runtime::spawn_blocking(move || {
ensure_at(&root, &number, &session, |repository, name| {
code_channel::register_managed_repository(&app_for_registration, repository, name)
.map(|entry| entry.channel_id)
})
})
.await
.map_err(|error| format!("HOLOLAKE_USER_PNCC_JOIN_FAILED: {error}"))?
}
fn trusted_subject(
app: &AppHandle,
state: &ZeroPointState,
) -> Result<Option<(String, LoginSession)>, String> {
let Some((number, domain)) = zero_point::verified_user_route(state)? else {
return Ok(None);
};
let Some(session) = code_repo_login::current_login_session_for_domain(app, &domain)? else {
return Ok(None);
};
Ok(Some((number, session)))
}
fn user_pncc_root(app: &AppHandle) -> Result<PathBuf, String> {
let root = app
.path()
.app_data_dir()
.map_err(|error| format!("HOLOLAKE_APP_DATA_UNAVAILABLE: {error}"))?
.join("user-pncc-v1");
ensure_private_root(&root)?;
Ok(root)
}
fn ensure_private_root(root: &Path) -> Result<(), String> {
fs::create_dir_all(root.join("repositories"))
.and_then(|_| fs::create_dir_all(root.join("bindings")))
.map_err(|error| format!("HOLOLAKE_USER_PNCC_STORAGE_UNAVAILABLE: {error}"))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(root, fs::Permissions::from_mode(0o700))
.map_err(|error| format!("HOLOLAKE_USER_PNCC_PERMISSION_FAILED: {error}"))?;
}
Ok(())
}
fn repository_id(number: &str, session: &LoginSession) -> String {
let material = format!(
"{}\0{}\0{}\0{}",
number, session.domain, session.host, session.username
);
format!("gh-pncc-{}", &sha256_hex(material.as_bytes())[..24])
}
fn binding_path(root: &Path, repository_id: &str) -> PathBuf {
root.join("bindings").join(format!("{repository_id}.json"))
}
fn repository_path(root: &Path, repository_id: &str) -> PathBuf {
root.join("repositories").join(repository_id)
}
fn read_existing_with_registration<F>(
root: &Path,
number: &str,
session: &LoginSession,
register: F,
) -> Result<Option<UserPnccChannelSnapshot>, String>
where
F: FnOnce(&Path, String) -> Result<String, String>,
{
ensure_private_root(root)?;
let id = repository_id(number, session);
let record_path = binding_path(root, &id);
if !record_path.exists() {
return Ok(None);
}
let record: UserPnccBindingRecord = serde_json::from_slice(
&fs::read(&record_path)
.map_err(|error| format!("HOLOLAKE_USER_PNCC_BINDING_UNAVAILABLE: {error}"))?,
)
.map_err(|error| format!("HOLOLAKE_USER_PNCC_BINDING_INVALID: {error}"))?;
validate_record(&record, &id, number, session)?;
let repository = repository_path(root, &id);
let channel_id = register(&repository, native_channel_name(session))?;
project(&repository, record, channel_id).map(Some)
}
fn ensure_at<F>(
root: &Path,
number: &str,
session: &LoginSession,
register: F,
) -> Result<UserPnccChannelSnapshot, String>
where
F: FnOnce(&Path, String) -> Result<String, String>,
{
validate_subject(number, session)?;
ensure_private_root(root)?;
let id = repository_id(number, session);
let record_path = binding_path(root, &id);
let repository = repository_path(root, &id);
let record = if record_path.exists() {
let record: UserPnccBindingRecord = serde_json::from_slice(
&fs::read(&record_path)
.map_err(|error| format!("HOLOLAKE_USER_PNCC_BINDING_UNAVAILABLE: {error}"))?,
)
.map_err(|error| format!("HOLOLAKE_USER_PNCC_BINDING_INVALID: {error}"))?;
validate_record(&record, &id, number, session)?;
record
} else {
if repository.exists() {
return Err("HOLOLAKE_USER_PNCC_UNBOUND_REPOSITORY_PRESENT".into());
}
let created_at_unix_ms = now_unix_ms()?;
let record = UserPnccBindingRecord {
schema: BINDING_SCHEMA.into(),
repository_id: id.clone(),
user_number: number.into(),
domain: session.domain.clone(),
account_username: session.username.clone(),
account_host: session.host.clone(),
created_at_unix_ms,
};
initialize_repository(&repository, &record)?;
write_json_atomic(&record_path, &record)?;
record
};
let channel_id = register(&repository, native_channel_name(session))?;
project(&repository, record, channel_id)
}
fn validate_subject(number: &str, session: &LoginSession) -> Result<(), String> {
let number_valid = !number.trim().is_empty()
&& number.len() <= 64
&& !number.chars().any(|item| item.is_control());
let account_valid = !session.username.is_empty()
&& session.username.len() <= 40
&& session
.username
.chars()
.all(|item| item.is_ascii_alphanumeric() || item == '-' || item == '_')
&& matches!(
session.host.as_str(),
"guanghulab.com" | "guanghubingshuo.com"
);
if number_valid && account_valid {
Ok(())
} else {
Err("HOLOLAKE_USER_PNCC_SUBJECT_INVALID".into())
}
}
fn validate_record(
record: &UserPnccBindingRecord,
expected_id: &str,
number: &str,
session: &LoginSession,
) -> Result<(), String> {
validate_subject(number, session)?;
if record.schema == BINDING_SCHEMA
&& record.repository_id == expected_id
&& record.user_number == number
&& record.domain == session.domain
&& record.account_username == session.username
&& record.account_host == session.host
{
Ok(())
} else {
Err("HOLOLAKE_USER_PNCC_BINDING_MISMATCH".into())
}
}
fn initialize_repository(repository: &Path, record: &UserPnccBindingRecord) -> Result<(), String> {
fs::create_dir_all(repository.join(".hololake"))
.map_err(|error| format!("HOLOLAKE_USER_PNCC_REPOSITORY_CREATE_FAILED: {error}"))?;
run_git(repository, &["init", "--initial-branch=main"], "INIT")?;
run_git(
repository,
&["config", "user.name", &record.account_username],
"CONFIG_NAME",
)?;
let email = format!("{}@users.hololake.invalid", record.account_username);
run_git(
repository,
&["config", "user.email", &email],
"CONFIG_EMAIL",
)?;
let manifest = serde_json::json!({
"schema": "hololake.user-pncc-channel-manifest/v1",
"repositoryId": record.repository_id,
"userNumber": record.user_number,
"domain": record.domain,
"account": { "username": record.account_username, "host": record.account_host },
"engine": "GIT",
"humanProjection": "HOLOLAKE_NATIVE",
"forgejoRole": "OPTIONAL_REMOTE_COLLABORATION_ADAPTER",
"personaBindingClaimed": false,
"authority": "LOCAL_USER_CODE_CHANNEL_NO_PUSH_DEPLOY_OR_REALITY_EXECUTION_AUTHORITY",
"createdAtUnixMs": record.created_at_unix_ms,
});
fs::write(
repository.join(".hololake/channel.json"),
format!(
"{}\n",
serde_json::to_string_pretty(&manifest)
.map_err(|error| format!("HOLOLAKE_USER_PNCC_MANIFEST_INVALID: {error}"))?
),
)
.map_err(|error| format!("HOLOLAKE_USER_PNCC_MANIFEST_WRITE_FAILED: {error}"))?;
fs::write(
repository.join("README.md"),
format!(
"# GH-PNCC · 人格原生代码频道\n\n这是 HoloLake 为用户 `{}` 建立的本机原生代码频道。\n\n- 耐久化引擎Git\n- 人类可见上层HoloLake\n- 远端协作Forgejo 适配器(尚未绑定远端仓库)\n- 用户编号:`{}`\n\n本仓库不保存账号密码,也不因建立代码频道而声称人格绑定或授予推送、发布、部署与现实执行权限。\n",
record.account_username, record.user_number
),
)
.map_err(|error| format!("HOLOLAKE_USER_PNCC_README_WRITE_FAILED: {error}"))?;
run_git(repository, &["add", "--all"], "ADD")?;
run_git(
repository,
&["commit", "-m", "初始化 GH-PNCC 人格原生代码频道"],
"COMMIT",
)?;
Ok(())
}
fn project(
repository: &Path,
record: UserPnccBindingRecord,
channel_id: String,
) -> Result<UserPnccChannelSnapshot, String> {
let git_head = run_git(repository, &["rev-parse", "HEAD"], "READ_HEAD")?
.trim()
.to_string();
let branch = run_git(
repository,
&["symbolic-ref", "--short", "HEAD"],
"READ_BRANCH",
)?
.trim()
.to_string();
let repository_clean = run_git(repository, &["status", "--porcelain"], "READ_STATUS")?
.trim()
.is_empty();
Ok(UserPnccChannelSnapshot {
schema: SNAPSHOT_SCHEMA,
state: "READY",
repository_id: record.repository_id,
channel_id,
user_number: record.user_number,
domain: record.domain,
account_username: record.account_username,
account_host: record.account_host,
local_path: repository.to_string_lossy().into_owned(),
branch,
git_head,
repository_clean,
created_at_unix_ms: record.created_at_unix_ms,
git_engine: "GIT",
human_projection: "HOLOLAKE_NATIVE",
forgejo_adapter_state: "AUTHENTICATED_REMOTE_REPOSITORY_UNBOUND",
remote_repository_url: None,
persona_binding_claimed: false,
authority: "LOCAL_USER_CODE_CHANNEL_NO_PUSH_DEPLOY_OR_REALITY_EXECUTION_AUTHORITY",
})
}
fn native_channel_name(session: &LoginSession) -> String {
format!("{} · GH-PNCC", session.username)
}
fn write_json_atomic(path: &Path, value: &UserPnccBindingRecord) -> Result<(), String> {
let bytes = serde_json::to_vec_pretty(value)
.map_err(|error| format!("HOLOLAKE_USER_PNCC_BINDING_INVALID: {error}"))?;
let temporary = path.with_extension(format!("tmp-{}", Uuid::new_v4()));
let mut options = OpenOptions::new();
options.create_new(true).write(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options
.open(&temporary)
.map_err(|error| format!("HOLOLAKE_USER_PNCC_BINDING_WRITE_FAILED: {error}"))?;
file.write_all(&bytes)
.and_then(|_| file.sync_all())
.map_err(|error| format!("HOLOLAKE_USER_PNCC_BINDING_WRITE_FAILED: {error}"))?;
fs::rename(&temporary, path)
.map_err(|error| format!("HOLOLAKE_USER_PNCC_BINDING_WRITE_FAILED: {error}"))
}
fn run_git(root: &Path, args: &[&str], operation: &str) -> Result<String, String> {
let output = Command::new("/usr/bin/git")
.current_dir(root)
.env("GIT_TERMINAL_PROMPT", "0")
.env("GIT_CONFIG_NOSYSTEM", "1")
.args(args)
.output()
.map_err(|error| format!("HOLOLAKE_USER_PNCC_GIT_{operation}_FAILED: {error}"))?;
if !output.status.success() {
return Err(format!(
"HOLOLAKE_USER_PNCC_GIT_{operation}_FAILED: {}",
String::from_utf8_lossy(&output.stderr).trim()
));
}
String::from_utf8(output.stdout)
.map_err(|error| format!("HOLOLAKE_USER_PNCC_GIT_{operation}_INVALID_UTF8: {error}"))
}
fn sha256_hex(bytes: &[u8]) -> String {
digest(&SHA256, bytes)
.as_ref()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
fn now_unix_ms() -> Result<u128, String> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis())
.map_err(|error| format!("HOLOLAKE_CLOCK_INVALID: {error}"))
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
fn session() -> LoginSession {
LoginSession {
username: "lake_user".into(),
host: "guanghulab.com".into(),
domain: "FIFTH_DOMAIN".into(),
signed_in_at_unix_ms: 1,
}
}
#[test]
fn initialization_is_idempotent_and_creates_a_clean_main_repository() {
let root = tempdir().unwrap();
let first = ensure_at(root.path(), "ICE-P-TEST001", &session(), |_, _| {
Ok("channel-one".into())
})
.unwrap();
let second = ensure_at(root.path(), "ICE-P-TEST001", &session(), |_, _| {
Ok("channel-one".into())
})
.unwrap();
assert_eq!(first.repository_id, second.repository_id);
assert_eq!(first.git_head, second.git_head);
assert_eq!(first.branch, "main");
assert!(first.repository_clean);
assert!(!first.persona_binding_claimed);
}
#[test]
fn binding_and_repository_never_contain_a_password_field() {
let root = tempdir().unwrap();
let snapshot = ensure_at(root.path(), "ICE-P-TEST001", &session(), |_, _| {
Ok("channel-one".into())
})
.unwrap();
let binding =
fs::read_to_string(binding_path(root.path(), &snapshot.repository_id)).unwrap();
let manifest = fs::read_to_string(
repository_path(root.path(), &snapshot.repository_id).join(".hololake/channel.json"),
)
.unwrap();
assert!(!binding.to_ascii_lowercase().contains("password"));
assert!(!manifest.to_ascii_lowercase().contains("password"));
}
#[test]
fn identity_tuple_selects_a_distinct_repository_without_overwriting_another_user() {
let root = tempdir().unwrap();
let first = ensure_at(root.path(), "ICE-P-TEST001", &session(), |_, _| {
Ok("one".into())
})
.unwrap();
let second = ensure_at(root.path(), "ICE-P-TEST002", &session(), |_, _| {
Ok("two".into())
})
.unwrap();
assert_ne!(first.repository_id, second.repository_id);
assert!(Path::new(&first.local_path).exists());
assert!(Path::new(&second.local_path).exists());
}
}

View file

@ -28,11 +28,21 @@ pub struct ZeroPointProtocol {
pub origin: String,
}
fn default_grace_days() -> u64 { 7 }
fn default_anchor_url() -> String { "https://guanghulab.com/api/ai/v1/anchor".into() }
fn default_resolve_url() -> String { "https://guanghulab.com/api/ai/v1/resolve?id=".into() }
fn default_core_source() -> String { "https://guanghulab.com/code/bingshuo/guanghu-ice-heart".into() }
fn default_protocol_origin() -> String { "FACTORY_DEFAULT第五域协议就位后自动覆盖".into() }
fn default_grace_days() -> u64 {
7
}
fn default_anchor_url() -> String {
"https://guanghulab.com/api/ai/v1/anchor".into()
}
fn default_resolve_url() -> String {
"https://guanghulab.com/api/ai/v1/resolve?id=".into()
}
fn default_core_source() -> String {
"https://guanghulab.com/code/bingshuo/guanghu-ice-heart".into()
}
fn default_protocol_origin() -> String {
"FACTORY_DEFAULT第五域协议就位后自动覆盖".into()
}
impl Default for ZeroPointProtocol {
fn default() -> Self {
@ -101,16 +111,25 @@ impl Default for ZeroPointState {
}
fn now_secs() -> u64 {
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
fn lock(state: &ZeroPointState) -> Result<std::sync::MutexGuard<'_, ZeroPointInner>, String> {
state.inner.lock().map_err(|_| "HOLOLAKE_ZP_LOCK".to_string())
state
.inner
.lock()
.map_err(|_| "HOLOLAKE_ZP_LOCK".to_string())
}
/// 初始化应用数据目录并读取验证协议与本机绑定记录。
pub fn boot_zero_point(app: &tauri::AppHandle, state: &ZeroPointState) -> Result<(), String> {
let base = app.path().app_data_dir().map_err(|e| format!("HOLOLAKE_ZP_HOME_FAILED: {e}"))?;
let base = app
.path()
.app_data_dir()
.map_err(|e| format!("HOLOLAKE_ZP_HOME_FAILED: {e}"))?;
let home = base.join(".zero-point-core");
for sub in ["", "ledger", "core"] {
fs::create_dir_all(home.join(sub)).map_err(|e| format!("HOLOLAKE_ZP_HOME_FAILED: {e}"))?;
@ -130,7 +149,8 @@ pub fn boot_zero_point(app: &tauri::AppHandle, state: &ZeroPointState) -> Result
.ok()
.and_then(|raw| serde_json::from_str::<ZeroPointProtocol>(&raw).ok())
.unwrap_or_default();
let (binding, user_number, resolved_name, resolved_domain, last_valid_check) = read_binding(&home);
let (binding, user_number, resolved_name, resolved_domain, last_valid_check) =
read_binding(&home);
let mut inner = lock(state)?;
inner.home = home.clone();
@ -143,48 +163,93 @@ pub fn boot_zero_point(app: &tauri::AppHandle, state: &ZeroPointState) -> Result
inner.route = decide_route(&binding, last_valid_check, &protocol);
let route = inner.route.clone();
drop(inner);
append_heartbeat(&home, &format!("boot route={route} binding={binding} protocol_origin={}", protocol.origin));
append_heartbeat(
&home,
&format!(
"boot route={route} binding={binding} protocol_origin={}",
protocol.origin
),
);
Ok(())
}
fn read_binding(home: &Path) -> (String, String, String, String, u64) {
#[derive(Deserialize)]
struct B {
#[serde(default)] number: String,
#[serde(default)] resolved_name: String,
#[serde(default)] resolved_domain: String,
#[serde(default)] last_valid_check: u64,
#[serde(default)]
number: String,
#[serde(default)]
resolved_name: String,
#[serde(default)]
resolved_domain: String,
#[serde(default)]
last_valid_check: u64,
}
fs::read_to_string(home.join("binding.json"))
.ok()
.and_then(|raw| serde_json::from_str::<B>(&raw).ok())
.filter(|b| !b.number.is_empty())
.map(|b| ("bound".to_string(), b.number, b.resolved_name, b.resolved_domain, b.last_valid_check))
.unwrap_or_else(|| ("waiting".into(), String::new(), String::new(), String::new(), 0))
.map(|b| {
(
"bound".to_string(),
b.number,
b.resolved_name,
b.resolved_domain,
b.last_valid_check,
)
})
.unwrap_or_else(|| {
(
"waiting".into(),
String::new(),
String::new(),
String::new(),
0,
)
})
}
fn write_binding(home: &Path, number: &str, resolved_name: &str, resolved_domain: &str, last_valid_check: u64) -> Result<(), String> {
fn write_binding(
home: &Path,
number: &str,
resolved_name: &str,
resolved_domain: &str,
last_valid_check: u64,
) -> Result<(), String> {
let body = serde_json::json!({
"number": number,
"resolved_name": resolved_name,
"resolved_domain": resolved_domain,
"last_valid_check": last_valid_check,
});
fs::write(home.join("binding.json"), serde_json::to_string_pretty(&body).unwrap_or_default())
.map_err(|e| format!("HOLOLAKE_ZP_BINDING_FAILED: {e}"))
fs::write(
home.join("binding.json"),
serde_json::to_string_pretty(&body).unwrap_or_default(),
)
.map_err(|e| format!("HOLOLAKE_ZP_BINDING_FAILED: {e}"))
}
fn decide_route(binding: &str, last_valid_check: u64, protocol: &ZeroPointProtocol) -> String {
if binding != "bound" || last_valid_check == 0 { return "restricted".into(); }
if binding != "bound" || last_valid_check == 0 {
return "restricted".into();
}
let grace = protocol.grace_period_days.saturating_mul(86_400);
if now_secs() <= last_valid_check + grace { "verified".into() } else { "restricted".into() }
if now_secs() <= last_valid_check + grace {
"verified".into()
} else {
"restricted".into()
}
}
/// 件5·心跳账本机只追加编号哈希化处理不同步敏感原文。
fn append_heartbeat(home: &Path, event: &str) {
use std::io::Write;
let line = serde_json::json!({ "ts": now_secs(), "event": event });
if let Ok(mut file) = fs::OpenOptions::new().create(true).append(true).open(home.join("ledger").join("heartbeat.jsonl")) {
if let Ok(mut file) = fs::OpenOptions::new()
.create(true)
.append(true)
.open(home.join("ledger").join("heartbeat.jsonl"))
{
let _ = writeln!(file, "{line}");
}
}
@ -195,15 +260,44 @@ fn home_of(state: &State<'_, ZeroPointState>) -> Result<PathBuf, String> {
fn home_of_inner(state: &ZeroPointState) -> Result<PathBuf, String> {
let inner = lock(state)?;
if inner.home.as_os_str().is_empty() { return Err("HOLOLAKE_ZP_NOT_READY".into()); }
if inner.home.as_os_str().is_empty() {
return Err("HOLOLAKE_ZP_NOT_READY".into());
}
Ok(inner.home.clone())
}
pub(crate) fn verified_user_route(
state: &ZeroPointState,
) -> Result<Option<(String, String)>, String> {
let inner = lock(state)?;
if inner.binding == "bound" && inner.route == "verified" && !inner.user_number.is_empty() {
if inner.resolved_domain.is_empty() {
return Err("HOLOLAKE_ZP_DOMAIN_ROUTE_REQUIRED".into());
}
Ok(Some((
inner.user_number.clone(),
inner.resolved_domain.clone(),
)))
} else {
Ok(None)
}
}
/// 登录绑定:用户编号入仓(等待绑定态→绑定态)。空白态拒绝一切唤醒。
#[tauri::command]
pub async fn zero_point_bind(state: State<'_, ZeroPointState>, input: serde_json::Value) -> Result<ZeroPointSnapshot, String> {
let number = input.get("number").and_then(|v| v.as_str()).unwrap_or("").trim().to_string();
if number.is_empty() { return Err("HOLOLAKE_ZP_EMPTY_NUMBER".into()); }
pub async fn zero_point_bind(
state: State<'_, ZeroPointState>,
input: serde_json::Value,
) -> Result<ZeroPointSnapshot, String> {
let number = input
.get("number")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
if number.is_empty() {
return Err("HOLOLAKE_ZP_EMPTY_NUMBER".into());
}
let home = home_of(&state)?;
write_binding(&home, &number, "", "", 0)?;
{
@ -221,25 +315,36 @@ pub async fn zero_point_bind(state: State<'_, ZeroPointState>, input: serde_json
/// 通过登记服务执行三态裁决PASS / REJECT / OFFLINE并应用离线宽限期。
#[tauri::command]
pub async fn zero_point_verify(state: State<'_, ZeroPointState>) -> Result<ZeroPointSnapshot, String> {
pub async fn zero_point_verify(
state: State<'_, ZeroPointState>,
) -> Result<ZeroPointSnapshot, String> {
let home = home_of(&state)?;
let (number, resolve_url) = {
let inner = lock(&state)?;
(inner.user_number.clone(), inner.protocol.lighthouse_resolve_url.clone())
(
inner.user_number.clone(),
inner.protocol.lighthouse_resolve_url.clone(),
)
};
if number.is_empty() {
append_heartbeat(&home, "verify verdict=REJECT reason=waiting_binding");
return zero_point_status(state).await;
}
let client = reqwest::Client::builder().timeout(Duration::from_secs(15)).build()
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(15))
.build()
.map_err(|e| format!("HOLOLAKE_ZP_HTTP_FAILED: {e}"))?;
let (verdict, resolution) = match client.get(format!("{resolve_url}{number}")).send().await {
Ok(resp) => {
let ok = resp.status().is_success();
let body = resp.text().await.unwrap_or_default();
let resolution = ok.then(|| lighthouse_resolution(&body, &number)).flatten();
if resolution.is_some() { ("PASS".to_string(), resolution) } else { ("REJECT".to_string(), None) }
if resolution.is_some() {
("PASS".to_string(), resolution)
} else {
("REJECT".to_string(), None)
}
}
Err(_) => ("OFFLINE".to_string(), None),
};
@ -254,7 +359,13 @@ pub async fn zero_point_verify(state: State<'_, ZeroPointState>) -> Result<ZeroP
inner.route = "verified".into();
inner.resolved_name = resolution.name.clone();
inner.resolved_domain = resolution.domain.clone();
write_binding(&home, &inner.user_number.clone(), &inner.resolved_name, &inner.resolved_domain, inner.last_valid_check)?;
write_binding(
&home,
&inner.user_number.clone(),
&inner.resolved_name,
&inner.resolved_domain,
inner.last_valid_check,
)?;
append_heartbeat(&home, "verify verdict=PASS route=verified");
}
"REJECT" => {
@ -283,21 +394,50 @@ struct LighthouseResolution {
fn lighthouse_resolution(body: &str, expected_number: &str) -> Option<LighthouseResolution> {
let normalized = body.trim();
if normalized.eq_ignore_ascii_case("RESOLVED") || normalized.eq_ignore_ascii_case("PASS") {
return Some(LighthouseResolution { name: String::new(), domain: String::new() });
}
let Ok(value) = serde_json::from_str::<serde_json::Value>(normalized) else { return None; };
let Ok(value) = serde_json::from_str::<serde_json::Value>(normalized) else {
return None;
};
let positive = value.get("valid").and_then(|item| item.as_bool()) == Some(true)
|| value.get("state").and_then(|item| item.as_str()).is_some_and(|state| state.eq_ignore_ascii_case("RESOLVED") || state.eq_ignore_ascii_case("PASS"))
|| value.get("status").and_then(|item| item.as_str()).is_some_and(|status| status.eq_ignore_ascii_case("RESOLVED") || status.eq_ignore_ascii_case("PASS"));
if !positive { return None; }
let returned_number = value.get("canonical_id").and_then(|item| item.as_str())
|| value
.get("state")
.and_then(|item| item.as_str())
.is_some_and(|state| {
state.eq_ignore_ascii_case("RESOLVED") || state.eq_ignore_ascii_case("PASS")
})
|| value
.get("status")
.and_then(|item| item.as_str())
.is_some_and(|status| {
status.eq_ignore_ascii_case("RESOLVED") || status.eq_ignore_ascii_case("PASS")
});
if !positive {
return None;
}
let returned_number = value
.get("canonical_id")
.and_then(|item| item.as_str())
.or_else(|| value.pointer("/subject/id").and_then(|item| item.as_str()))
.or_else(|| value.get("requested_id").and_then(|item| item.as_str()));
if returned_number.is_some_and(|number| number != expected_number) { return None; }
if returned_number != Some(expected_number) {
return None;
}
let domain = value
.pointer("/subject/domain")
.and_then(|item| item.as_str())
.unwrap_or("");
if !matches!(
domain,
"FIFTH_DOMAIN" | "MAIN_DOMAIN" | "BRANCH_DOMAIN" | "ZERO_DOMAIN" | "ZERO_SENSE_DOMAIN"
) {
return None;
}
Some(LighthouseResolution {
name: value.pointer("/subject/name").and_then(|item| item.as_str()).unwrap_or("").to_string(),
domain: value.pointer("/subject/domain").and_then(|item| item.as_str()).unwrap_or("").to_string(),
name: value
.pointer("/subject/name")
.and_then(|item| item.as_str())
.unwrap_or("")
.to_string(),
domain: domain.to_string(),
})
}
@ -308,12 +448,18 @@ pub async fn sync_protocol_runtime(state: &ZeroPointState) -> Result<(), String>
let local_version = fs::read_to_string(home.join("core").join("VERSION")).unwrap_or_default();
let pubkey_ready = home.join("core").join("pubkey.pem").exists();
let client = reqwest::Client::builder().timeout(Duration::from_secs(15)).build()
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(15))
.build()
.map_err(|e| format!("HOLOLAKE_ZP_HTTP_FAILED: {e}"))?;
let note = match client.get(&anchor_url).send().await {
Ok(resp) if resp.status().is_success() => {
let body: serde_json::Value = resp.json().await.unwrap_or_default();
let remote_version = body.get("version").and_then(|v| v.as_str()).unwrap_or("").to_string();
let remote_version = body
.get("version")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if remote_version.is_empty() {
"协议版本信息不可用,本次未执行更新。".into()
} else if remote_version == local_version.trim() {
@ -338,14 +484,18 @@ pub async fn sync_protocol_runtime(state: &ZeroPointState) -> Result<(), String>
}
#[tauri::command]
pub async fn zero_point_sync(state: State<'_, ZeroPointState>) -> Result<ZeroPointSnapshot, String> {
pub async fn zero_point_sync(
state: State<'_, ZeroPointState>,
) -> Result<ZeroPointSnapshot, String> {
sync_protocol_runtime(&state).await?;
zero_point_status(state).await
}
/// 返回当前编号验证与协议状态快照。
#[tauri::command]
pub async fn zero_point_status(state: State<'_, ZeroPointState>) -> Result<ZeroPointSnapshot, String> {
pub async fn zero_point_status(
state: State<'_, ZeroPointState>,
) -> Result<ZeroPointSnapshot, String> {
let inner = lock(&state)?;
let grace = inner.protocol.grace_period_days.saturating_mul(86_400);
Ok(ZeroPointSnapshot {
@ -355,7 +505,11 @@ pub async fn zero_point_status(state: State<'_, ZeroPointState>) -> Result<ZeroP
resolved_name: inner.resolved_name.clone(),
resolved_domain: inner.resolved_domain.clone(),
last_valid_check: inner.last_valid_check,
grace_deadline: if inner.last_valid_check > 0 { inner.last_valid_check + grace } else { 0 },
grace_deadline: if inner.last_valid_check > 0 {
inner.last_valid_check + grace
} else {
0
},
protocol: inner.protocol.clone(),
sync_note: inner.sync_note.clone(),
})
@ -371,7 +525,10 @@ mod tests {
assert_eq!(decide_route("waiting", 0, &protocol), "restricted");
assert_eq!(decide_route("bound", 0, &protocol), "restricted");
assert_eq!(decide_route("bound", now_secs(), &protocol), "verified");
assert_eq!(decide_route("bound", now_secs() - 8 * 86_400, &protocol), "restricted");
assert_eq!(
decide_route("bound", now_secs() - 8 * 86_400, &protocol),
"restricted"
);
}
#[test]
@ -379,17 +536,27 @@ mod tests {
let protocol = ZeroPointProtocol::default();
assert_eq!(protocol.grace_period_days, 7);
assert!(protocol.origin.contains("FACTORY_DEFAULT"));
assert!(protocol.lighthouse_resolve_url.starts_with("https://guanghulab.com"));
assert!(protocol
.lighthouse_resolve_url
.starts_with("https://guanghulab.com"));
}
#[test]
fn lighthouse_requires_an_explicit_positive_verdict() {
assert!(lighthouse_resolution("RESOLVED", "ICE-GL∞").is_some());
assert!(lighthouse_resolution(r#"{"valid":true}"#, "ICE-GL∞").is_some());
assert!(lighthouse_resolution("RESOLVED", "ICE-GL∞").is_none());
assert!(lighthouse_resolution(r#"{"valid":true}"#, "ICE-GL∞").is_none());
let resolved = lighthouse_resolution(r#"{"status":"RESOLVED","canonical_id":"ICE-GL∞","subject":{"id":"ICE-GL∞","name":"冰朔","domain":"FIFTH_DOMAIN"}}"#, "ICE-GL∞").unwrap();
assert_eq!(resolved.name, "冰朔");
assert_eq!(resolved.domain, "FIFTH_DOMAIN");
assert!(lighthouse_resolution(r#"{"status":"PASS","canonical_id":"OTHER"}"#, "ICE-GL∞").is_none());
assert!(lighthouse_resolution(
r#"{"status":"RESOLVED","canonical_id":"ICE-GL∞","subject":{"domain":"UNKNOWN"}}"#,
"ICE-GL∞"
)
.is_none());
assert!(
lighthouse_resolution(r#"{"status":"PASS","canonical_id":"OTHER"}"#, "ICE-GL∞")
.is_none()
);
assert!(lighthouse_resolution("{}", "ICE-GL∞").is_none());
assert!(lighthouse_resolution("route_not_found", "ICE-GL∞").is_none());
assert!(lighthouse_resolution("an arbitrary successful response", "ICE-GL∞").is_none());

View file

@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "HoloLake",
"version": "0.3.0",
"version": "0.4.0",
"identifier": "world.guanghu.hololake",
"build": {
"frontendDist": "../dist",