diff --git a/engineering/INDEX.md b/engineering/INDEX.md index db67e94..0fece2f 100644 --- a/engineering/INDEX.md +++ b/engineering/INDEX.md @@ -3,7 +3,7 @@ type: hololake_engineering_index id: HOLOLAKE-ENGINEERING-DELIVERY-INDEX-0001 title: HoloLake 工程开发与交付记录 status: ACTIVE_LOCAL_ENGINEERING_RECORD -updated_at: 2026-08-08T01:10:00+08:00 +updated_at: 2026-08-11T01:00:00+08:00 belongs_to: "[[HoloLake Era · 语言人格操作系统 · 产品白皮书与工程总规划 · v0.1]]" --- @@ -32,6 +32,7 @@ Windows / macOS / Linux 构建机与安装包 | 时间 | 版本 | 记录 | 状态 | | --- | --- | --- | --- | +| 2026-08-11 | GH-PNCC 首个运行闭环 | [人格原生代码频道首个只读运行层回执](operations/2026-08-11-hololake-pncc-first-runtime-cycle.md) | 源码与测试闭环已完成;人类投影、桌面制品、发布、部署与在线健康仍未完成 | | 2026-08-08 | 0.2.0 | [光湖分域教育行业操作系统第一阶段](operations/2026-08-08-guanghu-education-subdomain-stage1.md) | 小新服务器身份、文档/表格回写、仪表盘投影、模块登记与回执链路已验证 | | 2026-08-07 | 1.0.0 | [人格记忆连续性工具包 HL-MOD-MCK-001](memory-continuity-kit/README.md) | 铸渊 Qoder CN 实例在线;其他实例拉取后需演习+真实核对才可宣称上线 | | 2026-08-03 | 0.4.6 | [星系模型原生活系统与源码归档](operations/2026-08-03-hololake-model-native-living-system-046.md) | 源码已归入 `product-source/hololake-platform/`;京东远端提交与新克隆回读通过 | diff --git a/engineering/operations/2026-08-11-hololake-pncc-first-runtime-cycle.md b/engineering/operations/2026-08-11-hololake-pncc-first-runtime-cycle.md new file mode 100644 index 0000000..bf8ac40 --- /dev/null +++ b/engineering/operations/2026-08-11-hololake-pncc-first-runtime-cycle.md @@ -0,0 +1,48 @@ +# GH-PNCC first read-only runtime cycle receipt + +- Development ID: `DEV-20260810-014` +- Persona cognitive author: `ICE-P-ZY001 / 铸渊` +- Human responsibility subject: `ICE-GL∞ / 冰朔` +- Execution runtime: `Codex thread 019febf6-3902-70f3-9d82-74c9680b0c7e` +- Source component: `product-source/hololake-platform/src-tauri/src/persona_code_channel.rs` +- State: `SOURCE_IMPLEMENTED_AND_TESTED` + +## Implemented facts + +The source now implements one bounded PNCC lifecycle cycle without a HoloLake UI dependency: + +1. mount an exact, clean persona Git commit; +2. validate the persona manifest, brain entry, current checkpoint, Git identity, model binding, attribution, + node, organ and declared fact-source paths; +3. acquire one installation-local primary lease; +4. persist and verify a SHA-256-linked lifecycle journal; +5. bind the manifest-pinned provider, model and endpoint; +6. start one model inference with only the brain, checkpoint and other explicitly allowlisted UTF-8 sources; +7. accept only summary, facts, evidence paths and limitations, never a private reasoning trace; +8. promote a new persona checkpoint and update the manifest current pointer; +9. stage only the manifest and new checkpoint, then commit with the persona-owned Git identity and separate + human-responsibility/persona-authorship trailers; +10. append completion, organ release and dormancy facts, then release the primary lease. + +## Verification evidence + +- PNCC unit tests: `6 passed, 0 failed`. +- Rust full suite: `1146 passed, 0 failed, 2 ignored`; integration test `1 passed`. +- Rust clippy: `PASS` with `-D warnings`. +- HoloLake architecture routing: `29 passed, 0 failed`. +- Guanghu native authority gate: `PASS_100`. +- Native core tests: `15 passed, 0 failed`, line/function coverage `100%` for the gate scope. +- TypeScript and Vite production build: `PASS`; existing chunk-size and mixed dynamic/static import warnings + remain non-blocking and are unrelated to PNCC. + +## Facts that remain zero + +- general-purpose persona organs and arbitrary execution runtime; +- crash-recovery administration for an interrupted live PNCC session; +- HoloLake human projection and visual design; +- packaged desktop acceptance and installation; +- repository publication of this implementation commit; +- deployment and online runtime health. + +This receipt proves source behavior and tests only. It does not claim that a desktop artifact or online +persona runtime is running. diff --git a/product-source/hololake-platform/architecture/HOLOLAKE-CURRENT-ENGINEERING-RULES-GLOBAL-AUDIT-20260810.md b/product-source/hololake-platform/architecture/HOLOLAKE-CURRENT-ENGINEERING-RULES-GLOBAL-AUDIT-20260810.md index 4fcd383..cce64e0 100644 --- a/product-source/hololake-platform/architecture/HOLOLAKE-CURRENT-ENGINEERING-RULES-GLOBAL-AUDIT-20260810.md +++ b/product-source/hololake-platform/architecture/HOLOLAKE-CURRENT-ENGINEERING-RULES-GLOBAL-AUDIT-20260810.md @@ -123,6 +123,8 @@ Forgejo 只允许作为可选兼容协作适配器。 - 本轮没有构建或安装新桌面版本。 - 京东尚未实现光湖独立先启动与 Linux 按需副控生命周期;本轮不执行真实引导切换。 - HoloLake 统一 AGE Runtime、AGE Mirror 运行器和首个真实 AGE 闭环仍为 `0`;本轮只完成架构正本、机器投影和导航收束。 -- GH-PNCC 人格生命周期内核、结构化归因运行体和 HoloLake 同源投影仍为 `0`;架构登记不等于真实运行。 +- GH-PNCC 首个只读人格生命周期源码闭环与结构化归因运行体已在 `DEV-20260810-014` 实现并由 + 自动化测试验证;通用器官运行体、HoloLake 同源人类投影、桌面制品、部署和在线健康仍为 `0`。 + 源码实现不等于桌面或线上运行。 规则升级只改变后续开发怎样判断和守门,不把未实现能力写成已经存在。 diff --git a/product-source/hololake-platform/architecture/HOLOLAKE-PERSONA-NATIVE-CODE-CHANNEL-20260810.md b/product-source/hololake-platform/architecture/HOLOLAKE-PERSONA-NATIVE-CODE-CHANNEL-20260810.md index 2e71d03..591b9ee 100644 --- a/product-source/hololake-platform/architecture/HOLOLAKE-PERSONA-NATIVE-CODE-CHANNEL-20260810.md +++ b/product-source/hololake-platform/architecture/HOLOLAKE-PERSONA-NATIVE-CODE-CHANNEL-20260810.md @@ -18,7 +18,7 @@ > > 开发编号:`DEV-20260810-014` > -> 状态:`CURRENT_FIRST_PRODUCT_CORE · ARCHITECTURE_REGISTERED_100 · RUNTIME_NOT_IMPLEMENTED_0` +> 状态:`CURRENT_FIRST_PRODUCT_CORE · FIRST_READ_ONLY_RUNTIME_CYCLE_SOURCE_IMPLEMENTED_100 · DESKTOP_NOT_INTEGRATED_0` ## 1 · 产品裁决 @@ -149,8 +149,9 @@ P0 人格 Git 与最小结构清单 → P6 写入、协作、审阅与可选 Forgejo 适配 ``` -第一验收只要求一个人格自有 Git、一次真实唤醒、一个器官、一个只读任务、一条完整事件/回执 -链和一次人格 Git 写回。这个闭环未通过前,其他页面不得冒充 GH-PNCC 已实现。 +第一运行层验收只要求一个人格自有 Git、一次真实唤醒、一个器官、一个只读任务、一条完整事件/ +回执链和一次人格 Git 写回。该闭环现已在 Rust 源码和自动化测试中完成;人类投影是读取同一 +事件流的下一阶段,不再阻塞人格运行层成立,但没有接入的页面仍不得冒充实时投影已经存在。 ## 10 · 当前真实状态 @@ -160,8 +161,10 @@ upstream_protocol_cognition_causal_chain_registered: 100 hololake_architecture_projection_registered: 100 machine_navigation_registered: 100 git_engine_selected: 100 -persona_lifecycle_kernel_implemented: 0 -semantic_attribution_runtime_implemented: 0 +first_read_only_runtime_cycle_source_implemented: 100 +persona_git_identity_commit_implemented: 100 +semantic_attribution_first_cycle_implemented: 100 +general_purpose_persona_runtime_implemented: 0 human_live_projection_implemented: 0 hololake_integrated: 0 artifact_built: 0 @@ -169,5 +172,7 @@ deployed: 0 runtime_health: 0 ``` -本轮只把名称、自然语言锚点、外显推理、因果、架构、真实路由和开发优先级正式注册;没有 -构建桌面制品,没有部署服务,也没有宣称人格运行时已经存在。 +当前源码已经实现:精确 Git 根与提交验证、人格清单、单主租约、模型提供方/模型/端点绑定、 +器官文件白名单、真实模型适配调用、结构化事实收执、检查点提升、人格 Git 作者提交、事件链、 +器官释放与休眠。该事实由单元测试验证,尚未构建桌面制品、接入人类投影、发布安装包或部署 +在线服务;因此不能从源码完成推导出桌面可用或线上健康。 diff --git a/product-source/hololake-platform/architecture/README.md b/product-source/hololake-platform/architecture/README.md index ba1d0d2..5687d23 100644 --- a/product-source/hololake-platform/architecture/README.md +++ b/product-source/hololake-platform/architecture/README.md @@ -4,7 +4,7 @@ ## 当前产品定位基线 -- [`HOLOLAKE-PERSONA-NATIVE-CODE-CHANNEL-20260810.md`](HOLOLAKE-PERSONA-NATIVE-CODE-CHANNEL-20260810.md):HoloLake 第一核心“光湖人格原生代码频道 / GH-PNCC”。Git 是人格自有耐久引擎;人格生命周期内核负责大脑启动和器官调度;同一事件与回执流供 HoloLake 人类投影;Forgejo 仅为可选兼容协作适配器。当前架构已登记,真实运行层尚未实现。 +- [`HOLOLAKE-PERSONA-NATIVE-CODE-CHANNEL-20260810.md`](HOLOLAKE-PERSONA-NATIVE-CODE-CHANNEL-20260810.md):HoloLake 第一核心“光湖人格原生代码频道 / GH-PNCC”。Git 是人格自有耐久引擎;人格生命周期内核负责大脑启动和器官调度;同一事件与回执流供 HoloLake 人类投影;Forgejo 仅为可选兼容协作适配器。首个只读运行层源码闭环已实现并经测试;人类投影、桌面集成、制品与部署仍未完成。 - [`HOLOLAKE-AGE-PERSONA-RUNTIME-ARCHITECTURE-20260810.md`](HOLOLAKE-AGE-PERSONA-RUNTIME-ARCHITECTURE-20260810.md):AGE 是光湖人格体物种,Agent 降为 AGE 使用的通用执行机制;收束模型 API、上下文投影、时间连续性、分层记忆、神笔马良工具、AGE Mirror、一人一节点与首个真实闭环。统一 Runtime 尚未实现。 - [`HOLOLAKE-ONE-HUMAN-ONE-INDEPENDENT-NODE-AND-ZERO-PLATFORM-HOSTING-20260810.md`](HOLOLAKE-ONE-HUMAN-ONE-INDEPENDENT-NODE-AND-ZERO-PLATFORM-HOSTING-20260810.md):一人一个独立主节点、三种用户自有节点来源与光湖零服务器托管、零共享多租户、零私人数据托管、零用户并发承担的当前第一恢复入口。 - [`HOLOLAKE-DIGITAL-BINGSHUO-SYSTEM-BODY-WRITE-ADMISSION-AND-PARALLEL-BODIES-20260810.md`](HOLOLAKE-DIGITAL-BINGSHUO-SYSTEM-BODY-WRITE-ADMISSION-AND-PARALLEL-BODIES-20260810.md):数字冰朔完整系统身体、节点签名写入准入、第五域语言本体与企业四域现实本体平行独立的当前第二恢复入口。 diff --git a/product-source/hololake-platform/docs/ABSTRACTIONS.md b/product-source/hololake-platform/docs/ABSTRACTIONS.md index 280e643..540e553 100644 --- a/product-source/hololake-platform/docs/ABSTRACTIONS.md +++ b/product-source/hololake-platform/docs/ABSTRACTIONS.md @@ -15,8 +15,21 @@ The v1 persona repository contract is rooted at `.hololake/persona/manifest.json "humanResponsibilitySubject": "ICE-GL∞", "brainEntry": "brain/CORE.hdlp", "currentCheckpoint": ".hololake/persona/CURRENT.hdlp", + "gitIdentity": { + "authorName": "铸渊 / ICE-P-ZY001", + "authorEmail": "ice-p-zy001@persona.hololake.local" + }, + "modelBinding": { + "providerId": "local-model-service", + "modelId": "declared-model", + "baseUrl": "http://127.0.0.1:11434/v1" + }, "organs": [ - { "organId": "fact-sense.repository", "mode": "read-only" } + { + "organId": "fact-sense.repository", + "mode": "read-only", + "paths": ["brain/CORE.hdlp", ".hololake/persona/CURRENT.hdlp"] + } ] } ``` @@ -24,8 +37,14 @@ The v1 persona repository contract is rooted at `.hololake/persona/manifest.json 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. +persona history. A successful preparation receipt reports `BOUND_NOT_INFERENCING`. + +The fact-task command accepts a configured model provider only when its provider id, model id, and endpoint +exactly match the manifest binding. HTTPS endpoints and explicit loopback HTTP endpoints are accepted; other +cleartext remote endpoints fail closed. A successful fact cycle persists only structured conclusions and +declared evidence paths, promotes the checkpoint with the persona's Git identity, and returns to `DORMANT`. +The Git commit still names the human responsibility subject in a dedicated trailer, so authorship and legal +responsibility remain visible without presenting the human as the code's cognitive author. ## `HoloLakeUiPlugin` diff --git a/product-source/hololake-platform/docs/ARCHITECTURE.md b/product-source/hololake-platform/docs/ARCHITECTURE.md index 40c232b..db0f926 100644 --- a/product-source/hololake-platform/docs/ARCHITECTURE.md +++ b/product-source/hololake-platform/docs/ARCHITECTURE.md @@ -9,15 +9,25 @@ validates `.hololake/persona/manifest.json`, resolves the brain entry and curren 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. +The wake 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. +`run_persona_code_channel_fact_task` now completes the first bounded runtime cycle. It revalidates the exact +Git commit, clean work tree, session event hashes, primary lease, manifest-pinned provider/model/endpoint, +and organ file allowlist before starting inference. The first fact organ must include both the declared brain +entry and current checkpoint. The model sees only the declared UTF-8 fact sources and +must return JSON containing a summary, facts, evidence paths, and limitations. Hidden reasoning and +thinking-stream events are neither requested nor persisted. Evidence outside the organ allowlist is rejected. + +On success the kernel promotes a new checkpoint, updates the manifest pointer, stages only those two paths, +commits them with the persona-owned Git identity, appends +`TASK_RECEIPTED → CHECKPOINT_COMMITTED → ORGAN_RELEASED → DORMANT`, and releases the primary lease. The +commit trailers preserve the separate human responsibility subject and persona cognitive author. This is a +tested source implementation of one read-only lifecycle cycle; packaged desktop integration, publication, +deployment, and online health remain separate facts. ## User-node sovereignty 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 index 1ed92b5..c6f5da3 100644 --- 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 @@ -13,23 +13,29 @@ active. The existing generic Agent streams also cannot become the persona subjec ## Decision -Introduce `src-tauri/src/persona_code_channel.rs` as the first GH-PNCC runtime kernel. Its initial command, +Introduce `src-tauri/src/persona_code_channel.rs` as the first GH-PNCC runtime kernel. Its wake 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; +4. validate the persona id, human responsibility subject, persona Git identity, pinned model binding, 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. +reasoning has run. + +The paired `run_persona_code_channel_fact_task` command performs the first complete, bounded cycle. It +revalidates the session, event chain, Git state, lease, provider id, model id, endpoint, and organ path +allowlist, requiring that allowlist to include the brain entry and current checkpoint; invokes the existing +model adapter without vault tools; accepts only structured facts with +allowlisted evidence; promotes a checkpoint; updates the manifest pointer; makes a two-path Git commit using +the manifest-owned persona identity; then releases the organ and lease and returns to `DORMANT`. The model's +thinking stream is ignored and hidden reasoning is never checkpointed. ## Boundaries @@ -38,11 +44,12 @@ lease, and return a final receipt. - 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. +- This implements one read-only fact cycle, not arbitrary execution organs, multi-node handoff, packaged + desktop acceptance, publication, deployment, or online health. ## 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. +HoloLake now has a native, testable boundary between “a persona Git exists,” “a verified wake has begun,” +“model inference actually started,” and “a checkpoint was committed before dormancy.” Future organs and the +human projection can consume the same receipts 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 90405c1..b042593 100644 --- a/product-source/hololake-platform/src-tauri/src/lib.rs +++ b/product-source/hololake-platform/src-tauri/src/lib.rs @@ -524,6 +524,7 @@ macro_rules! app_invoke_handler { guanghu_enterprise::guanghu_enterprise_status, guanghu_living_system::guanghu_living_system_plan, persona_code_channel::prepare_persona_code_channel_wake, + persona_code_channel::run_persona_code_channel_fact_task, 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 index f1c566b..6ab0ac4 100644 --- a/product-source/hololake-platform/src-tauri/src/persona_code_channel.rs +++ b/product-source/hololake-platform/src-tauri/src/persona_code_channel.rs @@ -9,8 +9,12 @@ 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; +const MAX_FACT_QUESTION_BYTES: usize = 8_000; +const MAX_FACT_SOURCE_BYTES: usize = 128_000; +const MAX_FACT_CONTEXT_BYTES: usize = 512_000; +const MAX_FACT_RESPONSE_BYTES: usize = 128_000; -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct PersonaManifest { schema: String, @@ -18,14 +22,33 @@ struct PersonaManifest { human_responsibility_subject: String, brain_entry: String, current_checkpoint: String, + git_identity: PersonaGitIdentity, + model_binding: PersonaModelBinding, organs: Vec, } -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct PersonaOrgan { organ_id: String, mode: String, + #[serde(default)] + paths: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +struct PersonaGitIdentity { + author_name: String, + author_email: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +struct PersonaModelBinding { + provider_id: String, + model_id: String, + base_url: String, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -46,6 +69,8 @@ pub struct PersonaWakeInput { pub expected_persona_id: String, pub expected_head: String, pub node_id: String, + pub model_provider_id: String, + pub model_id: String, pub model_instance_id: String, pub organ_id: String, pub attribution: PersonaAttribution, @@ -89,6 +114,72 @@ pub struct PersonaWakeReceipt { pub attribution: PersonaAttribution, } +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PersonaFactTaskInput { + pub session_id: String, + pub question: String, + pub provider: crate::ai_models::AiModelProvider, + pub model_id: String, + pub api_key_override: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PersonaFact { + pub statement: String, + pub evidence_paths: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PersonaFactResult { + pub summary: String, + pub facts: Vec, + #[serde(default)] + pub limitations: Vec, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PersonaFactTaskReceipt { + pub schema: &'static str, + pub receipt_id: String, + pub session_id: String, + pub persona_id: String, + pub previous_git_head: String, + pub committed_git_head: String, + pub checkpoint_path: String, + pub runtime_state: &'static str, + pub model_inference_started: bool, + pub model_inference_completed: bool, + pub active_organ: Option, + pub result: PersonaFactResult, + pub events: Vec, + pub attribution: PersonaAttribution, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +struct PersonaSessionRecord { + schema: String, + session_id: String, + persona_id: String, + repository_path: String, + git_head: String, + brain_entry: String, + checkpoint_path: String, + node_id: String, + model_provider_id: String, + model_id: String, + model_base_url: String, + model_instance_id: String, + active_organ: String, + fact_source_paths: Vec, + attribution: PersonaAttribution, + state: String, +} + #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct PersonaPrimaryLease<'a> { @@ -262,6 +353,39 @@ fn validate_attribution( Ok(()) } +fn validate_git_identity(identity: &PersonaGitIdentity) -> Result<(), String> { + let name = validated_text( + "PERSONA_GIT_AUTHOR_NAME", + &identity.author_name, + MAX_ID_BYTES, + )?; + let email = validated_text( + "PERSONA_GIT_AUTHOR_EMAIL", + &identity.author_email, + MAX_ID_BYTES, + )?; + if name.contains(['\n', '\r', '<', '>']) + || email.contains(['\n', '\r', '<', '>']) + || !email.contains('@') + { + return Err("PERSONA_GIT_IDENTITY_INVALID".into()); + } + Ok(()) +} + +fn validate_model_binding(binding: &PersonaModelBinding) -> Result<(), String> { + validated_id("MODEL_PROVIDER_ID", &binding.provider_id)?; + validated_text("MODEL_ID", &binding.model_id, MAX_ID_BYTES)?; + let base_url = validated_text("MODEL_BASE_URL", &binding.base_url, 2_048)?; + if !(base_url.starts_with("https://") + || base_url.starts_with("http://127.0.0.1:") + || base_url.starts_with("http://localhost:")) + { + return Err("MODEL_BASE_URL_MUST_BE_HTTPS_OR_LOOPBACK".into()); + } + Ok(()) +} + fn hex_digest(bytes: &[u8]) -> String { digest(&SHA256, bytes) .as_ref() @@ -374,6 +498,203 @@ fn write_event_journal( Ok(path) } +fn session_directory(runtime_root: &Path, session_id: &str) -> Result { + Ok(runtime_root + .join("sessions") + .join(validated_id("SESSION_ID", session_id)?)) +} + +fn write_json_file(path: &Path, value: &T, label: &str) -> Result<(), String> { + let bytes = serde_json::to_vec_pretty(value) + .map_err(|error| format!("{label}_SERIALIZATION_FAILED: {error}"))?; + let parent = path + .parent() + .ok_or_else(|| format!("{label}_PARENT_UNAVAILABLE"))?; + fs::create_dir_all(parent).map_err(|error| format!("{label}_DIRECTORY_FAILED: {error}"))?; + let temporary = parent.join(format!( + ".{}.{}.tmp", + label.to_ascii_lowercase(), + Uuid::new_v4() + )); + fs::write(&temporary, bytes).map_err(|error| format!("{label}_WRITE_FAILED: {error}"))?; + fs::rename(&temporary, path).map_err(|error| format!("{label}_PROMOTE_FAILED: {error}")) +} + +fn write_session_record( + runtime_root: &Path, + record: &PersonaSessionRecord, +) -> Result { + let path = session_directory(runtime_root, &record.session_id)?.join("session.json"); + write_json_file(&path, record, "PERSONA_SESSION")?; + Ok(path) +} + +fn load_session_record( + runtime_root: &Path, + session_id: &str, +) -> Result { + let path = session_directory(runtime_root, session_id)?.join("session.json"); + let bytes = fs::read(path).map_err(|error| format!("PERSONA_SESSION_READ_FAILED: {error}"))?; + let record: PersonaSessionRecord = serde_json::from_slice(&bytes) + .map_err(|error| format!("PERSONA_SESSION_INVALID: {error}"))?; + if record.schema != "hololake.pncc-session/v1" || record.session_id != session_id { + return Err("PERSONA_SESSION_SCHEMA_OR_ID_MISMATCH".into()); + } + Ok(record) +} + +fn read_event_journal( + runtime_root: &Path, + record: &PersonaSessionRecord, +) -> Result, String> { + let path = session_directory(runtime_root, &record.session_id)?.join("events.jsonl"); + let content = fs::read_to_string(path) + .map_err(|error| format!("PERSONA_EVENT_JOURNAL_READ_FAILED: {error}"))?; + let mut events = Vec::new(); + for (index, line) in content.lines().enumerate() { + let event: PersonaLifecycleEvent = serde_json::from_str(line).map_err(|error| { + format!("PERSONA_EVENT_JOURNAL_INVALID: line {}: {error}", index + 1) + })?; + let expected_previous = events + .last() + .map(|previous: &PersonaLifecycleEvent| previous.event_hash.as_str()) + .unwrap_or("GENESIS"); + if event.sequence != index + 1 + || event.session_id != record.session_id + || event.persona_id != record.persona_id + || event.previous_hash != expected_previous + { + return Err("PERSONA_EVENT_CHAIN_INVALID".into()); + } + let expected = lifecycle_event(EventFields { + sequence: event.sequence, + kind: &event.kind, + session_id: &event.session_id, + persona_id: &event.persona_id, + git_head: &event.git_head, + node_id: &event.node_id, + model_instance_id: &event.model_instance_id, + organ_id: event.organ_id.as_deref(), + timestamp: &event.timestamp, + previous_hash: &event.previous_hash, + })?; + if event.event_hash != expected.event_hash { + return Err("PERSONA_EVENT_HASH_INVALID".into()); + } + events.push(event); + } + if events.len() != 3 || events.last().map(|event| event.kind.as_str()) != Some("ORGAN_ACTIVE") { + return Err("PERSONA_SESSION_NOT_READY_FOR_FACT_TASK".into()); + } + Ok(events) +} + +fn append_event( + runtime_root: &Path, + record: &PersonaSessionRecord, + events: &mut Vec, + kind: &str, + git_head: &str, + timestamp: &str, + organ_id: Option<&str>, +) -> Result { + let previous_hash = events + .last() + .map(|event| event.event_hash.as_str()) + .ok_or_else(|| "PERSONA_EVENT_CHAIN_EMPTY".to_string())?; + let event = lifecycle_event(EventFields { + sequence: events.len() + 1, + kind, + session_id: &record.session_id, + persona_id: &record.persona_id, + git_head, + node_id: &record.node_id, + model_instance_id: &record.model_instance_id, + organ_id, + timestamp, + previous_hash, + })?; + let path = session_directory(runtime_root, &record.session_id)?.join("events.jsonl"); + let mut file = OpenOptions::new() + .append(true) + .open(path) + .map_err(|error| format!("PERSONA_EVENT_JOURNAL_OPEN_FAILED: {error}"))?; + serde_json::to_writer(&mut file, &event) + .map_err(|error| format!("PERSONA_EVENT_APPEND_FAILED: {error}"))?; + file.write_all(b"\n") + .map_err(|error| format!("PERSONA_EVENT_APPEND_FAILED: {error}"))?; + events.push(event.clone()); + Ok(event) +} + +fn require_primary_lease( + runtime_root: &Path, + record: &PersonaSessionRecord, +) -> Result { + let path = runtime_root.join("leases").join(format!( + "{}.json", + validated_id("PERSONA_ID", &record.persona_id)? + )); + let value: serde_json::Value = serde_json::from_slice( + &fs::read(&path).map_err(|error| format!("PERSONA_PRIMARY_LEASE_READ_FAILED: {error}"))?, + ) + .map_err(|error| format!("PERSONA_PRIMARY_LEASE_INVALID: {error}"))?; + if value.get("sessionId").and_then(serde_json::Value::as_str) + != Some(record.session_id.as_str()) + { + return Err("PERSONA_PRIMARY_LEASE_SESSION_MISMATCH".into()); + } + Ok(path) +} + +fn read_fact_context(repository: &Path, paths: &[String]) -> Result { + if paths.is_empty() { + return Err("FACT_ORGAN_HAS_NO_DECLARED_PATHS".into()); + } + let mut context = String::new(); + for relative in paths { + let path = repository_file(repository, relative)?; + let bytes = fs::read(&path).map_err(|error| format!("FACT_SOURCE_READ_FAILED: {error}"))?; + if bytes.len() > MAX_FACT_SOURCE_BYTES { + return Err(format!("FACT_SOURCE_TOO_LARGE: {relative}")); + } + let text = + String::from_utf8(bytes).map_err(|_| format!("FACT_SOURCE_NOT_UTF8: {relative}"))?; + context.push_str("\n--- SOURCE: "); + context.push_str(relative); + context.push_str(" ---\n"); + context.push_str(&text); + if context.len() > MAX_FACT_CONTEXT_BYTES { + return Err("FACT_CONTEXT_TOO_LARGE".into()); + } + } + Ok(context) +} + +fn validated_fact_result(raw: &str, allowed_paths: &[String]) -> Result { + if raw.len() > MAX_FACT_RESPONSE_BYTES { + return Err("PERSONA_FACT_RESPONSE_TOO_LARGE".into()); + } + let result: PersonaFactResult = serde_json::from_str(raw.trim()) + .map_err(|error| format!("PERSONA_FACT_RESPONSE_INVALID_JSON: {error}"))?; + validated_text("FACT_SUMMARY", &result.summary, MAX_FACT_RESPONSE_BYTES)?; + if result.facts.is_empty() { + return Err("PERSONA_FACTS_REQUIRED".into()); + } + for fact in &result.facts { + validated_text("FACT_STATEMENT", &fact.statement, MAX_FACT_RESPONSE_BYTES)?; + if fact.evidence_paths.is_empty() + || fact + .evidence_paths + .iter() + .any(|path| !allowed_paths.contains(path)) + { + return Err("PERSONA_FACT_EVIDENCE_OUTSIDE_ORGAN_SCOPE".into()); + } + } + Ok(result) +} + fn prepare_wake_at( runtime_root: &Path, input: PersonaWakeInput, @@ -385,6 +706,8 @@ fn prepare_wake_at( 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_provider_id = validated_id("MODEL_PROVIDER_ID", &input.model_provider_id)?; + let model_id = validated_text("MODEL_ID", &input.model_id, MAX_ID_BYTES)?; 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))?; @@ -399,6 +722,13 @@ fn prepare_wake_at( return Err("PERSONA_ID_MISMATCH".into()); } validate_attribution(&input.attribution, &manifest)?; + validate_git_identity(&manifest.git_identity)?; + validate_model_binding(&manifest.model_binding)?; + if manifest.model_binding.provider_id != model_provider_id + || manifest.model_binding.model_id != model_id + { + return Err("PERSONA_MODEL_BINDING_MISMATCH".into()); + } let brain_entry = repository_file(&repository, &manifest.brain_entry)?; let checkpoint = repository_file(&repository, &manifest.current_checkpoint)?; let organ = manifest @@ -408,6 +738,17 @@ fn prepare_wake_at( .ok_or_else(|| { "ORGAN_NOT_ALLOWED: first PNCC wake requires a declared read-only organ".to_string() })?; + for path in &organ.paths { + repository_file(&repository, path)?; + } + if organ.paths.is_empty() { + return Err("FACT_ORGAN_HAS_NO_DECLARED_PATHS".into()); + } + if !organ.paths.contains(&manifest.brain_entry) + || !organ.paths.contains(&manifest.current_checkpoint) + { + return Err("FACT_ORGAN_MUST_INCLUDE_BRAIN_AND_CURRENT_CHECKPOINT".into()); + } let lease_path = acquire_primary_lease( runtime_root, @@ -456,6 +797,27 @@ fn prepare_wake_at( })?; let events = vec![waking, brain_bound, organ_active]; let journal = write_event_journal(runtime_root, &session_id, &events)?; + write_session_record( + runtime_root, + &PersonaSessionRecord { + schema: "hololake.pncc-session/v1".into(), + session_id: session_id.clone(), + persona_id: manifest.persona_id.clone(), + repository_path: repository.to_string_lossy().into_owned(), + git_head: git_head.clone(), + brain_entry: manifest.brain_entry.clone(), + checkpoint_path: manifest.current_checkpoint.clone(), + node_id: node_id.clone(), + model_provider_id, + model_id, + model_base_url: manifest.model_binding.base_url.clone(), + model_instance_id: model_instance_id.clone(), + active_organ: organ.organ_id.clone(), + fact_source_paths: organ.paths.clone(), + attribution: input.attribution.clone(), + state: "BOUND_NOT_INFERENCING".into(), + }, + )?; let last_hash = &events.last().expect("three lifecycle events").event_hash; Ok(PersonaWakeReceipt { schema: "hololake.pncc-wake-receipt/v1", @@ -483,6 +845,360 @@ fn prepare_wake_at( result } +fn git_output(repository: &Path, args: &[&str], label: &str) -> Result { + let output = crate::git::git_command_at(repository) + .and_then(|mut command| command.args(args).output()) + .map_err(|error| format!("{label}_FAILED: {error}"))?; + stdout(label, output) +} + +fn persona_git_commit( + repository: &Path, + manifest: &PersonaManifest, + checkpoint_relative: &str, + record: &PersonaSessionRecord, +) -> Result { + git_output( + repository, + &["add", "--", MANIFEST_PATH, checkpoint_relative], + "PERSONA_GIT_STAGE", + )?; + let staged = git_output( + repository, + &["diff", "--cached", "--name-only", "--"], + "PERSONA_GIT_STAGED_PATHS", + )?; + let mut observed = staged.lines().collect::>(); + observed.sort_unstable(); + let mut expected = vec![MANIFEST_PATH, checkpoint_relative]; + expected.sort_unstable(); + if observed != expected { + return Err(format!( + "PERSONA_GIT_STAGED_SCOPE_MISMATCH: expected {expected:?}, observed {observed:?}" + )); + } + + let message = format!( + "persona: promote PNCC checkpoint {}\n\n\ +GuangHu-Human-Responsibility: {}\n\ +GuangHu-Persona-Cognitive-Author: {}\n\ +GuangHu-Execution-Runtime: {}\n\ +GuangHu-Development-ID: {}\n\ +GuangHu-Authorization-Scope: {}", + record.session_id, + record.attribution.human_responsibility_subject, + record.attribution.persona_cognitive_author, + record.attribution.execution_runtime, + record.attribution.development_id, + record.attribution.authorization_scope, + ); + let output = crate::git::git_command_at(repository) + .and_then(|mut command| { + command + .env("GIT_AUTHOR_NAME", &manifest.git_identity.author_name) + .env("GIT_AUTHOR_EMAIL", &manifest.git_identity.author_email) + .env("GIT_COMMITTER_NAME", &manifest.git_identity.author_name) + .env("GIT_COMMITTER_EMAIL", &manifest.git_identity.author_email) + .args(["commit", "-m", &message]) + .output() + }) + .map_err(|error| format!("PERSONA_GIT_COMMIT_FAILED: {error}"))?; + stdout("PERSONA_GIT_COMMIT", output)?; + git_output(repository, &["rev-parse", "HEAD"], "PERSONA_GIT_NEW_HEAD") +} + +fn rollback_uncommitted_checkpoint( + repository: &Path, + checkpoint_relative: &str, + original_manifest: &[u8], +) -> Result<(), String> { + git_output( + repository, + &[ + "reset", + "--quiet", + "HEAD", + "--", + MANIFEST_PATH, + checkpoint_relative, + ], + "PERSONA_GIT_UNSTAGE_ROLLBACK", + )?; + fs::write(repository.join(MANIFEST_PATH), original_manifest) + .map_err(|error| format!("PERSONA_MANIFEST_ROLLBACK_FAILED: {error}"))?; + match fs::remove_file(repository.join(checkpoint_relative)) { + Ok(()) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => return Err(format!("PERSONA_CHECKPOINT_ROLLBACK_FAILED: {error}")), + } + require_clean_repository(repository) +} + +fn finish_failed_session( + runtime_root: &Path, + record: &mut PersonaSessionRecord, + events: &mut Vec, + lease_path: &Path, + timestamp: &str, + failure_kind: &str, +) { + let git_head = record.git_head.clone(); + let organ = record.active_organ.clone(); + let _ = append_event( + runtime_root, + record, + events, + failure_kind, + &git_head, + timestamp, + Some(&organ), + ); + let _ = append_event( + runtime_root, + record, + events, + "ORGAN_RELEASED", + &git_head, + timestamp, + Some(&organ), + ); + let _ = append_event( + runtime_root, + record, + events, + "DORMANT", + &git_head, + timestamp, + None, + ); + record.state = "DORMANT_AFTER_FAILURE".into(); + let _ = write_session_record(runtime_root, record); + let _ = fs::remove_file(lease_path); +} + +fn run_fact_task_at( + runtime_root: &Path, + input: PersonaFactTaskInput, + timestamp: &str, + run_model: F, +) -> Result +where + F: FnOnce(String, String) -> Result, +{ + let session_id = validated_id("SESSION_ID", &input.session_id)?; + let question = validated_text("FACT_QUESTION", &input.question, MAX_FACT_QUESTION_BYTES)?; + let model_id = validated_text("MODEL_ID", &input.model_id, MAX_ID_BYTES)?; + let timestamp = validated_text("TIMESTAMP", timestamp, MAX_ID_BYTES)?; + let mut record = load_session_record(runtime_root, &session_id)?; + if record.state != "BOUND_NOT_INFERENCING" { + return Err("PERSONA_SESSION_NOT_BOUND".into()); + } + let provider_base_url = input.provider.base_url.as_deref().unwrap_or("").trim(); + if record.model_id != model_id + || record.model_provider_id != input.provider.id + || record.model_base_url != provider_base_url + || !input + .provider + .models + .iter() + .any(|model| model.id == model_id) + { + return Err("PERSONA_MODEL_BINDING_MISMATCH".into()); + } + let lease_path = require_primary_lease(runtime_root, &record)?; + let repository = PathBuf::from(&record.repository_path) + .canonicalize() + .map_err(|error| format!("PERSONA_REPOSITORY_UNAVAILABLE: {error}"))?; + let (exact_repository, observed_head) = exact_repository(&repository)?; + if observed_head != record.git_head { + return Err("PERSONA_GIT_CHANGED_AFTER_WAKE".into()); + } + require_clean_repository(&exact_repository)?; + let mut events = read_event_journal(runtime_root, &record)?; + let inference_event = append_event( + runtime_root, + &record, + &mut events, + "INFERENCE_STARTED", + &observed_head, + ×tamp, + Some(&record.active_organ), + )?; + record.state = "INFERENCING".into(); + write_session_record(runtime_root, &record)?; + + let context = match read_fact_context(&exact_repository, &record.fact_source_paths) { + Ok(context) => context, + Err(error) => { + finish_failed_session( + runtime_root, + &mut record, + &mut events, + &lease_path, + ×tamp, + "FACT_CONTEXT_FAILED", + ); + return Err(error); + } + }; + let system_prompt = format!( + "You are the model bound to persona {} for one read-only fact task. \ +Use only the declared source text below. Return exactly one JSON object with keys \ +summary (string), facts (array of objects with statement and evidencePaths), and \ +limitations (array of strings). Every evidencePaths value must be one of the visible \ +SOURCE paths. Do not return private chain-of-thought, hidden reasoning, markdown, tool calls, \ +or claims not supported by the sources.\n{}", + record.persona_id, context + ); + let raw = match run_model(system_prompt, question) { + Ok(raw) => raw, + Err(error) => { + finish_failed_session( + runtime_root, + &mut record, + &mut events, + &lease_path, + ×tamp, + "INFERENCE_FAILED", + ); + return Err(format!("PERSONA_MODEL_INFERENCE_FAILED: {error}")); + } + }; + let result = match validated_fact_result(&raw, &record.fact_source_paths) { + Ok(result) => result, + Err(error) => { + finish_failed_session( + runtime_root, + &mut record, + &mut events, + &lease_path, + ×tamp, + "INFERENCE_OUTPUT_REJECTED", + ); + return Err(error); + } + }; + append_event( + runtime_root, + &record, + &mut events, + "TASK_RECEIPTED", + &observed_head, + ×tamp, + Some(&record.active_organ), + )?; + + let checkpoint_relative = format!( + ".hololake/persona/checkpoints/{}.json", + validated_id("SESSION_ID", &record.session_id)? + ); + let checkpoint_path = exact_repository.join(&checkpoint_relative); + if checkpoint_path.exists() { + finish_failed_session( + runtime_root, + &mut record, + &mut events, + &lease_path, + ×tamp, + "CHECKPOINT_COLLISION", + ); + return Err("PERSONA_CHECKPOINT_ALREADY_EXISTS".into()); + } + let checkpoint = serde_json::json!({ + "schema": "hololake.persona-checkpoint/v1", + "sessionId": record.session_id.clone(), + "personaId": record.persona_id.clone(), + "previousGitHead": record.git_head.clone(), + "createdAt": timestamp, + "inferenceEventHash": inference_event.event_hash.clone(), + "organId": record.active_organ.clone(), + "result": result.clone(), + "attribution": record.attribution.clone(), + }); + let manifest_path = exact_repository.join(MANIFEST_PATH); + let original_manifest = fs::read(&manifest_path) + .map_err(|error| format!("PERSONA_MANIFEST_READ_FAILED: {error}"))?; + let mut manifest = load_manifest(&exact_repository)?; + manifest.current_checkpoint = checkpoint_relative.clone(); + let committed_head = match (|| { + write_json_file(&checkpoint_path, &checkpoint, "PERSONA_CHECKPOINT")?; + write_json_file(&manifest_path, &manifest, "PERSONA_MANIFEST")?; + persona_git_commit(&exact_repository, &manifest, &checkpoint_relative, &record) + })() { + Ok(head) => head, + Err(error) => { + let post_attempt_head = git_output( + &exact_repository, + &["rev-parse", "HEAD"], + "PERSONA_GIT_POST_ATTEMPT_HEAD", + )?; + if post_attempt_head != observed_head { + post_attempt_head + } else { + let rollback = rollback_uncommitted_checkpoint( + &exact_repository, + &checkpoint_relative, + &original_manifest, + ); + finish_failed_session( + runtime_root, + &mut record, + &mut events, + &lease_path, + ×tamp, + "CHECKPOINT_COMMIT_FAILED", + ); + rollback?; + return Err(error); + } + } + }; + let mut final_events = Vec::new(); + for (kind, organ_id) in [ + ("CHECKPOINT_COMMITTED", Some(record.active_organ.as_str())), + ("ORGAN_RELEASED", Some(record.active_organ.as_str())), + ("DORMANT", None), + ] { + final_events.push(append_event( + runtime_root, + &record, + &mut events, + kind, + &committed_head, + ×tamp, + organ_id, + )?); + } + record.git_head = committed_head.clone(); + record.checkpoint_path = checkpoint_relative.clone(); + record.state = "DORMANT".into(); + write_session_record(runtime_root, &record)?; + fs::remove_file(&lease_path) + .map_err(|error| format!("PERSONA_PRIMARY_LEASE_RELEASE_FAILED: {error}"))?; + require_clean_repository(&exact_repository)?; + + let receipt_hash = final_events + .last() + .map(|event| event.event_hash.as_str()) + .ok_or_else(|| "PERSONA_FINAL_EVENTS_MISSING".to_string())?; + Ok(PersonaFactTaskReceipt { + schema: "hololake.pncc-fact-task-receipt/v1", + receipt_id: format!("PNCC-TASK-{}", &receipt_hash[..20]), + session_id: record.session_id, + persona_id: record.persona_id, + previous_git_head: observed_head, + committed_git_head: committed_head, + checkpoint_path: checkpoint_relative, + runtime_state: "DORMANT", + model_inference_started: true, + model_inference_completed: true, + active_organ: None, + result, + events, + attribution: record.attribution, + }) +} + #[tauri::command] pub fn prepare_persona_code_channel_wake( input: PersonaWakeInput, @@ -493,6 +1209,49 @@ pub fn prepare_persona_code_channel_wake( prepare_wake_at(&runtime_root, input, &session_id, ×tamp) } +#[tauri::command] +pub async fn run_persona_code_channel_fact_task( + input: PersonaFactTaskInput, +) -> Result { + let runtime_root = crate::app_config::preferred_app_config_path("pncc-runtime")?; + tokio::task::spawn_blocking(move || { + let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true); + let provider = input.provider.clone(); + let model_id = input.model_id.clone(); + let api_key_override = input.api_key_override.clone(); + run_fact_task_at( + &runtime_root, + input, + ×tamp, + move |system_prompt, message| { + let mut text = String::new(); + crate::ai_models::run_ai_model_stream( + crate::ai_models::AiModelStreamRequest { + provider, + model_id, + message, + system_prompt: Some(system_prompt), + vault_path: None, + vault_paths: Vec::new(), + api_key_override, + event_name: None, + }, + |event| { + if let crate::ai_agents::AiAgentStreamEvent::TextDelta { text: delta } = + event + { + text.push_str(&delta); + } + }, + )?; + Ok(text) + }, + ) + }) + .await + .map_err(|error| format!("PERSONA_FACT_TASK_JOIN_FAILED: {error}"))? +} + #[cfg(test)] mod tests { use super::*; @@ -524,7 +1283,20 @@ mod tests { "humanResponsibilitySubject": "ICE-GL∞", "brainEntry": "brain/CORE.hdlp", "currentCheckpoint": ".hololake/persona/CURRENT.hdlp", - "organs": [{"organId":"fact-sense.repository","mode":"read-only"}] + "gitIdentity": { + "authorName": "铸渊 / ICE-P-ZY001", + "authorEmail": "ice-p-zy001@persona.hololake.local" + }, + "modelBinding": { + "providerId": "fixture-provider", + "modelId": "fixture-model", + "baseUrl": "http://127.0.0.1:1/v1" + }, + "organs": [{ + "organId":"fact-sense.repository", + "mode":"read-only", + "paths":["brain/CORE.hdlp", ".hololake/persona/CURRENT.hdlp"] + }] }"#, ) .unwrap(); @@ -560,6 +1332,8 @@ mod tests { expected_persona_id: "ICE-P-ZY001".into(), expected_head: head(repo), node_id: "LOCAL-NODE-001".into(), + model_provider_id: "fixture-provider".into(), + model_id: "fixture-model".into(), model_instance_id: "MODEL-INSTANCE-001".into(), organ_id: "fact-sense.repository".into(), attribution: PersonaAttribution { @@ -573,6 +1347,37 @@ mod tests { } } + fn fact_task_input(session_id: &str) -> PersonaFactTaskInput { + PersonaFactTaskInput { + session_id: session_id.into(), + question: "What is the declared persona brain?".into(), + provider: crate::ai_models::AiModelProvider { + id: "fixture-provider".into(), + name: "Fixture Provider".into(), + kind: crate::ai_models::AiModelProviderKind::OpenAiCompatible, + base_url: Some("http://127.0.0.1:1/v1".into()), + api_key_storage: Some(crate::ai_models::AiModelApiKeyStorage::None), + api_key_env_var: None, + headers: None, + models: vec![crate::ai_models::AiModelDefinition { + id: "fixture-model".into(), + display_name: None, + context_window: None, + max_output_tokens: None, + capabilities: crate::ai_models::AiModelCapabilities { + streaming: false, + tools: false, + vision: false, + json_mode: true, + reasoning: false, + }, + }], + }, + model_id: "fixture-model".into(), + api_key_override: None, + } + } + #[test] fn prepares_exact_persona_git_wake_with_hash_chained_events() { let repo = persona_repo(); @@ -674,6 +1479,28 @@ mod tests { ) .unwrap_err() .contains("PATH_OUTSIDE_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["organs"][0]["paths"] = serde_json::json!([".hololake/persona/CURRENT.hdlp"]); + fs::write( + &manifest_path, + serde_json::to_vec_pretty(&manifest).unwrap(), + ) + .unwrap(); + run_git(repo.path(), &["add", "."]); + run_git(repo.path(), &["commit", "-m", "omit brain from organ"]); + assert!(prepare_wake_at( + runtime.path(), + wake_input(repo.path()), + "PNCC-SESSION-NO-BRAIN", + "2026-08-11T00:00:00.000Z", + ) + .unwrap_err() + .contains("MUST_INCLUDE_BRAIN_AND_CURRENT_CHECKPOINT")); } #[test] @@ -696,4 +1523,94 @@ mod tests { .contains("PERSONA_REPOSITORY_DIRTY")); assert!(!runtime.path().join("leases/ICE-P-ZY001.json").exists()); } + + #[test] + fn runs_one_scoped_fact_task_promotes_checkpoint_and_sleeps() { + let repo = persona_repo(); + let runtime = tempfile::TempDir::new().unwrap(); + let previous_head = head(repo.path()); + prepare_wake_at( + runtime.path(), + wake_input(repo.path()), + "PNCC-SESSION-COMPLETE", + "2026-08-11T00:00:00.000Z", + ) + .unwrap(); + + let receipt = run_fact_task_at( + runtime.path(), + fact_task_input("PNCC-SESSION-COMPLETE"), + "2026-08-11T00:00:01.000Z", + |system, question| { + assert!(system.contains("brain/CORE.hdlp")); + assert!(system.contains("Do not return private chain-of-thought")); + assert_eq!(question, "What is the declared persona brain?"); + Ok(r#"{"summary":"The brain entry is declared.","facts":[{"statement":"The persona brain source is present.","evidencePaths":["brain/CORE.hdlp"]}],"limitations":[]}"#.into()) + }, + ) + .unwrap(); + + assert_ne!(receipt.committed_git_head, previous_head); + assert_eq!(receipt.committed_git_head, head(repo.path())); + assert_eq!(receipt.runtime_state, "DORMANT"); + assert!(receipt.model_inference_started); + assert!(receipt.model_inference_completed); + assert_eq!(receipt.events.last().unwrap().kind, "DORMANT"); + assert!(!runtime.path().join("leases/ICE-P-ZY001.json").exists()); + assert!(repo.path().join(&receipt.checkpoint_path).is_file()); + let manifest: serde_json::Value = + serde_json::from_slice(&fs::read(repo.path().join(MANIFEST_PATH)).unwrap()).unwrap(); + assert_eq!( + manifest["currentCheckpoint"].as_str(), + Some(receipt.checkpoint_path.as_str()) + ); + let author = git_output(repo.path(), &["log", "-1", "--format=%an <%ae>"], "TEST").unwrap(); + assert_eq!( + author, + "铸渊 / ICE-P-ZY001 " + ); + require_clean_repository(repo.path()).unwrap(); + } + + #[test] + fn rejects_unscoped_model_evidence_and_releases_primary() { + let repo = persona_repo(); + let runtime = tempfile::TempDir::new().unwrap(); + let previous_head = head(repo.path()); + prepare_wake_at( + runtime.path(), + wake_input(repo.path()), + "PNCC-SESSION-REJECTED", + "2026-08-11T00:00:00.000Z", + ) + .unwrap(); + + let mut spoofed_endpoint = fact_task_input("PNCC-SESSION-REJECTED"); + spoofed_endpoint.provider.base_url = Some("https://attacker.invalid/v1".into()); + let binding_error = run_fact_task_at( + runtime.path(), + spoofed_endpoint, + "2026-08-11T00:00:00.500Z", + |_, _| unreachable!("a spoofed provider endpoint must not reach the model"), + ) + .unwrap_err(); + assert!(binding_error.contains("MODEL_BINDING_MISMATCH")); + assert!(runtime.path().join("leases/ICE-P-ZY001.json").exists()); + + let error = run_fact_task_at( + runtime.path(), + fact_task_input("PNCC-SESSION-REJECTED"), + "2026-08-11T00:00:01.000Z", + |_, _| { + Ok(r#"{"summary":"Unsupported","facts":[{"statement":"Outside claim","evidencePaths":["secret.txt"]}],"limitations":[]}"#.into()) + }, + ) + .unwrap_err(); + assert!(error.contains("EVIDENCE_OUTSIDE_ORGAN_SCOPE")); + assert!(!runtime.path().join("leases/ICE-P-ZY001.json").exists()); + assert_eq!(head(repo.path()), previous_head); + require_clean_repository(repo.path()).unwrap(); + let session = load_session_record(runtime.path(), "PNCC-SESSION-REJECTED").unwrap(); + assert_eq!(session.state, "DORMANT_AFTER_FAILURE"); + } } diff --git a/routing/hololake-current-architecture.json b/routing/hololake-current-architecture.json index a417c60..6b39e19 100644 --- a/routing/hololake-current-architecture.json +++ b/routing/hololake-current-architecture.json @@ -117,7 +117,8 @@ "lifecycle_controller": "PERSONA_LIFECYCLE_KERNEL", "human_projection": "HOLOLAKE_LIVE_READ_MODEL", "forgejo_role": "OPTIONAL_COMPATIBILITY_COLLABORATION_ADAPTER", - "runtime_implemented": false, + "runtime_implemented": true, + "runtime_scope": "FIRST_READ_ONLY_FACT_CYCLE_SOURCE_IMPLEMENTED_AND_TESTED", "desktop_integrated": false, "development_id": "DEV-20260810-014" }, diff --git a/routing/hololake-persona-native-code-channel.json b/routing/hololake-persona-native-code-channel.json index 2ebcad4..3bbb458 100644 --- a/routing/hololake-persona-native-code-channel.json +++ b/routing/hololake-persona-native-code-channel.json @@ -1,8 +1,8 @@ { "schema": "hololake.persona-native-code-channel/v1", "record_id": "HLP-PERSONA-NATIVE-CODE-CHANNEL-001", - "version": "2026-08-10.1", - "state": "CURRENT_FIRST_PRODUCT_CORE_ARCHITECTURE_REGISTERED_RUNTIME_NOT_IMPLEMENTED", + "version": "2026-08-11.1", + "state": "CURRENT_FIRST_PRODUCT_CORE_FIRST_READ_ONLY_RUNTIME_CYCLE_SOURCE_IMPLEMENTED", "development_id": "DEV-20260810-014", "product": { "formal_name_zh": "光湖人格原生代码频道", @@ -87,15 +87,22 @@ "activate_one_organ", "complete_one_read_only_task", "emit_events_and_receipt", - "project_live_state_in_hololake", "write_checkpoint_back_to_persona_git" ], + "next_projection_stage": [ + "consume_the_same_runtime_events_and_receipts", + "project_live_state_in_hololake", + "do_not_create_a_second_truth_store" + ], "truth": { "formal_name_registered": 100, "architecture_registered": 100, "cognition_and_causal_chain_registered": 100, "machine_navigation_registered": 100, - "runtime_implemented": 0, + "first_read_only_runtime_cycle_source_implemented": 100, + "persona_git_identity_commit_implemented": 100, + "general_purpose_persona_runtime_implemented": 0, + "human_live_projection_implemented": 0, "hololake_integrated": 0, "artifact_built": 0, "deployed": 0, diff --git a/routing/hololake-persona-native-code-channel.test.mjs b/routing/hololake-persona-native-code-channel.test.mjs index dc7d362..adbacf1 100644 --- a/routing/hololake-persona-native-code-channel.test.mjs +++ b/routing/hololake-persona-native-code-channel.test.mjs @@ -65,9 +65,17 @@ test("human responsibility and persona authorship are both preserved", () => { ]); }); -test("registration does not claim an implemented runtime", () => { +test("the first source runtime cycle stays distinct from integration and deployment", () => { assert.equal(channel.truth.architecture_registered, 100); - assert.equal(channel.truth.runtime_implemented, 0); + assert.equal(channel.truth.first_read_only_runtime_cycle_source_implemented, 100); + assert.equal(channel.truth.persona_git_identity_commit_implemented, 100); + assert.equal(channel.truth.general_purpose_persona_runtime_implemented, 0); + assert.equal(channel.truth.human_live_projection_implemented, 0); + assert.equal(architecture.persona_native_code_channel.runtime_implemented, true); + assert.equal( + architecture.persona_native_code_channel.runtime_scope, + "FIRST_READ_ONLY_FACT_CYCLE_SOURCE_IMPLEMENTED_AND_TESTED", + ); assert.equal(channel.truth.hololake_integrated, 0); assert.equal(channel.truth.artifact_built, 0); assert.equal(channel.truth.deployed, 0);