fix: integrate language channel through numbered IPC and isolate education module

正向合并正律化(2026-08-20):
- 语言人格频道与执行手脚训练按编号 IPC 正律接入(MOD-0035/0036,OP-0158..0163),
  剥离直连 tauri::command,恢复单一 numbered 网关铁律
- 教育模块按冰朔谕与 2026-08-19 隔离回执整体移出(证据归隔离区),留空模块占位
- 光湖主域门户挂公告与版本中心(冰朔要求的公告与版本更新主阵地)
- 知识工作台内嵌共同工作频道(PersonaChannelBody embedded + knowledgeFocus)
- foundation/stage-one 合同补聊天运行时验收声明;版本对齐 0.9.1
- 测试名册计数与断言更新至合并后现实:JS 180/180,Rust 194/194
This commit is contained in:
冰朔 2026-08-20 12:23:02 +08:00
commit add00a0377
42 changed files with 622 additions and 5580 deletions

View file

@ -463,6 +463,16 @@ dependencies = [
"url",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
@ -486,7 +496,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
dependencies = [
"bitflags 2.13.1",
"core-foundation",
"core-foundation 0.10.1",
"core-graphics-types",
"foreign-types",
"libc",
@ -499,7 +509,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
"bitflags 2.13.1",
"core-foundation",
"core-foundation 0.10.1",
"libc",
]
@ -1547,7 +1557,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hololake-native-desktop"
version = "0.5.0"
version = "0.9.1"
dependencies = [
"base64 0.22.1",
"calamine",
@ -1557,6 +1567,7 @@ dependencies = [
"fs2",
"futures-util",
"interprocess",
"keyring",
"minisign-verify",
"qrcode",
"quick-xml 0.31.0",
@ -2075,6 +2086,18 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "keyring"
version = "3.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
dependencies = [
"log",
"security-framework 2.11.1",
"security-framework 3.7.0",
"zeroize",
]
[[package]]
name = "kuchikiki"
version = "0.8.8-speedreader"
@ -3493,7 +3516,7 @@ dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework",
"security-framework 3.7.0",
]
[[package]]
@ -3512,7 +3535,7 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
dependencies = [
"core-foundation",
"core-foundation 0.10.1",
"core-foundation-sys",
"jni",
"log",
@ -3521,7 +3544,7 @@ dependencies = [
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki",
"security-framework",
"security-framework 3.7.0",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
@ -3632,6 +3655,19 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.13.1",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework"
version = "3.7.0"
@ -3639,7 +3675,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags 2.13.1",
"core-foundation",
"core-foundation 0.10.1",
"core-foundation-sys",
"libc",
"security-framework-sys",
@ -3900,6 +3936,16 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "simd-adler32"
version = "0.3.10"
@ -4140,7 +4186,7 @@ checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7"
dependencies = [
"bitflags 2.13.1",
"block2",
"core-foundation",
"core-foundation 0.10.1",
"core-graphics",
"crossbeam-channel",
"dispatch",
@ -4646,6 +4692,7 @@ dependencies = [
"libc",
"mio",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"windows-sys 0.61.2",
]

View file

@ -42,10 +42,10 @@ tokio = { version = "1", features = ["io-util", "process", "time"] }
futures-util = "0.3"
minisign-verify = "0.2.5"
qrcode = { version = "0.14", default-features = false, features = ["svg"] }
keyring = { version = "3.6.3", features = ["apple-native"] }
[target.'cfg(windows)'.dependencies]
widestring = "1"
[dev-dependencies]
tempfile = "3"
keyring = { version = "3.6.3", features = ["apple-native"] }

View file

@ -113,12 +113,10 @@ struct BoundaryDrillAnswer {
completion_policy: String,
}
#[tauri::command]
pub async fn get_execution_limb_snapshot(app: AppHandle) -> Result<ExecutionLimbSnapshot, String> {
snapshot(&app).await
}
#[tauri::command]
pub async fn run_execution_limb_training_drill(
app: AppHandle,
input: RunTrainingDrillInput,

View file

@ -7,8 +7,6 @@ mod code_repo_login;
mod direct_local_broker;
mod direct_local_session;
mod dynamic_capability_routing;
mod education_translation;
mod education_workspace;
mod execution_limb_training;
mod enterprise_work_channel;
mod external_ai_gateway;
@ -66,21 +64,7 @@ pub fn run() {
.plugin(tauri_plugin_dialog::init())
.manage(numbered_ipc::NumberedIpcState::default())
.manage(mobile_sync::MobileSyncState::default())
.invoke_handler(tauri::generate_handler![
numbered_ipc::numbered_ipc,
persona_channel_body::get_persona_channel_body,
persona_channel_body::register_trial_persona,
persona_channel_body::delete_trial_persona,
persona_channel_body::accept_persona_language_contract,
persona_channel_body::append_persona_language,
persona_language_channel::discover_model_providers,
persona_language_channel::upsert_model_provider,
persona_language_channel::get_conversation,
persona_language_channel::send_message,
execution_limb_training::get_execution_limb_snapshot,
execution_limb_training::run_execution_limb_training_drill,
])
.setup(|app| {
.invoke_handler(tauri::generate_handler![numbered_ipc::numbered_ipc,]) .setup(|app| {
// GLS 是 HoloLake 产品内核,不是开发机旁路服务。合同、依赖闭包、
// 自举编译器或回执账本任一不可用时,产品启动失败关闭。
gls_protocol_kernel::start_on_application_open(app.handle())?;

View file

@ -45,13 +45,6 @@ const PERSONA_CHANNEL_BODY_PACKAGE: &[u8] = include_bytes!(
const PERSONA_CHANNEL_BODY_SIGNATURE: &str = include_str!(
"../../fixtures/module-packages/HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001-0.1.0.ghmod.sig"
);
const EDUCATION_WORKBENCH_NUMBER: &str = "HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001";
const EDUCATION_WORKBENCH_PACKAGE: &[u8] = include_bytes!(
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod"
);
const EDUCATION_WORKBENCH_SIGNATURE: &str = include_str!(
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod.sig"
);
const WEB_NOVEL_WORKBENCH_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001";
const WEB_NOVEL_WORKBENCH_PACKAGE: &[u8] = include_bytes!(
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001-0.1.0.ghmod"
@ -123,11 +116,6 @@ const BUNDLED_MODULES: &[BundledModuleSource] = &[
package: PERSONA_CHANNEL_BODY_PACKAGE,
signature: PERSONA_CHANNEL_BODY_SIGNATURE,
},
BundledModuleSource {
module_number: EDUCATION_WORKBENCH_NUMBER,
package: EDUCATION_WORKBENCH_PACKAGE,
signature: EDUCATION_WORKBENCH_SIGNATURE,
},
BundledModuleSource {
module_number: WEB_NOVEL_WORKBENCH_NUMBER,
package: WEB_NOVEL_WORKBENCH_PACKAGE,
@ -1611,7 +1599,7 @@ 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(), 11);
assert_eq!(BUNDLED_MODULES.len(), 10);
}
#[test]
@ -1632,24 +1620,6 @@ mod tests {
assert!(is_sha256(&digest));
}
#[test]
fn bundled_education_workbench_is_an_official_signed_module() {
let (_, package, digest) = read_verified_package_bytes(
EDUCATION_WORKBENCH_PACKAGE,
EDUCATION_WORKBENCH_SIGNATURE,
RELEASE_TRUST_RAW,
)
.unwrap();
assert_eq!(package.manifest.module_number, EDUCATION_WORKBENCH_NUMBER);
assert_eq!(package.manifest.registration_class, "OFFICIAL_LIGHTHOUSE");
assert_eq!(package.manifest.adapter, "education-workbench-v1");
assert_eq!(package.manifest.permissions.len(), 8);
assert_eq!(
package.payload["adapterConfig"]["importDefaultScope"],
"UNASSIGNED"
);
assert!(is_sha256(&digest));
}
#[test]
fn bundled_web_novel_family_uses_one_signed_lifecycle_and_exact_numbers() {

View file

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

View file

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

View file

@ -234,67 +234,6 @@ pub(crate) async fn dispatch(
"channel_growth::update_channel_growth_sharing" => {
json(crate::channel_growth::update_channel_growth_sharing(app, input(&payload)?).await?)
}
"education_workspace::get_education_workspace_snapshot" => {
json(crate::education_workspace::get_education_workspace_snapshot(app).await?)
}
"education_workspace::create_education_document" => json(
crate::education_workspace::create_education_document(app, input(&payload)?).await?,
),
"education_workspace::read_education_document" => {
json(crate::education_workspace::read_education_document(app, input(&payload)?).await?)
}
"education_workspace::save_education_document" => {
json(crate::education_workspace::save_education_document(app, input(&payload)?).await?)
}
"education_workspace::archive_education_document" => json(
crate::education_workspace::archive_education_document(app, input(&payload)?).await?,
),
"education_workspace::create_education_table" => {
json(crate::education_workspace::create_education_table(app, input(&payload)?).await?)
}
"education_workspace::read_education_table" => {
json(crate::education_workspace::read_education_table(app, input(&payload)?).await?)
}
"education_workspace::save_education_table" => {
json(crate::education_workspace::save_education_table(app, input(&payload)?).await?)
}
"education_workspace::archive_education_table" => {
json(crate::education_workspace::archive_education_table(app, input(&payload)?).await?)
}
"education_workspace::assign_imported_table_to_education" => json(
crate::education_workspace::assign_imported_table_to_education(app, input(&payload)?)
.await?,
),
"education_translation::import_education_tables_from_dialog" => {
json(crate::education_translation::import_education_tables_from_dialog(app).await?)
}
"education_translation::export_education_table_to_dialog" => json(
crate::education_translation::export_education_table_to_dialog(app, input(&payload)?)
.await?,
),
"education_translation::get_education_recognition_capability" => {
json(crate::education_translation::get_education_recognition_capability(app)?)
}
"education_workspace::create_education_automation_rule" => json(
crate::education_workspace::create_education_automation_rule(app, input(&payload)?)
.await?,
),
"education_workspace::save_education_automation_rule" => json(
crate::education_workspace::save_education_automation_rule(app, input(&payload)?)
.await?,
),
"education_workspace::archive_education_automation_rule" => json(
crate::education_workspace::archive_education_automation_rule(app, input(&payload)?)
.await?,
),
"education_workspace::preview_education_automation_rule" => json(
crate::education_workspace::preview_education_automation_rule(app, input(&payload)?)
.await?,
),
"education_workspace::execute_education_automation_rule" => json(
crate::education_workspace::execute_education_automation_rule(app, input(&payload)?)
.await?,
),
"web_novel_workspace::get_web_novel_workspace_snapshot" => {
json(crate::web_novel_workspace::get_web_novel_workspace_snapshot(app).await?)
}
@ -450,6 +389,24 @@ pub(crate) async fn dispatch(
crate::personal_channel::transition_personal_channel_task(app, input(&payload)?)
.await?,
),
"persona_language_channel::discover_model_providers" => {
json(crate::persona_language_channel::discover_model_providers(app).await?)
}
"persona_language_channel::upsert_model_provider" => {
json(crate::persona_language_channel::upsert_model_provider(app, input(&payload)?).await?)
}
"persona_language_channel::get_conversation" => {
json(crate::persona_language_channel::get_conversation(app, input(&payload)?).await?)
}
"persona_language_channel::send_message" => {
json(crate::persona_language_channel::send_message(app, input(&payload)?).await?)
}
"execution_limb_training::get_execution_limb_snapshot" => {
json(crate::execution_limb_training::get_execution_limb_snapshot(app).await?)
}
"execution_limb_training::run_execution_limb_training_drill" => {
json(crate::execution_limb_training::run_execution_limb_training_drill(app, input(&payload)?).await?)
}
"persona_time_authority::issue_persona_time_ticket" => json(
crate::persona_time_authority::issue_persona_time_ticket(app, input(&payload)?).await?,
),

View file

@ -15,22 +15,28 @@ 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")]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RegisterTrialPersonaInput {
pub display_name: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[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")]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AcceptLanguageContractInput {
pub contract_version: String,
pub contract_text_sha256: String,
@ -40,7 +46,7 @@ pub struct AcceptLanguageContractInput {
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AppendPersonaLanguageInput {
pub persona_id: String,
pub speaker: String,
@ -73,16 +79,9 @@ pub struct PersonaChannelBodySnapshot {
pub history_mutation_allowed: bool,
}
#[tauri::command]
pub async fn get_persona_channel_body(
app: AppHandle,
) -> Result<PersonaChannelBodySnapshot, String> {
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}"))?
}
/// 语言频道读取已注册人格体摘要的共用入口2026-08-20 正向合并:自语言人格频道线移植)。
#[allow(clippy::too_many_lines)]
pub(crate) fn require_registered_persona(
app: &AppHandle,
persona_id: &str,
@ -110,11 +109,21 @@ pub(crate) fn require_registered_persona(
.ok_or_else(|| "HOLOLAKE_PERSONA_NOT_REGISTERED".to_string())
}
#[tauri::command]
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()?)
@ -123,11 +132,11 @@ pub async fn register_trial_persona(
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))?
}
#[tauri::command]
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()?)
@ -136,11 +145,11 @@ pub async fn delete_trial_persona(
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))?
}
#[tauri::command]
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()?)
@ -149,11 +158,11 @@ pub async fn accept_persona_language_contract(
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))?
}
#[tauri::command]
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()?)
@ -305,19 +314,20 @@ fn accept_contract_at(
return Err("HOLOLAKE_LANGUAGE_CONTRACT_INVALID".into());
}
let mut connection = open_database(database, now)?;
let receipt = sha256_hex(
format!(
"{}|{}|{}|{}",
input.contract_version, input.contract_text_sha256, input.promote_trial_history, now
)
.as_bytes(),
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![
input.contract_version,
input.contract_text_sha256.to_ascii_lowercase(),
contract_version,
contract_text_sha256,
now,
input.promote_trial_history,
receipt
@ -326,7 +336,12 @@ fn accept_contract_at(
.map_err(|error| {
format!("HOLOLAKE_LANGUAGE_CONTRACT_ALREADY_ACCEPTED_OR_INVALID: {error}")
})?;
if input.activate_immediately {
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)?;
@ -520,16 +535,20 @@ fn snapshot_at(database: &Path, now: i64) -> Result<PersonaChannelBodySnapshot,
}
fn snapshot_with_connection(
connection: &mut Connection,
_now: i64,
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}"))?;
let accepted = connection
.query_row(
"SELECT EXISTS(SELECT 1 FROM language_contract)",
[],
|row| row.get(0),
if started < 0
|| ends.checked_sub(started) != Some(TRIAL_DURATION_MS)
|| !matches!(
state.as_str(),
"REVERSIBLE_TRIAL" | "CONTRACT_REQUIRED_CHANNEL_STOPPED" | "IMMUTABLE_ACTIVE"
)
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|| (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
@ -555,6 +574,22 @@ fn snapshot_with_connection(
})
.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,
@ -571,6 +606,54 @@ fn snapshot_with_connection(
})
}
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,
@ -593,14 +676,40 @@ fn verify_immutable_chain(connection: &Connection) -> Result<String, String> {
.next()
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?
{
let event_id: String = row.get(1).unwrap();
let persona_id: String = row.get(2).unwrap();
let speaker: String = row.get(3).unwrap();
let language: String = row.get(4).unwrap();
let occurred: i64 = row.get(5).unwrap();
let stored_previous: String = row.get(6).unwrap();
let stored_hash: String = row.get(7).unwrap();
if row.get::<_, i64>(0).unwrap() != sequence
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(
@ -730,4 +839,58 @@ mod tests {
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");
}
}

View file

@ -141,7 +141,6 @@ pub struct ConversationSnapshot {
pub projection_invariant: &'static str,
}
#[tauri::command]
pub async fn discover_model_providers(app: AppHandle) -> Result<ProviderDiscoverySnapshot, String> {
let mut providers = Vec::new();
let mut seen = HashSet::new();
@ -228,7 +227,6 @@ pub async fn discover_model_providers(app: AppHandle) -> Result<ProviderDiscover
})
}
#[tauri::command]
pub async fn upsert_model_provider(
app: AppHandle,
input: UpsertProviderInput,
@ -279,7 +277,6 @@ pub async fn upsert_model_provider(
discover_model_providers(app).await
}
#[tauri::command]
pub async fn get_conversation(
app: AppHandle,
input: ConversationInput,
@ -292,7 +289,6 @@ pub async fn get_conversation(
.map_err(|error| format!("HOLOLAKE_PERSONA_CONVERSATION_JOIN_FAILED: {error}"))?
}
#[tauri::command]
pub async fn send_message(
app: AppHandle,
input: SendMessageInput,