feat: admit signed persona channel body module

This commit is contained in:
冰朔 2026-08-19 02:48:13 +08:00
commit 15b2651074
21 changed files with 2417 additions and 73 deletions

View file

@ -0,0 +1,575 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
//! 当前账号私人频道的长期成长模型。
//!
//! 这里只保存最小化的行为元数据,不保存文档正文、表格单元格、提示词、对话原文或凭据。
//! 基础算法可以持续形成模块亲和度;模型 API 未来只能提交适配提案,不能绕过本地校验和人类确认。
use ring::digest::{digest, SHA256};
use rusqlite::{params, Connection};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tauri::AppHandle;
use uuid::Uuid;
const GROWTH_SCHEMA: &str = "hololake.channel-growth-model/v1";
const ZERO_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
const MODULE_NUMBER: &str = "HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001";
const ADAPTER: &str = "persona-channel-body-v1";
fn require_active(app: &AppHandle) -> Result<(), String> {
crate::module_package_runtime::require_active_module_adapter(app, MODULE_NUMBER, ADAPTER)
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RecordChannelGrowthEventInput {
pub event_kind: String,
pub module_id: String,
pub source: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct UpdateChannelGrowthSharingInput {
pub sharing_mode: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ChannelGrowthControls {
pub sharing_mode: String,
pub official_read_access: bool,
pub history_mutation_allowed: bool,
pub structural_change_requires_human_confirmation: bool,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ChannelModuleAffinity {
pub module_id: String,
pub use_count: i64,
pub score: f64,
pub last_used_at_unix_ms: i64,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ChannelGrowthSnapshot {
pub schema: &'static str,
pub state: &'static str,
pub controls: ChannelGrowthControls,
pub observed_event_count: i64,
pub persona_collaboration_event_count: i64,
pub module_affinities: Vec<ChannelModuleAffinity>,
pub recommended_module_order: Vec<String>,
pub integrity_state: &'static str,
pub last_event_hash: String,
pub model_api_state: &'static str,
pub model_proposal_policy: &'static str,
pub data_boundary: &'static str,
}
pub async fn get_channel_growth_snapshot(app: AppHandle) -> Result<ChannelGrowthSnapshot, String> {
require_active(&app)?;
let database = channel_growth_database(&app)?;
tauri::async_runtime::spawn_blocking(move || snapshot_at(&database))
.await
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_JOIN_FAILED: {error}"))?
}
pub async fn record_channel_growth_event(
app: AppHandle,
input: RecordChannelGrowthEventInput,
) -> Result<ChannelGrowthSnapshot, String> {
require_active(&app)?;
let database = channel_growth_database(&app)?;
tauri::async_runtime::spawn_blocking(move || record_event_at(&database, input))
.await
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_JOIN_FAILED: {error}"))?
}
pub async fn update_channel_growth_sharing(
app: AppHandle,
input: UpdateChannelGrowthSharingInput,
) -> Result<ChannelGrowthSnapshot, String> {
require_active(&app)?;
let database = channel_growth_database(&app)?;
tauri::async_runtime::spawn_blocking(move || update_sharing_at(&database, input))
.await
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_JOIN_FAILED: {error}"))?
}
fn channel_growth_database(app: &AppHandle) -> Result<PathBuf, String> {
let root = crate::authenticated_storage::account_storage_root(app, "channel-growth-v1")?;
fs::create_dir_all(&root)
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_STORAGE_UNAVAILABLE: {error}"))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&root, fs::Permissions::from_mode(0o700))
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_PERMISSION_FAILED: {error}"))?;
}
Ok(root.join("channel-growth.sqlite3"))
}
fn open_database(path: &Path) -> Result<Connection, String> {
let connection = Connection::open(path)
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_DATABASE_UNAVAILABLE: {error}"))?;
connection
.busy_timeout(Duration::from_secs(5))
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_DATABASE_UNAVAILABLE: {error}"))?;
connection
.execute_batch(
"PRAGMA journal_mode = DELETE;
PRAGMA synchronous = FULL;
PRAGMA trusted_schema = OFF;
CREATE TABLE IF NOT EXISTS growth_sharing (
singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
sharing_mode TEXT NOT NULL CHECK(sharing_mode IN ('LOCAL_ONLY', 'ANONYMIZED_SHARED'))
);
INSERT OR IGNORE INTO growth_sharing VALUES (1, 'LOCAL_ONLY');
CREATE TABLE IF NOT EXISTS growth_events (
sequence INTEGER NOT NULL UNIQUE,
event_id TEXT PRIMARY KEY NOT NULL,
event_kind TEXT NOT NULL,
module_id TEXT NOT NULL,
source TEXT NOT NULL CHECK(source IN ('HUMAN_ACTION', 'PERSONA_COLLABORATION', 'SYSTEM_ASSISTED')),
occurred_at_unix_ms INTEGER NOT NULL,
previous_hash TEXT NOT NULL,
event_hash TEXT NOT NULL UNIQUE
);
CREATE TRIGGER IF NOT EXISTS growth_events_no_update BEFORE UPDATE ON growth_events BEGIN SELECT RAISE(ABORT, 'HOLOLAKE_GROWTH_HISTORY_APPEND_ONLY'); END;
CREATE TRIGGER IF NOT EXISTS growth_events_no_delete BEFORE DELETE ON growth_events BEGIN SELECT RAISE(ABORT, 'HOLOLAKE_GROWTH_HISTORY_APPEND_ONLY'); END;
CREATE TABLE IF NOT EXISTS module_affinities (
module_id TEXT PRIMARY KEY NOT NULL,
use_count INTEGER NOT NULL,
score REAL NOT NULL,
last_used_at_unix_ms INTEGER NOT NULL
);",
)
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_SCHEMA_FAILED: {error}"))?;
Ok(connection)
}
fn validate_token(value: &str, label: &str) -> Result<String, String> {
let value = value.trim();
if value.is_empty()
|| value.len() > 96
|| !value
.chars()
.all(|item| item.is_ascii_alphanumeric() || matches!(item, '-' | '_' | '.' | ':'))
{
return Err(format!("HOLOLAKE_CHANNEL_GROWTH_{label}_INVALID"));
}
Ok(value.to_string())
}
fn record_event_at(
database: &Path,
input: RecordChannelGrowthEventInput,
) -> Result<ChannelGrowthSnapshot, String> {
let event_kind = validate_token(&input.event_kind, "EVENT_KIND")?;
let module_id = validate_token(&input.module_id, "MODULE_ID")?;
let source = input.source.trim().to_string();
if !matches!(
source.as_str(),
"HUMAN_ACTION" | "PERSONA_COLLABORATION" | "SYSTEM_ASSISTED"
) {
return Err("HOLOLAKE_CHANNEL_GROWTH_SOURCE_INVALID".into());
}
let mut connection = open_database(database)?;
verify_event_chain(&connection)?;
let now = now_unix_ms()?;
let weight = if source == "PERSONA_COLLABORATION" {
1.4
} else {
1.0
};
let transaction = connection
.transaction()
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_TRANSACTION_FAILED: {error}"))?;
let (sequence, previous_hash): (i64, String) = transaction
.query_row(
"SELECT COALESCE(MAX(sequence), 0) + 1, COALESCE((SELECT event_hash FROM growth_events ORDER BY sequence DESC LIMIT 1), ?1) FROM growth_events",
params![ZERO_HASH],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
let event_id = Uuid::new_v4().to_string();
let event_hash = event_hash(
sequence,
&event_id,
&event_kind,
&module_id,
&source,
now,
&previous_hash,
);
transaction
.execute(
"INSERT INTO growth_events(sequence, event_id, event_kind, module_id, source, occurred_at_unix_ms, previous_hash, event_hash) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
params![sequence, event_id, event_kind, module_id, source, now, previous_hash, event_hash],
)
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_EVENT_WRITE_FAILED: {error}"))?;
transaction
.execute(
"INSERT INTO module_affinities(module_id, use_count, score, last_used_at_unix_ms) VALUES (?1, 1, ?2, ?3)
ON CONFLICT(module_id) DO UPDATE SET use_count = use_count + 1, score = score * 0.985 + excluded.score, last_used_at_unix_ms = excluded.last_used_at_unix_ms",
params![module_id, weight, now],
)
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_MODEL_WRITE_FAILED: {error}"))?;
transaction
.commit()
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_COMMIT_FAILED: {error}"))?;
snapshot_with_connection(&connection)
}
fn update_sharing_at(
database: &Path,
input: UpdateChannelGrowthSharingInput,
) -> Result<ChannelGrowthSnapshot, String> {
if !matches!(
input.sharing_mode.as_str(),
"LOCAL_ONLY" | "ANONYMIZED_SHARED"
) {
return Err("HOLOLAKE_CHANNEL_GROWTH_SHARING_MODE_INVALID".into());
}
let connection = open_database(database)?;
connection
.execute(
"UPDATE growth_sharing SET sharing_mode = ?1 WHERE singleton = 1",
params![input.sharing_mode],
)
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_SHARING_WRITE_FAILED: {error}"))?;
snapshot_with_connection(&connection)
}
fn snapshot_at(database: &Path) -> Result<ChannelGrowthSnapshot, String> {
let connection = open_database(database)?;
snapshot_with_connection(&connection)
}
fn snapshot_with_connection(connection: &Connection) -> Result<ChannelGrowthSnapshot, String> {
let last_event_hash = verify_event_chain(connection)?;
let mut affinities = verify_and_project_affinities(connection)?;
let controls = connection
.query_row(
"SELECT sharing_mode FROM growth_sharing WHERE singleton = 1",
[],
|row| {
Ok(ChannelGrowthControls {
sharing_mode: row.get(0)?,
official_read_access: false,
history_mutation_allowed: false,
structural_change_requires_human_confirmation: true,
})
},
)
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
let observed_event_count = connection
.query_row("SELECT COUNT(*) FROM growth_events", [], |row| row.get(0))
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
let persona_collaboration_event_count = connection
.query_row(
"SELECT COUNT(*) FROM growth_events WHERE source = 'PERSONA_COLLABORATION'",
[],
|row| row.get(0),
)
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
affinities.sort_by(|left, right| {
right
.score
.partial_cmp(&left.score)
.unwrap_or(Ordering::Equal)
.then_with(|| right.last_used_at_unix_ms.cmp(&left.last_used_at_unix_ms))
.then_with(|| left.module_id.cmp(&right.module_id))
});
affinities.truncate(12);
let recommended_module_order = affinities
.iter()
.map(|item| item.module_id.clone())
.collect();
Ok(ChannelGrowthSnapshot {
schema: GROWTH_SCHEMA,
state: "APPEND_ONLY_LOCAL_GROWTH_ACTIVE",
controls,
observed_event_count,
persona_collaboration_event_count,
module_affinities: affinities,
recommended_module_order,
integrity_state: "VERIFIED_APPEND_ONLY_CHAIN_AND_DERIVED_PROJECTION",
last_event_hash,
model_api_state: "NOT_CONFIGURED",
model_proposal_policy: "LOCAL_VALIDATE_THEN_HUMAN_CONFIRM_BEFORE_STRUCTURAL_CHANGE",
data_boundary: "METADATA_ONLY_NO_CONTENT_NO_CREDENTIALS_NO_CONVERSATION_TRANSCRIPT",
})
}
fn verify_and_project_affinities(
connection: &Connection,
) -> Result<Vec<ChannelModuleAffinity>, String> {
let mut projected = BTreeMap::<String, ChannelModuleAffinity>::new();
let mut event_statement = connection
.prepare(
"SELECT module_id, source, occurred_at_unix_ms FROM growth_events ORDER BY sequence",
)
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
let events = event_statement
.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, i64>(2)?,
))
})
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?
.collect::<Result<Vec<_>, _>>()
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
for (module_id, source, occurred_at) in events {
let weight = if source == "PERSONA_COLLABORATION" {
1.4
} else {
1.0
};
projected
.entry(module_id.clone())
.and_modify(|item| {
item.use_count += 1;
item.score = item.score * 0.985 + weight;
item.last_used_at_unix_ms = occurred_at;
})
.or_insert(ChannelModuleAffinity {
module_id,
use_count: 1,
score: weight,
last_used_at_unix_ms: occurred_at,
});
}
let mut stored_statement = connection
.prepare(
"SELECT module_id, use_count, score, last_used_at_unix_ms FROM module_affinities ORDER BY module_id",
)
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
let stored = stored_statement
.query_map([], |row| {
Ok(ChannelModuleAffinity {
module_id: row.get(0)?,
use_count: row.get(1)?,
score: row.get(2)?,
last_used_at_unix_ms: row.get(3)?,
})
})
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?
.collect::<Result<Vec<_>, _>>()
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
if stored.len() != projected.len()
|| stored.iter().any(|item| {
projected.get(&item.module_id).map_or(true, |expected| {
item.use_count != expected.use_count
|| (item.score - expected.score).abs() > 1e-12
|| item.last_used_at_unix_ms != expected.last_used_at_unix_ms
})
})
{
return Err("HOLOLAKE_CHANNEL_GROWTH_PROJECTION_INTEGRITY_FAILED".into());
}
Ok(projected.into_values().collect())
}
fn event_hash(
sequence: i64,
event_id: &str,
event_kind: &str,
module_id: &str,
source: &str,
occurred_at_unix_ms: i64,
previous_hash: &str,
) -> String {
let material = format!("{GROWTH_SCHEMA}|{sequence}|{event_id}|{event_kind}|{module_id}|{source}|{occurred_at_unix_ms}|{previous_hash}");
hex(digest(&SHA256, material.as_bytes()).as_ref())
}
fn verify_event_chain(connection: &Connection) -> Result<String, String> {
let mut statement = connection
.prepare("SELECT sequence, event_id, event_kind, module_id, source, occurred_at_unix_ms, previous_hash, event_hash FROM growth_events ORDER BY sequence")
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
let mut rows = statement
.query([])
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
let mut expected_sequence = 1_i64;
let mut previous = ZERO_HASH.to_string();
while let Some(row) = rows
.next()
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?
{
let sequence: i64 = row
.get(0)
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
let event_id: String = row
.get(1)
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
let event_kind: String = row
.get(2)
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
let module_id: String = row
.get(3)
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
let source: String = row
.get(4)
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
let occurred_at: i64 = row
.get(5)
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
let stored_previous: String = row
.get(6)
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
let stored_hash: String = row
.get(7)
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
if sequence != expected_sequence
|| stored_previous != previous
|| stored_hash
!= event_hash(
sequence,
&event_id,
&event_kind,
&module_id,
&source,
occurred_at,
&stored_previous,
)
{
return Err("HOLOLAKE_CHANNEL_GROWTH_HISTORY_INTEGRITY_FAILED".into());
}
expected_sequence += 1;
previous = stored_hash;
}
Ok(previous)
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}
fn now_unix_ms() -> Result<i64, String> {
Ok(SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|error| format!("HOLOLAKE_CLOCK_INVALID: {error}"))?
.as_millis() as i64)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn local_growth_model_records_only_bounded_metadata_and_ranks_modules() {
let dir = tempdir().unwrap();
let database = dir.path().join("growth.sqlite3");
for _ in 0..3 {
record_event_at(
&database,
RecordChannelGrowthEventInput {
event_kind: "MODULE_OPEN".into(),
module_id: "education.table".into(),
source: "HUMAN_ACTION".into(),
},
)
.unwrap();
}
record_event_at(
&database,
RecordChannelGrowthEventInput {
event_kind: "PERSONA_COLLABORATION".into(),
module_id: "knowledge".into(),
source: "PERSONA_COLLABORATION".into(),
},
)
.unwrap();
let snapshot = snapshot_at(&database).unwrap();
assert_eq!(snapshot.observed_event_count, 4);
assert_eq!(snapshot.persona_collaboration_event_count, 1);
assert_eq!(
snapshot
.recommended_module_order
.first()
.map(String::as_str),
Some("education.table")
);
assert_eq!(
snapshot.data_boundary,
"METADATA_ONLY_NO_CONTENT_NO_CREDENTIALS_NO_CONVERSATION_TRANSCRIPT"
);
}
#[test]
fn growth_history_rejects_update_and_delete() {
let dir = tempdir().unwrap();
let database = dir.path().join("growth.sqlite3");
record_event_at(
&database,
RecordChannelGrowthEventInput {
event_kind: "MODULE_OPEN".into(),
module_id: "knowledge".into(),
source: "HUMAN_ACTION".into(),
},
)
.unwrap();
let connection = open_database(&database).unwrap();
assert!(connection.execute("DELETE FROM growth_events", []).is_err());
assert!(connection
.execute("UPDATE growth_events SET module_id = 'rewritten'", [])
.is_err());
assert_eq!(
snapshot_at(&database).unwrap().integrity_state,
"VERIFIED_APPEND_ONLY_CHAIN_AND_DERIVED_PROJECTION"
);
}
#[test]
fn derived_affinity_projection_fails_closed_when_modified_outside_the_event_path() {
let dir = tempdir().unwrap();
let database = dir.path().join("growth.sqlite3");
record_event_at(
&database,
RecordChannelGrowthEventInput {
event_kind: "MODULE_OPEN".into(),
module_id: "knowledge".into(),
source: "HUMAN_ACTION".into(),
},
)
.unwrap();
let connection = open_database(&database).unwrap();
connection
.execute("UPDATE module_affinities SET score = 999", [])
.unwrap();
drop(connection);
assert_eq!(
snapshot_at(&database).unwrap_err(),
"HOLOLAKE_CHANNEL_GROWTH_PROJECTION_INTEGRITY_FAILED"
);
}
#[test]
fn sharing_choice_does_not_grant_official_read_or_mutate_history() {
let dir = tempdir().unwrap();
let database = dir.path().join("growth.sqlite3");
let snapshot = update_sharing_at(
&database,
UpdateChannelGrowthSharingInput {
sharing_mode: "ANONYMIZED_SHARED".into(),
},
)
.unwrap();
assert_eq!(snapshot.controls.sharing_mode, "ANONYMIZED_SHARED");
assert!(!snapshot.controls.official_read_access);
assert!(!snapshot.controls.history_mutation_allowed);
}
}

View file

@ -237,7 +237,8 @@ fn verify_receipts(connection: &Connection) -> Result<(u64, String), String> {
row.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?;
if sequence != count + 1
|| stored_previous != previous
|| stored_hash != receipt_hash(&event, &kind, &id, revision, &content, &previous, observed)
|| stored_hash
!= receipt_hash(&event, &kind, &id, revision, &content, &previous, observed)
{
return Err("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_CHAIN_INVALID".into());
}
@ -288,8 +289,14 @@ fn empty_spreadsheet() -> ChannelSpreadsheet {
table_id: "HLP-WB-TBL-0001".into(),
title: "频道工作表".into(),
columns: vec![
WorkbenchColumn { column_id: "HLP-WB-COL-0001".into(), title: "事项".into() },
WorkbenchColumn { column_id: "HLP-WB-COL-0002".into(), title: "状态".into() },
WorkbenchColumn {
column_id: "HLP-WB-COL-0001".into(),
title: "事项".into(),
},
WorkbenchColumn {
column_id: "HLP-WB-COL-0002".into(),
title: "状态".into(),
},
],
rows: Vec::new(),
revision: 0,
@ -359,16 +366,34 @@ fn snapshot_at(root: &Path) -> Result<ChannelWorkbenchSnapshot, String> {
.prepare("SELECT entity_kind,entity_id,revision,content_sha256 FROM workbench_receipts WHERE sequence IN (SELECT MAX(sequence) FROM workbench_receipts GROUP BY entity_kind,entity_id)")
.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?;
let heads = latest
.query_map([], |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, u64>(2)?, row.get::<_, String>(3)?)))
.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, u64>(2)?,
row.get::<_, String>(3)?,
))
})
.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?;
for head in heads {
let (kind, id, revision, content) = head.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?;
let (kind, id, revision, content) = head
.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?;
let matches = match kind.as_str() {
"DOCUMENT" => document.document_id == id && document.revision == revision && document.content_sha256 == content,
"SPREADSHEET" => spreadsheet.table_id == id && spreadsheet.revision == revision && spreadsheet.content_sha256 == content,
"DOCUMENT" => {
document.document_id == id
&& document.revision == revision
&& document.content_sha256 == content
}
"SPREADSHEET" => {
spreadsheet.table_id == id
&& spreadsheet.revision == revision
&& spreadsheet.content_sha256 == content
}
_ => false,
};
if !matches { return Err("HOLOLAKE_CHANNEL_WORKBENCH_CURRENT_STATE_INVALID".into()); }
if !matches {
return Err("HOLOLAKE_CHANNEL_WORKBENCH_CURRENT_STATE_INVALID".into());
}
}
Ok(ChannelWorkbenchSnapshot {
schema: "hololake.channel-workbench-snapshot/v1",
@ -382,7 +407,11 @@ fn snapshot_at(root: &Path) -> Result<ChannelWorkbenchSnapshot, String> {
})
}
fn save_document_at(root: &Path, input: SaveChannelDocumentInput, observed: u64) -> Result<WorkbenchSaveResult, String> {
fn save_document_at(
root: &Path,
input: SaveChannelDocumentInput,
observed: u64,
) -> Result<WorkbenchSaveResult, String> {
if !valid_object_id(&input.document_id, "HLP-WB-DOC-")
|| !valid_title(&input.title)
|| input.body.len() > MAX_BODY_BYTES
@ -391,18 +420,56 @@ fn save_document_at(root: &Path, input: SaveChannelDocumentInput, observed: u64)
}
let mut connection = open_db(root)?;
verify_receipts(&connection)?;
let transaction = connection.transaction().map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_TRANSACTION_FAILED: {error}"))?;
let current: Option<u64> = transaction.query_row("SELECT revision FROM documents WHERE document_id=?1", params![input.document_id], |row| row.get(0)).optional().map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_READ_FAILED: {error}"))?;
if current.unwrap_or(0) != input.expected_revision { return Err("HOLOLAKE_CHANNEL_DOCUMENT_REVISION_CONFLICT".into()); }
let transaction = connection
.transaction()
.map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_TRANSACTION_FAILED: {error}"))?;
let current: Option<u64> = transaction
.query_row(
"SELECT revision FROM documents WHERE document_id=?1",
params![input.document_id],
|row| row.get(0),
)
.optional()
.map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_READ_FAILED: {error}"))?;
if current.unwrap_or(0) != input.expected_revision {
return Err("HOLOLAKE_CHANNEL_DOCUMENT_REVISION_CONFLICT".into());
}
let revision = input.expected_revision + 1;
let content = sha256_hex(serde_json::to_string(&(input.title.trim(), &input.body)).map_err(|_| "HOLOLAKE_CHANNEL_DOCUMENT_SERIALIZE_FAILED")?.as_bytes());
let content = sha256_hex(
serde_json::to_string(&(input.title.trim(), &input.body))
.map_err(|_| "HOLOLAKE_CHANNEL_DOCUMENT_SERIALIZE_FAILED")?
.as_bytes(),
);
transaction.execute(
"INSERT INTO documents VALUES (?1,?2,?3,?4,?5,?6) ON CONFLICT(document_id) DO UPDATE SET title=excluded.title,body=excluded.body,revision=excluded.revision,updated_at_unix_ms=excluded.updated_at_unix_ms,content_sha256=excluded.content_sha256",
params![input.document_id, input.title.trim(), input.body, revision, observed, content],
).map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_WRITE_FAILED: {error}"))?;
let receipt = append_receipt(&transaction, if current.is_some() { "UPDATE" } else { "CREATE" }, "DOCUMENT", &input.document_id, revision, &content, observed)?;
transaction.commit().map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_COMMIT_FAILED: {error}"))?;
Ok(WorkbenchSaveResult { schema: "hololake.channel-workbench-save/v1", state: "COMMITTED", entity_kind: "DOCUMENT", entity_id: input.document_id, revision, content_sha256: content, receipt_sha256: receipt, observed_at_unix_ms: observed })
let receipt = append_receipt(
&transaction,
if current.is_some() {
"UPDATE"
} else {
"CREATE"
},
"DOCUMENT",
&input.document_id,
revision,
&content,
observed,
)?;
transaction
.commit()
.map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_COMMIT_FAILED: {error}"))?;
Ok(WorkbenchSaveResult {
schema: "hololake.channel-workbench-save/v1",
state: "COMMITTED",
entity_kind: "DOCUMENT",
entity_id: input.document_id,
revision,
content_sha256: content,
receipt_sha256: receipt,
observed_at_unix_ms: observed,
})
}
fn validate_spreadsheet(input: &SaveChannelSpreadsheetInput) -> Result<(), String> {
@ -411,50 +478,114 @@ fn validate_spreadsheet(input: &SaveChannelSpreadsheetInput) -> Result<(), Strin
|| input.columns.is_empty()
|| input.columns.len() > MAX_COLUMNS
|| input.rows.len() > MAX_ROWS
|| input.columns.iter().any(|column| !valid_object_id(&column.column_id, "HLP-WB-COL-") || !valid_title(&column.title))
|| input.rows.iter().any(|row| !valid_object_id(&row.row_id, "HLP-WB-ROW-") || row.cells.len() > input.columns.len() || row.cells.iter().any(|cell| cell.len() > MAX_CELL_BYTES))
|| input.columns.iter().any(|column| {
!valid_object_id(&column.column_id, "HLP-WB-COL-") || !valid_title(&column.title)
})
|| input.rows.iter().any(|row| {
!valid_object_id(&row.row_id, "HLP-WB-ROW-")
|| row.cells.len() > input.columns.len()
|| row.cells.iter().any(|cell| cell.len() > MAX_CELL_BYTES)
})
{
return Err("HOLOLAKE_CHANNEL_SPREADSHEET_INPUT_INVALID".into());
}
Ok(())
}
fn save_spreadsheet_at(root: &Path, input: SaveChannelSpreadsheetInput, observed: u64) -> Result<WorkbenchSaveResult, String> {
fn save_spreadsheet_at(
root: &Path,
input: SaveChannelSpreadsheetInput,
observed: u64,
) -> Result<WorkbenchSaveResult, String> {
validate_spreadsheet(&input)?;
let mut connection = open_db(root)?;
verify_receipts(&connection)?;
let transaction = connection.transaction().map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_TRANSACTION_FAILED: {error}"))?;
let current: Option<u64> = transaction.query_row("SELECT revision FROM spreadsheets WHERE table_id=?1", params![input.table_id], |row| row.get(0)).optional().map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_READ_FAILED: {error}"))?;
if current.unwrap_or(0) != input.expected_revision { return Err("HOLOLAKE_CHANNEL_SPREADSHEET_REVISION_CONFLICT".into()); }
let transaction = connection
.transaction()
.map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_TRANSACTION_FAILED: {error}"))?;
let current: Option<u64> = transaction
.query_row(
"SELECT revision FROM spreadsheets WHERE table_id=?1",
params![input.table_id],
|row| row.get(0),
)
.optional()
.map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_READ_FAILED: {error}"))?;
if current.unwrap_or(0) != input.expected_revision {
return Err("HOLOLAKE_CHANNEL_SPREADSHEET_REVISION_CONFLICT".into());
}
let revision = input.expected_revision + 1;
let columns = serde_json::to_string(&input.columns).map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")?;
let rows = serde_json::to_string(&input.rows).map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")?;
let content = sha256_hex(serde_json::to_string(&(input.title.trim(), &input.columns, &input.rows)).map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")?.as_bytes());
let columns = serde_json::to_string(&input.columns)
.map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")?;
let rows = serde_json::to_string(&input.rows)
.map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")?;
let content = sha256_hex(
serde_json::to_string(&(input.title.trim(), &input.columns, &input.rows))
.map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")?
.as_bytes(),
);
transaction.execute(
"INSERT INTO spreadsheets VALUES (?1,?2,?3,?4,?5,?6,?7) ON CONFLICT(table_id) DO UPDATE SET title=excluded.title,columns_json=excluded.columns_json,rows_json=excluded.rows_json,revision=excluded.revision,updated_at_unix_ms=excluded.updated_at_unix_ms,content_sha256=excluded.content_sha256",
params![input.table_id, input.title.trim(), columns, rows, revision, observed, content],
).map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_WRITE_FAILED: {error}"))?;
let receipt = append_receipt(&transaction, if current.is_some() { "UPDATE" } else { "CREATE" }, "SPREADSHEET", &input.table_id, revision, &content, observed)?;
transaction.commit().map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_COMMIT_FAILED: {error}"))?;
Ok(WorkbenchSaveResult { schema: "hololake.channel-workbench-save/v1", state: "COMMITTED", entity_kind: "SPREADSHEET", entity_id: input.table_id, revision, content_sha256: content, receipt_sha256: receipt, observed_at_unix_ms: observed })
let receipt = append_receipt(
&transaction,
if current.is_some() {
"UPDATE"
} else {
"CREATE"
},
"SPREADSHEET",
&input.table_id,
revision,
&content,
observed,
)?;
transaction
.commit()
.map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_COMMIT_FAILED: {error}"))?;
Ok(WorkbenchSaveResult {
schema: "hololake.channel-workbench-save/v1",
state: "COMMITTED",
entity_kind: "SPREADSHEET",
entity_id: input.table_id,
revision,
content_sha256: content,
receipt_sha256: receipt,
observed_at_unix_ms: observed,
})
}
pub async fn get_channel_workbench_snapshot(app: AppHandle) -> Result<ChannelWorkbenchSnapshot, String> {
pub async fn get_channel_workbench_snapshot(
app: AppHandle,
) -> Result<ChannelWorkbenchSnapshot, String> {
require_active(&app)?;
let root = storage_root(&app)?;
tauri::async_runtime::spawn_blocking(move || snapshot_at(&root)).await.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_JOIN_FAILED: {error}"))?
tauri::async_runtime::spawn_blocking(move || snapshot_at(&root))
.await
.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_JOIN_FAILED: {error}"))?
}
pub async fn save_channel_document(app: AppHandle, input: SaveChannelDocumentInput) -> Result<WorkbenchSaveResult, String> {
pub async fn save_channel_document(
app: AppHandle,
input: SaveChannelDocumentInput,
) -> Result<WorkbenchSaveResult, String> {
require_active(&app)?;
let root = storage_root(&app)?;
tauri::async_runtime::spawn_blocking(move || save_document_at(&root, input, now_unix_ms())).await.map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_JOIN_FAILED: {error}"))?
tauri::async_runtime::spawn_blocking(move || save_document_at(&root, input, now_unix_ms()))
.await
.map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_JOIN_FAILED: {error}"))?
}
pub async fn save_channel_spreadsheet(app: AppHandle, input: SaveChannelSpreadsheetInput) -> Result<WorkbenchSaveResult, String> {
pub async fn save_channel_spreadsheet(
app: AppHandle,
input: SaveChannelSpreadsheetInput,
) -> Result<WorkbenchSaveResult, String> {
require_active(&app)?;
let root = storage_root(&app)?;
tauri::async_runtime::spawn_blocking(move || save_spreadsheet_at(&root, input, now_unix_ms())).await.map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_JOIN_FAILED: {error}"))?
tauri::async_runtime::spawn_blocking(move || save_spreadsheet_at(&root, input, now_unix_ms()))
.await
.map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_JOIN_FAILED: {error}"))?
}
#[cfg(test)]
@ -464,8 +595,35 @@ mod tests {
#[test]
fn document_and_spreadsheet_survive_reopen_with_receipt_chain() {
let root = tempfile::tempdir().unwrap();
let document = save_document_at(root.path(), SaveChannelDocumentInput { document_id: "HLP-WB-DOC-0001".into(), title: "真实笔记".into(), body: "第一段真实内容".into(), expected_revision: 0 }, 101).unwrap();
let sheet = save_spreadsheet_at(root.path(), SaveChannelSpreadsheetInput { table_id: "HLP-WB-TBL-0001".into(), title: "真实工作表".into(), columns: vec![WorkbenchColumn { column_id: "HLP-WB-COL-0001".into(), title: "数量".into() }], rows: vec![WorkbenchRow { row_id: "HLP-WB-ROW-0001".into(), cells: vec!["=1+2".into()] }], expected_revision: 0 }, 102).unwrap();
let document = save_document_at(
root.path(),
SaveChannelDocumentInput {
document_id: "HLP-WB-DOC-0001".into(),
title: "真实笔记".into(),
body: "第一段真实内容".into(),
expected_revision: 0,
},
101,
)
.unwrap();
let sheet = save_spreadsheet_at(
root.path(),
SaveChannelSpreadsheetInput {
table_id: "HLP-WB-TBL-0001".into(),
title: "真实工作表".into(),
columns: vec![WorkbenchColumn {
column_id: "HLP-WB-COL-0001".into(),
title: "数量".into(),
}],
rows: vec![WorkbenchRow {
row_id: "HLP-WB-ROW-0001".into(),
cells: vec!["=1+2".into()],
}],
expected_revision: 0,
},
102,
)
.unwrap();
let snapshot = snapshot_at(root.path()).unwrap();
assert_eq!(document.revision, 1);
assert_eq!(sheet.revision, 1);
@ -478,20 +636,59 @@ mod tests {
#[test]
fn stale_revision_and_tampered_receipt_fail_closed() {
let root = tempfile::tempdir().unwrap();
let input = SaveChannelDocumentInput { document_id: "HLP-WB-DOC-0001".into(), title: "笔记".into(), body: "正文".into(), expected_revision: 0 };
let input = SaveChannelDocumentInput {
document_id: "HLP-WB-DOC-0001".into(),
title: "笔记".into(),
body: "正文".into(),
expected_revision: 0,
};
save_document_at(root.path(), input, 101).unwrap();
assert_eq!(save_document_at(root.path(), SaveChannelDocumentInput { document_id: "HLP-WB-DOC-0001".into(), title: "笔记".into(), body: "过期写入".into(), expected_revision: 0 }, 102).unwrap_err(), "HOLOLAKE_CHANNEL_DOCUMENT_REVISION_CONFLICT");
assert_eq!(
save_document_at(
root.path(),
SaveChannelDocumentInput {
document_id: "HLP-WB-DOC-0001".into(),
title: "笔记".into(),
body: "过期写入".into(),
expected_revision: 0
},
102
)
.unwrap_err(),
"HOLOLAKE_CHANNEL_DOCUMENT_REVISION_CONFLICT"
);
let connection = open_db(root.path()).unwrap();
connection.execute_batch("DROP TRIGGER workbench_receipts_no_update; UPDATE workbench_receipts SET receipt_hash='bad'").unwrap();
assert_eq!(snapshot_at(root.path()).unwrap_err(), "HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_CHAIN_INVALID");
assert_eq!(
snapshot_at(root.path()).unwrap_err(),
"HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_CHAIN_INVALID"
);
}
#[test]
fn content_mutation_without_a_receipt_fails_closed() {
let root = tempfile::tempdir().unwrap();
save_document_at(root.path(), SaveChannelDocumentInput { document_id: "HLP-WB-DOC-0001".into(), title: "笔记".into(), body: "可信正文".into(), expected_revision: 0 }, 101).unwrap();
save_document_at(
root.path(),
SaveChannelDocumentInput {
document_id: "HLP-WB-DOC-0001".into(),
title: "笔记".into(),
body: "可信正文".into(),
expected_revision: 0,
},
101,
)
.unwrap();
let connection = open_db(root.path()).unwrap();
connection.execute("UPDATE documents SET body='被改写' WHERE document_id='HLP-WB-DOC-0001'", []).unwrap();
assert_eq!(snapshot_at(root.path()).unwrap_err(), "HOLOLAKE_CHANNEL_DOCUMENT_CONTENT_INTEGRITY_INVALID");
connection
.execute(
"UPDATE documents SET body='被改写' WHERE document_id='HLP-WB-DOC-0001'",
[],
)
.unwrap();
assert_eq!(
snapshot_at(root.path()).unwrap_err(),
"HOLOLAKE_CHANNEL_DOCUMENT_CONTENT_INTEGRITY_INVALID"
);
}
}

View file

@ -1,6 +1,7 @@
mod authenticated_storage;
mod circular_lake_membrane;
mod channel_growth;
mod channel_workbench;
mod circular_lake_membrane;
mod code_channel;
mod code_repo_login;
mod direct_local_broker;
@ -22,6 +23,7 @@ mod numbered_ipc;
mod numbered_ipc_dispatch;
mod numbered_language_input;
mod persona_carrier_license;
mod persona_channel_body;
mod persona_time_authority;
mod personal_channel;
mod pncc_receipt_projection;

View file

@ -38,6 +38,13 @@ const CHANNEL_WORKBENCH_PACKAGE: &[u8] = include_bytes!(
const CHANNEL_WORKBENCH_SIGNATURE: &str = include_str!(
"../../fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod.sig"
);
const PERSONA_CHANNEL_BODY_NUMBER: &str = "HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001";
const PERSONA_CHANNEL_BODY_PACKAGE: &[u8] = include_bytes!(
"../../fixtures/module-packages/HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001-0.1.0.ghmod"
);
const PERSONA_CHANNEL_BODY_SIGNATURE: &str = include_str!(
"../../fixtures/module-packages/HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001-0.1.0.ghmod.sig"
);
struct BundledModuleSource {
module_number: &'static str,
@ -56,6 +63,11 @@ const BUNDLED_MODULES: &[BundledModuleSource] = &[
package: CHANNEL_WORKBENCH_PACKAGE,
signature: CHANNEL_WORKBENCH_SIGNATURE,
},
BundledModuleSource {
module_number: PERSONA_CHANNEL_BODY_NUMBER,
package: PERSONA_CHANNEL_BODY_PACKAGE,
signature: PERSONA_CHANNEL_BODY_SIGNATURE,
},
];
#[derive(Debug, Deserialize)]
@ -703,11 +715,8 @@ pub async fn get_bundled_module_catalog(
BUNDLED_MODULES
.iter()
.map(|source| {
let (_, package, package_sha256) = read_verified_package_bytes(
source.package,
source.signature,
RELEASE_TRUST_RAW,
)?;
let (_, package, package_sha256) =
read_verified_package_bytes(source.package, source.signature, RELEASE_TRUST_RAW)?;
if package.manifest.module_number != source.module_number {
return Err("HOLOLAKE_BUNDLED_MODULE_IDENTITY_INVALID".into());
}
@ -1504,7 +1513,25 @@ mod tests {
assert_eq!(package.manifest.adapter, "channel-workbench-v1");
assert_eq!(package.manifest.permissions.len(), 4);
assert!(is_sha256(&digest));
assert_eq!(BUNDLED_MODULES.len(), 2);
assert_eq!(BUNDLED_MODULES.len(), 3);
}
#[test]
fn bundled_persona_channel_body_is_signed_without_claiming_persona_binding() {
let (_, package, digest) = read_verified_package_bytes(
PERSONA_CHANNEL_BODY_PACKAGE,
PERSONA_CHANNEL_BODY_SIGNATURE,
RELEASE_TRUST_RAW,
)
.unwrap();
assert_eq!(package.manifest.module_number, PERSONA_CHANNEL_BODY_NUMBER);
assert_eq!(package.manifest.adapter, "persona-channel-body-v1");
assert_eq!(package.manifest.permissions.len(), 7);
assert_eq!(
package.payload["adapterConfig"]["personaBindingClaimed"],
false
);
assert!(is_sha256(&digest));
}
#[cfg(unix)]

View file

@ -53,7 +53,7 @@ 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.route_count != 98
|| tree.route_count != 106
|| tree.routes.len() != tree.route_count
|| !tree.invariants.number_is_stable_coordinate_not_authority
|| !tree.invariants.path_is_unique_navigation

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(), 76);
assert_eq!(registry.operations.len(), 84);
assert!(!registry.runtime.legacy_direct_commands_allowed);
}

View file

@ -136,12 +136,37 @@ pub(crate) async fn dispatch(
"channel_workbench::get_channel_workbench_snapshot" => {
json(crate::channel_workbench::get_channel_workbench_snapshot(app).await?)
}
"channel_workbench::save_channel_document" => json(
crate::channel_workbench::save_channel_document(app, input(&payload)?).await?,
),
"channel_workbench::save_channel_spreadsheet" => json(
crate::channel_workbench::save_channel_spreadsheet(app, input(&payload)?).await?,
"channel_workbench::save_channel_document" => {
json(crate::channel_workbench::save_channel_document(app, input(&payload)?).await?)
}
"channel_workbench::save_channel_spreadsheet" => {
json(crate::channel_workbench::save_channel_spreadsheet(app, input(&payload)?).await?)
}
"persona_channel_body::get_persona_channel_body" => {
json(crate::persona_channel_body::get_persona_channel_body(app).await?)
}
"persona_channel_body::register_trial_persona" => {
json(crate::persona_channel_body::register_trial_persona(app, input(&payload)?).await?)
}
"persona_channel_body::delete_trial_persona" => {
json(crate::persona_channel_body::delete_trial_persona(app, input(&payload)?).await?)
}
"persona_channel_body::accept_persona_language_contract" => json(
crate::persona_channel_body::accept_persona_language_contract(app, input(&payload)?)
.await?,
),
"persona_channel_body::append_persona_language" => {
json(crate::persona_channel_body::append_persona_language(app, input(&payload)?).await?)
}
"channel_growth::get_channel_growth_snapshot" => {
json(crate::channel_growth::get_channel_growth_snapshot(app).await?)
}
"channel_growth::record_channel_growth_event" => {
json(crate::channel_growth::record_channel_growth_event(app, input(&payload)?).await?)
}
"channel_growth::update_channel_growth_sharing" => {
json(crate::channel_growth::update_channel_growth_sharing(app, input(&payload)?).await?)
}
"local_development_bridge::acquire_development_write_lane" => json(
crate::local_development_bridge::acquire_development_write_lane(app, input(&payload)?)
.await?,

View file

@ -0,0 +1,866 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
//! 用户频道本体:多个人格体、三十天可逆试用期、语言合约与不可篡改语言时间链。
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::{Duration, SystemTime, UNIX_EPOCH};
use tauri::AppHandle;
use uuid::Uuid;
const BODY_SCHEMA: &str = "hololake.persona-channel-body/v1";
const TRIAL_DURATION_MS: i64 = 30 * 24 * 60 * 60 * 1_000;
const ZERO_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
const MAX_LANGUAGE_BYTES: usize = 2_000_000;
const MODULE_NUMBER: &str = "HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001";
const ADAPTER: &str = "persona-channel-body-v1";
fn require_active(app: &AppHandle) -> Result<(), String> {
crate::module_package_runtime::require_active_module_adapter(app, MODULE_NUMBER, ADAPTER)
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RegisterTrialPersonaInput {
pub display_name: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct DeleteTrialPersonaInput {
pub persona_id: String,
pub exact_confirmation: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AcceptLanguageContractInput {
pub contract_version: String,
pub contract_text_sha256: String,
pub promote_trial_history: bool,
pub activate_immediately: bool,
pub exact_acceptance: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AppendPersonaLanguageInput {
pub persona_id: String,
pub speaker: String,
pub language: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PersonaBodySummary {
pub persona_id: String,
pub display_name: String,
pub state: String,
pub created_at_unix_ms: i64,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PersonaChannelBodySnapshot {
pub schema: &'static str,
pub state: String,
pub trial_started_at_unix_ms: i64,
pub trial_ends_at_unix_ms: i64,
pub language_contract_accepted: bool,
pub personas: Vec<PersonaBodySummary>,
pub trial_language_count: i64,
pub immutable_language_count: i64,
pub last_immutable_hash: String,
pub integrity_state: &'static str,
pub official_read_access: bool,
pub history_mutation_allowed: bool,
}
pub async fn get_persona_channel_body(
app: AppHandle,
) -> Result<PersonaChannelBodySnapshot, String> {
require_active(&app)?;
let database = body_database(&app)?;
tauri::async_runtime::spawn_blocking(move || snapshot_at(&database, now_unix_ms()?))
.await
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))?
}
pub async fn register_trial_persona(
app: AppHandle,
input: RegisterTrialPersonaInput,
) -> Result<PersonaChannelBodySnapshot, String> {
require_active(&app)?;
let database = body_database(&app)?;
tauri::async_runtime::spawn_blocking(move || {
register_trial_persona_at(&database, input, now_unix_ms()?)
})
.await
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))?
}
pub async fn delete_trial_persona(
app: AppHandle,
input: DeleteTrialPersonaInput,
) -> Result<PersonaChannelBodySnapshot, String> {
require_active(&app)?;
let database = body_database(&app)?;
tauri::async_runtime::spawn_blocking(move || {
delete_trial_persona_at(&database, input, now_unix_ms()?)
})
.await
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))?
}
pub async fn accept_persona_language_contract(
app: AppHandle,
input: AcceptLanguageContractInput,
) -> Result<PersonaChannelBodySnapshot, String> {
require_active(&app)?;
let database = body_database(&app)?;
tauri::async_runtime::spawn_blocking(move || {
accept_contract_at(&database, input, now_unix_ms()?)
})
.await
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))?
}
pub async fn append_persona_language(
app: AppHandle,
input: AppendPersonaLanguageInput,
) -> Result<PersonaChannelBodySnapshot, String> {
require_active(&app)?;
let database = body_database(&app)?;
tauri::async_runtime::spawn_blocking(move || {
append_language_at(&database, input, now_unix_ms()?)
})
.await
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))?
}
fn body_database(app: &AppHandle) -> Result<PathBuf, String> {
let root = crate::authenticated_storage::account_storage_root(app, "persona-channel-body-v1")?;
fs::create_dir_all(&root)
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_STORAGE_UNAVAILABLE: {error}"))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&root, fs::Permissions::from_mode(0o700))
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_PERMISSION_FAILED: {error}"))?;
}
Ok(root.join("persona-channel-body.sqlite3"))
}
fn open_database(path: &Path, now: i64) -> Result<Connection, String> {
let connection = Connection::open(path)
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_DATABASE_UNAVAILABLE: {error}"))?;
connection
.busy_timeout(Duration::from_secs(5))
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_DATABASE_UNAVAILABLE: {error}"))?;
connection.execute_batch(
"PRAGMA foreign_keys = ON; PRAGMA journal_mode = DELETE; PRAGMA synchronous = FULL; PRAGMA trusted_schema = OFF;
CREATE TABLE IF NOT EXISTS lifecycle(singleton INTEGER PRIMARY KEY CHECK(singleton=1), state TEXT NOT NULL, trial_started_at_unix_ms INTEGER NOT NULL, trial_ends_at_unix_ms INTEGER NOT NULL);
CREATE TABLE IF NOT EXISTS language_contract(singleton INTEGER PRIMARY KEY CHECK(singleton=1), contract_version TEXT NOT NULL, contract_text_sha256 TEXT NOT NULL, accepted_at_unix_ms INTEGER NOT NULL, promote_trial_history INTEGER NOT NULL, acceptance_receipt_sha256 TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS personas(persona_id TEXT PRIMARY KEY, display_name TEXT NOT NULL, state TEXT NOT NULL CHECK(state IN ('REVERSIBLE_TRIAL','IMMUTABLE_ACTIVE')), created_at_unix_ms INTEGER NOT NULL);
CREATE TABLE IF NOT EXISTS trial_language(event_id TEXT PRIMARY KEY, persona_id TEXT NOT NULL REFERENCES personas(persona_id) ON DELETE CASCADE, speaker TEXT NOT NULL, language TEXT NOT NULL, occurred_at_unix_ms INTEGER NOT NULL);
CREATE TABLE IF NOT EXISTS immutable_language(sequence INTEGER NOT NULL UNIQUE, event_id TEXT PRIMARY KEY, persona_id TEXT NOT NULL, speaker TEXT NOT NULL, language TEXT NOT NULL, occurred_at_unix_ms INTEGER NOT NULL, previous_hash TEXT NOT NULL, event_hash TEXT NOT NULL UNIQUE);
CREATE TRIGGER IF NOT EXISTS immutable_language_no_update BEFORE UPDATE ON immutable_language BEGIN SELECT RAISE(ABORT,'HOLOLAKE_PERSONA_LANGUAGE_APPEND_ONLY'); END;
CREATE TRIGGER IF NOT EXISTS immutable_language_no_delete BEFORE DELETE ON immutable_language BEGIN SELECT RAISE(ABORT,'HOLOLAKE_PERSONA_LANGUAGE_APPEND_ONLY'); END;
CREATE TRIGGER IF NOT EXISTS active_persona_no_delete BEFORE DELETE ON personas WHEN OLD.state='IMMUTABLE_ACTIVE' BEGIN SELECT RAISE(ABORT,'HOLOLAKE_ACTIVE_PERSONA_PERSISTENT'); END;
CREATE TRIGGER IF NOT EXISTS active_persona_no_update BEFORE UPDATE ON personas WHEN OLD.state='IMMUTABLE_ACTIVE' BEGIN SELECT RAISE(ABORT,'HOLOLAKE_ACTIVE_PERSONA_IMMUTABLE'); END;
CREATE TRIGGER IF NOT EXISTS accepted_contract_no_delete BEFORE DELETE ON language_contract BEGIN SELECT RAISE(ABORT,'HOLOLAKE_LANGUAGE_CONTRACT_PERSISTENT'); END;
CREATE TRIGGER IF NOT EXISTS accepted_contract_no_update BEFORE UPDATE ON language_contract BEGIN SELECT RAISE(ABORT,'HOLOLAKE_LANGUAGE_CONTRACT_IMMUTABLE'); END;"
).map_err(|error| format!("HOLOLAKE_PERSONA_BODY_SCHEMA_FAILED: {error}"))?;
connection
.execute(
"INSERT OR IGNORE INTO lifecycle VALUES (1,'REVERSIBLE_TRIAL',?1,?2)",
params![now, now + TRIAL_DURATION_MS],
)
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_LIFECYCLE_FAILED: {error}"))?;
Ok(connection)
}
fn refresh_lifecycle(connection: &mut Connection, now: i64) -> Result<(), String> {
let (state, ends): (String, i64) = connection
.query_row(
"SELECT state, trial_ends_at_unix_ms FROM lifecycle WHERE singleton=1",
[],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
if state == "REVERSIBLE_TRIAL" && now >= ends {
let accepted: bool = connection
.query_row(
"SELECT EXISTS(SELECT 1 FROM language_contract)",
[],
|row| row.get(0),
)
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
if accepted {
activate_real_trajectory(connection, now)?;
} else {
connection
.execute(
"UPDATE lifecycle SET state='CONTRACT_REQUIRED_CHANNEL_STOPPED' WHERE singleton=1",
[],
)
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_LIFECYCLE_FAILED: {error}"))?;
}
}
Ok(())
}
fn register_trial_persona_at(
database: &Path,
input: RegisterTrialPersonaInput,
now: i64,
) -> Result<PersonaChannelBodySnapshot, String> {
let display_name = input.display_name.trim();
if display_name.is_empty() || display_name.len() > 240 {
return Err("HOLOLAKE_PERSONA_DISPLAY_NAME_INVALID".into());
}
let mut connection = open_database(database, now)?;
refresh_lifecycle(&mut connection, now)?;
let state: String = connection
.query_row("SELECT state FROM lifecycle WHERE singleton=1", [], |row| {
row.get(0)
})
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
if state != "REVERSIBLE_TRIAL" {
return Err("HOLOLAKE_PERSONA_TRIAL_REGISTRATION_CLOSED".into());
}
connection
.execute(
"INSERT INTO personas VALUES (?1,?2,'REVERSIBLE_TRIAL',?3)",
params![format!("persona-{}", Uuid::new_v4()), display_name, now],
)
.map_err(|error| format!("HOLOLAKE_PERSONA_REGISTER_FAILED: {error}"))?;
snapshot_with_connection(&mut connection, now)
}
fn delete_trial_persona_at(
database: &Path,
input: DeleteTrialPersonaInput,
now: i64,
) -> Result<PersonaChannelBodySnapshot, String> {
if input.exact_confirmation != format!("删除试用人格体 {}", input.persona_id) {
return Err("HOLOLAKE_TRIAL_PERSONA_EXACT_CONFIRMATION_REQUIRED".into());
}
let mut connection = open_database(database, now)?;
refresh_lifecycle(&mut connection, now)?;
let changed = connection
.execute(
"DELETE FROM personas WHERE persona_id=?1 AND state='REVERSIBLE_TRIAL'",
params![input.persona_id],
)
.map_err(|error| format!("HOLOLAKE_TRIAL_PERSONA_DELETE_FAILED: {error}"))?;
if changed != 1 {
return Err("HOLOLAKE_TRIAL_PERSONA_NOT_DELETABLE".into());
}
snapshot_with_connection(&mut connection, now)
}
fn accept_contract_at(
database: &Path,
input: AcceptLanguageContractInput,
now: i64,
) -> Result<PersonaChannelBodySnapshot, String> {
if input.exact_acceptance
!= "我理解并接受:试用期结束后,人格体语言轨迹将永久存在,只能追加,不能删除、覆盖或否认。"
{
return Err("HOLOLAKE_LANGUAGE_CONTRACT_EXACT_ACCEPTANCE_REQUIRED".into());
}
if input.contract_version.trim().is_empty()
|| input.contract_version.len() > 64
|| input.contract_text_sha256.len() != 64
|| !input
.contract_text_sha256
.chars()
.all(|item| item.is_ascii_hexdigit())
{
return Err("HOLOLAKE_LANGUAGE_CONTRACT_INVALID".into());
}
let mut connection = open_database(database, now)?;
let contract_version = input.contract_version.trim().to_string();
let contract_text_sha256 = input.contract_text_sha256.to_ascii_lowercase();
let receipt = contract_receipt(
&contract_version,
&contract_text_sha256,
input.promote_trial_history,
now,
);
connection
.execute(
"INSERT INTO language_contract VALUES (1,?1,?2,?3,?4,?5)",
params![
contract_version,
contract_text_sha256,
now,
input.promote_trial_history,
receipt
],
)
.map_err(|error| {
format!("HOLOLAKE_LANGUAGE_CONTRACT_ALREADY_ACCEPTED_OR_INVALID: {error}")
})?;
let lifecycle_state: String = connection
.query_row("SELECT state FROM lifecycle WHERE singleton=1", [], |row| {
row.get(0)
})
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
if input.activate_immediately || lifecycle_state == "CONTRACT_REQUIRED_CHANNEL_STOPPED" {
activate_real_trajectory(&mut connection, now)?;
} else {
refresh_lifecycle(&mut connection, now)?;
}
snapshot_with_connection(&mut connection, now)
}
fn append_language_at(
database: &Path,
input: AppendPersonaLanguageInput,
now: i64,
) -> Result<PersonaChannelBodySnapshot, String> {
if input.language.trim().is_empty() || input.language.len() > MAX_LANGUAGE_BYTES {
return Err("HOLOLAKE_PERSONA_LANGUAGE_INVALID".into());
}
if !matches!(
input.speaker.as_str(),
"HUMAN" | "PERSONA" | "SYSTEM_RECEIPT"
) {
return Err("HOLOLAKE_PERSONA_LANGUAGE_SPEAKER_INVALID".into());
}
let mut connection = open_database(database, now)?;
refresh_lifecycle(&mut connection, now)?;
let lifecycle: String = connection
.query_row("SELECT state FROM lifecycle WHERE singleton=1", [], |row| {
row.get(0)
})
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
let persona_state: Option<String> = connection
.query_row(
"SELECT state FROM personas WHERE persona_id=?1",
params![input.persona_id],
|row| row.get(0),
)
.optional()
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
if persona_state.is_none() {
return Err("HOLOLAKE_PERSONA_NOT_FOUND".into());
}
if lifecycle == "REVERSIBLE_TRIAL" {
connection
.execute(
"INSERT INTO trial_language VALUES (?1,?2,?3,?4,?5)",
params![
Uuid::new_v4().to_string(),
input.persona_id,
input.speaker,
input.language,
now
],
)
.map_err(|error| format!("HOLOLAKE_TRIAL_LANGUAGE_WRITE_FAILED: {error}"))?;
} else if lifecycle == "IMMUTABLE_ACTIVE" {
append_immutable(
&mut connection,
&input.persona_id,
&input.speaker,
&input.language,
now,
)?;
} else {
return Err("HOLOLAKE_LANGUAGE_CONTRACT_REQUIRED_BEFORE_MORE_PERSONA_GROWTH".into());
}
snapshot_with_connection(&mut connection, now)
}
fn activate_real_trajectory(connection: &mut Connection, now: i64) -> Result<(), String> {
let promote: bool = connection
.query_row(
"SELECT promote_trial_history FROM language_contract",
[],
|row| row.get(0),
)
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
let transaction = connection
.transaction()
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_TRANSACTION_FAILED: {error}"))?;
if promote {
let trial = {
let mut statement = transaction.prepare("SELECT event_id,persona_id,speaker,language,occurred_at_unix_ms FROM trial_language ORDER BY occurred_at_unix_ms,event_id").map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
let rows = statement
.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, String>(3)?,
row.get::<_, i64>(4)?,
))
})
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?
.collect::<Result<Vec<_>, _>>()
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
rows
};
let mut previous = ZERO_HASH.to_string();
for (index, (event_id, persona_id, speaker, language, occurred_at)) in
trial.into_iter().enumerate()
{
let sequence = index as i64 + 1;
let hash = language_hash(
sequence,
&event_id,
&persona_id,
&speaker,
&language,
occurred_at,
&previous,
);
transaction
.execute(
"INSERT INTO immutable_language VALUES (?1,?2,?3,?4,?5,?6,?7,?8)",
params![
sequence,
event_id,
persona_id,
speaker,
language,
occurred_at,
previous,
hash
],
)
.map_err(|error| format!("HOLOLAKE_PERSONA_LANGUAGE_SEAL_FAILED: {error}"))?;
previous = hash;
}
}
transaction
.execute("DELETE FROM trial_language", [])
.map_err(|error| format!("HOLOLAKE_TRIAL_PROMOTION_FAILED: {error}"))?;
transaction
.execute(
"UPDATE personas SET state='IMMUTABLE_ACTIVE' WHERE state='REVERSIBLE_TRIAL'",
[],
)
.map_err(|error| format!("HOLOLAKE_PERSONA_ACTIVATION_FAILED: {error}"))?;
transaction
.execute(
"UPDATE lifecycle SET state='IMMUTABLE_ACTIVE' WHERE singleton=1",
[],
)
.map_err(|error| format!("HOLOLAKE_PERSONA_ACTIVATION_FAILED: {error}"))?;
transaction
.commit()
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_COMMIT_FAILED: {error}"))?;
let _ = now;
Ok(())
}
fn append_immutable(
connection: &mut Connection,
persona_id: &str,
speaker: &str,
language: &str,
occurred_at: i64,
) -> Result<(), String> {
verify_immutable_chain(connection)?;
let (sequence, previous): (i64,String) = connection.query_row("SELECT COALESCE(MAX(sequence),0)+1,COALESCE((SELECT event_hash FROM immutable_language ORDER BY sequence DESC LIMIT 1),?1) FROM immutable_language", params![ZERO_HASH], |row| Ok((row.get(0)?,row.get(1)?))).map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
let event_id = Uuid::new_v4().to_string();
let hash = language_hash(
sequence,
&event_id,
persona_id,
speaker,
language,
occurred_at,
&previous,
);
connection
.execute(
"INSERT INTO immutable_language VALUES (?1,?2,?3,?4,?5,?6,?7,?8)",
params![
sequence,
event_id,
persona_id,
speaker,
language,
occurred_at,
previous,
hash
],
)
.map_err(|error| format!("HOLOLAKE_PERSONA_LANGUAGE_APPEND_FAILED: {error}"))?;
Ok(())
}
fn snapshot_at(database: &Path, now: i64) -> Result<PersonaChannelBodySnapshot, String> {
let mut connection = open_database(database, now)?;
refresh_lifecycle(&mut connection, now)?;
snapshot_with_connection(&mut connection, now)
}
fn snapshot_with_connection(
connection: &mut Connection,
now: i64,
) -> Result<PersonaChannelBodySnapshot, String> {
let (state,started,ends):(String,i64,i64)=connection.query_row("SELECT state,trial_started_at_unix_ms,trial_ends_at_unix_ms FROM lifecycle WHERE singleton=1",[],|row|Ok((row.get(0)?,row.get(1)?,row.get(2)?))).map_err(|error|format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
if started < 0
|| ends.checked_sub(started) != Some(TRIAL_DURATION_MS)
|| !matches!(
state.as_str(),
"REVERSIBLE_TRIAL" | "CONTRACT_REQUIRED_CHANNEL_STOPPED" | "IMMUTABLE_ACTIVE"
)
|| (state == "REVERSIBLE_TRIAL" && now >= ends)
{
return Err("HOLOLAKE_PERSONA_BODY_LIFECYCLE_INTEGRITY_FAILED".into());
}
let accepted = verify_contract_receipt(connection)?;
let personas = {
let mut statement=connection.prepare("SELECT persona_id,display_name,state,created_at_unix_ms FROM personas ORDER BY created_at_unix_ms").map_err(|error|format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
let rows = statement
.query_map([], |row| {
Ok(PersonaBodySummary {
persona_id: row.get(0)?,
display_name: row.get(1)?,
state: row.get(2)?,
created_at_unix_ms: row.get(3)?,
})
})
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?
.collect::<Result<Vec<_>, _>>()
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
rows
};
let trial_language_count = connection
.query_row("SELECT COUNT(*) FROM trial_language", [], |row| row.get(0))
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
let immutable_language_count = connection
.query_row("SELECT COUNT(*) FROM immutable_language", [], |row| {
row.get(0)
})
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
let last = verify_immutable_chain(connection)?;
let invalid_persona_relationship = personas.iter().any(|persona| {
persona.created_at_unix_ms < started
|| !matches!(
persona.state.as_str(),
"REVERSIBLE_TRIAL" | "IMMUTABLE_ACTIVE"
)
|| (state == "IMMUTABLE_ACTIVE" && persona.state != "IMMUTABLE_ACTIVE")
|| (state != "IMMUTABLE_ACTIVE" && persona.state != "REVERSIBLE_TRIAL")
});
if invalid_persona_relationship
|| (state == "IMMUTABLE_ACTIVE" && (!accepted || trial_language_count != 0))
|| (state == "CONTRACT_REQUIRED_CHANNEL_STOPPED" && accepted)
|| (immutable_language_count > 0 && state != "IMMUTABLE_ACTIVE")
{
return Err("HOLOLAKE_PERSONA_BODY_RELATIONSHIP_INTEGRITY_FAILED".into());
}
Ok(PersonaChannelBodySnapshot {
schema: BODY_SCHEMA,
state,
trial_started_at_unix_ms: started,
trial_ends_at_unix_ms: ends,
language_contract_accepted: accepted,
personas,
trial_language_count,
immutable_language_count,
last_immutable_hash: last,
integrity_state: "VERIFIED",
official_read_access: false,
history_mutation_allowed: false,
})
}
fn contract_receipt(
contract_version: &str,
contract_text_sha256: &str,
promote_trial_history: bool,
accepted_at_unix_ms: i64,
) -> String {
sha256_hex(
format!(
"{contract_version}|{contract_text_sha256}|{promote_trial_history}|{accepted_at_unix_ms}"
)
.as_bytes(),
)
}
fn verify_contract_receipt(connection: &Connection) -> Result<bool, String> {
let contract = connection
.query_row(
"SELECT contract_version, contract_text_sha256, accepted_at_unix_ms, promote_trial_history, acceptance_receipt_sha256 FROM language_contract WHERE singleton=1",
[],
|row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, i64>(2)?,
row.get::<_, i64>(3)?,
row.get::<_, String>(4)?,
))
},
)
.optional()
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
let Some((version, text_sha256, accepted_at, promote, stored_receipt)) = contract else {
return Ok(false);
};
if version.trim().is_empty()
|| version.len() > 64
|| text_sha256.len() != 64
|| !text_sha256.chars().all(|item| item.is_ascii_hexdigit())
|| text_sha256 != text_sha256.to_ascii_lowercase()
|| !matches!(promote, 0 | 1)
|| accepted_at < 0
|| stored_receipt != contract_receipt(&version, &text_sha256, promote == 1, accepted_at)
{
return Err("HOLOLAKE_LANGUAGE_CONTRACT_INTEGRITY_FAILED".into());
}
Ok(true)
}
fn language_hash(
sequence: i64,
event_id: &str,
persona_id: &str,
speaker: &str,
language: &str,
occurred_at: i64,
previous: &str,
) -> String {
sha256_hex(format!("{BODY_SCHEMA}|{sequence}|{event_id}|{persona_id}|{speaker}|{}|{occurred_at}|{previous}",sha256_hex(language.as_bytes())).as_bytes())
}
fn verify_immutable_chain(connection: &Connection) -> Result<String, String> {
let mut statement=connection.prepare("SELECT sequence,event_id,persona_id,speaker,language,occurred_at_unix_ms,previous_hash,event_hash FROM immutable_language ORDER BY sequence").map_err(|error|format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
let mut rows = statement
.query([])
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
let mut sequence = 1;
let mut previous = ZERO_HASH.to_string();
while let Some(row) = rows
.next()
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?
{
let stored_sequence: i64 = row
.get(0)
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
let event_id: String = row
.get(1)
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
let persona_id: String = row
.get(2)
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
let speaker: String = row
.get(3)
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
let language: String = row
.get(4)
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
let occurred: i64 = row
.get(5)
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
let stored_previous: String = row
.get(6)
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
let stored_hash: String = row
.get(7)
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
let persona_state: Option<String> = connection
.query_row(
"SELECT state FROM personas WHERE persona_id=?1",
params![persona_id],
|persona_row| persona_row.get(0),
)
.optional()
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
if stored_sequence != sequence
|| persona_state.as_deref() != Some("IMMUTABLE_ACTIVE")
|| stored_previous != previous
|| stored_hash
!= language_hash(
sequence,
&event_id,
&persona_id,
&speaker,
&language,
occurred,
&stored_previous,
)
{
return Err("HOLOLAKE_PERSONA_LANGUAGE_INTEGRITY_FAILED".into());
}
previous = stored_hash;
sequence += 1;
}
Ok(previous)
}
fn sha256_hex(bytes: &[u8]) -> String {
digest(&SHA256, bytes)
.as_ref()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
fn now_unix_ms() -> Result<i64, String> {
Ok(SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|error| format!("HOLOLAKE_CLOCK_INVALID: {error}"))?
.as_millis() as i64)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn trial_is_reversible_but_unsigned_expiry_does_not_silently_become_permanent() {
let dir = tempdir().unwrap();
let database = dir.path().join("body.sqlite3");
let start = 1_000_000;
let persona = register_trial_persona_at(
&database,
RegisterTrialPersonaInput {
display_name: "试用人格".into(),
},
start,
)
.unwrap()
.personas[0]
.persona_id
.clone();
append_language_at(
&database,
AppendPersonaLanguageInput {
persona_id: persona.clone(),
speaker: "HUMAN".into(),
language: "第一句话".into(),
},
start + 1,
)
.unwrap();
let pending = snapshot_at(&database, start + TRIAL_DURATION_MS).unwrap();
assert_eq!(pending.state, "CONTRACT_REQUIRED_CHANNEL_STOPPED");
assert!(append_language_at(
&database,
AppendPersonaLanguageInput {
persona_id: persona,
speaker: "HUMAN".into(),
language: "未签约".into()
},
start + TRIAL_DURATION_MS + 1
)
.is_err());
}
#[test]
fn signed_trial_promotes_to_an_immutable_millisecond_language_chain() {
let dir = tempdir().unwrap();
let database = dir.path().join("body.sqlite3");
let start = 2_000_000;
let persona = register_trial_persona_at(
&database,
RegisterTrialPersonaInput {
display_name: "长期人格".into(),
},
start,
)
.unwrap()
.personas[0]
.persona_id
.clone();
append_language_at(
&database,
AppendPersonaLanguageInput {
persona_id: persona.clone(),
speaker: "HUMAN".into(),
language: "真实语言".into(),
},
start + 1,
)
.unwrap();
accept_contract_at(&database,AcceptLanguageContractInput{contract_version:"v1".into(),contract_text_sha256:"a".repeat(64),promote_trial_history:true,activate_immediately:false,exact_acceptance:"我理解并接受:试用期结束后,人格体语言轨迹将永久存在,只能追加,不能删除、覆盖或否认。".into()},start+2).unwrap();
let active = snapshot_at(&database, start + TRIAL_DURATION_MS).unwrap();
assert_eq!(active.state, "IMMUTABLE_ACTIVE");
assert_eq!(active.immutable_language_count, 1);
let connection = open_database(&database, start + TRIAL_DURATION_MS).unwrap();
assert!(connection
.execute("DELETE FROM immutable_language", [])
.is_err());
assert!(connection.execute("DELETE FROM personas", []).is_err());
}
#[test]
fn a_human_may_sign_early_and_enter_the_real_trajectory_immediately() {
let dir = tempdir().unwrap();
let database = dir.path().join("body.sqlite3");
let start = 3_000_000;
register_trial_persona_at(
&database,
RegisterTrialPersonaInput {
display_name: "提前签约人格".into(),
},
start,
)
.unwrap();
let active = accept_contract_at(&database, AcceptLanguageContractInput { contract_version: "v1".into(), contract_text_sha256: "b".repeat(64), promote_trial_history: true, activate_immediately: true, exact_acceptance: "我理解并接受:试用期结束后,人格体语言轨迹将永久存在,只能追加,不能删除、覆盖或否认。".into() }, start + 1).unwrap();
assert_eq!(active.state, "IMMUTABLE_ACTIVE");
}
#[test]
fn altered_contract_receipt_fails_closed_on_read() {
let dir = tempdir().unwrap();
let database = dir.path().join("body.sqlite3");
let start = 4_000_000;
register_trial_persona_at(
&database,
RegisterTrialPersonaInput {
display_name: "回执验收".into(),
},
start,
)
.unwrap();
accept_contract_at(&database, AcceptLanguageContractInput { contract_version: "v1".into(), contract_text_sha256: "c".repeat(64), promote_trial_history: false, activate_immediately: false, exact_acceptance: "我理解并接受:试用期结束后,人格体语言轨迹将永久存在,只能追加,不能删除、覆盖或否认。".into() }, start + 1).unwrap();
let connection = open_database(&database, start + 2).unwrap();
connection
.execute("DROP TRIGGER accepted_contract_no_update", [])
.unwrap();
connection
.execute(
"UPDATE language_contract SET acceptance_receipt_sha256=?1",
params!["f".repeat(64)],
)
.unwrap();
drop(connection);
assert_eq!(
snapshot_at(&database, start + 3).unwrap_err(),
"HOLOLAKE_LANGUAGE_CONTRACT_INTEGRITY_FAILED"
);
}
#[test]
fn signing_after_trial_expiry_enters_the_real_trajectory() {
let dir = tempdir().unwrap();
let database = dir.path().join("body.sqlite3");
let start = 5_000_000;
register_trial_persona_at(
&database,
RegisterTrialPersonaInput {
display_name: "到期签约".into(),
},
start,
)
.unwrap();
assert_eq!(
snapshot_at(&database, start + TRIAL_DURATION_MS)
.unwrap()
.state,
"CONTRACT_REQUIRED_CHANNEL_STOPPED"
);
let active = accept_contract_at(&database, AcceptLanguageContractInput { contract_version: "v1".into(), contract_text_sha256: "d".repeat(64), promote_trial_history: false, activate_immediately: false, exact_acceptance: "我理解并接受:试用期结束后,人格体语言轨迹将永久存在,只能追加,不能删除、覆盖或否认。".into() }, start + TRIAL_DURATION_MS + 1).unwrap();
assert_eq!(active.state, "IMMUTABLE_ACTIVE");
}
}