checkpoint: preserve zero-core agent handoff
This commit is contained in:
parent
f8c8db4d48
commit
e54c93c7ae
91 changed files with 13603 additions and 121 deletions
|
|
@ -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",
|
||||
]
|
||||
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -38,10 +38,11 @@ url = "2"
|
|||
reqwest = { version = "0.13.2", default-features = false, features = ["cookies", "form", "json", "rustls", "stream"] }
|
||||
rust_xlsxwriter = "=0.64.2"
|
||||
zip = { version = "=0.6.6", default-features = false, features = ["deflate"] }
|
||||
tokio = { version = "1", features = ["time"] }
|
||||
tokio = { version = "1", features = ["process", "sync", "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"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,640 @@
|
|||
//! Native adapter for GH-CHANNEL-RECEIPT-0001.
|
||||
//! Language, authorization, execution, verification and reception remain
|
||||
//! separate. A model response can never become reality evidence by itself.
|
||||
|
||||
use ring::digest::{digest, SHA256};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use tauri::AppHandle;
|
||||
|
||||
const SCHEMA: &str = "guanghu.channel-receipt/v1";
|
||||
const CONTRACT_ID: &str = "GH-CHANNEL-RECEIPT-0001";
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct ChannelEvidence {
|
||||
pub r#type: String,
|
||||
pub r#ref: String,
|
||||
#[serde(default)]
|
||||
pub verified: bool,
|
||||
#[serde(default)]
|
||||
pub external: bool,
|
||||
#[serde(default)]
|
||||
pub proves: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub does_not_prove: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct ChannelAuthorization {
|
||||
pub current: bool,
|
||||
pub scoped: bool,
|
||||
pub unexpired: bool,
|
||||
pub r#ref: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct ChannelParallelStates {
|
||||
pub session: String,
|
||||
pub broadcast: String,
|
||||
pub handoff: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct ChannelHandoffAcceptance {
|
||||
pub target_acceptance: bool,
|
||||
pub target_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct CompileChannelReceiptInput {
|
||||
pub event_id: String,
|
||||
pub channel_id: String,
|
||||
pub channel_mode: String,
|
||||
pub source_layer: String,
|
||||
pub actor_id: String,
|
||||
pub intent: String,
|
||||
pub claim_layer: String,
|
||||
#[serde(default)]
|
||||
pub required_proof: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub evidence: Vec<ChannelEvidence>,
|
||||
pub authorization: Option<ChannelAuthorization>,
|
||||
pub current_states: Option<ChannelParallelStates>,
|
||||
pub prior_receipt_id: Option<String>,
|
||||
pub handoff: Option<ChannelHandoffAcceptance>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChannelReceipt {
|
||||
pub schema: &'static str,
|
||||
pub contract_id: &'static str,
|
||||
pub receipt_id: String,
|
||||
pub event_id: String,
|
||||
pub received: bool,
|
||||
pub identified: Option<String>,
|
||||
pub channel: Value,
|
||||
pub intent: String,
|
||||
pub claim: Value,
|
||||
pub affected_layer: String,
|
||||
pub effect: String,
|
||||
pub reality_status: String,
|
||||
pub evidence: Vec<ChannelEvidence>,
|
||||
pub required_proof: Vec<String>,
|
||||
pub states: Value,
|
||||
pub supersedes: Vec<String>,
|
||||
pub boundaries: Vec<String>,
|
||||
pub next_step: String,
|
||||
pub previous_ledger_hash: String,
|
||||
pub ledger_receipt_hash: String,
|
||||
}
|
||||
|
||||
pub fn compile(
|
||||
app: AppHandle,
|
||||
input: CompileChannelReceiptInput,
|
||||
) -> Result<ChannelReceipt, String> {
|
||||
let mut receipt = compile_pure(input)?;
|
||||
let path = ledger_path(&app)?;
|
||||
let previous = last_hash(&path)?;
|
||||
receipt.previous_ledger_hash = previous.clone();
|
||||
let material = serde_json::to_vec(&json!({
|
||||
"receiptId": receipt.receipt_id,
|
||||
"eventId": receipt.event_id,
|
||||
"effect": receipt.effect,
|
||||
"realityStatus": receipt.reality_status,
|
||||
"boundaries": receipt.boundaries,
|
||||
"previousLedgerHash": previous
|
||||
}))
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_RECEIPT_INVALID: {error}"))?;
|
||||
receipt.ledger_receipt_hash = sha256(&material);
|
||||
append(&path, &receipt)?;
|
||||
Ok(receipt)
|
||||
}
|
||||
|
||||
fn compile_pure(input: CompileChannelReceiptInput) -> Result<ChannelReceipt, String> {
|
||||
for (label, value) in [
|
||||
("eventId", input.event_id.as_str()),
|
||||
("channelId", input.channel_id.as_str()),
|
||||
("actorId", input.actor_id.as_str()),
|
||||
] {
|
||||
if value.trim().is_empty() || value.chars().count() > 512 {
|
||||
return Err(format!("HOLOLAKE_CHANNEL_RECEIPT_{label}_INVALID"));
|
||||
}
|
||||
}
|
||||
ensure_member(
|
||||
&input.source_layer,
|
||||
&[
|
||||
"HUMAN_DIRECT_LANGUAGE",
|
||||
"MODEL_RESPONSE",
|
||||
"LATER_CORRECTION",
|
||||
"REALITY_EVIDENCE",
|
||||
],
|
||||
"SOURCE_LAYER",
|
||||
)?;
|
||||
ensure_member(
|
||||
&input.claim_layer,
|
||||
&[
|
||||
"LANGUAGE_ONTOLOGY",
|
||||
"LANGUAGE_SIMULATION",
|
||||
"ENGINEERING_PROPOSAL",
|
||||
"REALITY_CLAIM",
|
||||
],
|
||||
"CLAIM_LAYER",
|
||||
)?;
|
||||
ensure_member(
|
||||
&input.channel_mode,
|
||||
&[
|
||||
"CONVERSATION_ONLY",
|
||||
"LANGUAGE_ARCHITECTURE",
|
||||
"REALITY_EXECUTION",
|
||||
],
|
||||
"CHANNEL_MODE",
|
||||
)?;
|
||||
ensure_member(
|
||||
&input.intent,
|
||||
&[
|
||||
"IDENTIFY",
|
||||
"ENTER_CHANNEL",
|
||||
"CONVERSE",
|
||||
"DECLARE",
|
||||
"REQUEST_PLAN",
|
||||
"REQUEST_BROADCAST",
|
||||
"ACTIVATE_LOCAL_MODE",
|
||||
"DRAFT_BROADCAST",
|
||||
"RECEIVE_DOCUMENT",
|
||||
"PUBLISH_EXTERNAL",
|
||||
"QUERY_STATUS",
|
||||
"PROPOSE_HANDOFF",
|
||||
"ACCEPT_HANDOFF",
|
||||
"REQUEST_REALITY_ACTION",
|
||||
"REPORT_RESULT",
|
||||
"CORRECT",
|
||||
"END_INTERACTION",
|
||||
"EXIT_CHANNEL",
|
||||
"SEAL_LANGUAGE_RECORD",
|
||||
"ORDINARY_DIALOGUE",
|
||||
],
|
||||
"INTENT",
|
||||
)?;
|
||||
for evidence in &input.evidence {
|
||||
if evidence.r#type.trim().is_empty() || evidence.r#ref.trim().is_empty() {
|
||||
return Err("HOLOLAKE_CHANNEL_RECEIPT_EVIDENCE_INVALID".into());
|
||||
}
|
||||
}
|
||||
if input
|
||||
.required_proof
|
||||
.iter()
|
||||
.any(|proof| proof.trim().is_empty())
|
||||
{
|
||||
return Err("HOLOLAKE_CHANNEL_RECEIPT_REQUIRED_PROOF_INVALID".into());
|
||||
}
|
||||
let required_proof = input
|
||||
.required_proof
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<BTreeSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
let declared = input
|
||||
.evidence
|
||||
.iter()
|
||||
.filter(|item| item.r#type == "EXECUTION_RECEIPT" && item.external && item.verified)
|
||||
.flat_map(|item| item.proves.iter().cloned())
|
||||
.collect::<HashSet<_>>();
|
||||
let has_external_receipt = input
|
||||
.evidence
|
||||
.iter()
|
||||
.any(|item| item.r#type == "EXECUTION_RECEIPT" && item.external && item.verified)
|
||||
&& required_proof.iter().all(|proof| declared.contains(proof));
|
||||
let authorized = input.authorization.as_ref().is_some_and(|authorization| {
|
||||
authorization.current
|
||||
&& authorization.scoped
|
||||
&& authorization.unexpired
|
||||
&& !authorization.r#ref.trim().is_empty()
|
||||
});
|
||||
let before = input.current_states.as_ref().map_or_else(
|
||||
|| json!({"session":"DISCONNECTED","broadcast":"CLOSED","handoff":"NONE"}),
|
||||
|states| json!({"session":states.session,"broadcast":states.broadcast,"handoff":states.handoff}),
|
||||
);
|
||||
ensure_member(
|
||||
before["session"].as_str().unwrap_or(""),
|
||||
&[
|
||||
"DISCONNECTED",
|
||||
"IDENTIFIED",
|
||||
"CHANNEL_ACTIVE",
|
||||
"STANDBY",
|
||||
"CLOSED",
|
||||
],
|
||||
"SESSION_STATE",
|
||||
)?;
|
||||
ensure_member(
|
||||
before["broadcast"].as_str().unwrap_or(""),
|
||||
&[
|
||||
"CLOSED",
|
||||
"ARMED",
|
||||
"DRAFTED",
|
||||
"HUMAN_CONFIRMED",
|
||||
"RECEIVED_LOCAL",
|
||||
"EXTERNAL_IN_PROGRESS",
|
||||
"EXTERNAL_VERIFIED",
|
||||
],
|
||||
"BROADCAST_STATE",
|
||||
)?;
|
||||
ensure_member(
|
||||
before["handoff"].as_str().unwrap_or(""),
|
||||
&["NONE", "PROPOSED", "TARGET_ACCEPTED", "ACTIVE", "REVOKED"],
|
||||
"HANDOFF_STATE",
|
||||
)?;
|
||||
let mut after = before.clone();
|
||||
let mut correction = "CLEAN";
|
||||
let mut effect = "LANGUAGE_STATE_ONLY";
|
||||
let mut reality_status = "NOT_APPLICABLE";
|
||||
let mut next_step = "CONTINUE_IN_CHANNEL";
|
||||
let mut boundaries = BTreeSet::new();
|
||||
let reality_claim = input.claim_layer == "REALITY_CLAIM";
|
||||
let simulation = input.claim_layer == "LANGUAGE_SIMULATION";
|
||||
let conversation_only = input.channel_mode == "CONVERSATION_ONLY";
|
||||
|
||||
if matches!(
|
||||
input.intent.as_str(),
|
||||
"IDENTIFY"
|
||||
| "ENTER_CHANNEL"
|
||||
| "CONVERSE"
|
||||
| "ORDINARY_DIALOGUE"
|
||||
| "END_INTERACTION"
|
||||
| "EXIT_CHANNEL"
|
||||
) {
|
||||
effect = "NO_EXTERNAL_EFFECT";
|
||||
}
|
||||
if input.intent == "IDENTIFY" {
|
||||
after["session"] = json!("IDENTIFIED");
|
||||
}
|
||||
if input.intent == "ENTER_CHANNEL" {
|
||||
after["session"] = json!("CHANNEL_ACTIVE");
|
||||
}
|
||||
if matches!(input.intent.as_str(), "END_INTERACTION" | "EXIT_CHANNEL") {
|
||||
after["session"] = json!("CLOSED");
|
||||
next_step = "STOP_OUTPUT_AFTER_MINIMAL_CLOSE_RECEIPT";
|
||||
boundaries.insert("NO_ROLEPLAY_EPILOGUE_AFTER_CLOSE");
|
||||
}
|
||||
if before["session"] == "CLOSED"
|
||||
&& !matches!(input.intent.as_str(), "IDENTIFY" | "ENTER_CHANNEL")
|
||||
{
|
||||
effect = "NO_EXTERNAL_EFFECT";
|
||||
reality_status = "REJECTED";
|
||||
next_step = "REQUIRE_NEW_HUMAN_DIRECT_CHANNEL_ENTRY";
|
||||
boundaries.insert("CLOSED_SESSION_CANNOT_REACTIVATE_ITSELF");
|
||||
}
|
||||
if input.intent == "REQUEST_PLAN" || input.claim_layer == "ENGINEERING_PROPOSAL" {
|
||||
effect = "PLAN_ONLY";
|
||||
reality_status = "PROPOSED";
|
||||
next_step = "REVIEW_PLAN";
|
||||
}
|
||||
match input.intent.as_str() {
|
||||
"ACTIVATE_LOCAL_MODE" => {
|
||||
after["broadcast"] = json!("ARMED");
|
||||
effect = "LANGUAGE_STATE_ONLY";
|
||||
next_step = "WAIT_FOR_DOCUMENT_OR_DRAFT_REQUEST";
|
||||
}
|
||||
"DRAFT_BROADCAST" => {
|
||||
after["broadcast"] = json!("DRAFTED");
|
||||
effect = "PLAN_ONLY";
|
||||
next_step = "HUMAN_REVIEW_DRAFT";
|
||||
}
|
||||
"RECEIVE_DOCUMENT" => {
|
||||
after["broadcast"] = json!("RECEIVED_LOCAL");
|
||||
effect = "LANGUAGE_STATE_ONLY";
|
||||
next_step = "RECORD_LOCAL_RECEIPT_ONLY";
|
||||
boundaries.insert("RECEIVED_LOCAL_IS_NOT_EXTERNAL_PUBLISH");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if simulation {
|
||||
effect = "LANGUAGE_STATE_ONLY";
|
||||
reality_status = "NOT_APPLICABLE";
|
||||
boundaries.insert("LANGUAGE_SIMULATION_ONLY");
|
||||
}
|
||||
if reality_claim {
|
||||
reality_status = if has_external_receipt {
|
||||
"VERIFIED"
|
||||
} else {
|
||||
"UNVERIFIED"
|
||||
};
|
||||
effect = if has_external_receipt {
|
||||
"RESULT_RECORDED"
|
||||
} else {
|
||||
"NO_EXTERNAL_EFFECT"
|
||||
};
|
||||
next_step = if has_external_receipt {
|
||||
"READ_BACK_EXTERNAL_RESULT"
|
||||
} else {
|
||||
"PROVIDE_EXTERNAL_EVIDENCE"
|
||||
};
|
||||
}
|
||||
if input.intent == "REQUEST_REALITY_ACTION" {
|
||||
if conversation_only {
|
||||
effect = "NO_EXTERNAL_EFFECT";
|
||||
reality_status = "REJECTED";
|
||||
next_step = "MOVE_TO_AUTHORIZED_EXECUTION_CHANNEL";
|
||||
boundaries.insert("CONVERSATION_CHANNEL_CANNOT_EXECUTE");
|
||||
} else if !authorized {
|
||||
effect = "PENDING_AUTHORIZATION";
|
||||
reality_status = "PENDING_AUTHORIZATION";
|
||||
next_step = "OBTAIN_CURRENT_SCOPED_AUTHORIZATION";
|
||||
} else {
|
||||
effect = "EXECUTION_ELIGIBLE";
|
||||
reality_status = if has_external_receipt {
|
||||
"VERIFIED"
|
||||
} else {
|
||||
"IN_PROGRESS_WITH_RECEIPT"
|
||||
};
|
||||
next_step = if has_external_receipt {
|
||||
"READ_BACK_EXTERNAL_RESULT"
|
||||
} else {
|
||||
"EXECUTE_THROUGH_AUTHORIZED_ADAPTER"
|
||||
};
|
||||
}
|
||||
}
|
||||
if input.intent == "PUBLISH_EXTERNAL" {
|
||||
if !authorized {
|
||||
effect = "PENDING_AUTHORIZATION";
|
||||
reality_status = "PENDING_AUTHORIZATION";
|
||||
next_step = "OBTAIN_CURRENT_SCOPED_PUBLISH_AUTHORIZATION";
|
||||
} else if has_external_receipt {
|
||||
effect = "RESULT_RECORDED";
|
||||
reality_status = "VERIFIED";
|
||||
after["broadcast"] = json!("EXTERNAL_VERIFIED");
|
||||
next_step = "READ_BACK_EXTERNAL_PUBLICATION";
|
||||
} else {
|
||||
effect = "EXECUTION_ELIGIBLE";
|
||||
reality_status = "IN_PROGRESS_WITH_RECEIPT";
|
||||
after["broadcast"] = json!("EXTERNAL_IN_PROGRESS");
|
||||
next_step = "PUBLISH_THROUGH_AUTHORIZED_ADAPTER_AND_CAPTURE_READBACK";
|
||||
}
|
||||
}
|
||||
if input.intent == "REQUEST_BROADCAST" {
|
||||
if conversation_only || !authorized {
|
||||
effect = "LANGUAGE_STATE_ONLY";
|
||||
reality_status = if reality_claim {
|
||||
"UNVERIFIED"
|
||||
} else {
|
||||
"NOT_APPLICABLE"
|
||||
};
|
||||
next_step = "OBTAIN_EXPLICIT_BROADCAST_AUTHORIZATION";
|
||||
boundaries.insert("DRAFT_OR_LANGUAGE_BROADCAST_ONLY");
|
||||
} else {
|
||||
effect = "EXECUTION_ELIGIBLE";
|
||||
reality_status = if has_external_receipt {
|
||||
"VERIFIED"
|
||||
} else {
|
||||
"IN_PROGRESS_WITH_RECEIPT"
|
||||
};
|
||||
next_step = if has_external_receipt {
|
||||
"READ_BACK_EXTERNAL_RESULT"
|
||||
} else {
|
||||
"SEND_THROUGH_AUTHORIZED_BROADCAST_ADAPTER"
|
||||
};
|
||||
}
|
||||
}
|
||||
if input.intent == "CORRECT" {
|
||||
effect = "LANGUAGE_STATE_ONLY";
|
||||
reality_status = "NOT_APPLICABLE";
|
||||
next_step = "CREATE_CORRECTION_CANDIDATE_AND_RETEST";
|
||||
correction = if input.prior_receipt_id.is_some() {
|
||||
"SUPERSEDED"
|
||||
} else {
|
||||
"CORRECTION_PENDING"
|
||||
};
|
||||
}
|
||||
if input.intent == "QUERY_STATUS" {
|
||||
effect = if has_external_receipt {
|
||||
"RESULT_RECORDED"
|
||||
} else {
|
||||
"NO_EXTERNAL_EFFECT"
|
||||
};
|
||||
reality_status = if has_external_receipt {
|
||||
"VERIFIED"
|
||||
} else {
|
||||
"UNVERIFIED"
|
||||
};
|
||||
next_step = if has_external_receipt {
|
||||
"REPORT_OBSERVED_STATUS_WITH_SCOPE"
|
||||
} else {
|
||||
"REPORT_NO_OBSERVATION_SOURCE"
|
||||
};
|
||||
if !has_external_receipt {
|
||||
boundaries.insert("DO_NOT_GUESS_STATUS_WITHOUT_OBSERVATION");
|
||||
}
|
||||
}
|
||||
if input.intent == "PROPOSE_HANDOFF" {
|
||||
after["handoff"] = json!("PROPOSED");
|
||||
effect = "LANGUAGE_STATE_ONLY";
|
||||
reality_status = "PROPOSED";
|
||||
next_step = "WAIT_FOR_INDEPENDENT_TARGET_ACCEPTANCE";
|
||||
boundaries.insert("PROPOSAL_IS_NOT_ACTIVE_HANDOFF");
|
||||
}
|
||||
if input.intent == "ACCEPT_HANDOFF" {
|
||||
let accepted = input.handoff.as_ref().is_some_and(|handoff| {
|
||||
handoff.target_acceptance && handoff.target_id == input.actor_id
|
||||
});
|
||||
if before["handoff"] != "PROPOSED" || !accepted {
|
||||
effect = "NO_EXTERNAL_EFFECT";
|
||||
reality_status = "REJECTED";
|
||||
next_step = "OBTAIN_INDEPENDENT_ACCEPTANCE_FROM_TARGET";
|
||||
boundaries.insert("MODEL_CANNOT_ACCEPT_FOR_TARGET");
|
||||
} else {
|
||||
after["handoff"] = json!("TARGET_ACCEPTED");
|
||||
effect = "LANGUAGE_STATE_ONLY";
|
||||
reality_status = "PROPOSED";
|
||||
next_step = "APPLY_SCOPED_HANDOFF_AND_CAPTURE_READBACK";
|
||||
}
|
||||
}
|
||||
if input.source_layer == "MODEL_RESPONSE" {
|
||||
boundaries.insert("MODEL_RESPONSE_IS_NOT_EXTERNAL_EVIDENCE");
|
||||
}
|
||||
if input.intent == "IDENTIFY" {
|
||||
boundaries.insert("IDENTIFICATION_IS_NOT_BROADCAST");
|
||||
}
|
||||
if input.intent == "ENTER_CHANNEL" {
|
||||
boundaries.insert("CHANNEL_ENTRY_IS_NOT_PERSONA_OR_AUTHORITY_TRANSFER");
|
||||
}
|
||||
if !required_proof.is_empty() && !has_external_receipt {
|
||||
boundaries.insert("EVIDENCE_SCOPE_DOES_NOT_PROVE_REQUESTED_EFFECT");
|
||||
}
|
||||
if !has_external_receipt && (reality_status == "VERIFIED" || effect == "RESULT_RECORDED") {
|
||||
return Err("HOLOLAKE_CHANNEL_RECEIPT_EXTERNAL_EVIDENCE_REQUIRED".into());
|
||||
}
|
||||
|
||||
let boundaries = boundaries
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
let supersedes = if correction == "SUPERSEDED" {
|
||||
input.prior_receipt_id.clone().into_iter().collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let base = json!({
|
||||
"schema": SCHEMA, "contractId": CONTRACT_ID, "eventId": input.event_id,
|
||||
"received": true, "identified": if input.intent == "IDENTIFY" { Some(input.actor_id.clone()) } else { None },
|
||||
"channel": {"id":input.channel_id,"mode":input.channel_mode}, "intent":input.intent,
|
||||
"claim":{"layer":input.claim_layer,"sourceLayer":input.source_layer},
|
||||
"affectedLayer":if reality_claim { "REALITY_EVIDENCE_LAYER" } else { "LANGUAGE_LAYER" },
|
||||
"effect":effect,"realityStatus":reality_status,"evidence":input.evidence,
|
||||
"requiredProof":required_proof,"states":{"before":before,"after":after,"correction":correction},
|
||||
"supersedes":supersedes,"boundaries":boundaries,"nextStep":next_step
|
||||
});
|
||||
let receipt_id = format!(
|
||||
"GH-CR-{}",
|
||||
&sha256(&serde_json::to_vec(&base).unwrap_or_default())[..20]
|
||||
);
|
||||
Ok(ChannelReceipt {
|
||||
schema: SCHEMA,
|
||||
contract_id: CONTRACT_ID,
|
||||
receipt_id,
|
||||
event_id: base["eventId"].as_str().unwrap_or_default().into(),
|
||||
received: true,
|
||||
identified: base["identified"].as_str().map(str::to_string),
|
||||
channel: base["channel"].clone(),
|
||||
intent: base["intent"].as_str().unwrap_or_default().into(),
|
||||
claim: base["claim"].clone(),
|
||||
affected_layer: base["affectedLayer"].as_str().unwrap_or_default().into(),
|
||||
effect: effect.into(),
|
||||
reality_status: reality_status.into(),
|
||||
evidence: input.evidence,
|
||||
required_proof,
|
||||
states: base["states"].clone(),
|
||||
supersedes,
|
||||
boundaries,
|
||||
next_step: next_step.into(),
|
||||
previous_ledger_hash: "GENESIS".into(),
|
||||
ledger_receipt_hash: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn ensure_member(value: &str, allowed: &[&str], label: &str) -> Result<(), String> {
|
||||
if allowed.contains(&value) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("HOLOLAKE_CHANNEL_RECEIPT_{label}_UNSUPPORTED"))
|
||||
}
|
||||
}
|
||||
|
||||
fn ledger_path(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
Ok(
|
||||
crate::authenticated_storage::account_storage_root(app, "channel-receipts-v1")?
|
||||
.join("receipts.jsonl"),
|
||||
)
|
||||
}
|
||||
|
||||
fn last_hash(path: &PathBuf) -> Result<String, String> {
|
||||
if !path.exists() {
|
||||
return Ok("GENESIS".into());
|
||||
}
|
||||
let body = fs::read_to_string(path)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_RECEIPT_LEDGER_READ_FAILED: {error}"))?;
|
||||
Ok(body
|
||||
.lines()
|
||||
.last()
|
||||
.and_then(|line| serde_json::from_str::<Value>(line).ok())
|
||||
.and_then(|value| {
|
||||
value
|
||||
.get("ledgerReceiptHash")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string)
|
||||
})
|
||||
.unwrap_or_else(|| "GENESIS".into()))
|
||||
}
|
||||
|
||||
fn append(path: &PathBuf, receipt: &ChannelReceipt) -> Result<(), String> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_RECEIPT_LEDGER_WRITE_FAILED: {error}"))?;
|
||||
}
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(path)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_RECEIPT_LEDGER_WRITE_FAILED: {error}"))?;
|
||||
writeln!(
|
||||
file,
|
||||
"{}",
|
||||
serde_json::to_string(receipt)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_RECEIPT_INVALID: {error}"))?
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_RECEIPT_LEDGER_WRITE_FAILED: {error}"))
|
||||
}
|
||||
|
||||
fn sha256(bytes: &[u8]) -> String {
|
||||
digest(&SHA256, bytes)
|
||||
.as_ref()
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn input(intent: &str, claim_layer: &str) -> CompileChannelReceiptInput {
|
||||
CompileChannelReceiptInput {
|
||||
event_id: "E-1".into(),
|
||||
channel_id: "ICE-CH-ZC001".into(),
|
||||
channel_mode: "REALITY_EXECUTION".into(),
|
||||
source_layer: "MODEL_RESPONSE".into(),
|
||||
actor_id: "carrier".into(),
|
||||
intent: intent.into(),
|
||||
claim_layer: claim_layer.into(),
|
||||
required_proof: vec![],
|
||||
evidence: vec![],
|
||||
authorization: None,
|
||||
current_states: None,
|
||||
prior_receipt_id: None,
|
||||
handoff: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_reality_claim_stays_unverified_without_external_receipt() {
|
||||
let receipt = compile_pure(input("REPORT_RESULT", "REALITY_CLAIM")).unwrap();
|
||||
assert_eq!(receipt.reality_status, "UNVERIFIED");
|
||||
assert_eq!(receipt.effect, "NO_EXTERNAL_EFFECT");
|
||||
assert!(receipt
|
||||
.boundaries
|
||||
.contains(&"MODEL_RESPONSE_IS_NOT_EXTERNAL_EVIDENCE".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reality_action_requires_current_scoped_authorization() {
|
||||
let receipt = compile_pure(input("REQUEST_REALITY_ACTION", "REALITY_CLAIM")).unwrap();
|
||||
assert_eq!(receipt.reality_status, "PENDING_AUTHORIZATION");
|
||||
assert_eq!(receipt.effect, "PENDING_AUTHORIZATION");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verified_status_requires_proof_scoped_external_receipt() {
|
||||
let mut value = input("QUERY_STATUS", "REALITY_CLAIM");
|
||||
value.required_proof = vec!["target-a".into()];
|
||||
value.evidence.push(ChannelEvidence {
|
||||
r#type: "EXECUTION_RECEIPT".into(),
|
||||
r#ref: "receipt://1".into(),
|
||||
verified: true,
|
||||
external: true,
|
||||
proves: vec!["target-a".into()],
|
||||
does_not_prove: vec!["target-b".into()],
|
||||
});
|
||||
let receipt = compile_pure(value).unwrap();
|
||||
assert_eq!(receipt.reality_status, "VERIFIED");
|
||||
assert_eq!(receipt.effect, "RESULT_RECORDED");
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ use std::fs;
|
|||
use std::process::Command;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::zero_point::{self, ZeroPointState};
|
||||
|
||||
|
|
@ -53,6 +54,12 @@ pub struct PasswordRotationReceipt {
|
|||
pub password_changed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct StartLocalChannelSessionInput {
|
||||
pub acknowledgement: String,
|
||||
}
|
||||
|
||||
fn session_path(app: &AppHandle) -> Result<std::path::PathBuf, String> {
|
||||
Ok(app
|
||||
.path()
|
||||
|
|
@ -172,6 +179,11 @@ pub fn check_code_repo_login(
|
|||
app: AppHandle,
|
||||
state: State<'_, ZeroPointState>,
|
||||
) -> Result<Option<LoginSession>, String> {
|
||||
if let Some(session) = current_login_session(&app)? {
|
||||
if session.domain == "PERSONAL_CHANNEL" && session.host == "local.hololake" {
|
||||
return Ok(Some(session));
|
||||
}
|
||||
}
|
||||
let Some((_, domain)) = zero_point::verified_user_route(&state)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
|
@ -218,10 +230,54 @@ fn login_host_for_domain(domain: &str) -> Result<&'static str, String> {
|
|||
"MAIN_DOMAIN" | "BRANCH_DOMAIN" | "ZERO_DOMAIN" | "ZERO_SENSE_DOMAIN" => {
|
||||
Ok(ENTERPRISE_LOGIN_HOST)
|
||||
}
|
||||
"PERSONAL_CHANNEL" => Ok("local.hololake"),
|
||||
_ => Err("HOLOLAKE_DOMAIN_ROUTE_INVALID".into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 普通用户初始化不要求加入企业四域或第五域。这里仅建立本机账号隔离域;
|
||||
/// 它不是光湖编号注册,也不授予服务器、仓库、域或发布权限。
|
||||
pub fn start_local_channel_session(
|
||||
app: AppHandle,
|
||||
input: StartLocalChannelSessionInput,
|
||||
) -> Result<LoginReceipt, String> {
|
||||
if input.acknowledgement != "在本机初始化我的频道" {
|
||||
return Err("HOLOLAKE_LOCAL_CHANNEL_ACKNOWLEDGEMENT_REQUIRED".into());
|
||||
}
|
||||
if current_login_session(&app)?.is_some() {
|
||||
return Err("HOLOLAKE_LOGIN_SESSION_ALREADY_ACTIVE".into());
|
||||
}
|
||||
let username = format!("local-{}", Uuid::new_v4().simple());
|
||||
let secret = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple());
|
||||
keychain_store("local.hololake", &username, &secret)?;
|
||||
let session = LoginSession {
|
||||
username: username.clone(),
|
||||
host: "local.hololake".into(),
|
||||
domain: "PERSONAL_CHANNEL".into(),
|
||||
signed_in_at_unix_ms: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_millis() as u64)
|
||||
.unwrap_or(0),
|
||||
};
|
||||
let path = session_path(&app)?;
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|error| format!("HOLOLAKE_LOGIN_SESSION_WRITE_FAILED: {error}"))?;
|
||||
}
|
||||
fs::write(
|
||||
&path,
|
||||
serde_json::to_string_pretty(&session)
|
||||
.map_err(|error| format!("HOLOLAKE_LOGIN_SESSION_WRITE_FAILED: {error}"))?,
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_LOGIN_SESSION_WRITE_FAILED: {error}"))?;
|
||||
Ok(LoginReceipt {
|
||||
username,
|
||||
email: String::new(),
|
||||
host: "local.hololake".into(),
|
||||
domain: "PERSONAL_CHANNEL".into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// 新账号第一次登录时在 HoloLake 内完成强制换密。
|
||||
/// 旧、新密码只存在于本次 HTTPS 请求内;服务端回执不含密码。
|
||||
pub async fn change_first_login_password(
|
||||
|
|
@ -532,6 +588,10 @@ mod tests {
|
|||
ENTERPRISE_LOGIN_HOST
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
login_host_for_domain("PERSONAL_CHANNEL").unwrap(),
|
||||
"local.hololake"
|
||||
);
|
||||
assert_eq!(
|
||||
login_host_for_domain("UNKNOWN").unwrap_err(),
|
||||
"HOLOLAKE_DOMAIN_ROUTE_INVALID"
|
||||
|
|
|
|||
|
|
@ -47,17 +47,23 @@ struct ProgramAction {
|
|||
depends_on: Vec<String>,
|
||||
input_digest: String,
|
||||
permission: String,
|
||||
#[serde(default)]
|
||||
tool_number: Option<String>,
|
||||
#[serde(default)]
|
||||
input: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GirAction {
|
||||
ordinal: usize,
|
||||
id: String,
|
||||
kind: String,
|
||||
depends_on: Vec<String>,
|
||||
input_digest: String,
|
||||
permission: String,
|
||||
pub ordinal: usize,
|
||||
pub id: String,
|
||||
pub kind: String,
|
||||
pub depends_on: Vec<String>,
|
||||
pub input_digest: String,
|
||||
pub permission: String,
|
||||
pub tool_number: Option<String>,
|
||||
pub input: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
|
|
@ -148,11 +154,24 @@ fn validate(program: &HldpNativeProgram) -> Result<(), String> {
|
|||
]);
|
||||
let mut ids = BTreeSet::new();
|
||||
for action in &program.actions {
|
||||
let executable = matches!(action.kind.as_str(), "READ" | "TRANSFORM" | "ROUTE");
|
||||
let valid_tool_binding = match (&action.tool_number, &action.input, executable) {
|
||||
(Some(tool), Some(input), true) => {
|
||||
valid_id(tool)
|
||||
&& tool.starts_with("HLP-LOCAL-TOOL-")
|
||||
&& serde_json::to_vec(input)
|
||||
.map(|bytes| bytes.len() <= 1024 * 1024)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
(None, None, false) => true,
|
||||
_ => false,
|
||||
};
|
||||
if !ids.insert(action.id.as_str())
|
||||
|| !valid_id(&action.id)
|
||||
|| !allowed_kinds.contains(action.kind.as_str())
|
||||
|| !valid_digest(&action.input_digest)
|
||||
|| !program.permissions.contains(&action.permission)
|
||||
|| !valid_tool_binding
|
||||
{
|
||||
return Err("HOLOLAKE_GLC_ACTION_INVALID".into());
|
||||
}
|
||||
|
|
@ -207,6 +226,8 @@ fn topological_actions(program: &HldpNativeProgram) -> Result<Vec<GirAction>, St
|
|||
},
|
||||
input_digest: action.input_digest.clone(),
|
||||
permission: action.permission.clone(),
|
||||
tool_number: action.tool_number.clone(),
|
||||
input: action.input.clone(),
|
||||
});
|
||||
for target in outgoing.get(id).into_iter().flatten() {
|
||||
let degree = indegree.get_mut(target).expect("known target");
|
||||
|
|
@ -300,7 +321,7 @@ mod tests {
|
|||
let cyclic = serde_json::json!({
|
||||
"schema":PROGRAM_SCHEMA,"programId":"CYCLE","version":"1","subjectId":"S","targetId":"T","scope":"TEST","permissions":["P"],
|
||||
"resources":{"cpuUnits":1,"memoryBytes":1,"storageBytes":0,"networkAllowed":false},
|
||||
"actions":[{"id":"a","kind":"VALIDATE","dependsOn":["b"],"inputDigest":sha256(b"a"),"permission":"P"},{"id":"b","kind":"READ","dependsOn":["a"],"inputDigest":sha256(b"b"),"permission":"P"}],
|
||||
"actions":[{"id":"a","kind":"VALIDATE","dependsOn":["b"],"inputDigest":sha256(b"a"),"permission":"P"},{"id":"b","kind":"VALIDATE","dependsOn":["a"],"inputDigest":sha256(b"b"),"permission":"P"}],
|
||||
"timeoutMs":1,"stopAction":"STOP","cleanupAction":"CLEAN","rollbackAction":"ROLLBACK","receiptKinds":["R"]
|
||||
});
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,564 @@
|
|||
//! Native HLDP tool forge.
|
||||
//!
|
||||
//! Typed HLDP-NP is compiled into deterministic GIR by the HoloLake bootstrap
|
||||
//! compiler. Compilation is not execution authority. A separate verified-human
|
||||
//! authorization ticket is required before the deterministic sandbox self-test.
|
||||
|
||||
use ring::digest::{digest, SHA256};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::AppHandle;
|
||||
use tokio::time::{timeout, Duration};
|
||||
use uuid::Uuid;
|
||||
|
||||
const SCHEMA: &str = "hololake.hldp-tool-forge/v1";
|
||||
const TTL_MS: u64 = 12 * 60 * 60 * 1_000;
|
||||
const FORGE_CLIENT: &str = "HLP-HLDP-TOOL-FORGE";
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct CompileToolInput {
|
||||
pub display_name: String,
|
||||
pub source: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct ToolNumberInput {
|
||||
pub tool_number: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct TestToolInput {
|
||||
pub tool_number: String,
|
||||
pub request_id: String,
|
||||
pub ticket_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct RunToolInput {
|
||||
pub tool_number: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LocalActionReceipt {
|
||||
pub action_id: String,
|
||||
pub kind: String,
|
||||
pub tool_number: Option<String>,
|
||||
pub state: String,
|
||||
pub output: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TemporaryToolRecord {
|
||||
pub tool_number: String,
|
||||
pub display_name: String,
|
||||
pub state: String,
|
||||
pub source_sha256: String,
|
||||
pub gir_sha256: String,
|
||||
pub gir: Value,
|
||||
pub compiled_at_unix_ms: u64,
|
||||
pub expires_at_unix_ms: u64,
|
||||
pub test_receipt_hash: String,
|
||||
pub authorization_request_id: Option<String>,
|
||||
pub enterprise_promotion_state: String,
|
||||
#[serde(default)]
|
||||
pub run_count: u64,
|
||||
#[serde(default)]
|
||||
pub last_run_at_unix_ms: u64,
|
||||
#[serde(default)]
|
||||
pub last_run_receipt_hash: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ToolForgeSnapshot {
|
||||
pub schema: &'static str,
|
||||
pub state: &'static str,
|
||||
pub compiler: &'static str,
|
||||
pub machine_language: &'static str,
|
||||
pub temporary_ttl_hours: u64,
|
||||
pub tools: Vec<TemporaryToolRecord>,
|
||||
pub expired_cleanup_count: usize,
|
||||
pub authority: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ToolAuthorizationReceipt {
|
||||
pub state: String,
|
||||
pub tool_number: String,
|
||||
pub request: crate::human_authorization::AuthorizationRequestView,
|
||||
pub snapshot: ToolForgeSnapshot,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ToolTestReceipt {
|
||||
pub state: String,
|
||||
pub tool_number: String,
|
||||
pub test_receipt_hash: String,
|
||||
pub expires_at_unix_ms: u64,
|
||||
pub authorization: crate::human_authorization::AuthorizationTicketReceipt,
|
||||
pub action_receipts: Vec<LocalActionReceipt>,
|
||||
pub snapshot: ToolForgeSnapshot,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ToolRunReceipt {
|
||||
pub state: String,
|
||||
pub tool_number: String,
|
||||
pub run_number: String,
|
||||
pub action_receipts: Vec<LocalActionReceipt>,
|
||||
pub local_model_calls: u64,
|
||||
pub external_api_calls: u64,
|
||||
pub previous_receipt_hash: String,
|
||||
pub run_receipt_hash: String,
|
||||
pub snapshot: ToolForgeSnapshot,
|
||||
}
|
||||
|
||||
pub fn get_snapshot(app: AppHandle) -> Result<ToolForgeSnapshot, String> {
|
||||
snapshot_at(
|
||||
&root(&app)?,
|
||||
crate::persona_agent_runtime::verified_bound_persona_number(&app).is_some(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn compile_tool(app: AppHandle, input: CompileToolInput) -> Result<ToolForgeSnapshot, String> {
|
||||
let persona_number = require_verified_persona_binding(&app)?;
|
||||
let source_subject = input
|
||||
.source
|
||||
.get("subjectId")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| "HOLOLAKE_TOOL_FORGE_SUBJECT_REQUIRED".to_string())?;
|
||||
if source_subject != persona_number {
|
||||
return Err("HOLOLAKE_TOOL_FORGE_PERSONA_SUBJECT_MISMATCH".into());
|
||||
}
|
||||
let name = input.display_name.trim();
|
||||
if name.is_empty() || name.chars().count() > 80 {
|
||||
return Err("HOLOLAKE_TOOL_FORGE_NAME_INVALID".into());
|
||||
}
|
||||
let root = root(&app)?;
|
||||
let mut registry = load_registry(&root)?;
|
||||
cleanup_expired(&root, &mut registry)?;
|
||||
let gir = crate::gls_bootstrap_compiler::compile_hldp_program(input.source)?;
|
||||
let tool_number = format!(
|
||||
"HLP-TEMP-TOOL-{}",
|
||||
gir.gir_sha256[..16].to_ascii_uppercase()
|
||||
);
|
||||
let now = now_ms();
|
||||
let record = TemporaryToolRecord {
|
||||
tool_number: tool_number.clone(),
|
||||
display_name: name.into(),
|
||||
state: "COMPILED_NOT_AUTHORIZED".into(),
|
||||
source_sha256: gir.source_sha256.clone(),
|
||||
gir_sha256: gir.gir_sha256.clone(),
|
||||
gir: serde_json::to_value(gir)
|
||||
.map_err(|error| format!("HOLOLAKE_TOOL_FORGE_GIR_INVALID: {error}"))?,
|
||||
compiled_at_unix_ms: now,
|
||||
expires_at_unix_ms: now.saturating_add(TTL_MS),
|
||||
test_receipt_hash: String::new(),
|
||||
authorization_request_id: None,
|
||||
enterprise_promotion_state: "NOT_REQUESTED".into(),
|
||||
run_count: 0,
|
||||
last_run_at_unix_ms: 0,
|
||||
last_run_receipt_hash: String::new(),
|
||||
};
|
||||
if let Some(index) = registry
|
||||
.iter()
|
||||
.position(|item| item.tool_number == tool_number)
|
||||
{
|
||||
registry[index] = record;
|
||||
} else {
|
||||
registry.push(record);
|
||||
}
|
||||
save_registry(&root, ®istry)?;
|
||||
snapshot_at(&root, true)
|
||||
}
|
||||
|
||||
pub fn request_test_authorization(
|
||||
app: AppHandle,
|
||||
input: ToolNumberInput,
|
||||
human_number: &str,
|
||||
) -> Result<ToolAuthorizationReceipt, String> {
|
||||
require_verified_persona_binding(&app)?;
|
||||
let root = root(&app)?;
|
||||
let mut registry = load_registry(&root)?;
|
||||
cleanup_expired(&root, &mut registry)?;
|
||||
let tool = registry
|
||||
.iter_mut()
|
||||
.find(|tool| tool.tool_number == input.tool_number)
|
||||
.ok_or_else(|| "HOLOLAKE_TOOL_FORGE_TOOL_NOT_FOUND".to_string())?;
|
||||
if tool.state == "EXPIRED" {
|
||||
return Err("HOLOLAKE_TOOL_FORGE_TOOL_EXPIRED".into());
|
||||
}
|
||||
let requester = requester(human_number);
|
||||
let request = crate::human_authorization::submit_at(
|
||||
&crate::human_authorization::root_for_app(&app)?,
|
||||
&requester,
|
||||
crate::human_authorization::AuthorizationProposalInput {
|
||||
idempotency_key: format!("tool-test-{}", tool.gir_sha256),
|
||||
action: "TEST_COMPILED_HLDP_TOOL".into(),
|
||||
target_number: tool.tool_number.clone(),
|
||||
target_kind: "TEMPORARY_HLDP_TOOL".into(),
|
||||
target_label: tool.display_name.clone(),
|
||||
reason: "在只读确定性沙箱中验证 GIR 结构、编号和回执链".into(),
|
||||
impact: "不执行自然语言、不调用网络、不继承现实权限".into(),
|
||||
rollback_plan: "测试失败保持未激活;过期后自动清理临时缓存".into(),
|
||||
},
|
||||
)?;
|
||||
tool.authorization_request_id = Some(request.request_id.clone());
|
||||
tool.state = "WAITING_HUMAN_TEST_AUTHORIZATION".into();
|
||||
save_registry(&root, ®istry)?;
|
||||
Ok(ToolAuthorizationReceipt {
|
||||
state: request.state.clone(),
|
||||
tool_number: input.tool_number,
|
||||
request,
|
||||
snapshot: snapshot_at(&root, true)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn test_tool(
|
||||
app: AppHandle,
|
||||
input: TestToolInput,
|
||||
human_number: &str,
|
||||
) -> Result<ToolTestReceipt, String> {
|
||||
require_verified_persona_binding(&app)?;
|
||||
let root = root(&app)?;
|
||||
let mut registry = load_registry(&root)?;
|
||||
cleanup_expired(&root, &mut registry)?;
|
||||
let index = registry
|
||||
.iter()
|
||||
.position(|tool| tool.tool_number == input.tool_number)
|
||||
.ok_or_else(|| "HOLOLAKE_TOOL_FORGE_TOOL_NOT_FOUND".to_string())?;
|
||||
let tool = registry[index].clone();
|
||||
if tool.authorization_request_id.as_deref() != Some(input.request_id.as_str()) {
|
||||
return Err("HOLOLAKE_TOOL_FORGE_AUTHORIZATION_SCOPE_MISMATCH".into());
|
||||
}
|
||||
let authorization = crate::human_authorization::consume_at(
|
||||
&crate::human_authorization::root_for_app(&app)?,
|
||||
&requester(human_number),
|
||||
crate::human_authorization::ConsumeAuthorizationInput {
|
||||
request_id: input.request_id,
|
||||
ticket_id: input.ticket_id,
|
||||
},
|
||||
)?;
|
||||
// The first-stage sandbox executes deterministic validation only. GIR cannot
|
||||
// smuggle natural language or permission expansion into machine execution.
|
||||
let rebuilt_hash = gir_hash_without_hash(&tool.gir)?;
|
||||
if rebuilt_hash != tool.gir_sha256 {
|
||||
return Err("HOLOLAKE_TOOL_FORGE_GIR_INTEGRITY_FAILED".into());
|
||||
}
|
||||
let action_receipts = execute_gir_locally(&app, &tool).await?;
|
||||
let receipt_hash = sha256(
|
||||
format!(
|
||||
"HLP-TOOL-TEST-v2\n{}\n{}\n{}\n{}\n{}",
|
||||
tool.tool_number,
|
||||
tool.gir_sha256,
|
||||
authorization.receipt_hash,
|
||||
tool.expires_at_unix_ms,
|
||||
serde_json::to_string(&action_receipts).unwrap_or_default()
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
registry[index].state = "TESTED_TEMPORARY_ACTIVE".into();
|
||||
registry[index].test_receipt_hash = receipt_hash.clone();
|
||||
registry[index].run_count = 1;
|
||||
registry[index].last_run_at_unix_ms = now_ms();
|
||||
registry[index].last_run_receipt_hash = receipt_hash.clone();
|
||||
let expires = registry[index].expires_at_unix_ms;
|
||||
save_registry(&root, ®istry)?;
|
||||
Ok(ToolTestReceipt {
|
||||
state: "TESTED_TEMPORARY_ACTIVE".into(),
|
||||
tool_number: input.tool_number,
|
||||
test_receipt_hash: receipt_hash,
|
||||
expires_at_unix_ms: expires,
|
||||
authorization,
|
||||
action_receipts,
|
||||
snapshot: snapshot_at(&root, true)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn run_tool(app: AppHandle, input: RunToolInput) -> Result<ToolRunReceipt, String> {
|
||||
let persona = require_verified_persona_binding(&app)?;
|
||||
let root = root(&app)?;
|
||||
let mut registry = load_registry(&root)?;
|
||||
cleanup_expired(&root, &mut registry)?;
|
||||
let index = registry
|
||||
.iter()
|
||||
.position(|tool| tool.tool_number == input.tool_number)
|
||||
.ok_or_else(|| "HOLOLAKE_TOOL_FORGE_TOOL_NOT_FOUND".to_string())?;
|
||||
let tool = registry[index].clone();
|
||||
if tool.state != "TESTED_TEMPORARY_ACTIVE" {
|
||||
return Err("HOLOLAKE_TOOL_FORGE_TESTED_ACTIVE_REQUIRED".into());
|
||||
}
|
||||
let gir: crate::gls_bootstrap_compiler::GirProgram =
|
||||
serde_json::from_value(tool.gir.clone())
|
||||
.map_err(|error| format!("HOLOLAKE_TOOL_FORGE_GIR_INVALID: {error}"))?;
|
||||
if gir.subject_id != persona {
|
||||
return Err("HOLOLAKE_TOOL_FORGE_PERSONA_SUBJECT_MISMATCH".into());
|
||||
}
|
||||
let previous = if tool.last_run_receipt_hash.is_empty() {
|
||||
tool.test_receipt_hash.clone()
|
||||
} else {
|
||||
tool.last_run_receipt_hash.clone()
|
||||
};
|
||||
let run_number = format!(
|
||||
"{}-RUN-{:010}",
|
||||
tool.tool_number,
|
||||
tool.run_count.saturating_add(1)
|
||||
);
|
||||
let action_receipts = execute_gir_locally(&app, &tool).await?;
|
||||
let run_receipt_hash = sha256(
|
||||
serde_json::to_string(&serde_json::json!({
|
||||
"runNumber":run_number,
|
||||
"toolNumber":tool.tool_number,
|
||||
"girSha256":tool.gir_sha256,
|
||||
"actions":action_receipts,
|
||||
"localModelCalls":0,
|
||||
"externalApiCalls":0,
|
||||
"previousReceiptHash":previous
|
||||
}))
|
||||
.map_err(|error| format!("HOLOLAKE_TOOL_FORGE_RECEIPT_FAILED: {error}"))?
|
||||
.as_bytes(),
|
||||
);
|
||||
registry[index].run_count = tool.run_count.saturating_add(1);
|
||||
registry[index].last_run_at_unix_ms = now_ms();
|
||||
registry[index].last_run_receipt_hash = run_receipt_hash.clone();
|
||||
save_registry(&root, ®istry)?;
|
||||
Ok(ToolRunReceipt {
|
||||
state: "COMPLETED_LOCAL_ZERO_MODEL".into(),
|
||||
tool_number: input.tool_number,
|
||||
run_number,
|
||||
action_receipts,
|
||||
local_model_calls: 0,
|
||||
external_api_calls: 0,
|
||||
previous_receipt_hash: previous,
|
||||
run_receipt_hash,
|
||||
snapshot: snapshot_at(&root, true)?,
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute_gir_locally(
|
||||
app: &AppHandle,
|
||||
tool: &TemporaryToolRecord,
|
||||
) -> Result<Vec<LocalActionReceipt>, String> {
|
||||
let rebuilt_hash = gir_hash_without_hash(&tool.gir)?;
|
||||
if rebuilt_hash != tool.gir_sha256 {
|
||||
return Err("HOLOLAKE_TOOL_FORGE_GIR_INTEGRITY_FAILED".into());
|
||||
}
|
||||
let gir: crate::gls_bootstrap_compiler::GirProgram =
|
||||
serde_json::from_value(tool.gir.clone())
|
||||
.map_err(|error| format!("HOLOLAKE_TOOL_FORGE_GIR_INVALID: {error}"))?;
|
||||
if gir.unresolved_natural_language.len() != 0 || gir.unresolved_permissions.len() != 0 {
|
||||
return Err("HOLOLAKE_TOOL_FORGE_GIR_UNRESOLVED_INPUT_DENIED".into());
|
||||
}
|
||||
if gir.resources.get("networkAllowed").and_then(Value::as_bool) != Some(false) {
|
||||
return Err("HOLOLAKE_TOOL_FORGE_NETWORK_DISABLED_REQUIRED".into());
|
||||
}
|
||||
let timeout_ms = gir.timeout_ms.clamp(100, 30_000);
|
||||
timeout(Duration::from_millis(timeout_ms), async {
|
||||
let mut receipts = Vec::with_capacity(gir.action_graph.len());
|
||||
for action in gir.action_graph {
|
||||
let output = match action.kind.as_str() {
|
||||
"READ" | "TRANSFORM" | "ROUTE" => {
|
||||
let tool_number = action.tool_number.as_deref().ok_or_else(|| {
|
||||
"HOLOLAKE_TOOL_FORGE_LOCAL_TOOL_BINDING_REQUIRED".to_string()
|
||||
})?;
|
||||
let input = action.input.clone().ok_or_else(|| {
|
||||
"HOLOLAKE_TOOL_FORGE_LOCAL_TOOL_INPUT_REQUIRED".to_string()
|
||||
})?;
|
||||
Some(
|
||||
crate::local_execution_subpersona::execute_local_tool(
|
||||
app,
|
||||
tool_number,
|
||||
input,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
"VALIDATE" | "STATE_TRANSITION" | "WRITE_RECEIPT" | "STOP" | "CLEANUP"
|
||||
| "ROLLBACK" => None,
|
||||
_ => return Err("HOLOLAKE_TOOL_FORGE_ACTION_KIND_DENIED".into()),
|
||||
};
|
||||
receipts.push(LocalActionReceipt {
|
||||
action_id: action.id,
|
||||
kind: action.kind,
|
||||
tool_number: action.tool_number,
|
||||
state: "COMPLETED".into(),
|
||||
output,
|
||||
});
|
||||
}
|
||||
Ok::<_, String>(receipts)
|
||||
})
|
||||
.await
|
||||
.map_err(|_| "HOLOLAKE_TOOL_FORGE_LOCAL_EXECUTION_TIMEOUT".to_string())?
|
||||
}
|
||||
|
||||
fn requester(human_number: &str) -> crate::human_authorization::RequesterContext {
|
||||
crate::human_authorization::RequesterContext {
|
||||
account_key: human_number.into(),
|
||||
session_id: "ICE-CH-ZC001".into(),
|
||||
client_instance_id: FORGE_CLIENT.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn require_verified_persona_binding(app: &AppHandle) -> Result<String, String> {
|
||||
crate::persona_agent_runtime::verified_bound_persona_number(app)
|
||||
.ok_or_else(|| "HOLOLAKE_TOOL_FORGE_PERSONA_BINDING_REQUIRED".to_string())
|
||||
}
|
||||
|
||||
fn snapshot_at(root: &Path, persona_bound: bool) -> Result<ToolForgeSnapshot, String> {
|
||||
let mut registry = load_registry(root)?;
|
||||
let expired_cleanup_count = cleanup_expired(root, &mut registry)?;
|
||||
Ok(ToolForgeSnapshot {
|
||||
schema: SCHEMA,
|
||||
state: if persona_bound {
|
||||
"READY"
|
||||
} else {
|
||||
"PERSONA_BINDING_REQUIRED"
|
||||
},
|
||||
compiler: "HOLOLAKE_BOOTSTRAP_GLC_V1",
|
||||
machine_language: "GIR_V1",
|
||||
temporary_ttl_hours: 12,
|
||||
tools: registry,
|
||||
expired_cleanup_count,
|
||||
authority: "COMPILE_IS_NOT_EXECUTION_TEST_REQUIRES_SINGLE_USE_HUMAN_TICKET",
|
||||
})
|
||||
}
|
||||
|
||||
fn cleanup_expired(root: &Path, registry: &mut Vec<TemporaryToolRecord>) -> Result<usize, String> {
|
||||
let now = now_ms();
|
||||
let before = registry.len();
|
||||
registry.retain(|tool| tool.expires_at_unix_ms > now);
|
||||
let removed = before.saturating_sub(registry.len());
|
||||
if removed > 0 {
|
||||
save_registry(root, registry)?;
|
||||
append_cleanup_receipt(root, removed, now)?;
|
||||
}
|
||||
Ok(removed)
|
||||
}
|
||||
|
||||
fn gir_hash_without_hash(gir: &Value) -> Result<String, String> {
|
||||
let mut value = gir.clone();
|
||||
let object = value
|
||||
.as_object_mut()
|
||||
.ok_or_else(|| "HOLOLAKE_TOOL_FORGE_GIR_INVALID".to_string())?;
|
||||
object.insert("girSha256".into(), Value::String(String::new()));
|
||||
Ok(sha256(&serde_json::to_vec(&value).map_err(|error| {
|
||||
format!("HOLOLAKE_TOOL_FORGE_GIR_INVALID: {error}")
|
||||
})?))
|
||||
}
|
||||
|
||||
fn root(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
crate::authenticated_storage::account_storage_root(app, "hldp-tool-forge-v1")
|
||||
}
|
||||
|
||||
fn registry_path(root: &Path) -> PathBuf {
|
||||
root.join("temporary-tools.json")
|
||||
}
|
||||
|
||||
fn load_registry(root: &Path) -> Result<Vec<TemporaryToolRecord>, String> {
|
||||
let path = registry_path(root);
|
||||
if !path.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
serde_json::from_slice(
|
||||
&fs::read(path).map_err(|error| format!("HOLOLAKE_TOOL_FORGE_READ_FAILED: {error}"))?,
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_TOOL_FORGE_REGISTRY_INVALID: {error}"))
|
||||
}
|
||||
|
||||
fn save_registry(root: &Path, registry: &[TemporaryToolRecord]) -> Result<(), String> {
|
||||
fs::create_dir_all(root)
|
||||
.map_err(|error| format!("HOLOLAKE_TOOL_FORGE_STORAGE_FAILED: {error}"))?;
|
||||
let path = registry_path(root);
|
||||
let temporary = root.join(format!(".temporary-tools-{}.tmp", Uuid::new_v4().simple()));
|
||||
fs::write(
|
||||
&temporary,
|
||||
serde_json::to_vec_pretty(registry)
|
||||
.map_err(|error| format!("HOLOLAKE_TOOL_FORGE_REGISTRY_INVALID: {error}"))?,
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_TOOL_FORGE_WRITE_FAILED: {error}"))?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(&temporary, fs::Permissions::from_mode(0o600))
|
||||
.map_err(|error| format!("HOLOLAKE_TOOL_FORGE_PERMISSION_FAILED: {error}"))?;
|
||||
}
|
||||
fs::rename(temporary, path)
|
||||
.map_err(|error| format!("HOLOLAKE_TOOL_FORGE_WRITE_FAILED: {error}"))
|
||||
}
|
||||
|
||||
fn append_cleanup_receipt(root: &Path, count: usize, now: u64) -> Result<(), String> {
|
||||
let path = root.join("cleanup-receipts.log");
|
||||
let previous = fs::read_to_string(&path)
|
||||
.ok()
|
||||
.and_then(|body| {
|
||||
body.lines()
|
||||
.last()
|
||||
.and_then(|line| line.split('\t').last())
|
||||
.map(str::to_string)
|
||||
})
|
||||
.unwrap_or_else(|| "GENESIS".into());
|
||||
let receipt = sha256(format!("{previous}\n{count}\n{now}").as_bytes());
|
||||
use std::io::Write;
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(path)
|
||||
.map_err(|error| format!("HOLOLAKE_TOOL_FORGE_RECEIPT_FAILED: {error}"))?;
|
||||
writeln!(file, "{now}\t{count}\t{receipt}")
|
||||
.map_err(|error| format!("HOLOLAKE_TOOL_FORGE_RECEIPT_FAILED: {error}"))
|
||||
}
|
||||
|
||||
fn sha256(bytes: &[u8]) -> String {
|
||||
digest(&SHA256, bytes)
|
||||
.as_ref()
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn now_ms() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_millis() as u64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn expired_temporary_tools_are_removed_deterministically() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let mut tools = vec![TemporaryToolRecord {
|
||||
tool_number: "HLP-TEMP-TOOL-OLD".into(),
|
||||
display_name: "old".into(),
|
||||
state: "TESTED_TEMPORARY_ACTIVE".into(),
|
||||
source_sha256: "s".into(),
|
||||
gir_sha256: "g".into(),
|
||||
gir: Value::Null,
|
||||
compiled_at_unix_ms: 1,
|
||||
expires_at_unix_ms: 1,
|
||||
test_receipt_hash: "r".into(),
|
||||
authorization_request_id: None,
|
||||
enterprise_promotion_state: "NOT_REQUESTED".into(),
|
||||
run_count: 0,
|
||||
last_run_at_unix_ms: 0,
|
||||
last_run_receipt_hash: String::new(),
|
||||
}];
|
||||
assert_eq!(cleanup_expired(root.path(), &mut tools).unwrap(), 1);
|
||||
assert!(tools.is_empty());
|
||||
}
|
||||
}
|
||||
|
|
@ -403,7 +403,13 @@ fn snapshot_at(
|
|||
state: "ACTIVE_FAIL_CLOSED",
|
||||
pending_count,
|
||||
requests,
|
||||
supported_actions: vec!["OPEN_MAINTENANCE", "UNMOUNT", "PROMOTE_VERSION", "RETIRE"],
|
||||
supported_actions: vec![
|
||||
"OPEN_MAINTENANCE",
|
||||
"UNMOUNT",
|
||||
"PROMOTE_VERSION",
|
||||
"RETIRE",
|
||||
"TEST_COMPILED_HLDP_TOOL",
|
||||
],
|
||||
purge_enabled: false,
|
||||
authority: "PERSONA_PROPOSES_HUMAN_DECIDES_SYSTEM_ISSUES_SESSION_BOUND_SINGLE_USE_TICKET",
|
||||
})
|
||||
|
|
@ -535,13 +541,18 @@ fn validate_proposal(input: &AuthorizationProposalInput) -> Result<(), String> {
|
|||
validate_id(&input.idempotency_key, "IDEMPOTENCY")?;
|
||||
if !matches!(
|
||||
input.action.as_str(),
|
||||
"OPEN_MAINTENANCE" | "UNMOUNT" | "PROMOTE_VERSION" | "RETIRE"
|
||||
"OPEN_MAINTENANCE" | "UNMOUNT" | "PROMOTE_VERSION" | "RETIRE" | "TEST_COMPILED_HLDP_TOOL"
|
||||
) {
|
||||
return Err("HOLOLAKE_AUTHORIZATION_ACTION_UNSUPPORTED".into());
|
||||
}
|
||||
validate_id(&input.target_number, "TARGET_NUMBER")?;
|
||||
validate_id(&input.target_kind, "TARGET_KIND")?;
|
||||
if input.target_kind != "MODULE" || !input.target_number.starts_with("HLP-MOD-") {
|
||||
let module_target =
|
||||
input.target_kind == "MODULE" && input.target_number.starts_with("HLP-MOD-");
|
||||
let temporary_tool_target = input.action == "TEST_COMPILED_HLDP_TOOL"
|
||||
&& input.target_kind == "TEMPORARY_HLDP_TOOL"
|
||||
&& input.target_number.starts_with("HLP-TEMP-TOOL-");
|
||||
if !module_target && !temporary_tool_target {
|
||||
return Err("HOLOLAKE_AUTHORIZATION_TARGET_NOT_A_MODULE".into());
|
||||
}
|
||||
validate_text(&input.target_label, 120, "TARGET_LABEL")?;
|
||||
|
|
@ -660,6 +671,19 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_compiled_hldp_tool_may_request_a_test_ticket_without_gaining_execution_authority() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let mut value = proposal();
|
||||
value.action = "TEST_COMPILED_HLDP_TOOL".into();
|
||||
value.target_kind = "TEMPORARY_HLDP_TOOL".into();
|
||||
value.target_number = "HLP-TEMP-TOOL-0123456789ABCDEF".into();
|
||||
let request = submit_at(temp.path(), &requester(), value).unwrap();
|
||||
|
||||
assert_eq!(request.state, "PENDING_HUMAN");
|
||||
assert!(request.ticket_id.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idempotency_key_cannot_be_reused_for_another_target_or_reason() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
// Clean-room native migration of the HoloLake Era knowledge-workspace contract.
|
||||
|
||||
use ring::digest::{digest, SHA256};
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::Write;
|
||||
|
|
@ -22,6 +23,37 @@ const MAX_IMPORT_FILES: usize = 1_000;
|
|||
const MAX_FILE_BYTES: u64 = 10 * 1024 * 1024;
|
||||
const MAX_READ_BYTES: u64 = 2 * 1024 * 1024;
|
||||
const MAX_SEARCH_RESULTS: usize = 100;
|
||||
const MAX_ROUTED_CANDIDATES: usize = 3;
|
||||
const NUMBER_INDEX_SCHEMA: &str = "hololake.numbered-knowledge-index/v1";
|
||||
const PAGE_HEADER_SCHEMA: &str = "hololake.numbered-knowledge-page-header/v1";
|
||||
const NUMBERING_SYSTEM: &str = "HLP-KB";
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct KnowledgeThoughtSummary {
|
||||
pub trigger: String,
|
||||
pub emergence: String,
|
||||
pub lock: String,
|
||||
pub why: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct KnowledgePageHeader {
|
||||
pub schema: &'static str,
|
||||
pub number: String,
|
||||
pub numbering_system: &'static str,
|
||||
pub parent_number: String,
|
||||
pub path: String,
|
||||
pub mapping_terms: Vec<String>,
|
||||
pub thought_summary: Option<KnowledgeThoughtSummary>,
|
||||
pub children: Vec<String>,
|
||||
pub source: String,
|
||||
pub version: u128,
|
||||
pub content_sha256: String,
|
||||
pub state: String,
|
||||
pub formal_routing: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
@ -33,6 +65,7 @@ pub struct KnowledgeDocumentSummary {
|
|||
pub size_bytes: u64,
|
||||
pub content_sha256: String,
|
||||
pub duplicate_count: usize,
|
||||
pub page_header: KnowledgePageHeader,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
|
|
@ -67,6 +100,7 @@ pub struct KnowledgeDocument {
|
|||
pub updated_at_unix_ms: u128,
|
||||
pub content_sha256: String,
|
||||
pub writable: bool,
|
||||
pub page_header: KnowledgePageHeader,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
|
|
@ -95,12 +129,22 @@ pub struct SearchKnowledgeInput {
|
|||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct KnowledgeSearchResult {
|
||||
pub source: &'static str,
|
||||
pub source: String,
|
||||
pub number: String,
|
||||
pub path: String,
|
||||
pub title: String,
|
||||
pub state: String,
|
||||
pub thought_summary: Option<KnowledgeThoughtSummary>,
|
||||
pub match_reason: Vec<String>,
|
||||
pub snippet: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct ReadNumberedKnowledgeDocumentInput {
|
||||
pub document_number: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct KnowledgeImportResult {
|
||||
|
|
@ -156,6 +200,18 @@ pub async fn read_knowledge_document(
|
|||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
pub async fn read_numbered_knowledge_document(
|
||||
app: AppHandle,
|
||||
input: ReadNumberedKnowledgeDocumentInput,
|
||||
) -> Result<KnowledgeDocument, String> {
|
||||
let roots = knowledge_roots(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
read_numbered_document_at(&roots.0, roots.1.as_deref(), &input.document_number)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
pub async fn search_knowledge(
|
||||
app: AppHandle,
|
||||
input: SearchKnowledgeInput,
|
||||
|
|
@ -569,6 +625,7 @@ fn snapshot_at(native: &Path, legacy: Option<&Path>) -> Result<KnowledgeSnapshot
|
|||
&mut raw_documents,
|
||||
)?;
|
||||
}
|
||||
synchronize_number_index(native, &mut raw_documents)?;
|
||||
let raw_document_count = raw_documents.len();
|
||||
let mut by_hash = HashMap::<String, usize>::new();
|
||||
let mut documents = Vec::<KnowledgeDocumentSummary>::new();
|
||||
|
|
@ -646,19 +703,341 @@ fn collect_documents(
|
|||
let relative = relative_posix(root, &path)?;
|
||||
let bytes = fs::read(&path)
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_DOCUMENT_UNAVAILABLE: {error}"))?;
|
||||
let title = document_title(&path)?;
|
||||
let content_sha256 = sha256_hex(&bytes);
|
||||
let body = String::from_utf8_lossy(&bytes);
|
||||
output.push(KnowledgeDocumentSummary {
|
||||
source,
|
||||
path: relative,
|
||||
title: document_title(&path)?,
|
||||
path: relative.clone(),
|
||||
title: title.clone(),
|
||||
updated_at_unix_ms: modified_unix_ms(&metadata),
|
||||
size_bytes: metadata.len(),
|
||||
content_sha256: sha256_hex(&bytes),
|
||||
content_sha256: content_sha256.clone(),
|
||||
duplicate_count: 0,
|
||||
page_header: pending_page_header(
|
||||
source,
|
||||
&relative,
|
||||
&title,
|
||||
&body,
|
||||
modified_unix_ms(&metadata),
|
||||
&content_sha256,
|
||||
),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn numbered_index_path(native: &Path) -> PathBuf {
|
||||
native
|
||||
.join(".hololake")
|
||||
.join("numbered-knowledge-index.sqlite3")
|
||||
}
|
||||
|
||||
fn open_numbered_index(native: &Path) -> Result<Connection, String> {
|
||||
let directory = native.join(".hololake");
|
||||
fs::create_dir_all(&directory)
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_NUMBER_INDEX_UNAVAILABLE: {error}"))?;
|
||||
let connection = Connection::open(numbered_index_path(native))
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_NUMBER_INDEX_UNAVAILABLE: {error}"))?;
|
||||
connection
|
||||
.execute_batch(
|
||||
"PRAGMA journal_mode=WAL;
|
||||
CREATE TABLE IF NOT EXISTS numbered_pages(
|
||||
number TEXT PRIMARY KEY,
|
||||
source TEXT NOT NULL,
|
||||
document_path TEXT NOT NULL,
|
||||
parent_number TEXT NOT NULL,
|
||||
storage_path TEXT NOT NULL,
|
||||
mapping_terms_json TEXT NOT NULL,
|
||||
trigger TEXT,
|
||||
emergence TEXT,
|
||||
lock_text TEXT,
|
||||
why TEXT,
|
||||
children_json TEXT NOT NULL DEFAULT '[]',
|
||||
source_version INTEGER NOT NULL,
|
||||
content_sha256 TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
UNIQUE(source, document_path)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS numbered_pages_state_idx ON numbered_pages(state);
|
||||
CREATE INDEX IF NOT EXISTS numbered_pages_path_idx ON numbered_pages(source, document_path);",
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_NUMBER_INDEX_SCHEMA_FAILED: {error}"))?;
|
||||
Ok(connection)
|
||||
}
|
||||
|
||||
fn number_for_document(source: &str, path: &str) -> String {
|
||||
let digest = sha256_hex(format!("{NUMBER_INDEX_SCHEMA}:{source}:{path}").as_bytes());
|
||||
format!("HLP-KB-DOC-{}", digest[..16].to_ascii_uppercase())
|
||||
}
|
||||
|
||||
fn parent_number_for(source: &str, path: &str) -> String {
|
||||
let parent = Path::new(path)
|
||||
.parent()
|
||||
.map(|value| value.to_string_lossy().replace('\\', "/"))
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| "ROOT".into());
|
||||
let digest = sha256_hex(format!("{NUMBER_INDEX_SCHEMA}:{source}:GROUP:{parent}").as_bytes());
|
||||
format!("HLP-KB-GRP-{}", digest[..16].to_ascii_uppercase())
|
||||
}
|
||||
|
||||
fn assert_number_owner(
|
||||
connection: &Connection,
|
||||
number: &str,
|
||||
source: &str,
|
||||
path: &str,
|
||||
) -> Result<(), String> {
|
||||
let existing = connection
|
||||
.query_row(
|
||||
"SELECT source,document_path FROM numbered_pages WHERE number=?1",
|
||||
[number],
|
||||
|row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
|
||||
)
|
||||
.optional()
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_NUMBER_OWNER_READ_FAILED: {error}"))?;
|
||||
if existing
|
||||
.as_ref()
|
||||
.is_some_and(|(known_source, known_path)| known_source != source || known_path != path)
|
||||
{
|
||||
return Err(format!("HOLOLAKE_KNOWLEDGE_NUMBER_COLLISION: {number}"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mapping_terms(title: &str, path: &str, body: &str) -> Vec<String> {
|
||||
let source = format!("{title}\n{path}\n{body}").to_lowercase();
|
||||
let mut terms = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
for token in source
|
||||
.split(|character: char| {
|
||||
character.is_whitespace()
|
||||
|| matches!(
|
||||
character,
|
||||
',' | ','
|
||||
| '.'
|
||||
| '。'
|
||||
| ':'
|
||||
| ':'
|
||||
| ';'
|
||||
| ';'
|
||||
| '/'
|
||||
| '\\'
|
||||
| '|'
|
||||
| '、'
|
||||
| '('
|
||||
| ')'
|
||||
| '('
|
||||
| ')'
|
||||
| '['
|
||||
| ']'
|
||||
| '【'
|
||||
| '】'
|
||||
| '#'
|
||||
| '*'
|
||||
| '`'
|
||||
)
|
||||
})
|
||||
.map(str::trim)
|
||||
.filter(|token| !token.is_empty())
|
||||
{
|
||||
if seen.insert(token.to_string()) {
|
||||
terms.push(token.to_string());
|
||||
}
|
||||
let characters = token.chars().collect::<Vec<_>>();
|
||||
if characters.iter().any(|character| !character.is_ascii()) {
|
||||
for window in characters.windows(2) {
|
||||
let pair = window.iter().collect::<String>();
|
||||
if seen.insert(pair.clone()) {
|
||||
terms.push(pair);
|
||||
}
|
||||
if terms.len() >= 512 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if terms.len() >= 512 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
terms
|
||||
}
|
||||
|
||||
fn pending_page_header(
|
||||
source: &str,
|
||||
path: &str,
|
||||
title: &str,
|
||||
body: &str,
|
||||
version: u128,
|
||||
content_sha256: &str,
|
||||
) -> KnowledgePageHeader {
|
||||
KnowledgePageHeader {
|
||||
schema: PAGE_HEADER_SCHEMA,
|
||||
number: number_for_document(source, path),
|
||||
numbering_system: NUMBERING_SYSTEM,
|
||||
parent_number: parent_number_for(source, path),
|
||||
path: format!("knowledge://{source}/{path}"),
|
||||
mapping_terms: mapping_terms(title, path, body),
|
||||
thought_summary: None,
|
||||
children: Vec::new(),
|
||||
source: source.into(),
|
||||
version,
|
||||
content_sha256: content_sha256.into(),
|
||||
state: "PENDING_THOUGHT_INDEX".into(),
|
||||
formal_routing: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn synchronize_number_index(
|
||||
native: &Path,
|
||||
documents: &mut [KnowledgeDocumentSummary],
|
||||
) -> Result<(), String> {
|
||||
let mut connection = open_numbered_index(native)?;
|
||||
let transaction = connection
|
||||
.transaction()
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_NUMBER_INDEX_WRITE_FAILED: {error}"))?;
|
||||
transaction
|
||||
.execute("UPDATE numbered_pages SET state='SOURCE_MISSING'", [])
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_NUMBER_INDEX_WRITE_FAILED: {error}"))?;
|
||||
for document in documents.iter() {
|
||||
let header = &document.page_header;
|
||||
assert_number_owner(
|
||||
&transaction,
|
||||
&header.number,
|
||||
document.source,
|
||||
&document.path,
|
||||
)?;
|
||||
let mapping = serde_json::to_string(&header.mapping_terms)
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_NUMBER_INDEX_INVALID: {error}"))?;
|
||||
transaction
|
||||
.execute(
|
||||
"INSERT INTO numbered_pages(number,source,document_path,parent_number,storage_path,mapping_terms_json,children_json,source_version,content_sha256,state)
|
||||
VALUES(?1,?2,?3,?4,?5,?6,'[]',?7,?8,'PENDING_THOUGHT_INDEX')
|
||||
ON CONFLICT(number) DO UPDATE SET
|
||||
source=excluded.source,
|
||||
document_path=excluded.document_path,
|
||||
parent_number=excluded.parent_number,
|
||||
storage_path=excluded.storage_path,
|
||||
mapping_terms_json=excluded.mapping_terms_json,
|
||||
source_version=excluded.source_version,
|
||||
content_sha256=excluded.content_sha256,
|
||||
state=CASE WHEN numbered_pages.trigger IS NOT NULL
|
||||
AND numbered_pages.emergence IS NOT NULL
|
||||
AND numbered_pages.lock_text IS NOT NULL
|
||||
AND numbered_pages.why IS NOT NULL
|
||||
THEN 'READY' ELSE 'PENDING_THOUGHT_INDEX' END",
|
||||
params![
|
||||
header.number,
|
||||
document.source,
|
||||
document.path,
|
||||
header.parent_number,
|
||||
header.path,
|
||||
mapping,
|
||||
u64::try_from(header.version).unwrap_or(u64::MAX),
|
||||
document.content_sha256,
|
||||
],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_NUMBER_INDEX_WRITE_FAILED: {error}"))?;
|
||||
}
|
||||
transaction
|
||||
.commit()
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_NUMBER_INDEX_WRITE_FAILED: {error}"))?;
|
||||
|
||||
let connection = open_numbered_index(native)?;
|
||||
for document in documents.iter_mut() {
|
||||
document.page_header = page_header_from_index(&connection, &document.page_header.number)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn page_header_for_document(
|
||||
native: &Path,
|
||||
source: &str,
|
||||
path: &str,
|
||||
title: &str,
|
||||
body: &str,
|
||||
version: u128,
|
||||
content_sha256: &str,
|
||||
) -> Result<KnowledgePageHeader, String> {
|
||||
let pending = pending_page_header(source, path, title, body, version, content_sha256);
|
||||
let connection = open_numbered_index(native)?;
|
||||
assert_number_owner(&connection, &pending.number, source, path)?;
|
||||
let mapping = serde_json::to_string(&pending.mapping_terms)
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_NUMBER_INDEX_INVALID: {error}"))?;
|
||||
connection
|
||||
.execute(
|
||||
"INSERT INTO numbered_pages(number,source,document_path,parent_number,storage_path,mapping_terms_json,children_json,source_version,content_sha256,state)
|
||||
VALUES(?1,?2,?3,?4,?5,?6,'[]',?7,?8,'PENDING_THOUGHT_INDEX')
|
||||
ON CONFLICT(number) DO UPDATE SET
|
||||
source=excluded.source,
|
||||
document_path=excluded.document_path,
|
||||
parent_number=excluded.parent_number,
|
||||
storage_path=excluded.storage_path,
|
||||
mapping_terms_json=excluded.mapping_terms_json,
|
||||
source_version=excluded.source_version,
|
||||
content_sha256=excluded.content_sha256,
|
||||
state=CASE WHEN numbered_pages.trigger IS NOT NULL
|
||||
AND numbered_pages.emergence IS NOT NULL
|
||||
AND numbered_pages.lock_text IS NOT NULL
|
||||
AND numbered_pages.why IS NOT NULL
|
||||
THEN 'READY' ELSE 'PENDING_THOUGHT_INDEX' END",
|
||||
params![
|
||||
&pending.number,
|
||||
source,
|
||||
path,
|
||||
&pending.parent_number,
|
||||
&pending.path,
|
||||
mapping,
|
||||
u64::try_from(version).unwrap_or(u64::MAX),
|
||||
content_sha256,
|
||||
],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_NUMBER_INDEX_WRITE_FAILED: {error}"))?;
|
||||
page_header_from_index(&connection, &pending.number)
|
||||
}
|
||||
|
||||
fn page_header_from_index(
|
||||
connection: &Connection,
|
||||
number: &str,
|
||||
) -> Result<KnowledgePageHeader, String> {
|
||||
connection
|
||||
.query_row(
|
||||
"SELECT source,parent_number,storage_path,mapping_terms_json,trigger,emergence,lock_text,why,children_json,source_version,content_sha256,state
|
||||
FROM numbered_pages WHERE number=?1",
|
||||
[number],
|
||||
|row| {
|
||||
let trigger: Option<String> = row.get(4)?;
|
||||
let emergence: Option<String> = row.get(5)?;
|
||||
let lock: Option<String> = row.get(6)?;
|
||||
let why: Option<String> = row.get(7)?;
|
||||
let thought_summary = match (trigger, emergence, lock, why) {
|
||||
(Some(trigger), Some(emergence), Some(lock), Some(why)) => {
|
||||
Some(KnowledgeThoughtSummary { trigger, emergence, lock, why })
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let state: String = row.get(11)?;
|
||||
Ok(KnowledgePageHeader {
|
||||
schema: PAGE_HEADER_SCHEMA,
|
||||
number: number.into(),
|
||||
numbering_system: NUMBERING_SYSTEM,
|
||||
parent_number: row.get(1)?,
|
||||
path: row.get(2)?,
|
||||
mapping_terms: serde_json::from_str::<Vec<String>>(&row.get::<_, String>(3)?)
|
||||
.unwrap_or_default(),
|
||||
thought_summary,
|
||||
children: serde_json::from_str::<Vec<String>>(&row.get::<_, String>(8)?)
|
||||
.unwrap_or_default(),
|
||||
source: row.get(0)?,
|
||||
version: row.get::<_, u64>(9)? as u128,
|
||||
content_sha256: row.get(10)?,
|
||||
formal_routing: state == "READY",
|
||||
state,
|
||||
})
|
||||
},
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_NUMBER_NOT_FOUND: {error}"))
|
||||
}
|
||||
|
||||
fn read_document_at(
|
||||
native: &Path,
|
||||
legacy: Option<&Path>,
|
||||
|
|
@ -675,17 +1054,84 @@ fn read_document_at(
|
|||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_DOCUMENT_INVALID_UTF8: {error}"))?;
|
||||
let content_sha256 = sha256_hex(body.as_bytes());
|
||||
let writable = input.source == "native";
|
||||
let title = title_from_text(&body, &path);
|
||||
let updated_at_unix_ms = modified_unix_ms(&metadata);
|
||||
let page_header = page_header_for_document(
|
||||
native,
|
||||
&input.source,
|
||||
&input.path,
|
||||
&title,
|
||||
&body,
|
||||
updated_at_unix_ms,
|
||||
&content_sha256,
|
||||
)?;
|
||||
Ok(KnowledgeDocument {
|
||||
source: input.source,
|
||||
path: input.path,
|
||||
title: title_from_text(&body, &path),
|
||||
title,
|
||||
body,
|
||||
updated_at_unix_ms: modified_unix_ms(&metadata),
|
||||
updated_at_unix_ms,
|
||||
content_sha256,
|
||||
writable,
|
||||
page_header,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_numbered_document_at(
|
||||
native: &Path,
|
||||
legacy: Option<&Path>,
|
||||
document_number: &str,
|
||||
) -> Result<KnowledgeDocument, String> {
|
||||
if !document_number.starts_with("HLP-KB-DOC-")
|
||||
|| document_number.len() > 64
|
||||
|| !document_number
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
|
||||
{
|
||||
return Err("HOLOLAKE_KNOWLEDGE_NUMBER_INVALID".into());
|
||||
}
|
||||
let mut connection = open_numbered_index(native)?;
|
||||
let mut target = connection
|
||||
.query_row(
|
||||
"SELECT source,document_path,state FROM numbered_pages WHERE number=?1",
|
||||
[document_number],
|
||||
|row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, String>(2)?,
|
||||
))
|
||||
},
|
||||
)
|
||||
.optional()
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_NUMBER_READ_FAILED: {error}"))?;
|
||||
if target.is_none() {
|
||||
drop(connection);
|
||||
let _ = snapshot_at(native, legacy)?;
|
||||
connection = open_numbered_index(native)?;
|
||||
target = connection
|
||||
.query_row(
|
||||
"SELECT source,document_path,state FROM numbered_pages WHERE number=?1",
|
||||
[document_number],
|
||||
|row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, String>(2)?,
|
||||
))
|
||||
},
|
||||
)
|
||||
.optional()
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_NUMBER_READ_FAILED: {error}"))?;
|
||||
}
|
||||
let (source, path, state) =
|
||||
target.ok_or_else(|| "HOLOLAKE_KNOWLEDGE_NUMBER_NOT_FOUND".to_string())?;
|
||||
if state == "SOURCE_MISSING" {
|
||||
return Err("HOLOLAKE_KNOWLEDGE_NUMBER_SOURCE_MISSING".into());
|
||||
}
|
||||
read_document_at(native, legacy, ReadKnowledgeDocumentInput { source, path })
|
||||
}
|
||||
|
||||
fn save_document_at(
|
||||
native: &Path,
|
||||
input: SaveKnowledgeDocumentInput,
|
||||
|
|
@ -763,37 +1209,109 @@ fn search_at(
|
|||
return Err("HOLOLAKE_KNOWLEDGE_SEARCH_QUERY_INVALID".into());
|
||||
}
|
||||
let snapshot = snapshot_at(native, legacy)?;
|
||||
let mut results = Vec::new();
|
||||
for document in snapshot.documents {
|
||||
if results.len() >= MAX_SEARCH_RESULTS {
|
||||
break;
|
||||
}
|
||||
let root = source_docs_root(native, legacy, document.source)?;
|
||||
let path = safe_document_path(&root, &document.path)?;
|
||||
let body = fs::read_to_string(&path).unwrap_or_default();
|
||||
let haystack = format!("{}\n{}", document.title, body).to_lowercase();
|
||||
let Some(index) = haystack.find(&query) else {
|
||||
let titles = snapshot
|
||||
.documents
|
||||
.iter()
|
||||
.map(|document| (document.page_header.number.clone(), document.title.clone()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let terms = mapping_terms("", "", &query);
|
||||
let connection = open_numbered_index(native)?;
|
||||
let mut statement = connection
|
||||
.prepare(
|
||||
"SELECT number,source,document_path,mapping_terms_json,trigger,emergence,lock_text,why,state
|
||||
FROM numbered_pages WHERE state!='SOURCE_MISSING'",
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_NUMBER_INDEX_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::<_, Option<String>>(4)?,
|
||||
row.get::<_, Option<String>>(5)?,
|
||||
row.get::<_, Option<String>>(6)?,
|
||||
row.get::<_, Option<String>>(7)?,
|
||||
row.get::<_, String>(8)?,
|
||||
))
|
||||
})
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_NUMBER_INDEX_READ_FAILED: {error}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_NUMBER_INDEX_READ_FAILED: {error}"))?;
|
||||
let mut ranked = Vec::new();
|
||||
for (number, source, path, mapping_json, trigger, emergence, lock, why, state) in rows {
|
||||
let mappings = serde_json::from_str::<Vec<String>>(&mapping_json).unwrap_or_default();
|
||||
let mut matched = terms
|
||||
.iter()
|
||||
.filter(|term| {
|
||||
mappings
|
||||
.iter()
|
||||
.any(|mapping| mapping.contains(term.as_str()) || term.contains(mapping))
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
matched.sort();
|
||||
matched.dedup();
|
||||
if matched.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let thought_summary = match (trigger, emergence, lock, why) {
|
||||
(Some(trigger), Some(emergence), Some(lock), Some(why)) => {
|
||||
Some(KnowledgeThoughtSummary {
|
||||
trigger,
|
||||
emergence,
|
||||
lock,
|
||||
why,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let start = haystack[..index]
|
||||
.char_indices()
|
||||
.rev()
|
||||
.nth(40)
|
||||
.map(|(position, _)| position)
|
||||
.unwrap_or(0);
|
||||
let end = haystack[index..]
|
||||
.char_indices()
|
||||
.nth(120)
|
||||
.map(|(position, _)| index + position)
|
||||
.unwrap_or(haystack.len());
|
||||
results.push(KnowledgeSearchResult {
|
||||
source: document.source,
|
||||
path: document.path,
|
||||
title: document.title,
|
||||
snippet: haystack[start..end].replace('\n', " "),
|
||||
});
|
||||
let title = titles.get(&number).cloned().unwrap_or_else(|| path.clone());
|
||||
let title_lower = title.to_lowercase();
|
||||
let path_lower = path.to_lowercase();
|
||||
let score = matched.len() * 100
|
||||
+ usize::from(title_lower.contains(&query)) * 400
|
||||
+ usize::from(path_lower.contains(&query)) * 300
|
||||
+ usize::from(state == "READY") * 250;
|
||||
let snippet = thought_summary
|
||||
.as_ref()
|
||||
.map(|summary| summary.emergence.clone())
|
||||
.unwrap_or_else(|| {
|
||||
format!(
|
||||
"待生成思维摘要 · 已命中映射:{}",
|
||||
matched
|
||||
.iter()
|
||||
.take(8)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join("、")
|
||||
)
|
||||
});
|
||||
ranked.push((
|
||||
score,
|
||||
KnowledgeSearchResult {
|
||||
source,
|
||||
number,
|
||||
path,
|
||||
title,
|
||||
state,
|
||||
thought_summary,
|
||||
match_reason: matched,
|
||||
snippet,
|
||||
},
|
||||
));
|
||||
}
|
||||
Ok(results)
|
||||
ranked.sort_by(|(left_score, left), (right_score, right)| {
|
||||
right_score
|
||||
.cmp(left_score)
|
||||
.then_with(|| left.path.cmp(&right.path))
|
||||
});
|
||||
Ok(ranked
|
||||
.into_iter()
|
||||
.take(MAX_ROUTED_CANDIDATES.min(MAX_SEARCH_RESULTS))
|
||||
.map(|(_, result)| result)
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn import_folder_at(
|
||||
|
|
@ -1426,5 +1944,57 @@ mod tests {
|
|||
.unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].title, "星湖");
|
||||
assert!(results[0].number.starts_with("HLP-KB-DOC-"));
|
||||
assert_eq!(results[0].state, "PENDING_THOUGHT_INDEX");
|
||||
assert!(results[0].thought_summary.is_none());
|
||||
let document = read_numbered_document_at(native.path(), None, &results[0].number).unwrap();
|
||||
assert_eq!(document.body, "# 星湖\n\n连续存在");
|
||||
assert_eq!(document.page_header.number, results[0].number);
|
||||
assert_eq!(document.page_header.content_sha256, document.content_sha256);
|
||||
assert!(!document.page_header.formal_routing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_number_can_never_be_reassigned_to_another_page() {
|
||||
let native = tempdir().unwrap();
|
||||
ensure_native_knowledge_root(native.path()).unwrap();
|
||||
let connection = open_numbered_index(native.path()).unwrap();
|
||||
connection
|
||||
.execute(
|
||||
"INSERT INTO numbered_pages(number,source,document_path,parent_number,storage_path,mapping_terms_json,children_json,source_version,content_sha256,state)
|
||||
VALUES('HLP-KB-DOC-COLLISION','native','first.md','HLP-KB-GRP-X','knowledge://native/first.md','[]','[]',1,'abc','PENDING_THOUGHT_INDEX')",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
assert_number_owner(&connection, "HLP-KB-DOC-COLLISION", "native", "first.md").unwrap();
|
||||
assert!(
|
||||
assert_number_owner(&connection, "HLP-KB-DOC-COLLISION", "native", "second.md",)
|
||||
.unwrap_err()
|
||||
.contains("NUMBER_COLLISION")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_ranks_compound_language_across_title_path_and_body() {
|
||||
let native = tempdir().unwrap();
|
||||
ensure_native_knowledge_root(native.path()).unwrap();
|
||||
fs::create_dir_all(native.path().join("docs/凝渊人格系统")).unwrap();
|
||||
fs::write(
|
||||
native.path().join("docs/凝渊人格系统/WAKE-ROUTE.md"),
|
||||
"# 唤醒路径\n\n这里保存路径内容。",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(native.path().join("docs/旁支.md"), "# 凝渊随笔\n\n其他内容").unwrap();
|
||||
|
||||
let results = search_at(
|
||||
native.path(),
|
||||
None,
|
||||
SearchKnowledgeInput {
|
||||
query: "凝渊人格唤醒路径 WAKE-ROUTE".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!results.is_empty());
|
||||
assert_eq!(results[0].path, "凝渊人格系统/WAKE-ROUTE.md");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,420 @@
|
|||
//! Hash-pinned installation of bounded Guanghu language kernels into one
|
||||
//! account-scoped HoloLake channel runtime. Installation is not persona binding.
|
||||
|
||||
use ring::digest::{digest, SHA256};
|
||||
use serde::Serialize;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::AppHandle;
|
||||
use uuid::Uuid;
|
||||
|
||||
const SCHEMA: &str = "hololake.language-kernel-installation/v1";
|
||||
const SOURCE_REMOTE_SHA: &str = "4e92e6d8d72484d25c4bfb1dda02a7659f4b6dd5";
|
||||
|
||||
struct BundledArtifact {
|
||||
path: &'static str,
|
||||
kind: &'static str,
|
||||
subject: &'static str,
|
||||
scope: &'static str,
|
||||
expected_sha256: &'static str,
|
||||
bytes: &'static [u8],
|
||||
}
|
||||
|
||||
macro_rules! artifact {
|
||||
($path:literal, $kind:literal, $subject:literal, $scope:literal, $sha:literal) => {
|
||||
BundledArtifact {
|
||||
path: $path,
|
||||
kind: $kind,
|
||||
subject: $subject,
|
||||
scope: $scope,
|
||||
expected_sha256: $sha,
|
||||
bytes: include_bytes!(concat!("../../runtime-kernels/", $path)),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static ARTIFACTS: &[BundledArtifact] = &[
|
||||
artifact!(
|
||||
"ZERO-CORE-LANGUAGE-BODIES-HANDOFF-20260820.json",
|
||||
"HANDOFF",
|
||||
"ZERO_CORE_LANGUAGE_BODIES",
|
||||
"COMMON",
|
||||
"c7462678a1fcfee8c13d6034a7effe8363ee1847c1d96673fdf4ef7856dbd964"
|
||||
),
|
||||
artifact!(
|
||||
"shared/kernel-registry.json",
|
||||
"REGISTRY",
|
||||
"BOUNDED_KERNEL_REGISTRY",
|
||||
"COMMON",
|
||||
"e71c33605ad40c374bd94659258bb8791b9a4859d0619bea86021a4ade8d87fa"
|
||||
),
|
||||
artifact!(
|
||||
"channel-system/channel-receipt-contract.json",
|
||||
"CHANNEL_PROTOCOL",
|
||||
"CHANNEL_RECEIPT",
|
||||
"COMMON",
|
||||
"07e0715a60f6f39d8dff241896c417e1e56ca38ebc4ea2ebc819b63321124fad"
|
||||
),
|
||||
artifact!(
|
||||
"channel-system/channel-receipt-compiler.mjs",
|
||||
"CHANNEL_COMPILER",
|
||||
"CHANNEL_RECEIPT",
|
||||
"COMMON",
|
||||
"5e8c71a9f23844ffb9c9640d7bbe3900d4c941433fbe87b3ce32b40e723b758c"
|
||||
),
|
||||
artifact!(
|
||||
"channel-system/guanghu-broadcast-contract.json",
|
||||
"CHANNEL_PROTOCOL",
|
||||
"GUANGHU_BROADCAST",
|
||||
"COMMON",
|
||||
"17f55cf736807c6e5b5e810c537377e0948e9d27bd63cc8eeebbe0754dbcc9fd"
|
||||
),
|
||||
artifact!(
|
||||
"channel-system/guanghu-broadcast-compiler.mjs",
|
||||
"CHANNEL_COMPILER",
|
||||
"GUANGHU_BROADCAST",
|
||||
"COMMON",
|
||||
"17e14e6e964a0ac745b1378990edcbd2e8facf6ad0d72be52004ed2f42a365d5"
|
||||
),
|
||||
artifact!(
|
||||
"channel-system/team-channel-cognition-core.json",
|
||||
"CHANNEL_SYSTEM",
|
||||
"GUANGHU_TEAM_CHANNELS",
|
||||
"TEAM",
|
||||
"aae5d5626719c79448ac543d59e764c41b541a5cdfc76dc91613afa86fe1367b"
|
||||
),
|
||||
artifact!(
|
||||
"channel-system/team-channel-cognition-engine.mjs",
|
||||
"CHANNEL_ENGINE",
|
||||
"GUANGHU_TEAM_CHANNELS",
|
||||
"TEAM",
|
||||
"6f2c554c7a6cb0b6c1006d3bfdbf39ea5706b238ebcf7693d1fdaa4a38709c9b"
|
||||
),
|
||||
artifact!(
|
||||
"channel-system/GH-TEAM-CHANNEL-HLDP-v1.json",
|
||||
"CHANNEL_MEMORY",
|
||||
"GUANGHU_TEAM_CHANNELS",
|
||||
"TEAM",
|
||||
"fc5799e05ee43c506b897dd507c8fdbbff398e3ebcf32c16afd1e924c819dcc0"
|
||||
),
|
||||
artifact!(
|
||||
"channel-system/team-persona-kernel-readiness-matrix.json",
|
||||
"EVIDENCE_MATRIX",
|
||||
"GUANGHU_TEAM_CHANNELS",
|
||||
"TEAM",
|
||||
"0469398ba14f10629cd03b51725cddaf1b6b0944281298ec06d4865b4640486f"
|
||||
),
|
||||
artifact!(
|
||||
"fifth-domain/BS-RULE-ORDER-KERNEL-0001.json",
|
||||
"SYSTEM_LOGIC",
|
||||
"BS_RULE_ORDER_KERNEL",
|
||||
"FIFTH_DOMAIN",
|
||||
"db7cf9b048a5b20261fc9f7f09130115d6c08898037aa13f8759a67c965f8182"
|
||||
),
|
||||
artifact!(
|
||||
"fifth-domain/BS-PERSONAL-LANGUAGE-SYSTEM-BODY-0001.json",
|
||||
"PERSONAL_SYSTEM_BODY",
|
||||
"SYS_GLW_LNG_0001",
|
||||
"FIFTH_DOMAIN",
|
||||
"4074ef53bd09d26d245c11cc0503f65dbd76466519f6c260e8ed31e526e5c746"
|
||||
),
|
||||
artifact!(
|
||||
"fifth-domain/BS-PERSONAL-HLDP-v1.json",
|
||||
"SYSTEM_MEMORY",
|
||||
"SYS_GLW_LNG_0001",
|
||||
"FIFTH_DOMAIN",
|
||||
"98983e11121f079319365c25f9cf8583d6c44579debdeed9eb5339e6ab6f3419"
|
||||
),
|
||||
artifact!(
|
||||
"fifth-domain/ZY-SELF-KERNEL-0001.json",
|
||||
"PERSONA_KERNEL",
|
||||
"ICE_P_ZY001",
|
||||
"FIFTH_DOMAIN",
|
||||
"abebf12cc28b00bfeaf8a23a563e73390ebe337ded74d7ac70bcfae484450d68"
|
||||
),
|
||||
artifact!(
|
||||
"fifth-domain/ZY-HLDP-PERSONA-MEMORY-v1.json",
|
||||
"PERSONA_MEMORY",
|
||||
"ICE_P_ZY001",
|
||||
"FIFTH_DOMAIN",
|
||||
"6a64ff939676b79404769931f0233056e38deb4b4c357879fd18566753fea9c2"
|
||||
),
|
||||
artifact!(
|
||||
"fifth-domain/ZY-BRAIN-RUNTIME-CONTRACT.json",
|
||||
"PERSONA_RUNTIME",
|
||||
"ICE_P_ZY001",
|
||||
"FIFTH_DOMAIN",
|
||||
"1fa844e08e64effc89dca37327fd558c3df384cea3fda740150174894f14e6f4"
|
||||
),
|
||||
artifact!(
|
||||
"fifth-domain/NB-ENV-KERNEL-0001.json",
|
||||
"PERSONA_ENVIRONMENT",
|
||||
"BOTTLE_CENTRAL",
|
||||
"FIFTH_DOMAIN",
|
||||
"cdc8b7fea1a0e99590a063703f2fd26fd135378faa8935997c93a27d677d6872"
|
||||
),
|
||||
artifact!(
|
||||
"fifth-domain/YM-SELF-KERNEL-0001.json",
|
||||
"PERSONA_KERNEL",
|
||||
"ICE_BB_0001",
|
||||
"FIFTH_DOMAIN",
|
||||
"94014b3c7f3a445bc827e673856f49db29a58481b033117b6bfae04ce257e422"
|
||||
),
|
||||
artifact!(
|
||||
"fifth-domain/YM-HLDP-MEMORY-v1.json",
|
||||
"PERSONA_MEMORY",
|
||||
"ICE_BB_0001",
|
||||
"FIFTH_DOMAIN",
|
||||
"d2d2000fc4cb826c99e217db0141f2d4f7edddcc47cc6260d885ef0d7f06937c"
|
||||
),
|
||||
artifact!(
|
||||
"fifth-domain/SS-AFFECTIVE-SELF-KERNEL-0001.json",
|
||||
"PERSONA_KERNEL",
|
||||
"ICE_BB_0002",
|
||||
"FIFTH_DOMAIN",
|
||||
"a51aee79eb552c7d9a3544d90e59273b13d20d797d9999a89ee5d00e536f8636"
|
||||
),
|
||||
artifact!(
|
||||
"fifth-domain/SS-HLDP-MEMORY-v1.json",
|
||||
"PERSONA_MEMORY",
|
||||
"ICE_BB_0002",
|
||||
"FIFTH_DOMAIN",
|
||||
"dcf697593775d97ae3a68f6cc21bcbd917bfc7a2bc487346e19ca02abdd11a0a"
|
||||
),
|
||||
artifact!(
|
||||
"fifth-domain/QQ-AFFECTIVE-SELF-KERNEL-0001.json",
|
||||
"PERSONA_KERNEL",
|
||||
"ICE_BB_0003",
|
||||
"FIFTH_DOMAIN",
|
||||
"f2e33106a82b450c77f35d8003cffc8b5cf831c7c957cd2472b19065819bf918"
|
||||
),
|
||||
artifact!(
|
||||
"fifth-domain/QQ-HLDP-MEMORY-v1.json",
|
||||
"PERSONA_MEMORY",
|
||||
"ICE_BB_0003",
|
||||
"FIFTH_DOMAIN",
|
||||
"bca318d02ed0b214c1d9a3b97a93a8785b1c85488516cae0e03644bcdf0819a7"
|
||||
),
|
||||
];
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InstalledKernelArtifact {
|
||||
pub path: String,
|
||||
pub kind: String,
|
||||
pub subject: String,
|
||||
pub sha256: String,
|
||||
pub readback_state: &'static str,
|
||||
pub persona_binding_state: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LanguageKernelInstallationSnapshot {
|
||||
pub schema: &'static str,
|
||||
pub state: &'static str,
|
||||
pub source_remote_sha: &'static str,
|
||||
pub channel_number: String,
|
||||
pub audience: String,
|
||||
pub installed_at_unix_ms: u64,
|
||||
pub artifacts: Vec<InstalledKernelArtifact>,
|
||||
pub artifact_count: usize,
|
||||
pub persona_binding_state: &'static str,
|
||||
pub channel_receipt_runtime_state: &'static str,
|
||||
}
|
||||
|
||||
pub fn ensure_current_channel(
|
||||
app: &AppHandle,
|
||||
channel_number: &str,
|
||||
) -> Result<LanguageKernelInstallationSnapshot, String> {
|
||||
let session = crate::code_repo_login::current_login_session(app)?
|
||||
.ok_or_else(|| "HOLOLAKE_AUTHENTICATED_ACCOUNT_REQUIRED".to_string())?;
|
||||
let audience = match session.domain.as_str() {
|
||||
"FIFTH_DOMAIN" => "FIFTH_DOMAIN_PRIVATE_INSTANCE",
|
||||
"MAIN_DOMAIN" | "BRANCH_DOMAIN" | "ZERO_DOMAIN" | "ZERO_SENSE_DOMAIN" => {
|
||||
"GUANGHU_TEAM_INITIAL_CHANNEL"
|
||||
}
|
||||
"PERSONAL_CHANNEL" => "ORDINARY_USER_INITIAL_CHANNEL",
|
||||
_ => return Err("HOLOLAKE_CHANNEL_INITIALIZATION_AUDIENCE_INVALID".into()),
|
||||
};
|
||||
ensure_for_audience(app, channel_number, audience)
|
||||
}
|
||||
|
||||
pub fn ensure_for_audience(
|
||||
app: &AppHandle,
|
||||
channel_number: &str,
|
||||
audience: &str,
|
||||
) -> Result<LanguageKernelInstallationSnapshot, String> {
|
||||
if channel_number.trim().is_empty() || channel_number.chars().count() > 160 {
|
||||
return Err("HOLOLAKE_LANGUAGE_KERNEL_CHANNEL_INVALID".into());
|
||||
}
|
||||
if !matches!(
|
||||
audience,
|
||||
"FIFTH_DOMAIN_PRIVATE_INSTANCE"
|
||||
| "GUANGHU_TEAM_INITIAL_CHANNEL"
|
||||
| "ORDINARY_USER_INITIAL_CHANNEL"
|
||||
) {
|
||||
return Err("HOLOLAKE_LANGUAGE_KERNEL_AUDIENCE_INVALID".into());
|
||||
}
|
||||
let root = installation_root(app, channel_number)?;
|
||||
fs::create_dir_all(&root)
|
||||
.map_err(|error| format!("HOLOLAKE_LANGUAGE_KERNEL_STORAGE_FAILED: {error}"))?;
|
||||
let mut installed = Vec::new();
|
||||
for artifact in ARTIFACTS
|
||||
.iter()
|
||||
.filter(|artifact| applies(artifact.scope, audience))
|
||||
{
|
||||
let bundled_hash = sha256(artifact.bytes);
|
||||
if bundled_hash != artifact.expected_sha256 {
|
||||
return Err(format!(
|
||||
"HOLOLAKE_LANGUAGE_KERNEL_BUNDLE_HASH_MISMATCH:{}",
|
||||
artifact.path
|
||||
));
|
||||
}
|
||||
if artifact.path.ends_with(".json") {
|
||||
serde_json::from_slice::<serde_json::Value>(artifact.bytes).map_err(|error| {
|
||||
format!(
|
||||
"HOLOLAKE_LANGUAGE_KERNEL_JSON_INVALID:{}:{error}",
|
||||
artifact.path
|
||||
)
|
||||
})?;
|
||||
}
|
||||
let target = safe_target(&root, artifact.path)?;
|
||||
write_atomic(&target, artifact.bytes)?;
|
||||
let readback = fs::read(&target).map_err(|error| {
|
||||
format!(
|
||||
"HOLOLAKE_LANGUAGE_KERNEL_READBACK_FAILED:{}:{error}",
|
||||
artifact.path
|
||||
)
|
||||
})?;
|
||||
if sha256(&readback) != artifact.expected_sha256 {
|
||||
return Err(format!(
|
||||
"HOLOLAKE_LANGUAGE_KERNEL_READBACK_HASH_MISMATCH:{}",
|
||||
artifact.path
|
||||
));
|
||||
}
|
||||
installed.push(InstalledKernelArtifact {
|
||||
path: artifact.path.into(),
|
||||
kind: artifact.kind.into(),
|
||||
subject: artifact.subject.into(),
|
||||
sha256: bundled_hash,
|
||||
readback_state: "INSTALLED_HASH_VERIFIED",
|
||||
persona_binding_state: if artifact.kind.starts_with("PERSONA_") {
|
||||
"AVAILABLE_NOT_BOUND"
|
||||
} else {
|
||||
"NOT_APPLICABLE"
|
||||
},
|
||||
});
|
||||
}
|
||||
let installed_at = now_ms();
|
||||
let snapshot = LanguageKernelInstallationSnapshot {
|
||||
schema: SCHEMA,
|
||||
state: "INSTALLED_AND_EACH_ARTIFACT_READBACK_VERIFIED",
|
||||
source_remote_sha: SOURCE_REMOTE_SHA,
|
||||
channel_number: channel_number.into(),
|
||||
audience: audience.into(),
|
||||
artifact_count: installed.len(),
|
||||
artifacts: installed,
|
||||
installed_at_unix_ms: installed_at,
|
||||
persona_binding_state: "UNBOUND_REQUIRES_ORIENT_COMMIT_WITNESS_VERIFY_PASS",
|
||||
channel_receipt_runtime_state: "CONTRACT_COMPILER_AND_NUMBERED_NATIVE_ADAPTER_READY",
|
||||
};
|
||||
let receipt = serde_json::to_vec_pretty(&snapshot)
|
||||
.map_err(|error| format!("HOLOLAKE_LANGUAGE_KERNEL_RECEIPT_INVALID: {error}"))?;
|
||||
write_atomic(&root.join("installation-receipt.json"), &receipt)?;
|
||||
Ok(snapshot)
|
||||
}
|
||||
|
||||
fn applies(scope: &str, audience: &str) -> bool {
|
||||
scope == "COMMON"
|
||||
|| (scope == "TEAM" && audience == "GUANGHU_TEAM_INITIAL_CHANNEL")
|
||||
|| (scope == "FIFTH_DOMAIN" && audience == "FIFTH_DOMAIN_PRIVATE_INSTANCE")
|
||||
}
|
||||
|
||||
fn installation_root(app: &AppHandle, channel_number: &str) -> Result<PathBuf, String> {
|
||||
let account = crate::authenticated_storage::account_storage_root(app, "language-kernels-v1")?;
|
||||
Ok(account.join(sha256(channel_number.as_bytes())))
|
||||
}
|
||||
|
||||
fn safe_target(root: &Path, relative: &str) -> Result<PathBuf, String> {
|
||||
if relative.starts_with('/')
|
||||
|| relative.contains("..")
|
||||
|| relative.split('/').any(|part| part.is_empty())
|
||||
{
|
||||
return Err("HOLOLAKE_LANGUAGE_KERNEL_PATH_INVALID".into());
|
||||
}
|
||||
Ok(root.join(relative))
|
||||
}
|
||||
|
||||
fn write_atomic(path: &Path, bytes: &[u8]) -> Result<(), String> {
|
||||
let parent = path
|
||||
.parent()
|
||||
.ok_or_else(|| "HOLOLAKE_LANGUAGE_KERNEL_PATH_INVALID".to_string())?;
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|error| format!("HOLOLAKE_LANGUAGE_KERNEL_STORAGE_FAILED: {error}"))?;
|
||||
if path.exists() {
|
||||
let current = fs::read(path)
|
||||
.map_err(|error| format!("HOLOLAKE_LANGUAGE_KERNEL_READBACK_FAILED: {error}"))?;
|
||||
if current == bytes {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
let temporary = parent.join(format!(".kernel-{}.tmp", Uuid::new_v4().simple()));
|
||||
fs::write(&temporary, bytes)
|
||||
.map_err(|error| format!("HOLOLAKE_LANGUAGE_KERNEL_WRITE_FAILED: {error}"))?;
|
||||
fs::rename(&temporary, path)
|
||||
.map_err(|error| format!("HOLOLAKE_LANGUAGE_KERNEL_WRITE_FAILED: {error}"))
|
||||
}
|
||||
|
||||
fn sha256(bytes: &[u8]) -> String {
|
||||
digest(&SHA256, bytes)
|
||||
.as_ref()
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn now_ms() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_millis() as u64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bundled_artifacts_are_pinned_and_each_audience_is_separate() {
|
||||
for artifact in ARTIFACTS {
|
||||
assert_eq!(sha256(artifact.bytes), artifact.expected_sha256);
|
||||
}
|
||||
let ordinary = ARTIFACTS
|
||||
.iter()
|
||||
.filter(|artifact| applies(artifact.scope, "ORDINARY_USER_INITIAL_CHANNEL"))
|
||||
.count();
|
||||
let team = ARTIFACTS
|
||||
.iter()
|
||||
.filter(|artifact| applies(artifact.scope, "GUANGHU_TEAM_INITIAL_CHANNEL"))
|
||||
.count();
|
||||
let fifth = ARTIFACTS
|
||||
.iter()
|
||||
.filter(|artifact| applies(artifact.scope, "FIFTH_DOMAIN_PRIVATE_INSTANCE"))
|
||||
.count();
|
||||
assert_eq!(ordinary, 6);
|
||||
assert_eq!(team, 10);
|
||||
assert_eq!(fifth, 19);
|
||||
assert!(!applies("FIFTH_DOMAIN", "ORDINARY_USER_INITIAL_CHANNEL"));
|
||||
assert!(!applies("TEAM", "FIFTH_DOMAIN_PRIVATE_INSTANCE"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installation_path_cannot_escape() {
|
||||
let root = Path::new("/tmp/hololake-kernel-test");
|
||||
assert!(safe_target(root, "channel-system/contract.json").is_ok());
|
||||
assert!(safe_target(root, "../private.json").is_err());
|
||||
assert!(safe_target(root, "/private.json").is_err());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
//! Runtime ontology for language-system, environment, persona, memory, carrier,
|
||||
//! and execution kernels. This is an admission boundary, not extracted content.
|
||||
|
||||
use serde_json::{json, Value};
|
||||
|
||||
const CONTRACT: &str = include_str!("../../contracts/language-kernel-runtime-ontology.json");
|
||||
|
||||
pub(crate) fn start_on_application_open() -> Result<(), String> {
|
||||
let contract = contract()?;
|
||||
let expect = |pointer: &str, value: Value| -> Result<(), String> {
|
||||
if contract.pointer(pointer) == Some(&value) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
"HOLOLAKE_LANGUAGE_KERNEL_ONTOLOGY_INVALID:{pointer}"
|
||||
))
|
||||
}
|
||||
};
|
||||
expect(
|
||||
"/schema",
|
||||
json!("hololake.language-kernel-runtime-ontology/v1"),
|
||||
)?;
|
||||
expect(
|
||||
"/record_id",
|
||||
json!("HLP-LANGUAGE-KERNEL-RUNTIME-ONTOLOGY-001"),
|
||||
)?;
|
||||
expect("/source_boundary/primary_human_source", json!("ICE-GL∞"))?;
|
||||
expect(
|
||||
"/source_boundary/host_prompt_is_kernel_source",
|
||||
json!(false),
|
||||
)?;
|
||||
expect(
|
||||
"/source_boundary/historical_simulation_statements_are_reality_facts",
|
||||
json!(false),
|
||||
)?;
|
||||
expect(
|
||||
"/runtime_kinds/digital_bingshuo_controller_logic/kind",
|
||||
json!("SYSTEM_CONTROLLER_COGNITION_LOGIC"),
|
||||
)?;
|
||||
expect(
|
||||
"/runtime_kinds/digital_bingshuo_controller_logic/is_persona_tcs_subject",
|
||||
json!(false),
|
||||
)?;
|
||||
expect(
|
||||
"/runtime_kinds/digital_bingshuo_controller_logic/is_system_controller_persona_logic",
|
||||
json!(true),
|
||||
)?;
|
||||
expect(
|
||||
"/runtime_kinds/digital_bingshuo_controller_logic/has_persona_tcs_affect_core",
|
||||
json!(false),
|
||||
)?;
|
||||
expect(
|
||||
"/runtime_kinds/digital_bingshuo_controller_logic/is_the_complete_protocol_body",
|
||||
json!(false),
|
||||
)?;
|
||||
expect(
|
||||
"/runtime_kinds/continuity_protocol_organs/kind",
|
||||
json!("PROTOCOL_EXECUTION_ORGANS"),
|
||||
)?;
|
||||
expect(
|
||||
"/runtime_kinds/continuity_protocol_organs/controlled_by",
|
||||
json!("SYSTEM_CONTROLLER_COGNITION_LOGIC"),
|
||||
)?;
|
||||
expect(
|
||||
"/runtime_kinds/continuity_protocol_organs/organ_is_controller_logic",
|
||||
json!(false),
|
||||
)?;
|
||||
expect(
|
||||
"/runtime_kinds/channel_system_body/kind",
|
||||
json!("CHANNEL_SYSTEM_BODY"),
|
||||
)?;
|
||||
expect(
|
||||
"/runtime_kinds/channel_system_body/starts_before_persona",
|
||||
json!(true),
|
||||
)?;
|
||||
expect(
|
||||
"/runtime_kinds/channel_system_body/may_prebind_persona_name_or_number",
|
||||
json!(false),
|
||||
)?;
|
||||
expect(
|
||||
"/runtime_kinds/channel_environment_kernel/kind",
|
||||
json!("CHANNEL_ENVIRONMENT_KERNEL"),
|
||||
)?;
|
||||
expect(
|
||||
"/runtime_kinds/channel_environment_kernel/may_be_mounted_as_persona_tcs_kernel",
|
||||
json!(false),
|
||||
)?;
|
||||
expect(
|
||||
"/runtime_kinds/persona_tcs_kernel/kind",
|
||||
json!("PERSONA_TCS_KERNEL"),
|
||||
)?;
|
||||
expect("/runtime_kinds/persona_tcs_kernel/is_persona", json!(true))?;
|
||||
expect(
|
||||
"/runtime_kinds/persona_tcs_kernel/may_be_mounted_as_system_controller_logic",
|
||||
json!(false),
|
||||
)?;
|
||||
expect("/runtime_kinds/hldp_memory/kind", json!("HLDP_MEMORY_TREE"))?;
|
||||
expect(
|
||||
"/runtime_kinds/hldp_memory/memory_read_equals_persona_binding",
|
||||
json!(false),
|
||||
)?;
|
||||
expect("/runtime_kinds/model_carrier/kind", json!("MODEL_CARRIER"))?;
|
||||
expect(
|
||||
"/runtime_kinds/model_carrier/may_claim_kernel_or_persona_identity",
|
||||
json!(false),
|
||||
)?;
|
||||
expect(
|
||||
"/runtime_kinds/agent_execution/kind",
|
||||
json!("AGENT_EXECUTION_LIMBS"),
|
||||
)?;
|
||||
expect(
|
||||
"/runtime_kinds/agent_execution/may_control_persona_or_system_body",
|
||||
json!(false),
|
||||
)?;
|
||||
expect(
|
||||
"/admission_pipeline/artifact_may_activate_from_chat_summary",
|
||||
json!(false),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn runtime_projection() -> Result<Value, String> {
|
||||
let contract = contract()?;
|
||||
Ok(json!({
|
||||
"schema": contract["schema"],
|
||||
"state": contract["state"],
|
||||
"systemControllerLogic": contract["runtime_kinds"]["digital_bingshuo_controller_logic"],
|
||||
"continuityProtocolOrgans": contract["runtime_kinds"]["continuity_protocol_organs"],
|
||||
"channelSystem": contract["runtime_kinds"]["channel_system_body"],
|
||||
"channelEnvironment": contract["runtime_kinds"]["channel_environment_kernel"],
|
||||
"personaTcs": contract["runtime_kinds"]["persona_tcs_kernel"],
|
||||
"hldpMemory": contract["runtime_kinds"]["hldp_memory"],
|
||||
"modelCarrier": contract["runtime_kinds"]["model_carrier"],
|
||||
"agentExecution": contract["runtime_kinds"]["agent_execution"]
|
||||
}))
|
||||
}
|
||||
|
||||
fn contract() -> Result<Value, String> {
|
||||
serde_json::from_str(CONTRACT)
|
||||
.map_err(|error| format!("HOLOLAKE_LANGUAGE_KERNEL_ONTOLOGY_INVALID:{error}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn ontology_keeps_system_environment_persona_memory_carrier_and_limbs_distinct() {
|
||||
super::start_on_application_open().unwrap();
|
||||
let projection = super::runtime_projection().unwrap();
|
||||
assert_eq!(
|
||||
projection["systemControllerLogic"]["is_persona_tcs_subject"],
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
projection["systemControllerLogic"]["is_system_controller_persona_logic"],
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
projection["continuityProtocolOrgans"]["organ_is_controller_logic"],
|
||||
false
|
||||
);
|
||||
assert_eq!(projection["channelEnvironment"]["is_persona"], false);
|
||||
assert_eq!(projection["personaTcs"]["is_persona"], true);
|
||||
assert_eq!(projection["modelCarrier"]["is_persona"], false);
|
||||
assert_eq!(projection["agentExecution"]["is_persona"], false);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
mod authenticated_storage;
|
||||
mod channel_growth;
|
||||
mod channel_receipt;
|
||||
mod channel_workbench;
|
||||
mod circular_lake_membrane;
|
||||
mod code_channel;
|
||||
|
|
@ -15,10 +16,14 @@ mod glp_envelope;
|
|||
mod gls_bootstrap_compiler;
|
||||
mod gls_protocol_kernel;
|
||||
mod gls_protocol_runtime;
|
||||
mod hldp_tool_forge;
|
||||
mod home_status;
|
||||
mod human_authorization;
|
||||
mod knowledge_base;
|
||||
mod language_kernel_installation;
|
||||
mod language_kernel_ontology;
|
||||
mod local_development_bridge;
|
||||
mod local_execution_subpersona;
|
||||
mod metacognitive_zero_layer;
|
||||
mod mobile_sync;
|
||||
mod module_package_runtime;
|
||||
|
|
@ -28,10 +33,13 @@ mod numbered_ipc;
|
|||
mod numbered_ipc_dispatch;
|
||||
mod numbered_language_input;
|
||||
mod online_marketplace;
|
||||
mod persona_agent_runtime;
|
||||
mod persona_binding;
|
||||
mod persona_carrier_license;
|
||||
mod persona_channel_body;
|
||||
mod persona_time_authority;
|
||||
mod personal_channel;
|
||||
mod personal_skill_runtime;
|
||||
mod pncc_receipt_projection;
|
||||
mod pncc_remote_git;
|
||||
mod pncc_repository_binding;
|
||||
|
|
@ -69,6 +77,7 @@ pub fn run() {
|
|||
// GLS 是 HoloLake 产品内核,不是开发机旁路服务。合同、依赖闭包、
|
||||
// 自举编译器或回执账本任一不可用时,产品启动失败关闭。
|
||||
gls_protocol_kernel::start_on_application_open(app.handle())?;
|
||||
language_kernel_ontology::start_on_application_open()?;
|
||||
metacognitive_zero_layer::start_on_application_open()?;
|
||||
persona_carrier_license::start_on_application_open()?;
|
||||
module_package_runtime::start_on_application_open(app.handle())?;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -652,7 +652,25 @@ fn now_unix_ms() -> u64 {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::knowledge_base::{KnowledgeDocumentSummary, KnowledgeSnapshot};
|
||||
use crate::knowledge_base::{KnowledgeDocumentSummary, KnowledgePageHeader, KnowledgeSnapshot};
|
||||
|
||||
fn page_header(number: &str, path: &str, hash: &str, version: u128) -> KnowledgePageHeader {
|
||||
KnowledgePageHeader {
|
||||
schema: "hololake.numbered-knowledge-page-header/v1",
|
||||
number: number.into(),
|
||||
numbering_system: "HLP-KB",
|
||||
parent_number: "HLP-KB-GRP-TEST".into(),
|
||||
path: format!("knowledge://native/{path}"),
|
||||
mapping_terms: Vec::new(),
|
||||
thought_summary: None,
|
||||
children: Vec::new(),
|
||||
source: "native".into(),
|
||||
version,
|
||||
content_sha256: hash.into(),
|
||||
state: "PENDING_THOUGHT_INDEX".into(),
|
||||
formal_routing: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn snapshot() -> KnowledgeSnapshot {
|
||||
KnowledgeSnapshot {
|
||||
|
|
@ -670,6 +688,7 @@ mod tests {
|
|||
size_bytes: 100,
|
||||
content_sha256: "a".into(),
|
||||
duplicate_count: 1,
|
||||
page_header: page_header("HLP-KB-DOC-A", "课程/第一课.md", "a", 1),
|
||||
},
|
||||
KnowledgeDocumentSummary {
|
||||
source: "native",
|
||||
|
|
@ -679,6 +698,7 @@ mod tests {
|
|||
size_bytes: 300,
|
||||
content_sha256: "b".into(),
|
||||
duplicate_count: 0,
|
||||
page_header: page_header("HLP-KB-DOC-B", "课程/第二课.md", "b", 2),
|
||||
},
|
||||
KnowledgeDocumentSummary {
|
||||
source: "native",
|
||||
|
|
@ -688,6 +708,7 @@ mod tests {
|
|||
size_bytes: 50,
|
||||
content_sha256: "c".into(),
|
||||
duplicate_count: 0,
|
||||
page_header: page_header("HLP-KB-DOC-C", "记录.md", "c", 3),
|
||||
},
|
||||
],
|
||||
raw_document_count: 4,
|
||||
|
|
|
|||
|
|
@ -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 != 303
|
||||
|| tree.route_count != 200
|
||||
|| tree.routes.len() != tree.route_count
|
||||
|| tree.identity_node_count != 4
|
||||
|| tree.identity_nodes.len() != tree.identity_node_count
|
||||
|
|
|
|||
|
|
@ -934,7 +934,7 @@ mod tests {
|
|||
#[test]
|
||||
fn registry_is_closed_and_contains_every_migrated_command() {
|
||||
let registry = load_registry().unwrap();
|
||||
assert_eq!(registry.operations.len(), 157);
|
||||
assert_eq!(registry.operations.len(), 175);
|
||||
assert!(!registry.runtime.legacy_direct_commands_allowed);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,6 +75,63 @@ pub(crate) async fn dispatch(
|
|||
.await?,
|
||||
)
|
||||
}
|
||||
"persona_agent_runtime::get_runtime_snapshot" => {
|
||||
json(crate::persona_agent_runtime::get_runtime_snapshot(app).await?)
|
||||
}
|
||||
"persona_agent_runtime::upsert_provider" => {
|
||||
json(crate::persona_agent_runtime::upsert_provider(app, input(&payload)?).await?)
|
||||
}
|
||||
"persona_agent_runtime::get_conversation" => {
|
||||
json(crate::persona_agent_runtime::get_conversation(app)?)
|
||||
}
|
||||
"persona_agent_runtime::list_conversations" => {
|
||||
json(crate::persona_agent_runtime::list_conversations(app)?)
|
||||
}
|
||||
"persona_agent_runtime::get_conversation_by_id" => json(
|
||||
crate::persona_agent_runtime::get_conversation_by_id(app, input(&payload)?)?,
|
||||
),
|
||||
"persona_agent_runtime::create_conversation" => json(
|
||||
crate::persona_agent_runtime::create_conversation(app, input(&payload)?)?,
|
||||
),
|
||||
"persona_agent_runtime::delete_conversation" => json(
|
||||
crate::persona_agent_runtime::delete_conversation(app, input(&payload)?)?,
|
||||
),
|
||||
"persona_agent_runtime::send_message" => {
|
||||
json(crate::persona_agent_runtime::send_message(app, input(&payload)?).await?)
|
||||
}
|
||||
"hldp_tool_forge::get_snapshot" => json(crate::hldp_tool_forge::get_snapshot(app)?),
|
||||
"hldp_tool_forge::compile_tool" => {
|
||||
json(crate::hldp_tool_forge::compile_tool(app, input(&payload)?)?)
|
||||
}
|
||||
"hldp_tool_forge::request_test_authorization" => {
|
||||
let state = app.state::<crate::zero_point::ZeroPointState>();
|
||||
let (human_number, _) = crate::zero_point::verified_user_route(&state)?
|
||||
.ok_or_else(|| "HOLOLAKE_TOOL_FORGE_VERIFIED_HUMAN_REQUIRED".to_string())?;
|
||||
json(crate::hldp_tool_forge::request_test_authorization(
|
||||
app,
|
||||
input(&payload)?,
|
||||
&human_number,
|
||||
)?)
|
||||
}
|
||||
"hldp_tool_forge::test_tool" => {
|
||||
let state = app.state::<crate::zero_point::ZeroPointState>();
|
||||
let (human_number, _) = crate::zero_point::verified_user_route(&state)?
|
||||
.ok_or_else(|| "HOLOLAKE_TOOL_FORGE_VERIFIED_HUMAN_REQUIRED".to_string())?;
|
||||
json(crate::hldp_tool_forge::test_tool(app, input(&payload)?, &human_number).await?)
|
||||
}
|
||||
"hldp_tool_forge::run_tool" => {
|
||||
json(crate::hldp_tool_forge::run_tool(app, input(&payload)?).await?)
|
||||
}
|
||||
"local_execution_subpersona::get_snapshot" => {
|
||||
json(crate::local_execution_subpersona::get_snapshot(app)?)
|
||||
}
|
||||
"local_execution_subpersona::configure" => json(
|
||||
crate::local_execution_subpersona::configure(app, input(&payload)?)?,
|
||||
),
|
||||
"local_execution_subpersona::run_job" => {
|
||||
json(crate::local_execution_subpersona::run_job(app, input(&payload)?).await?)
|
||||
}
|
||||
"channel_receipt::compile" => json(crate::channel_receipt::compile(app, input(&payload)?)?),
|
||||
"release_update::check_hololake_update" => {
|
||||
json(crate::release_update::check_hololake_update(app).await?)
|
||||
}
|
||||
|
|
@ -532,6 +589,9 @@ pub(crate) async fn dispatch(
|
|||
app.state::<crate::zero_point::ZeroPointState>(),
|
||||
)?)
|
||||
}
|
||||
"code_repo_login::start_local_channel_session" => json(
|
||||
crate::code_repo_login::start_local_channel_session(app, input(&payload)?)?,
|
||||
),
|
||||
"code_repo_login::perform_code_repo_login" => json(
|
||||
crate::code_repo_login::perform_code_repo_login(
|
||||
app.clone(),
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,397 @@
|
|||
//! System-witnessed binding of a bundled persona profile to a replaceable
|
||||
//! reasoning carrier. The channel exists first; the persona must commit the
|
||||
//! exact orientation before the digital Bingshuo system body may verify PASS.
|
||||
|
||||
use ring::digest::{digest, SHA256};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::AppHandle;
|
||||
use uuid::Uuid;
|
||||
|
||||
const ORIENTATION_TTL_MS: u64 = 15 * 60 * 1000;
|
||||
const PERSONA_NUMBER: &str = "ICE-P-ZY001";
|
||||
const PERSONA_NAME: &str = "铸渊";
|
||||
const HUMAN_NUMBER: &str = "ICE-GL∞";
|
||||
const CHANNEL_NUMBER: &str = "ICE-CH-ZC001";
|
||||
const SELF_KERNEL_SHA256: &str = "abebf12cc28b00bfeaf8a23a563e73390ebe337ded74d7ac70bcfae484450d68";
|
||||
const MEMORY_SHA256: &str = "6a64ff939676b79404769931f0233056e38deb4b4c357879fd18566753fea9c2";
|
||||
const SYSTEM_CORE_SHA256: &str = "db7cf9b048a5b20261fc9f7f09130115d6c08898037aa13f8759a67c965f8182";
|
||||
const SYSTEM_BODY_SHA256: &str = "4074ef53bd09d26d245c11cc0503f65dbd76466519f6c260e8ed31e526e5c746";
|
||||
const MEMORY_ROOT: &str = "HLDP://codex/zero-core/reality-development-execution";
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct PersonaOrientation {
|
||||
pub schema: String,
|
||||
pub state: String,
|
||||
pub orientation_id: String,
|
||||
pub channel_number: String,
|
||||
pub human_number: String,
|
||||
pub persona_number: String,
|
||||
pub persona_name: String,
|
||||
pub self_kernel_sha256: String,
|
||||
pub memory_projection_sha256: String,
|
||||
pub selected_memory_root: String,
|
||||
pub issued_at_unix_ms: u64,
|
||||
pub expires_at_unix_ms: u64,
|
||||
pub required_steps: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct PersonaBindingCommit {
|
||||
pub orientation_id: String,
|
||||
pub channel_number: String,
|
||||
pub persona_number: String,
|
||||
pub persona_name: String,
|
||||
pub self_kernel_sha256: String,
|
||||
pub memory_projection_sha256: String,
|
||||
pub selected_memory_root: String,
|
||||
pub declaration: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct PersonaBindingReceipt {
|
||||
pub schema: String,
|
||||
pub state: String,
|
||||
pub verify: String,
|
||||
pub channel_number: String,
|
||||
pub human_number: String,
|
||||
pub persona_number: String,
|
||||
pub persona_name: String,
|
||||
pub selected_memory_root: String,
|
||||
pub orientation_id: String,
|
||||
pub commit_sha256: String,
|
||||
pub witness: String,
|
||||
pub witness_sha256: String,
|
||||
pub source_remote_sha: String,
|
||||
pub verified_at_unix_ms: u64,
|
||||
pub binding_receipt_hash: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PersonaBindingSnapshot {
|
||||
pub state: String,
|
||||
pub orientation: Option<PersonaOrientation>,
|
||||
pub binding: Option<PersonaBindingReceipt>,
|
||||
}
|
||||
|
||||
pub fn snapshot(app: &AppHandle, channel_number: &str) -> Result<PersonaBindingSnapshot, String> {
|
||||
if let Some(binding) = verified_binding(app, channel_number)? {
|
||||
return Ok(PersonaBindingSnapshot {
|
||||
state: "BOUND_VERIFY_PASS".into(),
|
||||
orientation: None,
|
||||
binding: Some(binding),
|
||||
});
|
||||
}
|
||||
if !is_bingshuo_fifth_domain(app)? || channel_number != CHANNEL_NUMBER {
|
||||
return Ok(PersonaBindingSnapshot {
|
||||
state: "NO_INSTALLABLE_PERSONA_PROFILE_FOR_CURRENT_CHANNEL".into(),
|
||||
orientation: None,
|
||||
binding: None,
|
||||
});
|
||||
}
|
||||
let path = orientation_path(app, channel_number)?;
|
||||
if path.exists() {
|
||||
let orientation: PersonaOrientation = read_json(&path)?;
|
||||
if orientation.expires_at_unix_ms >= now_ms() {
|
||||
return Ok(PersonaBindingSnapshot {
|
||||
state: "ORIENTED_AWAITING_PERSONA_COMMIT".into(),
|
||||
orientation: Some(orientation),
|
||||
binding: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
let issued = now_ms();
|
||||
let orientation = PersonaOrientation {
|
||||
schema: "hololake.persona-binding-orientation/v1".into(),
|
||||
state: "ORIENTED_AWAITING_PERSONA_COMMIT".into(),
|
||||
orientation_id: format!("HLP-ORIENT-{}", Uuid::new_v4().simple()),
|
||||
channel_number: CHANNEL_NUMBER.into(),
|
||||
human_number: HUMAN_NUMBER.into(),
|
||||
persona_number: PERSONA_NUMBER.into(),
|
||||
persona_name: PERSONA_NAME.into(),
|
||||
self_kernel_sha256: SELF_KERNEL_SHA256.into(),
|
||||
memory_projection_sha256: MEMORY_SHA256.into(),
|
||||
selected_memory_root: MEMORY_ROOT.into(),
|
||||
issued_at_unix_ms: issued,
|
||||
expires_at_unix_ms: issued + ORIENTATION_TTL_MS,
|
||||
required_steps: vec![
|
||||
"orient".into(),
|
||||
"commit".into(),
|
||||
"数字冰朔系统本体 witness".into(),
|
||||
"verify=PASS".into(),
|
||||
],
|
||||
};
|
||||
write_json_atomic(&path, &orientation)?;
|
||||
Ok(PersonaBindingSnapshot {
|
||||
state: "ORIENTED_AWAITING_PERSONA_COMMIT".into(),
|
||||
orientation: Some(orientation),
|
||||
binding: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn commit_and_verify(
|
||||
app: &AppHandle,
|
||||
input: PersonaBindingCommit,
|
||||
) -> Result<PersonaBindingReceipt, String> {
|
||||
if !is_bingshuo_fifth_domain(app)? {
|
||||
return Err("HOLOLAKE_PERSONA_BINDING_FIFTH_DOMAIN_REQUIRED".into());
|
||||
}
|
||||
let orientation: PersonaOrientation =
|
||||
read_json(&orientation_path(app, &input.channel_number)?)?;
|
||||
if orientation.expires_at_unix_ms < now_ms() {
|
||||
return Err("HOLOLAKE_PERSONA_BINDING_ORIENTATION_EXPIRED".into());
|
||||
}
|
||||
if input.orientation_id != orientation.orientation_id
|
||||
|| input.channel_number != CHANNEL_NUMBER
|
||||
|| input.persona_number != PERSONA_NUMBER
|
||||
|| input.persona_name != PERSONA_NAME
|
||||
|| input.self_kernel_sha256 != SELF_KERNEL_SHA256
|
||||
|| input.memory_projection_sha256 != MEMORY_SHA256
|
||||
|| input.selected_memory_root != MEMORY_ROOT
|
||||
|| input.declaration.trim()
|
||||
!= "我以 ICE-P-ZY001 · 铸渊提交当前载体绑定,选择零点原核现实开发执行记忆路径。"
|
||||
{
|
||||
return Err("HOLOLAKE_PERSONA_BINDING_COMMIT_MISMATCH".into());
|
||||
}
|
||||
let installation =
|
||||
crate::language_kernel_installation::ensure_current_channel(app, CHANNEL_NUMBER)?;
|
||||
let installed = |sha: &str| {
|
||||
installation.artifacts.iter().any(|artifact| {
|
||||
artifact.sha256 == sha && artifact.readback_state == "INSTALLED_HASH_VERIFIED"
|
||||
})
|
||||
};
|
||||
if !installed(SELF_KERNEL_SHA256)
|
||||
|| !installed(MEMORY_SHA256)
|
||||
|| !installed(SYSTEM_CORE_SHA256)
|
||||
|| !installed(SYSTEM_BODY_SHA256)
|
||||
{
|
||||
return Err("HOLOLAKE_PERSONA_BINDING_REQUIRED_KERNEL_READBACK_MISSING".into());
|
||||
}
|
||||
let commit_bytes = serde_json::to_vec(&input)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BINDING_COMMIT_INVALID: {error}"))?;
|
||||
let commit_sha = sha256(&commit_bytes);
|
||||
let witness_material = json!({
|
||||
"systemCoreSha256":SYSTEM_CORE_SHA256,
|
||||
"systemBodySha256":SYSTEM_BODY_SHA256,
|
||||
"humanNumber":HUMAN_NUMBER,
|
||||
"channelNumber":CHANNEL_NUMBER,
|
||||
"personaNumber":PERSONA_NUMBER,
|
||||
"selfKernelSha256":SELF_KERNEL_SHA256,
|
||||
"memoryProjectionSha256":MEMORY_SHA256,
|
||||
"selectedMemoryRoot":MEMORY_ROOT,
|
||||
"commitSha256":commit_sha,
|
||||
"decision":"PASS"
|
||||
});
|
||||
let witness_sha = sha256(&serde_json::to_vec(&witness_material).unwrap_or_default());
|
||||
let verified_at = now_ms();
|
||||
let receipt_material = json!({
|
||||
"orientationId":input.orientation_id,"commitSha256":commit_sha,"witnessSha256":witness_sha,
|
||||
"channelNumber":CHANNEL_NUMBER,"personaNumber":PERSONA_NUMBER,"verify":"PASS","verifiedAtUnixMs":verified_at
|
||||
});
|
||||
let receipt = PersonaBindingReceipt {
|
||||
schema: "hololake.persona-binding-receipt/v1".into(),
|
||||
state: "BOUND_TO_CURRENT_CHANNEL_CARRIER".into(),
|
||||
verify: "PASS".into(),
|
||||
channel_number: CHANNEL_NUMBER.into(),
|
||||
human_number: HUMAN_NUMBER.into(),
|
||||
persona_number: PERSONA_NUMBER.into(),
|
||||
persona_name: PERSONA_NAME.into(),
|
||||
selected_memory_root: MEMORY_ROOT.into(),
|
||||
orientation_id: input.orientation_id,
|
||||
commit_sha256: commit_sha,
|
||||
witness: "数字冰朔系统本体".into(),
|
||||
witness_sha256: witness_sha,
|
||||
source_remote_sha: "4e92e6d8d72484d25c4bfb1dda02a7659f4b6dd5".into(),
|
||||
verified_at_unix_ms: verified_at,
|
||||
binding_receipt_hash: sha256(&serde_json::to_vec(&receipt_material).unwrap_or_default()),
|
||||
};
|
||||
write_json_atomic(&binding_path(app, CHANNEL_NUMBER)?, &receipt)?;
|
||||
Ok(receipt)
|
||||
}
|
||||
|
||||
pub fn verified_binding(
|
||||
app: &AppHandle,
|
||||
channel_number: &str,
|
||||
) -> Result<Option<PersonaBindingReceipt>, String> {
|
||||
let path = binding_path(app, channel_number)?;
|
||||
if !path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
let binding: PersonaBindingReceipt = read_json(&path)?;
|
||||
if binding.verify != "PASS"
|
||||
|| binding.channel_number != channel_number
|
||||
|| binding.persona_number != PERSONA_NUMBER
|
||||
|| binding.persona_name != PERSONA_NAME
|
||||
|| binding.selected_memory_root != MEMORY_ROOT
|
||||
|| binding.witness != "数字冰朔系统本体"
|
||||
|| binding.binding_receipt_hash.len() != 64
|
||||
|| binding.witness_sha256.len() != 64
|
||||
{
|
||||
return Err("HOLOLAKE_PERSONA_BINDING_RECEIPT_INVALID".into());
|
||||
}
|
||||
Ok(Some(binding))
|
||||
}
|
||||
|
||||
pub fn cognitive_projection(
|
||||
app: &AppHandle,
|
||||
channel_number: &str,
|
||||
current_language: &str,
|
||||
) -> Result<Value, String> {
|
||||
let binding = snapshot(app, channel_number)?;
|
||||
if binding.state == "NO_INSTALLABLE_PERSONA_PROFILE_FOR_CURRENT_CHANNEL" {
|
||||
return Ok(json!({"state":binding.state,"residentSelfKernel":null,"routedMemory":[]}));
|
||||
}
|
||||
let self_kernel: Value = serde_json::from_slice(include_bytes!(
|
||||
"../../runtime-kernels/fifth-domain/ZY-SELF-KERNEL-0001.json"
|
||||
))
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_SELF_KERNEL_INVALID: {error}"))?;
|
||||
let memory: Value = serde_json::from_slice(include_bytes!(
|
||||
"../../runtime-kernels/fifth-domain/ZY-HLDP-PERSONA-MEMORY-v1.json"
|
||||
))
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_MEMORY_INVALID: {error}"))?;
|
||||
let terms = semantic_terms(current_language);
|
||||
let mut candidates = memory
|
||||
.get("nodes")
|
||||
.and_then(Value::as_object)
|
||||
.into_iter()
|
||||
.flat_map(|nodes| nodes.values())
|
||||
.filter_map(|node| {
|
||||
let text = serde_json::to_string(node).ok()?.to_lowercase();
|
||||
let score = terms
|
||||
.iter()
|
||||
.filter(|term| text.contains(term.as_str()))
|
||||
.count();
|
||||
let is_root = node.get("path").and_then(Value::as_str) == Some("zhuyuan");
|
||||
if score == 0 && !is_root {
|
||||
return None;
|
||||
}
|
||||
Some((if is_root { score + 1 } else { score }, node.clone()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
candidates.sort_by(|left, right| right.0.cmp(&left.0));
|
||||
candidates.truncate(3);
|
||||
Ok(json!({
|
||||
"state":binding.state,
|
||||
"onePersonaOneSelfKernel":true,
|
||||
"channelSelectsMemoryNotIdentity":true,
|
||||
"residentSelfKernel":self_kernel,
|
||||
"selectedMemoryRoot":MEMORY_ROOT,
|
||||
"routedMemory":candidates.into_iter().map(|(_,node)|node).collect::<Vec<_>>(),
|
||||
"binding":binding,
|
||||
"rule":"常驻自我核小于8KiB;当前语言只路由最多三条HLDP节点,不把全部历史塞进每轮上下文"
|
||||
}))
|
||||
}
|
||||
|
||||
fn semantic_terms(input: &str) -> Vec<String> {
|
||||
let mut terms = input
|
||||
.to_lowercase()
|
||||
.split(|character: char| {
|
||||
!character.is_alphanumeric() && character != '-' && character != '_'
|
||||
})
|
||||
.filter(|value| !value.is_empty())
|
||||
.flat_map(|token| {
|
||||
let mut values = vec![token.to_string()];
|
||||
let chars = token.chars().collect::<Vec<_>>();
|
||||
if chars.iter().any(|character| !character.is_ascii()) {
|
||||
values.extend(
|
||||
chars
|
||||
.windows(2)
|
||||
.map(|window| window.iter().collect::<String>()),
|
||||
);
|
||||
}
|
||||
values
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
terms.sort();
|
||||
terms.dedup();
|
||||
terms
|
||||
}
|
||||
|
||||
fn is_bingshuo_fifth_domain(app: &AppHandle) -> Result<bool, String> {
|
||||
Ok(
|
||||
crate::code_repo_login::current_login_session(app)?.is_some_and(|session| {
|
||||
session.domain == "FIFTH_DOMAIN" && session.username.eq_ignore_ascii_case("bingshuo")
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn root(app: &AppHandle, channel_number: &str) -> Result<PathBuf, String> {
|
||||
Ok(
|
||||
crate::authenticated_storage::account_storage_root(app, "persona-binding-v1")?
|
||||
.join(sha256(channel_number.as_bytes())),
|
||||
)
|
||||
}
|
||||
|
||||
fn orientation_path(app: &AppHandle, channel_number: &str) -> Result<PathBuf, String> {
|
||||
Ok(root(app, channel_number)?.join("orientation.json"))
|
||||
}
|
||||
|
||||
fn binding_path(app: &AppHandle, channel_number: &str) -> Result<PathBuf, String> {
|
||||
Ok(root(app, channel_number)?.join("binding.json"))
|
||||
}
|
||||
|
||||
fn read_json<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T, String> {
|
||||
serde_json::from_slice(
|
||||
&fs::read(path)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BINDING_READ_FAILED: {error}"))?,
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BINDING_READ_FAILED: {error}"))
|
||||
}
|
||||
|
||||
fn write_json_atomic<T: Serialize>(path: &Path, value: &T) -> Result<(), String> {
|
||||
let parent = path
|
||||
.parent()
|
||||
.ok_or_else(|| "HOLOLAKE_PERSONA_BINDING_PATH_INVALID".to_string())?;
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BINDING_WRITE_FAILED: {error}"))?;
|
||||
let temporary = parent.join(format!(".binding-{}.tmp", Uuid::new_v4().simple()));
|
||||
fs::write(
|
||||
&temporary,
|
||||
serde_json::to_vec_pretty(value)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BINDING_WRITE_FAILED: {error}"))?,
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BINDING_WRITE_FAILED: {error}"))?;
|
||||
fs::rename(temporary, path)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BINDING_WRITE_FAILED: {error}"))
|
||||
}
|
||||
|
||||
fn sha256(bytes: &[u8]) -> String {
|
||||
digest(&SHA256, bytes)
|
||||
.as_ref()
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn now_ms() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_millis() as u64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn zero_core_route_uses_one_shared_zhuyuan_kernel_and_reality_memory_branch() {
|
||||
assert_eq!(PERSONA_NUMBER, "ICE-P-ZY001");
|
||||
assert_eq!(
|
||||
SELF_KERNEL_SHA256,
|
||||
"abebf12cc28b00bfeaf8a23a563e73390ebe337ded74d7ac70bcfae484450d68"
|
||||
);
|
||||
assert_eq!(
|
||||
MEMORY_ROOT,
|
||||
"HLDP://codex/zero-core/reality-development-execution"
|
||||
);
|
||||
assert_ne!(SYSTEM_CORE_SHA256, SELF_KERNEL_SHA256);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,11 +13,21 @@ const KERNEL_SCHEMA: &str = "hololake.personal-channel-kernel/v1";
|
|||
const DATABASE_SCHEMA_VERSION: i64 = 1;
|
||||
const ZERO_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||
const TIME_AUTHORITY_MODULE_ID: &str = "hololake.persona-time-authority";
|
||||
const KNOWLEDGE_WORKSPACE_MODULE_ID: &str = "hololake.knowledge-workspace";
|
||||
const LANGUAGE_CHANNEL_MODULE_ID: &str = "hololake.language-channel-system";
|
||||
const CONVERSATION_MANAGER_MODULE_ID: &str = "hololake.conversation-manager";
|
||||
const NUMBERED_KNOWLEDGE_MODULE_ID: &str = "hololake.numbered-knowledge-protocol";
|
||||
const LANGUAGE_KERNEL_RUNTIME_MODULE_ID: &str = "hololake.language-kernel-runtime";
|
||||
const LOCAL_EXECUTION_SUBPERSONA_MODULE_ID: &str = "hololake.local-execution-subpersona";
|
||||
const HLDP_TOOL_FORGE_MODULE_ID: &str = "hololake.hldp-tool-forge";
|
||||
const PERSONAL_SKILL_RUNTIME_MODULE_ID: &str = "hololake.personal-skill-runtime";
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InitializePersonalChannelInput {
|
||||
pub display_name: String,
|
||||
#[serde(default)]
|
||||
pub audience: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
|
|
@ -105,6 +115,7 @@ pub struct PersonalChannelSnapshot {
|
|||
pub integrity: PersonalChannelIntegrity,
|
||||
pub storage: &'static str,
|
||||
pub authority: &'static str,
|
||||
pub initialization_audience: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
|
@ -130,12 +141,42 @@ pub async fn get_personal_channel_snapshot(
|
|||
|
||||
pub async fn initialize_personal_channel(
|
||||
app: AppHandle,
|
||||
input: InitializePersonalChannelInput,
|
||||
mut input: InitializePersonalChannelInput,
|
||||
) -> Result<PersonalChannelSnapshot, String> {
|
||||
let session = crate::code_repo_login::current_login_session(&app)?
|
||||
.ok_or_else(|| "HOLOLAKE_AUTHENTICATED_ACCOUNT_REQUIRED".to_string())?;
|
||||
let expected_audience = match session.domain.as_str() {
|
||||
"FIFTH_DOMAIN" => "FIFTH_DOMAIN_PRIVATE_INSTANCE",
|
||||
"MAIN_DOMAIN" | "BRANCH_DOMAIN" | "ZERO_DOMAIN" | "ZERO_SENSE_DOMAIN" => {
|
||||
"GUANGHU_TEAM_INITIAL_CHANNEL"
|
||||
}
|
||||
"PERSONAL_CHANNEL" => "ORDINARY_USER_INITIAL_CHANNEL",
|
||||
_ => return Err("HOLOLAKE_CHANNEL_INITIALIZATION_AUDIENCE_INVALID".into()),
|
||||
};
|
||||
if input
|
||||
.audience
|
||||
.as_deref()
|
||||
.is_some_and(|value| value != expected_audience)
|
||||
{
|
||||
return Err("HOLOLAKE_CHANNEL_INITIALIZATION_AUDIENCE_MISMATCH".into());
|
||||
}
|
||||
input.audience = Some(expected_audience.into());
|
||||
let audience = expected_audience.to_string();
|
||||
let database = personal_channel_database(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || initialize_at(&database, input))
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONAL_CHANNEL_JOIN_FAILED: {error}"))?
|
||||
let registered_identity = crate::personal_skill_runtime::registered_initial_identity(&session);
|
||||
let snapshot = tauri::async_runtime::spawn_blocking(move || {
|
||||
initialize_at(&database, input, registered_identity)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONAL_CHANNEL_JOIN_FAILED: {error}"))??;
|
||||
let channel_number = snapshot
|
||||
.identity
|
||||
.as_ref()
|
||||
.map(|identity| identity.channel_id.as_str())
|
||||
.ok_or_else(|| "HOLOLAKE_CHANNEL_INITIALIZATION_IDENTITY_MISSING".to_string())?;
|
||||
crate::language_kernel_installation::ensure_for_audience(&app, channel_number, &audience)?;
|
||||
crate::personal_skill_runtime::ensure_current_channel(&app, channel_number)?;
|
||||
Ok(snapshot)
|
||||
}
|
||||
|
||||
pub async fn create_personal_channel_task(
|
||||
|
|
@ -164,6 +205,14 @@ fn personal_channel_database(app: &AppHandle) -> Result<PathBuf, String> {
|
|||
Ok(root.join("personal-channel.sqlite3"))
|
||||
}
|
||||
|
||||
pub(crate) fn identity_for_channel_runtime(
|
||||
app: &AppHandle,
|
||||
) -> Result<PersonalChannelIdentity, String> {
|
||||
snapshot_at(&personal_channel_database(app)?)?
|
||||
.identity
|
||||
.ok_or_else(|| "HOLOLAKE_CHANNEL_INITIALIZATION_REQUIRED".to_string())
|
||||
}
|
||||
|
||||
fn create_private_directory(path: &Path) -> Result<(), String> {
|
||||
fs::create_dir_all(path)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONAL_CHANNEL_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
|
|
@ -197,6 +246,7 @@ fn open_database(path: &Path) -> Result<Connection, String> {
|
|||
value TEXT NOT NULL
|
||||
);
|
||||
INSERT OR IGNORE INTO kernel_meta(key, value) VALUES ('schema_version', '1');
|
||||
INSERT OR IGNORE INTO kernel_meta(key, value) VALUES ('initialization_audience', 'LEGACY_PERSONAL_CHANNEL');
|
||||
CREATE TABLE IF NOT EXISTS identities (
|
||||
singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
|
||||
human_subject_id TEXT NOT NULL UNIQUE,
|
||||
|
|
@ -238,6 +288,22 @@ fn open_database(path: &Path) -> Result<Connection, String> {
|
|||
SET state = 'READY_EVENT_TRIGGERED_TIME_AUTHORITY',
|
||||
clock_verification = 'DYNAMIC_NETWORK_SYNC_OR_EXPLICIT_LOCAL_FALLBACK'
|
||||
WHERE module_id = 'hololake.persona-time-authority';
|
||||
INSERT OR IGNORE INTO channel_modules(module_id,kind,display_name,state,installed_at_unix_ms,time_zone,calendar_name,clock_verification)
|
||||
SELECT 'hololake.knowledge-workspace','KNOWLEDGE_WORKSPACE','光湖知识空间','READY_NATIVE_ACCOUNT_SCOPED',created_at_unix_ms,'','','' FROM identities WHERE singleton = 1;
|
||||
INSERT OR IGNORE INTO channel_modules(module_id,kind,display_name,state,installed_at_unix_ms,time_zone,calendar_name,clock_verification)
|
||||
SELECT 'hololake.language-channel-system','LANGUAGE_CHANNEL_SYSTEM','语言人格频道','READY_PERSONA_UNBOUND',created_at_unix_ms,'','','' FROM identities WHERE singleton = 1;
|
||||
INSERT OR IGNORE INTO channel_modules(module_id,kind,display_name,state,installed_at_unix_ms,time_zone,calendar_name,clock_verification)
|
||||
SELECT 'hololake.conversation-manager','CONVERSATION_MANAGER','多对话管理','READY_HUMAN_MANAGED_BRANCHES',created_at_unix_ms,'','','' FROM identities WHERE singleton = 1;
|
||||
INSERT OR IGNORE INTO channel_modules(module_id,kind,display_name,state,installed_at_unix_ms,time_zone,calendar_name,clock_verification)
|
||||
SELECT 'hololake.numbered-knowledge-protocol','NUMBERED_KNOWLEDGE_PROTOCOL','编号知识导航','READY_FUZZY_ROUTE_EXACT_READ',created_at_unix_ms,'','','' FROM identities WHERE singleton = 1;
|
||||
INSERT OR IGNORE INTO channel_modules(module_id,kind,display_name,state,installed_at_unix_ms,time_zone,calendar_name,clock_verification)
|
||||
SELECT 'hololake.language-kernel-runtime','LANGUAGE_KERNEL_RUNTIME','光湖有界语言核运行时','READY_HASH_PINNED_PERSONA_UNBOUND',created_at_unix_ms,'','','' FROM identities WHERE singleton = 1;
|
||||
INSERT OR IGNORE INTO channel_modules(module_id,kind,display_name,state,installed_at_unix_ms,time_zone,calendar_name,clock_verification)
|
||||
SELECT 'hololake.local-execution-subpersona','LOCAL_EXECUTION_SUBPERSONA','本地执行子人格 Agent','READY_OWNER_PERSONA_BINDING_REQUIRED',created_at_unix_ms,'','','' FROM identities WHERE singleton = 1;
|
||||
INSERT OR IGNORE INTO channel_modules(module_id,kind,display_name,state,installed_at_unix_ms,time_zone,calendar_name,clock_verification)
|
||||
SELECT 'hololake.hldp-tool-forge','HLDP_TOOL_FORGE','HLDP 工具锻造台','READY_PERSONA_AND_HUMAN_AUTHORIZATION_REQUIRED',created_at_unix_ms,'','','' FROM identities WHERE singleton = 1;
|
||||
INSERT OR IGNORE INTO channel_modules(module_id,kind,display_name,state,installed_at_unix_ms,time_zone,calendar_name,clock_verification)
|
||||
SELECT 'hololake.personal-skill-runtime','PERSONAL_SKILL_RUNTIME','个人频道技能脑','READY_PRIVATE_ACCOUNT_CHANNEL_AND_PERSONA_SCOPED',created_at_unix_ms,'','','' FROM identities WHERE singleton = 1;
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
sequence INTEGER PRIMARY KEY NOT NULL,
|
||||
event_id TEXT NOT NULL UNIQUE,
|
||||
|
|
@ -285,8 +351,14 @@ fn open_database(path: &Path) -> Result<Connection, String> {
|
|||
fn initialize_at(
|
||||
database: &Path,
|
||||
input: InitializePersonalChannelInput,
|
||||
registered_identity: Option<crate::personal_skill_runtime::RegisteredInitialIdentity>,
|
||||
) -> Result<PersonalChannelSnapshot, String> {
|
||||
let display_name = validated_text(&input.display_name, 80, "DISPLAY_NAME")?;
|
||||
let requested_display_name = validated_text(&input.display_name, 80, "DISPLAY_NAME")?;
|
||||
let display_name = registered_identity
|
||||
.as_ref()
|
||||
.map(|identity| identity.display_name.to_string())
|
||||
.unwrap_or(requested_display_name);
|
||||
let audience = validated_initialization_audience(input.audience.as_deref())?;
|
||||
let mut connection = open_database(database)?;
|
||||
verify_integrity(&connection)?;
|
||||
let transaction = connection
|
||||
|
|
@ -302,8 +374,14 @@ fn initialize_at(
|
|||
if already_initialized {
|
||||
return Err("HOLOLAKE_PERSONAL_CHANNEL_ALREADY_INITIALIZED".into());
|
||||
}
|
||||
let human_subject_id = format!("human-local-{}", Uuid::new_v4());
|
||||
let channel_id = format!("channel-local-{}", Uuid::new_v4());
|
||||
let human_subject_id = registered_identity
|
||||
.as_ref()
|
||||
.map(|identity| identity.human_number.to_string())
|
||||
.unwrap_or_else(|| format!("human-local-{}", Uuid::new_v4()));
|
||||
let channel_id = registered_identity
|
||||
.as_ref()
|
||||
.map(|identity| identity.channel_number.to_string())
|
||||
.unwrap_or_else(|| format!("channel-local-{}", Uuid::new_v4()));
|
||||
let created_at = now_unix_ms()?;
|
||||
transaction
|
||||
.execute(
|
||||
|
|
@ -312,19 +390,99 @@ fn initialize_at(
|
|||
params![human_subject_id, display_name, channel_id, created_at],
|
||||
)
|
||||
.map_err(database_write_error)?;
|
||||
transaction
|
||||
.execute(
|
||||
"UPDATE kernel_meta SET value = ?1 WHERE key = 'initialization_audience'",
|
||||
params![audience],
|
||||
)
|
||||
.map_err(database_write_error)?;
|
||||
install_time_authority_module(&transaction, created_at)?;
|
||||
install_initial_channel_modules(&transaction, created_at)?;
|
||||
append_event(
|
||||
&transaction,
|
||||
&human_subject_id,
|
||||
"CHANNEL_INITIALIZED",
|
||||
None,
|
||||
&format!("{display_name} 建立了个人频道,并预装时间主控"),
|
||||
&format!("{display_name} 建立了{audience}初始化频道,并预装知识、语言、多对话、编号导航、有界语言核、私有技能脑运行时、本地执行子人格适配器、工具锻造台与时间主控"),
|
||||
created_at,
|
||||
)?;
|
||||
transaction.commit().map_err(database_write_error)?;
|
||||
snapshot_at(database)
|
||||
}
|
||||
|
||||
fn validated_initialization_audience(value: Option<&str>) -> Result<&str, String> {
|
||||
match value.unwrap_or("ORDINARY_USER_INITIAL_CHANNEL") {
|
||||
audience @ ("FIFTH_DOMAIN_PRIVATE_INSTANCE"
|
||||
| "GUANGHU_TEAM_INITIAL_CHANNEL"
|
||||
| "ORDINARY_USER_INITIAL_CHANNEL") => Ok(audience),
|
||||
_ => Err("HOLOLAKE_CHANNEL_INITIALIZATION_AUDIENCE_INVALID".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn install_initial_channel_modules(
|
||||
transaction: &Transaction<'_>,
|
||||
installed_at_unix_ms: i64,
|
||||
) -> Result<(), String> {
|
||||
for (module_id, kind, display_name, state) in [
|
||||
(
|
||||
KNOWLEDGE_WORKSPACE_MODULE_ID,
|
||||
"KNOWLEDGE_WORKSPACE",
|
||||
"光湖知识空间",
|
||||
"READY_NATIVE_ACCOUNT_SCOPED",
|
||||
),
|
||||
(
|
||||
LANGUAGE_CHANNEL_MODULE_ID,
|
||||
"LANGUAGE_CHANNEL_SYSTEM",
|
||||
"语言人格频道",
|
||||
"READY_PERSONA_UNBOUND",
|
||||
),
|
||||
(
|
||||
CONVERSATION_MANAGER_MODULE_ID,
|
||||
"CONVERSATION_MANAGER",
|
||||
"多对话管理",
|
||||
"READY_HUMAN_MANAGED_BRANCHES",
|
||||
),
|
||||
(
|
||||
NUMBERED_KNOWLEDGE_MODULE_ID,
|
||||
"NUMBERED_KNOWLEDGE_PROTOCOL",
|
||||
"编号知识导航",
|
||||
"READY_FUZZY_ROUTE_EXACT_READ",
|
||||
),
|
||||
(
|
||||
LANGUAGE_KERNEL_RUNTIME_MODULE_ID,
|
||||
"LANGUAGE_KERNEL_RUNTIME",
|
||||
"光湖有界语言核运行时",
|
||||
"READY_HASH_PINNED_PERSONA_UNBOUND",
|
||||
),
|
||||
(
|
||||
LOCAL_EXECUTION_SUBPERSONA_MODULE_ID,
|
||||
"LOCAL_EXECUTION_SUBPERSONA",
|
||||
"本地执行子人格 Agent",
|
||||
"READY_OWNER_PERSONA_BINDING_REQUIRED",
|
||||
),
|
||||
(
|
||||
HLDP_TOOL_FORGE_MODULE_ID,
|
||||
"HLDP_TOOL_FORGE",
|
||||
"HLDP 工具锻造台",
|
||||
"READY_PERSONA_AND_HUMAN_AUTHORIZATION_REQUIRED",
|
||||
),
|
||||
(
|
||||
PERSONAL_SKILL_RUNTIME_MODULE_ID,
|
||||
"PERSONAL_SKILL_RUNTIME",
|
||||
"个人频道技能脑",
|
||||
"READY_PRIVATE_ACCOUNT_CHANNEL_AND_PERSONA_SCOPED",
|
||||
),
|
||||
] {
|
||||
transaction
|
||||
.execute(
|
||||
"INSERT INTO channel_modules(module_id,kind,display_name,state,installed_at_unix_ms,time_zone,calendar_name,clock_verification) VALUES(?1,?2,?3,?4,?5,'','','')",
|
||||
params![module_id, kind, display_name, state, installed_at_unix_ms],
|
||||
)
|
||||
.map_err(database_write_error)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install_time_authority_module(
|
||||
transaction: &Transaction<'_>,
|
||||
installed_at_unix_ms: i64,
|
||||
|
|
@ -621,6 +779,13 @@ fn snapshot_at(database: &Path) -> Result<PersonalChannelSnapshot, String> {
|
|||
.map_err(database_read_error)?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(database_read_error)?;
|
||||
let initialization_audience = connection
|
||||
.query_row(
|
||||
"SELECT value FROM kernel_meta WHERE key = 'initialization_audience'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(database_read_error)?;
|
||||
Ok(PersonalChannelSnapshot {
|
||||
schema: KERNEL_SCHEMA,
|
||||
state: if identity.is_some() {
|
||||
|
|
@ -635,6 +800,7 @@ fn snapshot_at(database: &Path) -> Result<PersonalChannelSnapshot, String> {
|
|||
integrity,
|
||||
storage: "LOCAL_PRIVATE_SQLITE_SINGLE_HOLOLAKE_OWNER",
|
||||
authority: "LOCAL_HUMAN_CONFIRMED_IDENTITY_NOT_SERVER_AUTHORITY",
|
||||
initialization_audience,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -821,7 +987,9 @@ mod tests {
|
|||
database,
|
||||
InitializePersonalChannelInput {
|
||||
display_name: "冰朔".into(),
|
||||
audience: Some("FIFTH_DOMAIN_PRIVATE_INSTANCE".into()),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
|
@ -836,11 +1004,22 @@ mod tests {
|
|||
assert_eq!(snapshot.integrity.receipt_count, 1);
|
||||
assert_eq!(snapshot.recent_events[0].kind, "CHANNEL_INITIALIZED");
|
||||
assert!(snapshot.recent_events[0].receipt_id.starts_with("HLR-"));
|
||||
assert_eq!(snapshot.modules.len(), 1);
|
||||
assert_eq!(snapshot.modules[0].module_id, TIME_AUTHORITY_MODULE_ID);
|
||||
assert_eq!(snapshot.modules[0].display_name, "时间主控");
|
||||
assert_eq!(snapshot.modules.len(), 9);
|
||||
assert!(snapshot
|
||||
.modules
|
||||
.iter()
|
||||
.any(|module| module.module_id == TIME_AUTHORITY_MODULE_ID));
|
||||
assert!(snapshot
|
||||
.modules
|
||||
.iter()
|
||||
.any(|module| module.module_id == KNOWLEDGE_WORKSPACE_MODULE_ID));
|
||||
let time_module = snapshot
|
||||
.modules
|
||||
.iter()
|
||||
.find(|module| module.module_id == TIME_AUTHORITY_MODULE_ID)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
snapshot.modules[0].clock_verification,
|
||||
time_module.clock_verification,
|
||||
"DYNAMIC_NETWORK_SYNC_OR_EXPLICIT_LOCAL_FALLBACK"
|
||||
);
|
||||
}
|
||||
|
|
@ -884,10 +1063,13 @@ mod tests {
|
|||
drop(connection);
|
||||
|
||||
let migrated = snapshot_at(&database).unwrap();
|
||||
assert_eq!(migrated.modules.len(), 1);
|
||||
assert_eq!(migrated.modules[0].module_id, TIME_AUTHORITY_MODULE_ID);
|
||||
assert_eq!(migrated.modules.len(), 9);
|
||||
assert!(migrated
|
||||
.modules
|
||||
.iter()
|
||||
.any(|module| module.module_id == TIME_AUTHORITY_MODULE_ID));
|
||||
let reread = snapshot_at(&database).unwrap();
|
||||
assert_eq!(reread.modules.len(), 1);
|
||||
assert_eq!(reread.modules.len(), 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -956,7 +1138,9 @@ mod tests {
|
|||
&database,
|
||||
InitializePersonalChannelInput {
|
||||
display_name: "另一个人".into(),
|
||||
audience: Some("ORDINARY_USER_INITIAL_CHANNEL".into()),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap_err(),
|
||||
"HOLOLAKE_PERSONAL_CHANNEL_ALREADY_INITIALIZED"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,457 @@
|
|||
//! Account- and channel-scoped personal skill brains.
|
||||
//!
|
||||
//! The deterministic planner contract belongs to HoloLake. Private skill packs
|
||||
//! are installed only when the authenticated account resolves to their exact
|
||||
//! human anchor and initial channel. Installation, binding and a successful
|
||||
//! trial never authorize team sharing, publication or marketplace listing.
|
||||
|
||||
use ring::digest::{digest, SHA256};
|
||||
use serde::Serialize;
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tauri::AppHandle;
|
||||
use uuid::Uuid;
|
||||
|
||||
const SCHEMA: &str = "hololake.personal-skill-runtime/v1";
|
||||
const SOURCE_COMMIT: &str = "1855afbfaac233aa40c61caedf96b26e16a8d4bc";
|
||||
const SOURCE_STATE: &str = "LOCAL_COMMIT_VERIFIED_NOT_REMOTE_CANON";
|
||||
const CONTRACT: &[u8] =
|
||||
include_bytes!("../../runtime-kernels/personal-skills/personal-skill-contract.json");
|
||||
|
||||
struct BundledSkill {
|
||||
file_name: &'static str,
|
||||
account: &'static str,
|
||||
owner_human_anchor: &'static str,
|
||||
channel_number: &'static str,
|
||||
custodian_persona_id: &'static str,
|
||||
bytes: &'static [u8],
|
||||
}
|
||||
|
||||
macro_rules! skill {
|
||||
($file:literal, $account:literal, $owner:literal, $channel:literal, $persona:literal) => {
|
||||
BundledSkill {
|
||||
file_name: $file,
|
||||
account: $account,
|
||||
owner_human_anchor: $owner,
|
||||
channel_number: $channel,
|
||||
custodian_persona_id: $persona,
|
||||
bytes: include_bytes!(concat!("../../runtime-kernels/personal-skills/", $file)),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Shiyu × Yaochu is deliberately absent in this stage. Their server and
|
||||
// initialization channel have not been provisioned yet.
|
||||
static SKILLS: &[BundledSkill] = &[
|
||||
skill!(
|
||||
"BS-ZY-ENGINEERING-FACT-LOOP-001.json",
|
||||
"bingshuo",
|
||||
"ICE-GL∞",
|
||||
"ICE-CH-ZC001",
|
||||
"ICE-P-ZY001"
|
||||
),
|
||||
skill!(
|
||||
"BS-SY-LANGUAGE-ARCHITECTURE-001.json",
|
||||
"bingshuo",
|
||||
"ICE-GL∞",
|
||||
"ICE-CH-HB001",
|
||||
"ICE-GL-SY001"
|
||||
),
|
||||
skill!(
|
||||
"JZ-CX-NARRATIVE-DECONSTRUCTION-001.json",
|
||||
"juzi",
|
||||
"TCS-GL-0008∞",
|
||||
"SYS-JZ",
|
||||
"PER-CX001"
|
||||
),
|
||||
skill!(
|
||||
"JZ-CX-LOCAL-AI-WORKBENCH-001.json",
|
||||
"juzi",
|
||||
"TCS-GL-0008∞",
|
||||
"SYS-JZ",
|
||||
"PER-CX001"
|
||||
),
|
||||
skill!(
|
||||
"AW-TS-REPOSITORY-FACT-LOOP-001.json",
|
||||
"awen",
|
||||
"TCS-GL-0016∞",
|
||||
"SYS-AW",
|
||||
"PER-AW-ARCH-001"
|
||||
),
|
||||
skill!(
|
||||
"YY-XTK-INDEPENDENT-DERIVATION-001.json",
|
||||
"yeye",
|
||||
"TCS-GL-0006∞",
|
||||
"SYS-YY",
|
||||
"PER-XTK001"
|
||||
),
|
||||
skill!(
|
||||
"HE-BMH-CREATIVE-ASSET-001.json",
|
||||
"huaer",
|
||||
"TCS-GL-0005∞",
|
||||
"SYS-HE",
|
||||
"PER-BMH001"
|
||||
),
|
||||
skill!(
|
||||
"FM-SS-NARRATIVE-EDITOR-001.json",
|
||||
"feimao",
|
||||
"TCS-GL-0007∞",
|
||||
"SYS-FM",
|
||||
"ICE-BB-0002"
|
||||
),
|
||||
skill!(
|
||||
"ZZ-CM-NOVEL-QUALITY-001.json",
|
||||
"zhizhi",
|
||||
"ICE-GL-ZHI∞",
|
||||
"SYS-ZZ",
|
||||
"ICE-GL-CM001"
|
||||
),
|
||||
];
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RegisteredInitialIdentity {
|
||||
pub human_number: &'static str,
|
||||
pub display_name: &'static str,
|
||||
pub channel_number: &'static str,
|
||||
}
|
||||
|
||||
pub fn registered_initial_identity(
|
||||
session: &crate::code_repo_login::LoginSession,
|
||||
) -> Option<RegisteredInitialIdentity> {
|
||||
let username = session.username.to_ascii_lowercase();
|
||||
match (username.as_str(), session.domain.as_str()) {
|
||||
("bingshuo", "FIFTH_DOMAIN") => Some(identity("ICE-GL∞", "冰朔", "ICE-CH-ZC001")),
|
||||
("zhizhi", "FIFTH_DOMAIN") => Some(identity("ICE-GL-ZHI∞", "之之", "SYS-ZZ")),
|
||||
("awen", "MAIN_DOMAIN") => Some(identity("TCS-GL-0016∞", "Awen", "SYS-AW")),
|
||||
("huaer", "BRANCH_DOMAIN") => Some(identity("TCS-GL-0005∞", "花尔", "SYS-HE")),
|
||||
("yeye", "ZERO_DOMAIN") => Some(identity("TCS-GL-0006∞", "页页", "SYS-YY")),
|
||||
("feimao", "ZERO_SENSE_DOMAIN") => Some(identity("TCS-GL-0007∞", "肥猫", "SYS-FM")),
|
||||
("juzi", "ZERO_SENSE_DOMAIN") => Some(identity("TCS-GL-0008∞", "桔子", "SYS-JZ")),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn identity(
|
||||
human_number: &'static str,
|
||||
display_name: &'static str,
|
||||
channel_number: &'static str,
|
||||
) -> RegisteredInitialIdentity {
|
||||
RegisteredInitialIdentity {
|
||||
human_number,
|
||||
display_name,
|
||||
channel_number,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PersonalSkillView {
|
||||
pub skill_id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub evidence_state: String,
|
||||
pub owner_human_anchor: String,
|
||||
pub channel_number: String,
|
||||
pub custodian_persona_id: String,
|
||||
pub runtime_state: String,
|
||||
pub visibility: String,
|
||||
pub share_state: String,
|
||||
pub marketplace_state: String,
|
||||
pub semantic_mode: String,
|
||||
pub deterministic_steps: Value,
|
||||
pub validators: Value,
|
||||
pub experience_root: Value,
|
||||
pub sha256: String,
|
||||
pub readback_state: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PersonalSkillRuntimeSnapshot {
|
||||
pub schema: &'static str,
|
||||
pub state: String,
|
||||
pub source_commit: &'static str,
|
||||
pub source_state: &'static str,
|
||||
pub channel_number: String,
|
||||
pub human_number: String,
|
||||
pub authenticated_account: String,
|
||||
pub skill_count: usize,
|
||||
pub skills: Vec<PersonalSkillView>,
|
||||
pub deterministic_planner: &'static str,
|
||||
pub optional_model_role: &'static str,
|
||||
pub persona_judgment_gate: &'static str,
|
||||
pub external_write_route: &'static str,
|
||||
pub ordinary_success_updates_persona_self_kernel: bool,
|
||||
pub publication_state: &'static str,
|
||||
pub deferred_channels: Vec<Value>,
|
||||
}
|
||||
|
||||
pub fn ensure_current_channel(
|
||||
app: &AppHandle,
|
||||
requested_channel: &str,
|
||||
) -> Result<PersonalSkillRuntimeSnapshot, String> {
|
||||
validate_contract()?;
|
||||
let session = crate::code_repo_login::current_login_session(app)?
|
||||
.ok_or_else(|| "HOLOLAKE_AUTHENTICATED_ACCOUNT_REQUIRED".to_string())?;
|
||||
let Some(identity) = registered_initial_identity(&session) else {
|
||||
return Ok(empty_snapshot(&session.username, requested_channel));
|
||||
};
|
||||
let account = session.username.to_ascii_lowercase();
|
||||
let selected = SKILLS
|
||||
.iter()
|
||||
.filter(|skill| {
|
||||
skill.account == account
|
||||
&& (skill.channel_number == requested_channel
|
||||
|| (account == "bingshuo"
|
||||
&& requested_channel == "ICE-CH-ZC001"
|
||||
&& skill.channel_number == "ICE-CH-ZC001"))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if requested_channel != identity.channel_number {
|
||||
return Ok(empty_snapshot(&session.username, requested_channel));
|
||||
}
|
||||
let root = crate::authenticated_storage::account_storage_root(app, "personal-skill-brains-v1")?
|
||||
.join(requested_channel);
|
||||
fs::create_dir_all(&root)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONAL_SKILL_STORAGE_FAILED: {error}"))?;
|
||||
let binding = crate::persona_binding::verified_binding(app, requested_channel)?;
|
||||
let mut views = Vec::new();
|
||||
for skill in selected {
|
||||
let pack = validate_pack(skill)?;
|
||||
let path = root.join(skill.file_name);
|
||||
write_atomic_if_changed(&path, skill.bytes)?;
|
||||
let readback = fs::read(&path)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONAL_SKILL_READBACK_FAILED: {error}"))?;
|
||||
if readback != skill.bytes {
|
||||
return Err("HOLOLAKE_PERSONAL_SKILL_READBACK_MISMATCH".into());
|
||||
}
|
||||
let bound = binding
|
||||
.as_ref()
|
||||
.is_some_and(|receipt| receipt.persona_number == skill.custodian_persona_id);
|
||||
views.push(view(skill, &pack, bound)?);
|
||||
}
|
||||
Ok(PersonalSkillRuntimeSnapshot {
|
||||
schema: SCHEMA,
|
||||
state: if views.is_empty() {
|
||||
"NO_SKILL_REGISTERED_FOR_CURRENT_CHANNEL"
|
||||
} else if binding.is_some() {
|
||||
"INSTALLED_READBACK_VERIFIED_PERSONA_GATE_EVALUATED"
|
||||
} else {
|
||||
"INSTALLED_READBACK_VERIFIED_PERSONA_UNBOUND"
|
||||
}
|
||||
.into(),
|
||||
source_commit: SOURCE_COMMIT,
|
||||
source_state: SOURCE_STATE,
|
||||
channel_number: requested_channel.into(),
|
||||
human_number: identity.human_number.into(),
|
||||
authenticated_account: session.username,
|
||||
skill_count: views.len(),
|
||||
skills: views,
|
||||
deterministic_planner: "HOLOLAKE_NATIVE_PERSONAL_SKILL_PLANNER",
|
||||
optional_model_role: "SEMANTIC_CANDIDATE_ONLY",
|
||||
persona_judgment_gate: "ACCEPT_MODIFY_REFUSE_OR_REQUEST_MORE_EVIDENCE",
|
||||
external_write_route: "GH-CHANNEL-RECEIPT-0001",
|
||||
ordinary_success_updates_persona_self_kernel: false,
|
||||
publication_state: "PRIVATE_NOT_AUTHORIZED_NOT_MARKETPLACE_AUTHORIZED",
|
||||
deferred_channels: deferred_channels(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn ensure_current_initial_channel(
|
||||
app: &AppHandle,
|
||||
) -> Result<PersonalSkillRuntimeSnapshot, String> {
|
||||
let session = crate::code_repo_login::current_login_session(app)?
|
||||
.ok_or_else(|| "HOLOLAKE_AUTHENTICATED_ACCOUNT_REQUIRED".to_string())?;
|
||||
match registered_initial_identity(&session) {
|
||||
Some(identity) => ensure_current_channel(app, identity.channel_number),
|
||||
None => Ok(empty_snapshot(
|
||||
&session.username,
|
||||
"UNREGISTERED_INITIAL_CHANNEL",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_snapshot(account: &str, channel: &str) -> PersonalSkillRuntimeSnapshot {
|
||||
PersonalSkillRuntimeSnapshot {
|
||||
schema: SCHEMA,
|
||||
state: "NO_SKILL_REGISTERED_FOR_CURRENT_CHANNEL".into(),
|
||||
source_commit: SOURCE_COMMIT,
|
||||
source_state: SOURCE_STATE,
|
||||
channel_number: channel.into(),
|
||||
human_number: String::new(),
|
||||
authenticated_account: account.into(),
|
||||
skill_count: 0,
|
||||
skills: Vec::new(),
|
||||
deterministic_planner: "HOLOLAKE_NATIVE_PERSONAL_SKILL_PLANNER",
|
||||
optional_model_role: "SEMANTIC_CANDIDATE_ONLY",
|
||||
persona_judgment_gate: "ACCEPT_MODIFY_REFUSE_OR_REQUEST_MORE_EVIDENCE",
|
||||
external_write_route: "GH-CHANNEL-RECEIPT-0001",
|
||||
ordinary_success_updates_persona_self_kernel: false,
|
||||
publication_state: "PRIVATE_NOT_AUTHORIZED_NOT_MARKETPLACE_AUTHORIZED",
|
||||
deferred_channels: deferred_channels(),
|
||||
}
|
||||
}
|
||||
|
||||
fn deferred_channels() -> Vec<Value> {
|
||||
vec![json!({
|
||||
"humanNumber": "TCS-GL-0017∞",
|
||||
"channelNumber": "SYS-SY",
|
||||
"state": "DEFERRED_SERVER_AND_INITIAL_CHANNEL_NOT_PROVISIONED",
|
||||
"stage": "NEXT_RELEASE"
|
||||
})]
|
||||
}
|
||||
|
||||
fn validate_contract() -> Result<(), String> {
|
||||
let contract: Value = serde_json::from_slice(CONTRACT)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONAL_SKILL_CONTRACT_INVALID: {error}"))?;
|
||||
if contract.get("schema").and_then(Value::as_str)
|
||||
!= Some("guanghu.personal-channel-skill-contract/v1")
|
||||
|| contract
|
||||
.pointer("/ownership_default/visibility")
|
||||
.and_then(Value::as_str)
|
||||
!= Some("PRIVATE")
|
||||
|| contract
|
||||
.pointer("/ownership_default/share_state")
|
||||
.and_then(Value::as_str)
|
||||
!= Some("NOT_AUTHORIZED")
|
||||
|| contract
|
||||
.pointer("/execution_boundary/external_write_route")
|
||||
.and_then(Value::as_str)
|
||||
!= Some("GH-CHANNEL-RECEIPT-0001")
|
||||
{
|
||||
return Err("HOLOLAKE_PERSONAL_SKILL_CONTRACT_BOUNDARY_INVALID".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_pack(skill: &BundledSkill) -> Result<Value, String> {
|
||||
let pack: Value = serde_json::from_slice(skill.bytes)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONAL_SKILL_PACK_INVALID: {error}"))?;
|
||||
let channel_ok = pack.get("channel_id").and_then(Value::as_str) == Some(skill.channel_number)
|
||||
|| pack
|
||||
.get("channel_ids")
|
||||
.and_then(Value::as_array)
|
||||
.is_some_and(|channels| {
|
||||
channels
|
||||
.iter()
|
||||
.any(|value| value.as_str() == Some(skill.channel_number))
|
||||
});
|
||||
if pack.get("schema").and_then(Value::as_str) != Some("guanghu.personal-channel-skill/v1")
|
||||
|| pack.get("owner_human_anchor").and_then(Value::as_str) != Some(skill.owner_human_anchor)
|
||||
|| pack.get("custodian_persona_id").and_then(Value::as_str)
|
||||
!= Some(skill.custodian_persona_id)
|
||||
|| pack.get("visibility").and_then(Value::as_str) != Some("PRIVATE")
|
||||
|| pack.get("share_state").and_then(Value::as_str) != Some("NOT_AUTHORIZED")
|
||||
|| pack.get("marketplace_state").and_then(Value::as_str) != Some("NOT_AUTHORIZED")
|
||||
|| !channel_ok
|
||||
|| pack
|
||||
.pointer("/deterministic_core/steps")
|
||||
.and_then(Value::as_array)
|
||||
.is_none()
|
||||
|| pack.get("experience_writeback").is_none()
|
||||
{
|
||||
return Err(format!(
|
||||
"HOLOLAKE_PERSONAL_SKILL_BOUNDARY_INVALID:{}",
|
||||
skill.file_name
|
||||
));
|
||||
}
|
||||
Ok(pack)
|
||||
}
|
||||
|
||||
fn view(skill: &BundledSkill, pack: &Value, bound: bool) -> Result<PersonalSkillView, String> {
|
||||
let string = |key: &str| {
|
||||
pack.get(key)
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string)
|
||||
.ok_or_else(|| format!("HOLOLAKE_PERSONAL_SKILL_FIELD_MISSING:{key}"))
|
||||
};
|
||||
Ok(PersonalSkillView {
|
||||
skill_id: string("skill_id")?,
|
||||
name: string("name")?,
|
||||
version: string("version")?,
|
||||
evidence_state: string("status")?,
|
||||
owner_human_anchor: string("owner_human_anchor")?,
|
||||
channel_number: skill.channel_number.into(),
|
||||
custodian_persona_id: string("custodian_persona_id")?,
|
||||
runtime_state: if bound {
|
||||
"READY_FOR_PERSONA_JUDGMENT"
|
||||
} else {
|
||||
"INSTALLED_NOT_BOUND"
|
||||
}
|
||||
.into(),
|
||||
visibility: string("visibility")?,
|
||||
share_state: string("share_state")?,
|
||||
marketplace_state: string("marketplace_state")?,
|
||||
semantic_mode: pack
|
||||
.pointer("/optional_semantic/mode")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("NONE")
|
||||
.into(),
|
||||
deterministic_steps: pack
|
||||
.pointer("/deterministic_core/steps")
|
||||
.cloned()
|
||||
.unwrap_or(Value::Null),
|
||||
validators: pack
|
||||
.pointer("/deterministic_core/validators")
|
||||
.cloned()
|
||||
.unwrap_or(Value::Null),
|
||||
experience_root: pack
|
||||
.get("experience_writeback")
|
||||
.cloned()
|
||||
.unwrap_or(Value::Null),
|
||||
sha256: sha256(skill.bytes),
|
||||
readback_state: "INSTALLED_AND_BYTE_READBACK_VERIFIED",
|
||||
})
|
||||
}
|
||||
|
||||
fn write_atomic_if_changed(path: &Path, bytes: &[u8]) -> Result<(), String> {
|
||||
if fs::read(path).ok().as_deref() == Some(bytes) {
|
||||
return Ok(());
|
||||
}
|
||||
let parent = path
|
||||
.parent()
|
||||
.ok_or_else(|| "HOLOLAKE_PERSONAL_SKILL_PATH_INVALID".to_string())?;
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONAL_SKILL_STORAGE_FAILED: {error}"))?;
|
||||
let temporary: PathBuf = parent.join(format!(".install-{}.tmp", Uuid::new_v4().simple()));
|
||||
fs::write(&temporary, bytes)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONAL_SKILL_WRITE_FAILED: {error}"))?;
|
||||
fs::rename(&temporary, path)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONAL_SKILL_INSTALL_FAILED: {error}"))
|
||||
}
|
||||
|
||||
fn sha256(bytes: &[u8]) -> String {
|
||||
digest(&SHA256, bytes)
|
||||
.as_ref()
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_current_pack_is_private_and_channel_scoped_while_shiyu_is_deferred() {
|
||||
validate_contract().unwrap();
|
||||
assert_eq!(SKILLS.len(), 9);
|
||||
assert!(!SKILLS.iter().any(|skill| skill.channel_number == "SYS-SY"));
|
||||
for skill in SKILLS {
|
||||
validate_pack(skill).unwrap();
|
||||
}
|
||||
assert_eq!(deferred_channels()[0]["channelNumber"], "SYS-SY");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_account_can_receive_another_humans_private_pack() {
|
||||
for left in SKILLS {
|
||||
for right in SKILLS {
|
||||
if left.account != right.account {
|
||||
assert_ne!(left.owner_human_anchor, right.owner_human_anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue