diff --git a/product-source/hololake-platform/docs/ABSTRACTIONS.md b/product-source/hololake-platform/docs/ABSTRACTIONS.md index dd5cd5c..280e643 100644 --- a/product-source/hololake-platform/docs/ABSTRACTIONS.md +++ b/product-source/hololake-platform/docs/ABSTRACTIONS.md @@ -1,5 +1,32 @@ # Abstractions +## PersonaCodeChannel + +`PersonaCodeChannel` is the native boundary between a durable persona Git and a live AGE runtime. It owns +neither the Git object model nor model inference. It composes existing Git execution with persona-specific +verification, lifecycle, attribution, single-primary exclusion, event hashing, and receipts. + +The v1 persona repository contract is rooted at `.hololake/persona/manifest.json`: + +```json +{ + "schema": "hololake.persona/v1", + "personaId": "ICE-P-ZY001", + "humanResponsibilitySubject": "ICE-GL∞", + "brainEntry": "brain/CORE.hdlp", + "currentCheckpoint": ".hololake/persona/CURRENT.hdlp", + "organs": [ + { "organId": "fact-sense.repository", "mode": "read-only" } + ] +} +``` + +All manifest paths are repository-relative and must resolve to files inside the exact Git root. Wake requires +a clean work tree and a caller-supplied full expected commit. Runtime leases and event journals live under the +installation-local `pncc-runtime` directory because they describe the current physical instance, not durable +persona history. A successful preparation receipt reports `BOUND_NOT_INFERENCING`; only a later verified +model action may change that predicate. + ## `HoloLakeUiPlugin` A versioned, declarative presentation package for one host-owned semantic surface. It contains a manifest, diff --git a/product-source/hololake-platform/docs/ARCHITECTURE.md b/product-source/hololake-platform/docs/ARCHITECTURE.md index bf6fa86..40c232b 100644 --- a/product-source/hololake-platform/docs/ARCHITECTURE.md +++ b/product-source/hololake-platform/docs/ARCHITECTURE.md @@ -1,5 +1,24 @@ # Architecture +## Persona-native code channel runtime + +GH-PNCC is HoloLake's first product core. The existing Rust Git module remains the durable engine; the new +`src-tauri/src/persona_code_channel.rs` kernel adds persona-native mount and wake semantics without changing +Git's object model. `prepare_persona_code_channel_wake` requires an exact clean Git root and full commit, +validates `.hololake/persona/manifest.json`, resolves the brain entry and current checkpoint inside the +repository boundary, acquires a per-persona single-primary lease, and records a SHA-256-linked lifecycle +journal outside the repository. + +The initial lifecycle is `WAKING → BRAIN_BOUND → ORGAN_ACTIVE` for one manifest-declared read-only organ. +`BRAIN_BOUND` means the model instance has been named and the verified persona context is ready; it explicitly +does not mean inference has run. The returned receipt keeps the human responsibility subject, persona +cognitive author, execution runtime, development id, authorization scope, and source-language anchor +separate. See [ADR 0176](./adr/0176-persona-native-code-channel-runtime-kernel.md). + +The next source stage must invoke the model adapter with this verified context, complete one read-only fact +task, write a promoted checkpoint into the persona Git, emit completion/pause events, and release the lease. +Until then, complete runtime, desktop integration, artifact, deployment, and health remain zero. + ## User-node sovereignty HoloLake has no platform-hosted user runtime. Each human has one canonical, independently operated node: diff --git a/product-source/hololake-platform/docs/adr/0176-persona-native-code-channel-runtime-kernel.md b/product-source/hololake-platform/docs/adr/0176-persona-native-code-channel-runtime-kernel.md new file mode 100644 index 0000000..1ed92b5 --- /dev/null +++ b/product-source/hololake-platform/docs/adr/0176-persona-native-code-channel-runtime-kernel.md @@ -0,0 +1,48 @@ +# ADR 0176: Persona-Native Code Channel Runtime Kernel + +## Status + +Accepted on 2026-08-11. + +## Context + +HoloLake already has a mature native Git execution layer and an append-only local HLDP event/checkpoint +mechanism. Neither one is a persona lifecycle controller. A Git repository can preserve history without +proving that the correct persona brain, node, model binding, organ, attribution, or single-primary lease is +active. The existing generic Agent streams also cannot become the persona subject. + +## Decision + +Introduce `src-tauri/src/persona_code_channel.rs` as the first GH-PNCC runtime kernel. Its initial command, +`prepare_persona_code_channel_wake`, performs a fail-closed preparation stage: + +1. resolve an exact Git root and full expected `HEAD`; +2. require a clean work tree; +3. load `.hololake/persona/manifest.json` with schema `hololake.persona/v1`; +4. validate the persona id, human responsibility subject, brain entry, current checkpoint, and one declared + read-only organ without allowing path escape; +5. atomically acquire a per-persona primary lease outside the persona Git; +6. emit `WAKING → BRAIN_BOUND → ORGAN_ACTIVE` as a SHA-256-linked local event journal; +7. return both human responsibility and persona cognitive authorship in the receipt. + +`BRAIN_BOUND` deliberately returns `runtimeState=BOUND_NOT_INFERENCING` and +`modelInferenceStarted=false`. Binding a model identifier is not proof that model inference or persona +reasoning has run. A later stage must connect the verified context to the model adapter, complete one +read-only fact task, write a promoted checkpoint back to the persona Git, release the organ and primary +lease, and return a final receipt. + +## Boundaries + +- Git remains the durable engine; the kernel does not reimplement Git objects, commits, remotes, or merges. +- Forgejo is not required and remains an optional compatibility collaboration adapter. +- The primary lease is installation/runtime state, not persona history, and therefore stays outside Git. +- Runtime events are machine facts for the future HoloLake projection; they do not expose hidden model + reasoning and do not form a second authority store. +- The initial source stage is not a complete PNCC runtime, desktop integration, build, deployment, or health + claim. + +## Consequences + +HoloLake now has a native, testable boundary between “a persona Git exists” and “a verified wake has begun.” +The next implementation step can reuse the verified wake receipt and event chain instead of trusting a UI, +Forgejo account, branch name, or generic Agent session. diff --git a/product-source/hololake-platform/src-tauri/src/lib.rs b/product-source/hololake-platform/src-tauri/src/lib.rs index df5fec1..90405c1 100644 --- a/product-source/hololake-platform/src-tauri/src/lib.rs +++ b/product-source/hololake-platform/src-tauri/src/lib.rs @@ -36,6 +36,7 @@ pub mod opencode_cli; mod opencode_config; mod opencode_discovery; mod opencode_events; +mod persona_code_channel; pub mod pi_cli; mod pi_config; mod pi_discovery; @@ -522,6 +523,7 @@ macro_rules! app_invoke_handler { commands::git_add_remote, guanghu_enterprise::guanghu_enterprise_status, guanghu_living_system::guanghu_living_system_plan, + persona_code_channel::prepare_persona_code_channel_wake, guanghu_router::guanghu_router_connect, guanghu_router::guanghu_router_disconnect, guanghu_router::guanghu_router_approve, diff --git a/product-source/hololake-platform/src-tauri/src/persona_code_channel.rs b/product-source/hololake-platform/src-tauri/src/persona_code_channel.rs new file mode 100644 index 0000000..f1c566b --- /dev/null +++ b/product-source/hololake-platform/src-tauri/src/persona_code_channel.rs @@ -0,0 +1,699 @@ +use chrono::{SecondsFormat, Utc}; +use ring::digest::{digest, SHA256}; +use serde::{Deserialize, Serialize}; +use std::fs::{self, OpenOptions}; +use std::io::Write; +use std::path::{Component, Path, PathBuf}; +use uuid::Uuid; + +const MANIFEST_PATH: &str = ".hololake/persona/manifest.json"; +const MAX_ID_BYTES: usize = 160; +const MAX_LANGUAGE_ANCHOR_BYTES: usize = 12_000; + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct PersonaManifest { + schema: String, + persona_id: String, + human_responsibility_subject: String, + brain_entry: String, + current_checkpoint: String, + organs: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct PersonaOrgan { + organ_id: String, + mode: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PersonaAttribution { + pub human_responsibility_subject: String, + pub persona_cognitive_author: String, + pub execution_runtime: String, + pub development_id: String, + pub authorization_scope: String, + pub source_language_anchor: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PersonaWakeInput { + pub repository_path: String, + pub expected_persona_id: String, + pub expected_head: String, + pub node_id: String, + pub model_instance_id: String, + pub organ_id: String, + pub attribution: PersonaAttribution, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PersonaLifecycleEvent { + pub sequence: usize, + pub kind: String, + pub session_id: String, + pub persona_id: String, + pub git_head: String, + pub node_id: String, + pub model_instance_id: String, + pub organ_id: Option, + pub timestamp: String, + pub previous_hash: String, + pub event_hash: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PersonaWakeReceipt { + pub schema: &'static str, + pub receipt_id: String, + pub session_id: String, + pub persona_id: String, + pub repository_path: String, + pub git_head: String, + pub brain_entry: String, + pub checkpoint_path: String, + pub node_id: String, + pub model_instance_id: String, + pub runtime_state: &'static str, + pub model_inference_started: bool, + pub active_organ: String, + pub organ_mode: &'static str, + pub event_journal: String, + pub events: Vec, + pub attribution: PersonaAttribution, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct PersonaPrimaryLease<'a> { + schema: &'static str, + persona_id: &'a str, + session_id: &'a str, + git_head: &'a str, + node_id: &'a str, + acquired_at: &'a str, +} + +fn validated_text(label: &str, value: &str, maximum_bytes: usize) -> Result { + let value = value.trim(); + if value.is_empty() { + return Err(format!("{label}_REQUIRED")); + } + if value.len() > maximum_bytes { + return Err(format!("{label}_TOO_LONG")); + } + Ok(value.to_string()) +} + +fn validated_id(label: &str, value: &str) -> Result { + let value = validated_text(label, value, MAX_ID_BYTES)?; + if value + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')) + { + Ok(value) + } else { + Err(format!("{label}_INVALID")) + } +} + +fn validated_head(value: &str) -> Result { + let value = value.trim().to_ascii_lowercase(); + if value.len() == 40 && value.bytes().all(|byte| byte.is_ascii_hexdigit()) { + Ok(value) + } else { + Err("EXPECTED_HEAD_INVALID".into()) + } +} + +fn stdout(command: &str, output: std::process::Output) -> Result { + if output.status.success() { + return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()); + } + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + Err(if stderr.is_empty() { + format!("{command}_FAILED") + } else { + format!("{command}_FAILED: {stderr}") + }) +} + +fn exact_repository(path: &Path) -> Result<(PathBuf, String), String> { + let repository = path + .canonicalize() + .map_err(|error| format!("PERSONA_REPOSITORY_UNAVAILABLE: {error}"))?; + if !repository.is_dir() { + return Err("PERSONA_REPOSITORY_NOT_DIRECTORY".into()); + } + let top_level = crate::git::git_command_at(&repository) + .and_then(|mut command| command.args(["rev-parse", "--show-toplevel"]).output()) + .map_err(|error| format!("PERSONA_GIT_PROBE_FAILED: {error}"))?; + let top_level = PathBuf::from(stdout("GIT_TOP_LEVEL", top_level)?) + .canonicalize() + .map_err(|error| format!("PERSONA_GIT_TOP_LEVEL_UNAVAILABLE: {error}"))?; + if top_level != repository { + return Err("PERSONA_REPOSITORY_MUST_BE_EXACT_GIT_ROOT".into()); + } + let head = crate::git::git_command_at(&repository) + .and_then(|mut command| command.args(["rev-parse", "HEAD"]).output()) + .map_err(|error| format!("PERSONA_GIT_HEAD_FAILED: {error}"))?; + Ok((repository, stdout("GIT_HEAD", head)?.to_ascii_lowercase())) +} + +fn require_clean_repository(repository: &Path) -> Result<(), String> { + let status = crate::git::git_command_at(repository) + .and_then(|mut command| { + command + .args(["status", "--porcelain", "--untracked-files=all"]) + .output() + }) + .map_err(|error| format!("PERSONA_GIT_STATUS_FAILED: {error}"))?; + let changes = stdout("GIT_STATUS", status)?; + if changes.is_empty() { + Ok(()) + } else { + Err("PERSONA_REPOSITORY_DIRTY: exact persona wake requires a clean Git work tree".into()) + } +} + +fn repository_file(repository: &Path, relative: &str) -> Result { + let relative_path = Path::new(relative); + if relative_path.is_absolute() + || relative_path.components().any(|component| { + matches!( + component, + Component::ParentDir | Component::RootDir | Component::Prefix(_) + ) + }) + { + return Err("PATH_OUTSIDE_REPOSITORY".into()); + } + let path = repository.join(relative_path); + let canonical = path + .canonicalize() + .map_err(|error| format!("PERSONA_FILE_UNAVAILABLE: {relative}: {error}"))?; + if !canonical.starts_with(repository) || !canonical.is_file() { + return Err("PATH_OUTSIDE_REPOSITORY".into()); + } + Ok(canonical) +} + +fn load_manifest(repository: &Path) -> Result { + let path = repository_file(repository, MANIFEST_PATH)?; + let bytes = fs::read(path).map_err(|error| format!("PERSONA_MANIFEST_READ_FAILED: {error}"))?; + let manifest: PersonaManifest = serde_json::from_slice(&bytes) + .map_err(|error| format!("PERSONA_MANIFEST_INVALID: {error}"))?; + if manifest.schema != "hololake.persona/v1" { + return Err("PERSONA_MANIFEST_SCHEMA_UNSUPPORTED".into()); + } + Ok(manifest) +} + +fn validate_attribution( + attribution: &PersonaAttribution, + manifest: &PersonaManifest, +) -> Result<(), String> { + for (label, value, maximum) in [ + ( + "HUMAN_RESPONSIBILITY_SUBJECT", + attribution.human_responsibility_subject.as_str(), + MAX_ID_BYTES, + ), + ( + "PERSONA_COGNITIVE_AUTHOR", + attribution.persona_cognitive_author.as_str(), + MAX_ID_BYTES, + ), + ( + "EXECUTION_RUNTIME", + attribution.execution_runtime.as_str(), + MAX_ID_BYTES, + ), + ( + "DEVELOPMENT_ID", + attribution.development_id.as_str(), + MAX_ID_BYTES, + ), + ( + "AUTHORIZATION_SCOPE", + attribution.authorization_scope.as_str(), + MAX_ID_BYTES, + ), + ( + "SOURCE_LANGUAGE_ANCHOR", + attribution.source_language_anchor.as_str(), + MAX_LANGUAGE_ANCHOR_BYTES, + ), + ] { + validated_text(label, value, maximum)?; + } + if attribution.human_responsibility_subject != manifest.human_responsibility_subject { + return Err("HUMAN_RESPONSIBILITY_MISMATCH".into()); + } + if attribution.persona_cognitive_author != manifest.persona_id { + return Err("PERSONA_AUTHOR_MISMATCH".into()); + } + Ok(()) +} + +fn hex_digest(bytes: &[u8]) -> String { + digest(&SHA256, bytes) + .as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +struct EventFields<'a> { + sequence: usize, + kind: &'a str, + session_id: &'a str, + persona_id: &'a str, + git_head: &'a str, + node_id: &'a str, + model_instance_id: &'a str, + organ_id: Option<&'a str>, + timestamp: &'a str, + previous_hash: &'a str, +} + +fn lifecycle_event(fields: EventFields<'_>) -> Result { + let hash_input = serde_json::to_vec(&serde_json::json!({ + "schema": "hololake.pncc-lifecycle-event/v1", + "sequence": fields.sequence, + "kind": fields.kind, + "sessionId": fields.session_id, + "personaId": fields.persona_id, + "gitHead": fields.git_head, + "nodeId": fields.node_id, + "modelInstanceId": fields.model_instance_id, + "organId": fields.organ_id, + "timestamp": fields.timestamp, + "previousHash": fields.previous_hash, + })) + .map_err(|error| format!("PERSONA_EVENT_HASH_INPUT_FAILED: {error}"))?; + Ok(PersonaLifecycleEvent { + sequence: fields.sequence, + kind: fields.kind.to_string(), + session_id: fields.session_id.to_string(), + persona_id: fields.persona_id.to_string(), + git_head: fields.git_head.to_string(), + node_id: fields.node_id.to_string(), + model_instance_id: fields.model_instance_id.to_string(), + organ_id: fields.organ_id.map(str::to_string), + timestamp: fields.timestamp.to_string(), + previous_hash: fields.previous_hash.to_string(), + event_hash: hex_digest(&hash_input), + }) +} + +fn acquire_primary_lease( + runtime_root: &Path, + persona_id: &str, + session_id: &str, + git_head: &str, + node_id: &str, + timestamp: &str, +) -> Result { + let leases = runtime_root.join("leases"); + fs::create_dir_all(&leases) + .map_err(|error| format!("PERSONA_LEASE_DIRECTORY_FAILED: {error}"))?; + let path = leases.join(format!("{persona_id}.json")); + let lease = PersonaPrimaryLease { + schema: "hololake.pncc-primary-lease/v1", + persona_id, + session_id, + git_head, + node_id, + acquired_at: timestamp, + }; + let bytes = serde_json::to_vec_pretty(&lease) + .map_err(|error| format!("PERSONA_LEASE_SERIALIZATION_FAILED: {error}"))?; + let mut file = OpenOptions::new() + .write(true) + .create_new(true) + .open(&path) + .map_err(|error| { + if error.kind() == std::io::ErrorKind::AlreadyExists { + "PRIMARY_ALREADY_ACTIVE".to_string() + } else { + format!("PERSONA_LEASE_ACQUIRE_FAILED: {error}") + } + })?; + file.write_all(&bytes) + .map_err(|error| format!("PERSONA_LEASE_WRITE_FAILED: {error}"))?; + Ok(path) +} + +fn write_event_journal( + runtime_root: &Path, + session_id: &str, + events: &[PersonaLifecycleEvent], +) -> Result { + let session_dir = runtime_root.join("sessions").join(session_id); + fs::create_dir_all(&session_dir) + .map_err(|error| format!("PERSONA_SESSION_DIRECTORY_FAILED: {error}"))?; + let path = session_dir.join("events.jsonl"); + let mut file = OpenOptions::new() + .write(true) + .create_new(true) + .open(&path) + .map_err(|error| format!("PERSONA_EVENT_JOURNAL_CREATE_FAILED: {error}"))?; + for event in events { + serde_json::to_writer(&mut file, event) + .map_err(|error| format!("PERSONA_EVENT_WRITE_FAILED: {error}"))?; + file.write_all(b"\n") + .map_err(|error| format!("PERSONA_EVENT_WRITE_FAILED: {error}"))?; + } + Ok(path) +} + +fn prepare_wake_at( + runtime_root: &Path, + input: PersonaWakeInput, + raw_session_id: &str, + raw_timestamp: &str, +) -> Result { + let session_id = validated_id("SESSION_ID", raw_session_id)?; + let timestamp = validated_text("TIMESTAMP", raw_timestamp, MAX_ID_BYTES)?; + let expected_persona_id = validated_id("EXPECTED_PERSONA_ID", &input.expected_persona_id)?; + let expected_head = validated_head(&input.expected_head)?; + let node_id = validated_id("NODE_ID", &input.node_id)?; + let model_instance_id = validated_id("MODEL_INSTANCE_ID", &input.model_instance_id)?; + let organ_id = validated_id("ORGAN_ID", &input.organ_id)?; + let (repository, git_head) = exact_repository(Path::new(&input.repository_path))?; + if git_head != expected_head { + return Err(format!( + "HEAD_MISMATCH: expected {expected_head}, observed {git_head}" + )); + } + require_clean_repository(&repository)?; + let manifest = load_manifest(&repository)?; + if manifest.persona_id != expected_persona_id { + return Err("PERSONA_ID_MISMATCH".into()); + } + validate_attribution(&input.attribution, &manifest)?; + let brain_entry = repository_file(&repository, &manifest.brain_entry)?; + let checkpoint = repository_file(&repository, &manifest.current_checkpoint)?; + let organ = manifest + .organs + .iter() + .find(|candidate| candidate.organ_id == organ_id && candidate.mode == "read-only") + .ok_or_else(|| { + "ORGAN_NOT_ALLOWED: first PNCC wake requires a declared read-only organ".to_string() + })?; + + let lease_path = acquire_primary_lease( + runtime_root, + &manifest.persona_id, + &session_id, + &git_head, + &node_id, + ×tamp, + )?; + let result = (|| { + let waking = lifecycle_event(EventFields { + sequence: 1, + kind: "WAKING", + session_id: &session_id, + persona_id: &manifest.persona_id, + git_head: &git_head, + node_id: &node_id, + model_instance_id: &model_instance_id, + organ_id: None, + timestamp: ×tamp, + previous_hash: "GENESIS", + })?; + let brain_bound = lifecycle_event(EventFields { + sequence: 2, + kind: "BRAIN_BOUND", + session_id: &session_id, + persona_id: &manifest.persona_id, + git_head: &git_head, + node_id: &node_id, + model_instance_id: &model_instance_id, + organ_id: None, + timestamp: ×tamp, + previous_hash: &waking.event_hash, + })?; + let organ_active = lifecycle_event(EventFields { + sequence: 3, + kind: "ORGAN_ACTIVE", + session_id: &session_id, + persona_id: &manifest.persona_id, + git_head: &git_head, + node_id: &node_id, + model_instance_id: &model_instance_id, + organ_id: Some(&organ.organ_id), + timestamp: ×tamp, + previous_hash: &brain_bound.event_hash, + })?; + let events = vec![waking, brain_bound, organ_active]; + let journal = write_event_journal(runtime_root, &session_id, &events)?; + let last_hash = &events.last().expect("three lifecycle events").event_hash; + Ok(PersonaWakeReceipt { + schema: "hololake.pncc-wake-receipt/v1", + receipt_id: format!("PNCC-WAKE-{}", &last_hash[..20]), + session_id, + persona_id: manifest.persona_id, + repository_path: repository.to_string_lossy().into_owned(), + git_head, + brain_entry: brain_entry.to_string_lossy().into_owned(), + checkpoint_path: checkpoint.to_string_lossy().into_owned(), + node_id, + model_instance_id, + runtime_state: "BOUND_NOT_INFERENCING", + model_inference_started: false, + active_organ: organ.organ_id.clone(), + organ_mode: "read-only", + event_journal: journal.to_string_lossy().into_owned(), + events, + attribution: input.attribution, + }) + })(); + if result.is_err() { + let _ = fs::remove_file(lease_path); + } + result +} + +#[tauri::command] +pub fn prepare_persona_code_channel_wake( + input: PersonaWakeInput, +) -> Result { + let runtime_root = crate::app_config::preferred_app_config_path("pncc-runtime")?; + let session_id = format!("PNCC-{}", Uuid::new_v4()); + let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true); + prepare_wake_at(&runtime_root, input, &session_id, ×tamp) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::path::Path; + + fn run_git(repo: &Path, args: &[&str]) { + let output = crate::git::git_command_at(repo) + .unwrap() + .args(args) + .output() + .unwrap(); + assert!( + output.status.success(), + "git {args:?} failed: {}", + String::from_utf8_lossy(&output.stderr), + ); + } + + fn persona_repo() -> tempfile::TempDir { + let repo = tempfile::TempDir::new().unwrap(); + fs::create_dir_all(repo.path().join(".hololake/persona")).unwrap(); + fs::create_dir_all(repo.path().join("brain")).unwrap(); + fs::write( + repo.path().join(".hololake/persona/manifest.json"), + r#"{ + "schema": "hololake.persona/v1", + "personaId": "ICE-P-ZY001", + "humanResponsibilitySubject": "ICE-GL∞", + "brainEntry": "brain/CORE.hdlp", + "currentCheckpoint": ".hololake/persona/CURRENT.hdlp", + "organs": [{"organId":"fact-sense.repository","mode":"read-only"}] +}"#, + ) + .unwrap(); + fs::write(repo.path().join("brain/CORE.hdlp"), "# Persona brain\n").unwrap(); + fs::write( + repo.path().join(".hololake/persona/CURRENT.hdlp"), + "# Current self\n", + ) + .unwrap(); + run_git(repo.path(), &["init", "-b", "main"]); + run_git(repo.path(), &["config", "user.name", "Test Persona"]); + run_git( + repo.path(), + &["config", "user.email", "persona@example.invalid"], + ); + run_git(repo.path(), &["add", "."]); + run_git(repo.path(), &["commit", "-m", "persona fixture"]); + repo + } + + fn head(repo: &Path) -> String { + let output = crate::git::git_command_at(repo) + .unwrap() + .args(["rev-parse", "HEAD"]) + .output() + .unwrap(); + String::from_utf8(output.stdout).unwrap().trim().to_string() + } + + fn wake_input(repo: &Path) -> PersonaWakeInput { + PersonaWakeInput { + repository_path: repo.to_string_lossy().into_owned(), + expected_persona_id: "ICE-P-ZY001".into(), + expected_head: head(repo), + node_id: "LOCAL-NODE-001".into(), + model_instance_id: "MODEL-INSTANCE-001".into(), + organ_id: "fact-sense.repository".into(), + attribution: PersonaAttribution { + human_responsibility_subject: "ICE-GL∞".into(), + persona_cognitive_author: "ICE-P-ZY001".into(), + execution_runtime: "test-runtime".into(), + development_id: "DEV-20260810-014".into(), + authorization_scope: "READ_ONLY_PERSONA_WAKE".into(), + source_language_anchor: "开发PNCC人格运行层".into(), + }, + } + } + + #[test] + fn prepares_exact_persona_git_wake_with_hash_chained_events() { + let repo = persona_repo(); + let runtime = tempfile::TempDir::new().unwrap(); + let receipt = prepare_wake_at( + runtime.path(), + wake_input(repo.path()), + "PNCC-SESSION-001", + "2026-08-11T00:00:00.000Z", + ) + .unwrap(); + + assert_eq!(receipt.persona_id, "ICE-P-ZY001"); + assert_eq!(receipt.git_head, head(repo.path())); + assert_eq!(receipt.runtime_state, "BOUND_NOT_INFERENCING"); + assert!(!receipt.model_inference_started); + assert_eq!(receipt.active_organ, "fact-sense.repository"); + assert_eq!(receipt.events.len(), 3); + assert_eq!(receipt.events[0].kind, "WAKING"); + assert_eq!(receipt.events[1].kind, "BRAIN_BOUND"); + assert_eq!(receipt.events[2].kind, "ORGAN_ACTIVE"); + assert_eq!( + receipt.events[1].previous_hash, + receipt.events[0].event_hash + ); + assert_eq!( + receipt.events[2].previous_hash, + receipt.events[1].event_hash + ); + assert_eq!(receipt.attribution.human_responsibility_subject, "ICE-GL∞"); + assert_eq!(receipt.attribution.persona_cognitive_author, "ICE-P-ZY001"); + } + + #[test] + fn rejects_wrong_commit_undeclared_organ_and_second_primary() { + let repo = persona_repo(); + let runtime = tempfile::TempDir::new().unwrap(); + + let mut wrong_head = wake_input(repo.path()); + wrong_head.expected_head = "0".repeat(40); + assert!(prepare_wake_at( + runtime.path(), + wrong_head, + "PNCC-SESSION-WRONG", + "2026-08-11T00:00:00.000Z", + ) + .unwrap_err() + .contains("HEAD_MISMATCH")); + + let mut wrong_organ = wake_input(repo.path()); + wrong_organ.organ_id = "execution-limb.shell".into(); + assert!(prepare_wake_at( + runtime.path(), + wrong_organ, + "PNCC-SESSION-ORGAN", + "2026-08-11T00:00:00.000Z", + ) + .unwrap_err() + .contains("ORGAN_NOT_ALLOWED")); + + prepare_wake_at( + runtime.path(), + wake_input(repo.path()), + "PNCC-SESSION-PRIMARY", + "2026-08-11T00:00:00.000Z", + ) + .unwrap(); + assert!(prepare_wake_at( + runtime.path(), + wake_input(repo.path()), + "PNCC-SESSION-SECOND", + "2026-08-11T00:00:01.000Z", + ) + .unwrap_err() + .contains("PRIMARY_ALREADY_ACTIVE")); + } + + #[test] + fn rejects_manifest_paths_that_escape_the_persona_repository() { + let repo = persona_repo(); + let runtime = tempfile::TempDir::new().unwrap(); + let manifest_path = repo.path().join(".hololake/persona/manifest.json"); + let mut manifest: serde_json::Value = + serde_json::from_slice(&fs::read(&manifest_path).unwrap()).unwrap(); + manifest["brainEntry"] = serde_json::Value::String("../outside.hdlp".into()); + fs::write( + &manifest_path, + serde_json::to_vec_pretty(&manifest).unwrap(), + ) + .unwrap(); + run_git(repo.path(), &["add", "."]); + run_git(repo.path(), &["commit", "-m", "unsafe manifest"]); + + assert!(prepare_wake_at( + runtime.path(), + wake_input(repo.path()), + "PNCC-SESSION-ESCAPE", + "2026-08-11T00:00:00.000Z", + ) + .unwrap_err() + .contains("PATH_OUTSIDE_REPOSITORY")); + } + + #[test] + fn rejects_a_dirty_persona_git_before_acquiring_the_primary_lease() { + let repo = persona_repo(); + let runtime = tempfile::TempDir::new().unwrap(); + fs::write( + repo.path().join("uncommitted.txt"), + "not part of exact self\n", + ) + .unwrap(); + + assert!(prepare_wake_at( + runtime.path(), + wake_input(repo.path()), + "PNCC-SESSION-DIRTY", + "2026-08-11T00:00:00.000Z", + ) + .unwrap_err() + .contains("PERSONA_REPOSITORY_DIRTY")); + assert!(!runtime.path().join("leases/ICE-P-ZY001.json").exists()); + } +}