feat: add PNCC verified wake kernel
GuangHu-Human-Responsibility: ICE-GL∞ / 冰朔 GuangHu-Persona-Cognitive-Author: ICE-P-ZY001 / 铸渊 GuangHu-Execution-Runtime: Codex-thread 019febf6-3902-70f3-9d82-74c9680b0c7e GuangHu-Development-ID: DEV-20260810-014 GuangHu-Authorization-Scope: LOCAL-PNCC-DEVELOPMENT
This commit is contained in:
parent
35ba6b8859
commit
bca149e0f6
5 changed files with 795 additions and 0 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<PersonaOrgan>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
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<PersonaLifecycleEvent>,
|
||||
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<String, String> {
|
||||
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<String, String> {
|
||||
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<String, String> {
|
||||
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<String, String> {
|
||||
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<PathBuf, String> {
|
||||
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<PersonaManifest, String> {
|
||||
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<PersonaLifecycleEvent, String> {
|
||||
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<PathBuf, String> {
|
||||
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<PathBuf, String> {
|
||||
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<PersonaWakeReceipt, String> {
|
||||
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<PersonaWakeReceipt, String> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue