feat: ship dual-signed online module marketplace

This commit is contained in:
冰朔 2026-08-19 18:24:50 +08:00
commit b0eeade03d
40 changed files with 5774 additions and 111 deletions

View file

@ -10,6 +10,12 @@ use crate::dynamic_capability_routing::{
routing_root as dynamic_routing_root, DynamicNodeRegistry, ResolveCapabilityRouteInput,
SignedNodeHealth,
};
use crate::human_authorization::{
consume_at as consume_authorization_at,
root_from_session_root as authorization_root_from_session_root,
status_for_requester_at as authorization_status_at, submit_at as submit_authorization_at,
AuthorizationProposalInput, ConsumeAuthorizationInput, RequesterContext,
};
use crate::local_development_bridge::{
account_key_for as development_account_key_for, acquire_at as acquire_development_lane_at,
inspect_at as inspect_development_lane_at, release_at as release_development_lane_at,
@ -203,6 +209,7 @@ struct BrokerStorageRoots {
persona_time: PathBuf,
persona_license: PathBuf,
development: PathBuf,
authorization: PathBuf,
}
impl Drop for DirectLocalBrokerHandle {
@ -245,6 +252,9 @@ enum BrokerRequest {
AcquireDevelopmentWriteLane(AuthenticatedDevelopmentLaneInput),
InspectDevelopmentWriteLane(AuthenticatedDevelopmentInspectInput),
ReleaseDevelopmentWriteLane(AuthenticatedDevelopmentReleaseInput),
SubmitHumanAuthorizationRequest(AuthenticatedAuthorizationProposalInput),
GetHumanAuthorizationStatus(AuthenticatedWorkEnvironmentInput),
ConsumeHumanAuthorizationTicket(AuthenticatedAuthorizationConsumeInput),
}
#[derive(Clone, Debug, Deserialize)]
@ -350,6 +360,20 @@ struct AuthenticatedWorkEnvironmentInput {
session: AuthenticateSessionInput,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct AuthenticatedAuthorizationProposalInput {
session: AuthenticateSessionInput,
proposal: AuthorizationProposalInput,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct AuthenticatedAuthorizationConsumeInput {
session: AuthenticateSessionInput,
ticket: ConsumeAuthorizationInput,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct AuthenticatedPersonaCarrierLicenseInput {
@ -444,7 +468,7 @@ fn load_number_registry() -> Result<BrokerNumberRegistry, String> {
|| registry.runtime.unknown_or_mismatched_coordinate != "FAIL_CLOSED"
|| !registry.runtime.request_nonce_required
|| registry.runtime.transport_is_authority
|| registry.operations.len() != 22
|| registry.operations.len() != 25
{
return Err("HOLOLAKE_NUMBERED_BROKER_REGISTRY_BOUNDARY_INVALID".into());
}
@ -843,6 +867,7 @@ fn start_at(
let development_root = development_root_from_session_root(&session_root)?;
fs::create_dir_all(&development_root)
.map_err(|error| format!("HOLOLAKE_BRIDGE_STORAGE_UNAVAILABLE: {error}"))?;
let authorization_root = authorization_root_from_session_root(&session_root)?;
#[cfg(unix)]
if let Some(parent) = socket_path.parent() {
fs::create_dir_all(parent)
@ -903,6 +928,7 @@ fn start_at(
persona_time: persona_time_root,
persona_license: persona_license_root,
development: development_root,
authorization: authorization_root,
},
&worker_shutdown,
&worker_authenticated_connections,
@ -1025,6 +1051,7 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
let persona_time_root = &roots.persona_time;
let persona_license_root = &roots.persona_license;
let development_root = &roots.development;
let authorization_root = &roots.authorization;
let (request, _, _) = match decode_numbered_broker_request(bytes) {
Ok(request) => request,
Err(error) => {
@ -1295,6 +1322,72 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
serde_json::to_value(receipt).map_err(|error| error.to_string())
})
}
BrokerRequest::SubmitHumanAuthorizationRequest(input) => {
authenticate_context_at(session_root, &input.session)
.and_then(|context| {
require_persona_mode_operation_at(
persona_license_root,
&context.account_key,
&context.session_id,
&context.client_instance_id,
"SUBMIT_HUMAN_AUTHORIZATION_REQUEST",
now_unix_ms()?,
)?;
submit_authorization_at(
authorization_root,
&RequesterContext {
account_key: context.account_key,
session_id: context.session_id,
client_instance_id: context.client_instance_id,
},
input.proposal,
)
})
.and_then(|receipt| {
serde_json::to_value(receipt).map_err(|error| error.to_string())
})
}
BrokerRequest::GetHumanAuthorizationStatus(input) => {
authenticate_context_at(session_root, &input.session)
.and_then(|context| {
authorization_status_at(
authorization_root,
&RequesterContext {
account_key: context.account_key,
session_id: context.session_id,
client_instance_id: context.client_instance_id,
},
)
})
.and_then(|snapshot| {
serde_json::to_value(snapshot).map_err(|error| error.to_string())
})
}
BrokerRequest::ConsumeHumanAuthorizationTicket(input) => {
authenticate_context_at(session_root, &input.session)
.and_then(|context| {
require_persona_mode_operation_at(
persona_license_root,
&context.account_key,
&context.session_id,
&context.client_instance_id,
"CONSUME_HUMAN_AUTHORIZATION_TICKET",
now_unix_ms()?,
)?;
consume_authorization_at(
authorization_root,
&RequesterContext {
account_key: context.account_key,
session_id: context.session_id,
client_instance_id: context.client_instance_id,
},
input.ticket,
)
})
.and_then(|receipt| {
serde_json::to_value(receipt).map_err(|error| error.to_string())
})
}
};
match result {
Ok(value) => BrokerResponse::success(value),

View file

@ -120,6 +120,18 @@ pub struct SessionEventReceipt {
pub receipt_id: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DirectSessionProjection {
pub session_id: String,
pub lane_id: String,
pub client_instance_id: String,
pub state: &'static str,
pub opened_at_unix_ms: u128,
pub observed_at_unix_ms: u128,
pub last_event_sequence: u64,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct SessionRecord {
@ -262,6 +274,52 @@ pub(crate) fn active_session_count_at(root: &Path) -> Result<usize, String> {
Ok(count)
}
pub(crate) fn active_session_projections_at(
root: &Path,
) -> Result<Vec<DirectSessionProjection>, String> {
let accounts = root.join("accounts");
if !accounts.exists() {
return Ok(Vec::new());
}
let now = now_unix_ms()?;
let mut projections = Vec::new();
for entry in fs::read_dir(&accounts)
.map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_STORAGE_UNAVAILABLE: {error}"))?
{
let entry = entry
.map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_STORAGE_UNAVAILABLE: {error}"))?;
if !entry
.file_type()
.map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_STORAGE_UNAVAILABLE: {error}"))?
.is_dir()
{
continue;
}
let active_path = entry.path().join("active-session.json");
if !active_path.exists() {
continue;
}
let active: ActiveSessionRecord = read_json(&active_path, "ACTIVE_SESSION")?;
let record = read_session(&session_path(root, &active.account_key, &active.session_id))?;
let state = if now.saturating_sub(record.observed_at_unix_ms) <= 45_000 {
"LIVE"
} else {
"RESUMABLE"
};
projections.push(DirectSessionProjection {
session_id: record.session_id,
lane_id: record.lane_id,
client_instance_id: record.client_instance_id,
state,
opened_at_unix_ms: record.opened_at_unix_ms,
observed_at_unix_ms: record.observed_at_unix_ms,
last_event_sequence: record.last_event_sequence,
});
}
projections.sort_by(|left, right| right.observed_at_unix_ms.cmp(&left.observed_at_unix_ms));
Ok(projections)
}
pub(crate) fn open_at(
root: &Path,
input: OpenSessionInput,

View file

@ -1,7 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::direct_local_broker::DirectLocalBrokerState;
use crate::direct_local_session::{active_session_count_at, direct_session_root};
use crate::direct_local_session::{
active_session_count_at, active_session_projections_at, direct_session_root,
DirectSessionProjection,
};
use crate::pncc_receipt_projection::{pncc_projection_root, projection_event_count_at};
use crate::pncc_repository_binding::{mounted_repository_count_at, pncc_repository_mount_root};
use crate::release_trust::release_trust_state;
@ -15,6 +18,7 @@ pub struct HoloLakeHomeStatus {
pub direct_local_broker_state: &'static str,
pub direct_connection_count: usize,
pub resumable_session_count: usize,
pub direct_sessions: Vec<DirectSessionProjection>,
pub code_repository_mount_count: usize,
pub pncc_receipt_count: usize,
pub update_state: &'static str,
@ -37,6 +41,7 @@ pub fn get_hololake_home_status(
direct_local_broker_state: "WAITING_FOR_LOGIN",
direct_connection_count: 0,
resumable_session_count: 0,
direct_sessions: Vec::new(),
code_repository_mount_count: 0,
pncc_receipt_count: 0,
update_state: release_trust_state()?,
@ -60,6 +65,7 @@ pub fn get_hololake_home_status(
direct_local_broker_state: "READY",
direct_connection_count: broker.active_connection_count(),
resumable_session_count: active_session_count_at(&session_root)?,
direct_sessions: active_session_projections_at(&session_root)?,
code_repository_mount_count: mounted_repository_count_at(&mount_root)?,
pncc_receipt_count: projection_event_count_at(&projection_root)?,
update_state: release_trust_state()?,

View file

@ -0,0 +1,704 @@
//! Human-in-the-loop authorization for numbered lifecycle work.
//!
//! Persona carriers may propose an exact action from an authenticated local session.
//! Only the verified human route may approve it. Approval creates a short-lived,
//! session-bound ticket that is consumed once and leaves a hash-chained receipt.
use ring::digest::{digest, SHA256};
use rusqlite::{params, Connection, OptionalExtension};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use tauri::AppHandle;
use uuid::Uuid;
const SCHEMA: &str = "hololake.human-authorization/v1";
const CONTRACT: &str = include_str!("../../contracts/human-authorization.json");
const REQUEST_TTL_MS: u64 = 24 * 60 * 60 * 1_000;
const TICKET_TTL_MS: u64 = 15 * 60 * 1_000;
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AuthorizationProposalInput {
pub idempotency_key: String,
pub action: String,
pub target_number: String,
pub target_kind: String,
pub target_label: String,
pub reason: String,
pub impact: String,
pub rollback_plan: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct DecideAuthorizationInput {
pub request_id: String,
pub decision: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ConsumeAuthorizationInput {
pub request_id: String,
pub ticket_id: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthorizationRequestView {
pub request_id: String,
pub action: String,
pub target_number: String,
pub target_kind: String,
pub target_label: String,
pub reason: String,
pub impact: String,
pub rollback_plan: String,
pub requester_label: String,
pub state: String,
pub created_at_unix_ms: u64,
pub expires_at_unix_ms: u64,
pub human_number: Option<String>,
pub decided_at_unix_ms: Option<u64>,
pub ticket_id: Option<String>,
pub ticket_expires_at_unix_ms: Option<u64>,
pub consumed_at_unix_ms: Option<u64>,
pub receipt_hash: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthorizationCenterSnapshot {
pub schema: &'static str,
pub state: &'static str,
pub pending_count: usize,
pub requests: Vec<AuthorizationRequestView>,
pub supported_actions: Vec<&'static str>,
pub purge_enabled: bool,
pub authority: &'static str,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthorizationTicketReceipt {
pub schema: &'static str,
pub state: &'static str,
pub request_id: String,
pub ticket_id: String,
pub action: String,
pub target_number: String,
pub scope_sha256: String,
pub consumed_at_unix_ms: u64,
pub receipt_hash: String,
pub next_operation: &'static str,
}
#[derive(Clone, Debug)]
pub struct RequesterContext {
pub account_key: String,
pub session_id: String,
pub client_instance_id: String,
}
pub fn root_for_app(app: &AppHandle) -> Result<PathBuf, String> {
crate::authenticated_storage::account_storage_root(app, "human-authorization-v1")
}
pub fn start_on_application_open() -> Result<(), String> {
let contract: serde_json::Value = serde_json::from_str(CONTRACT)
.map_err(|error| format!("HOLOLAKE_AUTHORIZATION_CONTRACT_INVALID: {error}"))?;
if contract.get("schema").and_then(serde_json::Value::as_str)
!= Some("hololake.human-authorization-contract/v1")
|| contract
.get("record_id")
.and_then(serde_json::Value::as_str)
!= Some("HLP-HUMAN-AUTHORIZATION-001")
|| contract
.pointer("/ticket/ttl_ms")
.and_then(serde_json::Value::as_u64)
!= Some(TICKET_TTL_MS)
|| contract
.pointer("/request/ttl_ms")
.and_then(serde_json::Value::as_u64)
!= Some(REQUEST_TTL_MS)
|| contract
.pointer("/destructive_purge/enabled")
.and_then(serde_json::Value::as_bool)
!= Some(false)
|| contract
.pointer("/ticket/single_use")
.and_then(serde_json::Value::as_bool)
!= Some(true)
{
return Err("HOLOLAKE_AUTHORIZATION_CONTRACT_BOUNDARY_INVALID".into());
}
Ok(())
}
pub fn root_from_session_root(session_root: &Path) -> Result<PathBuf, String> {
let account_root = session_root
.parent()
.ok_or("HOLOLAKE_AUTHORIZATION_STORAGE_BOUNDARY_INVALID")?;
let root = account_root.join("human-authorization-v1");
fs::create_dir_all(&root)
.map_err(|error| format!("HOLOLAKE_AUTHORIZATION_STORAGE_UNAVAILABLE: {error}"))?;
Ok(root)
}
pub fn get_center(app: &AppHandle) -> Result<AuthorizationCenterSnapshot, String> {
snapshot_at(&root_for_app(app)?, None)
}
pub fn decide(
app: &AppHandle,
input: DecideAuthorizationInput,
human_number: &str,
) -> Result<AuthorizationRequestView, String> {
decide_at(&root_for_app(app)?, input, human_number, now_ms()?)
}
pub fn submit_at(
root: &Path,
requester: &RequesterContext,
input: AuthorizationProposalInput,
) -> Result<AuthorizationRequestView, String> {
validate_proposal(&input)?;
let now = now_ms()?;
let mut connection = open_db(root)?;
let transaction = connection.transaction().map_err(db_error("TRANSACTION"))?;
if let Some(existing) = transaction
.query_row(
"SELECT request_id FROM authorization_requests WHERE requester_account_key=?1 AND idempotency_key=?2",
params![requester.account_key, input.idempotency_key],
|row| row.get::<_, String>(0),
)
.optional()
.map_err(db_error("READ"))?
{
let existing = read_request_with_owner(&transaction, &existing)?;
if existing.requester_session_id != requester.session_id
|| existing.requester_client_instance_id != requester.client_instance_id
|| existing.view.action != input.action
|| existing.view.target_number != input.target_number
|| existing.view.target_kind != input.target_kind
|| existing.view.target_label != input.target_label
|| existing.view.reason != input.reason
|| existing.view.impact != input.impact
|| existing.view.rollback_plan != input.rollback_plan
{
return Err("HOLOLAKE_AUTHORIZATION_IDEMPOTENCY_CONFLICT".into());
}
let view = existing.view;
transaction.commit().map_err(db_error("COMMIT"))?;
return Ok(view);
}
let request_id = format!("HLP-AUTH-REQ-{}", Uuid::new_v4().simple());
let expires = now.saturating_add(REQUEST_TTL_MS);
transaction.execute(
"INSERT INTO authorization_requests(
request_id,idempotency_key,requester_account_key,requester_session_id,requester_client_instance_id,
action,target_number,target_kind,target_label,reason,impact,rollback_plan,state,
created_at_unix_ms,expires_at_unix_ms,receipt_hash
) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,'PENDING_HUMAN',?13,?14,'GENESIS')",
params![
request_id, input.idempotency_key, requester.account_key, requester.session_id,
requester.client_instance_id, input.action, input.target_number, input.target_kind,
input.target_label, input.reason, input.impact, input.rollback_plan, now, expires
],
).map_err(db_error("INSERT"))?;
let receipt = append_receipt(&transaction, &request_id, "SUBMITTED", "PENDING_HUMAN", now)?;
transaction
.execute(
"UPDATE authorization_requests SET receipt_hash=?2 WHERE request_id=?1",
params![request_id, receipt],
)
.map_err(db_error("UPDATE"))?;
let view = read_request(&transaction, &request_id)?;
transaction.commit().map_err(db_error("COMMIT"))?;
Ok(view)
}
pub fn status_for_requester_at(
root: &Path,
requester: &RequesterContext,
) -> Result<AuthorizationCenterSnapshot, String> {
snapshot_at(root, Some(requester))
}
pub fn decide_at(
root: &Path,
input: DecideAuthorizationInput,
human_number: &str,
now: u64,
) -> Result<AuthorizationRequestView, String> {
validate_id(&input.request_id, "REQUEST")?;
if !matches!(input.decision.as_str(), "APPROVE" | "DENY") {
return Err("HOLOLAKE_AUTHORIZATION_DECISION_INVALID".into());
}
validate_id(human_number, "HUMAN_NUMBER")?;
let mut connection = open_db(root)?;
let transaction = connection.transaction().map_err(db_error("TRANSACTION"))?;
expire_pending(&transaction, now)?;
let current = read_request(&transaction, &input.request_id)?;
if current.state != "PENDING_HUMAN" {
let same = (input.decision == "APPROVE" && current.state == "APPROVED")
|| (input.decision == "DENY" && current.state == "DENIED");
if same && current.human_number.as_deref() == Some(human_number) {
transaction.commit().map_err(db_error("COMMIT"))?;
return Ok(current);
}
return Err("HOLOLAKE_AUTHORIZATION_REQUEST_NOT_PENDING".into());
}
let (state, ticket_id, ticket_expires) = if input.decision == "APPROVE" {
(
"APPROVED",
Some(format!("HLP-AUTH-TICKET-{}", Uuid::new_v4().simple())),
Some(now.saturating_add(TICKET_TTL_MS)),
)
} else {
("DENIED", None, None)
};
transaction
.execute(
"UPDATE authorization_requests SET state=?2,human_number=?3,decided_at_unix_ms=?4,
ticket_id=?5,ticket_expires_at_unix_ms=?6 WHERE request_id=?1 AND state='PENDING_HUMAN'",
params![
input.request_id,
state,
human_number,
now,
ticket_id,
ticket_expires
],
)
.map_err(db_error("UPDATE"))?;
let receipt = append_receipt(&transaction, &input.request_id, &input.decision, state, now)?;
transaction
.execute(
"UPDATE authorization_requests SET receipt_hash=?2 WHERE request_id=?1",
params![input.request_id, receipt],
)
.map_err(db_error("UPDATE"))?;
let view = read_request(&transaction, &input.request_id)?;
transaction.commit().map_err(db_error("COMMIT"))?;
Ok(view)
}
pub fn consume_at(
root: &Path,
requester: &RequesterContext,
input: ConsumeAuthorizationInput,
) -> Result<AuthorizationTicketReceipt, String> {
validate_id(&input.request_id, "REQUEST")?;
validate_id(&input.ticket_id, "TICKET")?;
let now = now_ms()?;
let mut connection = open_db(root)?;
let transaction = connection.transaction().map_err(db_error("TRANSACTION"))?;
let request = read_request_with_owner(&transaction, &input.request_id)?;
if request.requester_account_key != requester.account_key
|| request.requester_session_id != requester.session_id
|| request.requester_client_instance_id != requester.client_instance_id
{
return Err("HOLOLAKE_AUTHORIZATION_TICKET_SESSION_MISMATCH".into());
}
if request.view.state == "CONSUMED" {
return Err("HOLOLAKE_AUTHORIZATION_TICKET_REPLAYED".into());
}
if request.view.state != "APPROVED"
|| request.view.ticket_id.as_deref() != Some(input.ticket_id.as_str())
{
return Err("HOLOLAKE_AUTHORIZATION_TICKET_NOT_APPROVED".into());
}
if request.view.ticket_expires_at_unix_ms.unwrap_or(0) < now {
transaction.execute(
"UPDATE authorization_requests SET state='EXPIRED' WHERE request_id=?1 AND state='APPROVED'",
params![input.request_id],
).map_err(db_error("UPDATE"))?;
let receipt = append_receipt(
&transaction,
&input.request_id,
"TICKET_EXPIRED",
"EXPIRED",
now,
)?;
transaction
.execute(
"UPDATE authorization_requests SET receipt_hash=?2 WHERE request_id=?1",
params![input.request_id, receipt],
)
.map_err(db_error("UPDATE"))?;
transaction.commit().map_err(db_error("COMMIT"))?;
return Err("HOLOLAKE_AUTHORIZATION_TICKET_EXPIRED".into());
}
let scope_sha256 = sha256_hex(
format!(
"{}\n{}\n{}\n{}\n{}",
input.request_id,
input.ticket_id,
request.view.action,
request.view.target_number,
requester.session_id
)
.as_bytes(),
);
transaction.execute(
"UPDATE authorization_requests SET state='CONSUMED',consumed_at_unix_ms=?2 WHERE request_id=?1 AND state='APPROVED'",
params![input.request_id, now],
).map_err(db_error("UPDATE"))?;
let receipt_hash =
append_receipt(&transaction, &input.request_id, "CONSUMED", "CONSUMED", now)?;
transaction
.execute(
"UPDATE authorization_requests SET receipt_hash=?2 WHERE request_id=?1",
params![input.request_id, receipt_hash],
)
.map_err(db_error("UPDATE"))?;
transaction.commit().map_err(db_error("COMMIT"))?;
Ok(AuthorizationTicketReceipt {
schema: SCHEMA,
state: "CONSUMED_ONCE",
request_id: input.request_id,
ticket_id: input.ticket_id,
action: request.view.action,
target_number: request.view.target_number,
scope_sha256,
consumed_at_unix_ms: now,
receipt_hash,
next_operation: "EXECUTE_ONLY_THE_EXACT_AUTHORIZED_ACTION_AND_APPEND_REALITY_RECEIPT",
})
}
fn snapshot_at(
root: &Path,
requester: Option<&RequesterContext>,
) -> Result<AuthorizationCenterSnapshot, String> {
let connection = open_db(root)?;
expire_pending(&connection, now_ms()?)?;
let (sql, args): (&str, Vec<&dyn rusqlite::ToSql>) = match requester {
Some(value) => (
"SELECT request_id FROM authorization_requests WHERE requester_account_key=?1 AND requester_session_id=?2 AND requester_client_instance_id=?3 ORDER BY created_at_unix_ms DESC LIMIT 100",
vec![&value.account_key, &value.session_id, &value.client_instance_id],
),
None => ("SELECT request_id FROM authorization_requests ORDER BY created_at_unix_ms DESC LIMIT 100", vec![]),
};
let mut statement = connection.prepare(sql).map_err(db_error("READ"))?;
let ids = statement
.query_map(args.as_slice(), |row| row.get::<_, String>(0))
.map_err(db_error("READ"))?
.collect::<Result<Vec<_>, _>>()
.map_err(db_error("READ"))?;
drop(statement);
let requests = ids
.iter()
.map(|id| read_request(&connection, id))
.collect::<Result<Vec<_>, _>>()?;
let pending_count = requests
.iter()
.filter(|item| item.state == "PENDING_HUMAN")
.count();
Ok(AuthorizationCenterSnapshot {
schema: SCHEMA,
state: "ACTIVE_FAIL_CLOSED",
pending_count,
requests,
supported_actions: vec!["OPEN_MAINTENANCE", "UNMOUNT", "PROMOTE_VERSION", "RETIRE"],
purge_enabled: false,
authority: "PERSONA_PROPOSES_HUMAN_DECIDES_SYSTEM_ISSUES_SESSION_BOUND_SINGLE_USE_TICKET",
})
}
struct OwnedRequest {
view: AuthorizationRequestView,
requester_account_key: String,
requester_session_id: String,
requester_client_instance_id: String,
}
fn read_request(
connection: &Connection,
request_id: &str,
) -> Result<AuthorizationRequestView, String> {
Ok(read_request_with_owner(connection, request_id)?.view)
}
fn read_request_with_owner(
connection: &Connection,
request_id: &str,
) -> Result<OwnedRequest, String> {
connection.query_row(
"SELECT request_id,action,target_number,target_kind,target_label,reason,impact,rollback_plan,
requester_client_instance_id,state,created_at_unix_ms,expires_at_unix_ms,human_number,
decided_at_unix_ms,ticket_id,ticket_expires_at_unix_ms,consumed_at_unix_ms,receipt_hash,
requester_account_key,requester_session_id FROM authorization_requests WHERE request_id=?1",
params![request_id],
|row| Ok(OwnedRequest {
view: AuthorizationRequestView {
request_id: row.get(0)?, action: row.get(1)?, target_number: row.get(2)?,
target_kind: row.get(3)?, target_label: row.get(4)?, reason: row.get(5)?,
impact: row.get(6)?, rollback_plan: row.get(7)?, requester_label: row.get(8)?,
state: row.get(9)?, created_at_unix_ms: row.get(10)?, expires_at_unix_ms: row.get(11)?,
human_number: row.get(12)?, decided_at_unix_ms: row.get(13)?, ticket_id: row.get(14)?,
ticket_expires_at_unix_ms: row.get(15)?, consumed_at_unix_ms: row.get(16)?, receipt_hash: row.get(17)?,
},
requester_account_key: row.get(18)?, requester_session_id: row.get(19)?,
requester_client_instance_id: row.get(8)?,
}),
).optional().map_err(db_error("READ"))?
.ok_or_else(|| "HOLOLAKE_AUTHORIZATION_REQUEST_UNKNOWN".into())
}
fn open_db(root: &Path) -> Result<Connection, String> {
fs::create_dir_all(root)
.map_err(|error| format!("HOLOLAKE_AUTHORIZATION_STORAGE_UNAVAILABLE: {error}"))?;
let connection =
Connection::open(root.join("human-authorization.sqlite3")).map_err(db_error("OPEN"))?;
connection
.busy_timeout(std::time::Duration::from_secs(5))
.map_err(db_error("TIMEOUT"))?;
connection.execute_batch(
"PRAGMA journal_mode=WAL;
CREATE TABLE IF NOT EXISTS authorization_requests(
request_id TEXT PRIMARY KEY,idempotency_key TEXT NOT NULL,requester_account_key TEXT NOT NULL,
requester_session_id TEXT NOT NULL,requester_client_instance_id TEXT NOT NULL,action TEXT NOT NULL,
target_number TEXT NOT NULL,target_kind TEXT NOT NULL,target_label TEXT NOT NULL,reason TEXT NOT NULL,
impact TEXT NOT NULL,rollback_plan TEXT NOT NULL,state TEXT NOT NULL,created_at_unix_ms INTEGER NOT NULL,
expires_at_unix_ms INTEGER NOT NULL,human_number TEXT,decided_at_unix_ms INTEGER,ticket_id TEXT UNIQUE,
ticket_expires_at_unix_ms INTEGER,consumed_at_unix_ms INTEGER,receipt_hash TEXT NOT NULL,
UNIQUE(requester_account_key,idempotency_key)
);
CREATE TABLE IF NOT EXISTS authorization_receipts(
sequence INTEGER PRIMARY KEY AUTOINCREMENT,request_id TEXT NOT NULL,event TEXT NOT NULL,state TEXT NOT NULL,
observed_at_unix_ms INTEGER NOT NULL,previous_receipt_hash TEXT NOT NULL,receipt_hash TEXT NOT NULL UNIQUE
);"
).map_err(db_error("SCHEMA"))?;
Ok(connection)
}
fn expire_pending(connection: &Connection, now: u64) -> Result<(), String> {
let mut statement = connection
.prepare("SELECT request_id FROM authorization_requests WHERE state='PENDING_HUMAN' AND expires_at_unix_ms < ?1")
.map_err(db_error("EXPIRE_READ"))?;
let request_ids = statement
.query_map(params![now], |row| row.get::<_, String>(0))
.map_err(db_error("EXPIRE_READ"))?
.collect::<Result<Vec<_>, _>>()
.map_err(db_error("EXPIRE_READ"))?;
drop(statement);
for request_id in request_ids {
connection
.execute(
"UPDATE authorization_requests SET state='EXPIRED' WHERE request_id=?1 AND state='PENDING_HUMAN'",
params![request_id],
)
.map_err(db_error("EXPIRE"))?;
let receipt = append_receipt(connection, &request_id, "REQUEST_EXPIRED", "EXPIRED", now)?;
connection
.execute(
"UPDATE authorization_requests SET receipt_hash=?2 WHERE request_id=?1",
params![request_id, receipt],
)
.map_err(db_error("EXPIRE"))?;
}
Ok(())
}
fn append_receipt(
connection: &Connection,
request_id: &str,
event: &str,
state: &str,
now: u64,
) -> Result<String, String> {
let previous = connection
.query_row(
"SELECT receipt_hash FROM authorization_receipts ORDER BY sequence DESC LIMIT 1",
[],
|row| row.get::<_, String>(0),
)
.optional()
.map_err(db_error("RECEIPT_READ"))?
.unwrap_or_else(|| "GENESIS".into());
let receipt_hash = sha256_hex(
format!("HLP-AUTH-RECEIPT-v1\n{previous}\n{request_id}\n{event}\n{state}\n{now}")
.as_bytes(),
);
connection.execute(
"INSERT INTO authorization_receipts(request_id,event,state,observed_at_unix_ms,previous_receipt_hash,receipt_hash) VALUES(?1,?2,?3,?4,?5,?6)",
params![request_id, event, state, now, previous, receipt_hash],
).map_err(db_error("RECEIPT_WRITE"))?;
Ok(receipt_hash)
}
fn validate_proposal(input: &AuthorizationProposalInput) -> Result<(), String> {
validate_id(&input.idempotency_key, "IDEMPOTENCY")?;
if !matches!(
input.action.as_str(),
"OPEN_MAINTENANCE" | "UNMOUNT" | "PROMOTE_VERSION" | "RETIRE"
) {
return Err("HOLOLAKE_AUTHORIZATION_ACTION_UNSUPPORTED".into());
}
validate_id(&input.target_number, "TARGET_NUMBER")?;
validate_id(&input.target_kind, "TARGET_KIND")?;
if input.target_kind != "MODULE" || !input.target_number.starts_with("HLP-MOD-") {
return Err("HOLOLAKE_AUTHORIZATION_TARGET_NOT_A_MODULE".into());
}
validate_text(&input.target_label, 120, "TARGET_LABEL")?;
validate_text(&input.reason, 2_000, "REASON")?;
validate_text(&input.impact, 2_000, "IMPACT")?;
validate_text(&input.rollback_plan, 2_000, "ROLLBACK")
}
fn validate_id(value: &str, label: &str) -> Result<(), String> {
if value.is_empty()
|| value.len() > 160
|| value.chars().any(|c| c.is_control() || c.is_whitespace())
{
Err(format!("HOLOLAKE_AUTHORIZATION_{label}_INVALID"))
} else {
Ok(())
}
}
fn validate_text(value: &str, max: usize, label: &str) -> Result<(), String> {
if value.trim().is_empty() || value.len() > max || value.chars().any(|c| c == '\0') {
Err(format!("HOLOLAKE_AUTHORIZATION_{label}_INVALID"))
} else {
Ok(())
}
}
fn now_ms() -> Result<u64, String> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis() as u64)
.map_err(|error| format!("HOLOLAKE_SYSTEM_CLOCK_INVALID: {error}"))
}
fn sha256_hex(bytes: &[u8]) -> String {
digest(&SHA256, bytes)
.as_ref()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
fn db_error(stage: &'static str) -> impl FnOnce(rusqlite::Error) -> String {
move |error| format!("HOLOLAKE_AUTHORIZATION_DB_{stage}_FAILED: {error}")
}
#[cfg(test)]
mod tests {
use super::*;
fn requester() -> RequesterContext {
RequesterContext {
account_key: "account".into(),
session_id: "session".into(),
client_instance_id: "agent-codex".into(),
}
}
fn proposal() -> AuthorizationProposalInput {
AuthorizationProposalInput {
idempotency_key: "proposal-1".into(),
action: "OPEN_MAINTENANCE".into(),
target_number: "HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001".into(),
target_kind: "MODULE".into(),
target_label: "教育工作台".into(),
reason: "移出问题模块并在干净容器中重建".into(),
impact: "旧模块保持不可写,维护区允许受控修改".into(),
rollback_plan: "关闭维护票据并恢复原挂载".into(),
}
}
#[test]
fn human_approval_ticket_is_session_bound_and_single_use() {
let temp = tempfile::tempdir().unwrap();
let created = submit_at(temp.path(), &requester(), proposal()).unwrap();
let approved = decide_at(
temp.path(),
DecideAuthorizationInput {
request_id: created.request_id.clone(),
decision: "APPROVE".into(),
},
"ICE-GL∞",
now_ms().unwrap(),
)
.unwrap();
let ticket = approved.ticket_id.unwrap();
let consumed = consume_at(
temp.path(),
&requester(),
ConsumeAuthorizationInput {
request_id: created.request_id.clone(),
ticket_id: ticket.clone(),
},
)
.unwrap();
assert_eq!(consumed.state, "CONSUMED_ONCE");
let replay = consume_at(
temp.path(),
&requester(),
ConsumeAuthorizationInput {
request_id: created.request_id,
ticket_id: ticket,
},
)
.unwrap_err();
assert_eq!(replay, "HOLOLAKE_AUTHORIZATION_TICKET_REPLAYED");
}
#[test]
fn purge_is_not_an_available_action() {
let mut value = proposal();
value.action = "PURGE".into();
assert_eq!(
submit_at(tempfile::tempdir().unwrap().path(), &requester(), value).unwrap_err(),
"HOLOLAKE_AUTHORIZATION_ACTION_UNSUPPORTED"
);
}
#[test]
fn idempotency_key_cannot_be_reused_for_another_target_or_reason() {
let temp = tempfile::tempdir().unwrap();
submit_at(temp.path(), &requester(), proposal()).unwrap();
let mut changed = proposal();
changed.reason = "另一个原因".into();
assert_eq!(
submit_at(temp.path(), &requester(), changed).unwrap_err(),
"HOLOLAKE_AUTHORIZATION_IDEMPOTENCY_CONFLICT"
);
}
#[test]
fn approved_ticket_cannot_move_to_another_agent_session() {
let temp = tempfile::tempdir().unwrap();
let created = submit_at(temp.path(), &requester(), proposal()).unwrap();
let approved = decide_at(
temp.path(),
DecideAuthorizationInput {
request_id: created.request_id.clone(),
decision: "APPROVE".into(),
},
"ICE-GL∞",
now_ms().unwrap(),
)
.unwrap();
let mut another = requester();
another.session_id = "another-session".into();
assert_eq!(
consume_at(
temp.path(),
&another,
ConsumeAuthorizationInput {
request_id: created.request_id,
ticket_id: approved.ticket_id.unwrap(),
},
)
.unwrap_err(),
"HOLOLAKE_AUTHORIZATION_TICKET_SESSION_MISMATCH"
);
}
}

View file

@ -15,6 +15,7 @@ mod gls_bootstrap_compiler;
mod gls_protocol_kernel;
mod gls_protocol_runtime;
mod home_status;
mod human_authorization;
mod knowledge_base;
mod local_development_bridge;
mod metacognitive_zero_layer;
@ -25,6 +26,7 @@ mod number_coordinate_tree;
mod numbered_ipc;
mod numbered_ipc_dispatch;
mod numbered_language_input;
mod online_marketplace;
mod persona_carrier_license;
mod persona_channel_body;
mod persona_time_authority;
@ -33,6 +35,7 @@ mod pncc_receipt_projection;
mod pncc_remote_git;
mod pncc_repository_binding;
mod pncc_server_projection;
mod public_zero_core_distribution;
mod release_trust;
mod release_update;
mod user_pncc_channel;
@ -64,9 +67,11 @@ pub fn run() {
metacognitive_zero_layer::start_on_application_open()?;
persona_carrier_license::start_on_application_open()?;
module_package_runtime::start_on_application_open(app.handle())?;
online_marketplace::start_on_application_open(app.handle())?;
number_coordinate_tree::start_on_application_open()?;
numbered_language_input::validate_contract()?;
numbered_ipc::start_on_application_open(app.handle())?;
human_authorization::start_on_application_open()?;
// 软件打开即先启动时间主控并发起联网校时;失败只降级,不阻塞人进入 HoloLake。
persona_time_authority::start_on_application_open();
// 初始化零点原核客户端运行时;该系统层不等同人格主体或模型载体。

View file

@ -217,64 +217,64 @@ struct ModuleSelfTest {
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct VerifyModulePackageInput {
package_path: String,
signature_path: String,
pub(crate) package_path: String,
pub(crate) signature_path: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct InstallModulePackageInput {
package_path: String,
signature_path: String,
expected_package_sha256: String,
human_confirmed_permission_expansion: bool,
pub(crate) package_path: String,
pub(crate) signature_path: String,
pub(crate) expected_package_sha256: String,
pub(crate) human_confirmed_permission_expansion: bool,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ModuleNumberInput {
module_number: String,
pub(crate) module_number: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct VerifiedModulePackage {
state: String,
package_sha256: String,
module_number: String,
display_name: String,
version: String,
adapter: String,
permissions: Vec<String>,
signature_verified: bool,
package_code_executed: bool,
pub(crate) state: String,
pub(crate) package_sha256: String,
pub(crate) module_number: String,
pub(crate) display_name: String,
pub(crate) version: String,
pub(crate) adapter: String,
pub(crate) permissions: Vec<String>,
pub(crate) signature_verified: bool,
pub(crate) package_code_executed: bool,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InstalledModule {
module_number: String,
display_name: String,
version: String,
adapter: String,
state: String,
package_sha256: String,
previous_package_sha256: Option<String>,
permissions: Vec<String>,
user_data_preserved: bool,
updated_at_unix_ms: u64,
pub(crate) module_number: String,
pub(crate) display_name: String,
pub(crate) version: String,
pub(crate) adapter: String,
pub(crate) state: String,
pub(crate) package_sha256: String,
pub(crate) previous_package_sha256: Option<String>,
pub(crate) permissions: Vec<String>,
pub(crate) user_data_preserved: bool,
pub(crate) updated_at_unix_ms: u64,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ModuleRuntimeSnapshot {
schema: &'static str,
state: &'static str,
host_version: &'static str,
package_code_executed: bool,
modules: Vec<InstalledModule>,
receipt_count: u64,
latest_receipt_hash: String,
pub(crate) schema: &'static str,
pub(crate) state: &'static str,
pub(crate) host_version: &'static str,
pub(crate) package_code_executed: bool,
pub(crate) modules: Vec<InstalledModule>,
pub(crate) receipt_count: u64,
pub(crate) latest_receipt_hash: String,
}
#[derive(Clone, Debug, Serialize)]

View file

@ -88,8 +88,8 @@ fn validate_tree() -> Result<(), String> {
|| tree.record_id != "HLP-UNIFIED-NUMBER-TREE-001"
|| tree.state != "MACHINE_COMPILED_STARTUP_ENFORCED"
|| tree.root_number != "HLP-NUMBER-WORLD-ROOT-001"
|| tree.coordinate_count != 272
|| tree.route_count != 169
|| tree.coordinate_count != 283
|| tree.route_count != 180
|| tree.routes.len() != tree.route_count
|| tree.identity_node_count != 4
|| tree.identity_nodes.len() != tree.identity_node_count

View file

@ -934,7 +934,7 @@ mod tests {
#[test]
fn registry_is_closed_and_contains_every_migrated_command() {
let registry = load_registry().unwrap();
assert_eq!(registry.operations.len(), 147);
assert_eq!(registry.operations.len(), 155);
assert!(!registry.runtime.legacy_direct_commands_allowed);
}

View file

@ -43,6 +43,19 @@ pub(crate) async fn dispatch(
app.state::<crate::direct_local_broker::DirectLocalBrokerState>(),
)?)
}
"human_authorization::get_authorization_center" => {
json(crate::human_authorization::get_center(&app)?)
}
"human_authorization::decide_authorization_request" => {
let state = app.state::<crate::zero_point::ZeroPointState>();
let (human_number, _) = crate::zero_point::verified_user_route(&state)?
.ok_or_else(|| "HOLOLAKE_AUTHORIZATION_VERIFIED_HUMAN_REQUIRED".to_string())?;
json(crate::human_authorization::decide(
&app,
input(&payload)?,
&human_number,
)?)
}
"release_update::check_hololake_update" => {
json(crate::release_update::check_hololake_update(app).await?)
}
@ -126,6 +139,24 @@ pub(crate) async fn dispatch(
"module_package_runtime::activate_bundled_module" => json(
crate::module_package_runtime::activate_bundled_module(app, input(&payload)?).await?,
),
"online_marketplace::get_marketplace_snapshot" => {
json(crate::online_marketplace::get_marketplace_snapshot(app).await?)
}
"online_marketplace::sync_marketplace_catalog" => {
json(crate::online_marketplace::sync_marketplace_catalog(app).await?)
}
"online_marketplace::install_marketplace_item" => {
json(crate::online_marketplace::install_marketplace_item(app, input(&payload)?).await?)
}
"online_marketplace::uninstall_marketplace_item" => json(
crate::online_marketplace::uninstall_marketplace_item(app, input(&payload)?).await?,
),
"online_marketplace::rollback_marketplace_item" => {
json(crate::online_marketplace::rollback_marketplace_item(app, input(&payload)?).await?)
}
"online_marketplace::get_active_cognitive_skills" => {
json(crate::online_marketplace::get_active_cognitive_skills(app).await?)
}
"mobile_sync::start_mobile_sync" => json(crate::mobile_sync::start_mobile_sync(app)?),
"mobile_sync::get_mobile_sync_status" => {
json(crate::mobile_sync::get_mobile_sync_status(app)?)

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize};
use tauri::{Manager, State};
/// 零点原核运行协议参数。PROTOCOL.json 存在时优先读取;否则使用明确标注的出厂默认值。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ZeroPointProtocol {
#[serde(default = "default_grace_days")]
@ -34,7 +34,7 @@ fn default_grace_days() -> u64 {
7
}
fn default_anchor_url() -> String {
"https://guanghulab.com/api/ai/v1/anchor".into()
"https://guanghu.chat/api/hololake/zero-core/lamp".into()
}
fn default_resolve_url() -> String {
"https://guanghulab.com/api/ai/v1/resolve?id=".into()
@ -79,6 +79,8 @@ pub struct ZeroPointSnapshot {
pub protocol: ZeroPointProtocol,
/// 底层协议静默比对的最近结论。
pub sync_note: String,
/// 企业服务器公众零点原核投影的签名分发状态;不包含私人第五域数据。
pub public_distribution: crate::public_zero_core_distribution::PublicZeroCoreStatus,
}
pub struct ZeroPointState {
@ -151,9 +153,12 @@ pub fn boot_zero_point(app: &tauri::AppHandle, state: &ZeroPointState) -> Result
fs::write(&charter, text).map_err(|e| format!("HOLOLAKE_ZP_CHARTER_FAILED: {e}"))?;
// 协议参数PROTOCOL.json 在则读(第五域就位即自动生效),缺则出厂兜底。
let protocol = fs::read_to_string(home.join("PROTOCOL.json"))
.ok()
.and_then(|raw| serde_json::from_str::<ZeroPointProtocol>(&raw).ok())
let protocol = crate::public_zero_core_distribution::load_active_protocol(&home)?
.or_else(|| {
fs::read_to_string(home.join("PROTOCOL.json"))
.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);
@ -472,45 +477,34 @@ fn lighthouse_resolution(body: &str, expected_number: &str) -> Option<Lighthouse
})
}
/// 静默比对底层协议版本。没有验签公钥或完整发布验证流程时保持失败关闭
/// 静默同步公众零点原核协议。双签信任根未配置或任一验证失败时保持当前版本
pub async fn sync_protocol_runtime(state: &ZeroPointState) -> Result<(), String> {
let home = home_of_inner(state)?;
let anchor_url = { lock(state)?.protocol.lighthouse_anchor_url.clone() };
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()
.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();
if remote_version.is_empty() {
"协议版本信息不可用,本次未执行更新。".into()
} else if remote_version == local_version.trim() {
append_heartbeat(&home, "sync verdict=ALREADY_CURRENT");
"协议版本已是最新。".into()
} else if !pubkey_ready {
append_heartbeat(&home, "sync verdict=UPDATE_PENDING_SIGNATURE_KEY_ABSENT");
"发现新版本,但验签公钥尚未配置;更新未执行。".into()
} else {
// 验签三闸(来源/签名/版本单调)完整施工待第五域发布管道就位。
append_heartbeat(&home, "sync verdict=UPDATE_FOUND_GATE_PENDING");
"发现新版本,但发布验证流程尚未就绪;更新未执行。".into()
match crate::public_zero_core_distribution::sync_public_zero_core(&home, &anchor_url).await {
Ok(outcome) => {
if let Some(protocol) = outcome.activated_protocol {
lock(state)?.protocol = protocol;
}
append_heartbeat(
&home,
&format!(
"sync verdict={} epoch={} version={}",
outcome.status.state, outcome.status.epoch, outcome.status.version
),
);
lock(state)?.sync_note = outcome.note;
}
_ => {
append_heartbeat(&home, "sync verdict=ANCHOR_UNREACHABLE");
"协议服务当前不可达,本次未完成版本检查。".into()
Err(error) => {
append_heartbeat(&home, &format!("sync verdict=FAIL_CLOSED reason={error}"));
lock(state)?.sync_note = match error.as_str() {
"HOLOLAKE_PUBLIC_ZERO_CORE_TRUST_UNPROVISIONED" => {
"公众零点原核双签公钥尚未配置,本次更新保持失败关闭。".into()
}
_ => "公众零点原核更新未通过完整验真,本机继续使用上一个可信版本。".into(),
};
}
};
lock(state)?.sync_note = note;
}
Ok(())
}
@ -526,6 +520,7 @@ pub async fn zero_point_status(
state: State<'_, ZeroPointState>,
) -> Result<ZeroPointSnapshot, String> {
let inner = lock(&state)?;
let public_distribution = crate::public_zero_core_distribution::status_at(&inner.home)?;
let grace = inner.protocol.grace_period_days.saturating_mul(86_400);
Ok(ZeroPointSnapshot {
route: inner.route.clone(),
@ -541,6 +536,7 @@ pub async fn zero_point_status(
},
protocol: inner.protocol.clone(),
sync_note: inner.sync_note.clone(),
public_distribution,
})
}
@ -571,6 +567,10 @@ mod tests {
assert!(protocol
.enterprise_resolve_url
.starts_with("https://guanghu.chat"));
assert_eq!(
protocol.lighthouse_anchor_url,
"https://guanghu.chat/api/hololake/zero-core/lamp"
);
}
#[test]