feat: ship dual-signed online module marketplace
This commit is contained in:
parent
3153771bf2
commit
b0eeade03d
40 changed files with 5774 additions and 111 deletions
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()?,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
// 初始化零点原核客户端运行时;该系统层不等同人格主体或模型载体。
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
File diff suppressed because it is too large
Load diff
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Reference in a new issue