diff --git a/product-source/hololake-native-desktop/contracts/pncc-stage-one.json b/product-source/hololake-native-desktop/contracts/pncc-stage-one.json index 0c9dcb618..815eee090 100644 --- a/product-source/hololake-native-desktop/contracts/pncc-stage-one.json +++ b/product-source/hololake-native-desktop/contracts/pncc-stage-one.json @@ -37,6 +37,21 @@ "model_instance_fields_allowed": false, "empty_means_offline": false }, + "jd_server_projection": { + "mode": "LIVE_READ_ONLY_MINIMUM_STATUS", + "transport": "DEDICATED_SSH_TO_SERVER_LOOPBACK", + "public_endpoint_created": false, + "repository_path_returned": false, + "repository_content_returned": false, + "credentials_returned": false, + "write_authority": false, + "expected_node_id": "JD-FD-PRIMARY", + "expected_persona_id": "ICE-P-ZY001", + "carrier_binding_must_remain": "UNBOUND_EVIDENCE_REQUIRED", + "model_inference_must_remain": false, + "reality_execution_must_remain": false, + "implemented": true + }, "mount_registration": { "webview_arbitrary_path_or_url_registration_allowed": false, "external_ai_registration_allowed": false, diff --git a/product-source/hololake-native-desktop/docs/ARCHITECTURE.md b/product-source/hololake-native-desktop/docs/ARCHITECTURE.md index 0b79ef98f..24b0b36e3 100644 --- a/product-source/hololake-native-desktop/docs/ARCHITECTURE.md +++ b/product-source/hololake-native-desktop/docs/ARCHITECTURE.md @@ -32,6 +32,10 @@ Before an update replaces the application, the runtime verifies and keeps one bo The rollback executor is implemented, but production updater activation remains blocked until the JD controller publishes the exact trust endpoint and public key, the signed release pipeline is evidenced, and the public macOS build is Apple-notarized. +## JD PNCC human projection + +HoloLake 0.2.0 adds a live, read-only projection of the PNCC resident runtime on `JD-FD-PRIMARY`. The native shell invokes the computer's pre-registered dedicated SSH alias and asks the server only for its loopback `127.0.0.1:3923/v1/status` document. The response is schema-bounded to the exact node and persona, refuses any claim that the carrier is bound or that model/reality execution is active, and never returns a repository path, repository content, credential, or write authority. No public PNCC endpoint is created. An unavailable bridge is displayed as unavailable rather than replaced by cached evidence. + ## Release pipeline `npm run release:macos -- release/inputs/.json` is the only product-owned macOS release entry. It fails before building unless the embedded trust contains the exact registered HoloLake HTTPS endpoint and updater public key, the immutable `v` tag equals the clean `main` head, and the Developer ID, Tauri updater-signing and Apple notarization credential sets are supplied at runtime. The pipeline runs all product and Rust gates, creates updater artifacts through a temporary Tauri override, then requires strict code-signature verification, Gatekeeper acceptance and stapled Apple notarization before writing the HoloLake broadcast and receipts. diff --git a/product-source/hololake-native-desktop/foundation.json b/product-source/hololake-native-desktop/foundation.json index 52bfa1c93..3d172e8a6 100644 --- a/product-source/hololake-native-desktop/foundation.json +++ b/product-source/hololake-native-desktop/foundation.json @@ -74,6 +74,13 @@ "pncc_repository_binding_implemented": true, "pncc_remote_incremental_object_channel_implemented": true, "pncc_receipt_projection_implemented": true, + "pncc_jd_live_server_projection_implemented": true, + "pncc_jd_live_server_projection_transport": "DEDICATED_SSH_TO_SERVER_LOOPBACK_READ_ONLY", + "pncc_jd_live_server_projection_public_endpoint_created": false, + "pncc_jd_live_server_projection_repository_content_exposed": false, + "pncc_jd_live_server_projection_write_authority": false, + "pncc_jd_live_server_projection_carrier_state": "UNBOUND_EVIDENCE_REQUIRED", + "installed_local_product_version": "0.2.0", "pncc_authenticated_direct_broker_integration_implemented": true, "pncc_human_mount_registration_implemented": true, "pncc_human_mount_registration_gate": "SATISFIED_NATIVE_FILE_PICKER_EXACT_CONFIRMATION", diff --git a/product-source/hololake-native-desktop/package-lock.json b/product-source/hololake-native-desktop/package-lock.json index 0c34413df..5f96c7783 100644 --- a/product-source/hololake-native-desktop/package-lock.json +++ b/product-source/hololake-native-desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "hololake-native-desktop", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hololake-native-desktop", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "@tauri-apps/api": "2.10.1", "@tauri-apps/plugin-process": "2.3.1", diff --git a/product-source/hololake-native-desktop/package.json b/product-source/hololake-native-desktop/package.json index 86ea575d4..00537788f 100644 --- a/product-source/hololake-native-desktop/package.json +++ b/product-source/hololake-native-desktop/package.json @@ -1,7 +1,7 @@ { "name": "hololake-native-desktop", "private": true, - "version": "0.1.0", + "version": "0.2.0", "type": "module", "scripts": { "dev": "vite", diff --git a/product-source/hololake-native-desktop/scripts/pncc-stage-one.test.mjs b/product-source/hololake-native-desktop/scripts/pncc-stage-one.test.mjs index 044c59cc4..e4b00c6d2 100644 --- a/product-source/hololake-native-desktop/scripts/pncc-stage-one.test.mjs +++ b/product-source/hololake-native-desktop/scripts/pncc-stage-one.test.mjs @@ -57,17 +57,41 @@ test('neither webview nor external AI can register an arbitrary PNCC source', () assert.doesNotMatch(broker, /RegisterPncc(Repository|Remote)Mount/) }) +test('JD live projection stays private, minimum and read-only', () => { + const projection = contract.jd_server_projection + assert.equal(projection.mode, 'LIVE_READ_ONLY_MINIMUM_STATUS') + assert.equal(projection.transport, 'DEDICATED_SSH_TO_SERVER_LOOPBACK') + assert.equal(projection.public_endpoint_created, false) + assert.equal(projection.repository_path_returned, false) + assert.equal(projection.repository_content_returned, false) + assert.equal(projection.credentials_returned, false) + assert.equal(projection.write_authority, false) + assert.equal(projection.carrier_binding_must_remain, 'UNBOUND_EVIDENCE_REQUIRED') + assert.equal(projection.model_inference_must_remain, false) + assert.equal(projection.reality_execution_must_remain, false) + const source = readText('src-tauri/src/pncc_server_projection.rs') + assert.match(source, /127\.0\.0\.1:3923\/v1\/status/) + assert.match(source, /StrictHostKeyChecking=yes/) + assert.doesNotMatch(source, /repository_path:/) +}) + test('foundation and Rust modules report the implemented PNCC boundary', () => { assert.equal(foundation.pncc_stage_one_contract, 'contracts/pncc-stage-one.json') assert.equal(foundation.pncc_repository_binding_implemented, true) assert.equal(foundation.pncc_remote_incremental_object_channel_implemented, true) assert.equal(foundation.pncc_receipt_projection_implemented, true) + assert.equal(foundation.pncc_jd_live_server_projection_implemented, true) + assert.equal(foundation.pncc_jd_live_server_projection_public_endpoint_created, false) + assert.equal(foundation.pncc_jd_live_server_projection_repository_content_exposed, false) + assert.equal(foundation.pncc_jd_live_server_projection_write_authority, false) + assert.equal(foundation.pncc_jd_live_server_projection_carrier_state, 'UNBOUND_EVIDENCE_REQUIRED') assert.equal(foundation.pncc_human_mount_registration_implemented, true) assert.equal(foundation.pncc_internal_model_inference_implemented, false) assert.equal(foundation.pncc_execution_limb_implemented, false) for (const relative of [ 'src-tauri/src/pncc_repository_binding.rs', 'src-tauri/src/pncc_remote_git.rs', + 'src-tauri/src/pncc_server_projection.rs', ]) assert.equal(fs.existsSync(path.join(root, relative)), true) const productionBindingSource = readText('src-tauri/src/pncc_repository_binding.rs').split('\n#[cfg(test)]\nmod tests')[0] assert.doesNotMatch(productionBindingSource, /modelProvider|model_id|api[_-]?key/i) diff --git a/product-source/hololake-native-desktop/src-tauri/Cargo.lock b/product-source/hololake-native-desktop/src-tauri/Cargo.lock index 9f44d285f..402fa6ab2 100644 --- a/product-source/hololake-native-desktop/src-tauri/Cargo.lock +++ b/product-source/hololake-native-desktop/src-tauri/Cargo.lock @@ -1345,7 +1345,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hololake-native-desktop" -version = "0.1.0" +version = "0.2.0" dependencies = [ "base64 0.22.1", "dirs", diff --git a/product-source/hololake-native-desktop/src-tauri/Cargo.toml b/product-source/hololake-native-desktop/src-tauri/Cargo.toml index 990524931..d81bad7c1 100644 --- a/product-source/hololake-native-desktop/src-tauri/Cargo.toml +++ b/product-source/hololake-native-desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hololake-native-desktop" -version = "0.1.0" +version = "0.2.0" description = "HoloLake native desktop foundation" authors = ["HoloLake"] license = "AGPL-3.0-or-later" diff --git a/product-source/hololake-native-desktop/src-tauri/src/lib.rs b/product-source/hololake-native-desktop/src-tauri/src/lib.rs index ab6503fed..d98808259 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/lib.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/lib.rs @@ -6,6 +6,7 @@ mod local_development_bridge; mod pncc_receipt_projection; mod pncc_remote_git; mod pncc_repository_binding; +mod pncc_server_projection; mod release_trust; mod release_update; @@ -37,6 +38,7 @@ pub fn run() { pncc_repository_binding::select_pncc_repository_candidate, pncc_repository_binding::confirm_pncc_repository_mount, pncc_receipt_projection::query_pncc_receipt_projection, + pncc_server_projection::query_jd_pncc_server_projection, ]) .setup(|app| { let broker = direct_local_broker::start(app.handle())?; diff --git a/product-source/hololake-native-desktop/src-tauri/src/pncc_server_projection.rs b/product-source/hololake-native-desktop/src-tauri/src/pncc_server_projection.rs new file mode 100644 index 000000000..fc0ae4d24 --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/pncc_server_projection.rs @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +use serde::{Deserialize, Serialize}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +const SSH_TARGET: &str = "jd-fd-primary"; +const EXPECTED_NODE_ID: &str = "JD-FD-PRIMARY"; +const EXPECTED_PERSONA_ID: &str = "ICE-P-ZY001"; +const REMOTE_STATUS_COMMAND: &str = + "curl --fail --silent --show-error --max-time 3 http://127.0.0.1:3923/v1/status"; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct RuntimeStatusEvidence { + schema: String, + state: String, + persona_id: String, + human_responsibility_subject: String, + node_id: String, + boot_id: String, + git_head: String, + repository_receipt_id: String, + carrier_binding_state: String, + persona_carrier_bound: bool, + model_inference_started: bool, + reality_execution_allowed: bool, + primary_lease_held: bool, + event_count: usize, + event_chain_head: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ServerPnccProjection { + schema: &'static str, + state: String, + persona_id: String, + human_responsibility_subject: String, + node_id: String, + boot_id: String, + git_head: String, + carrier_binding_state: String, + persona_carrier_bound: bool, + model_inference_started: bool, + reality_execution_allowed: bool, + primary_lease_held: bool, + event_count: usize, + event_chain_head: String, + observed_at_unix_ms: u128, + transport: &'static str, + repository_content_exposed: bool, + write_authority: bool, +} + +fn exact_lower_hex(value: &str, length: usize) -> bool { + value.len() == length + && value + .bytes() + .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) +} + +fn bounded_text(value: &str, maximum: usize) -> bool { + !value.is_empty() + && value.len() <= maximum + && value.trim() == value + && !value.chars().any(char::is_control) +} + +fn parse_projection(bytes: &[u8]) -> Result { + if bytes.is_empty() || bytes.len() > 16 * 1024 { + return Err("PNCC_SERVER_PROJECTION_SIZE_INVALID".into()); + } + let evidence: RuntimeStatusEvidence = serde_json::from_slice(bytes) + .map_err(|_| "PNCC_SERVER_PROJECTION_JSON_INVALID".to_string())?; + if evidence.schema != "guanghu.pncc-runtime-status/v1" + || evidence.node_id != EXPECTED_NODE_ID + || evidence.persona_id != EXPECTED_PERSONA_ID + || evidence.state != "RESIDENT_BOUND_CARRIER_UNBOUND" + || evidence.carrier_binding_state != "UNBOUND_EVIDENCE_REQUIRED" + || evidence.persona_carrier_bound + || evidence.model_inference_started + || evidence.reality_execution_allowed + || !evidence.primary_lease_held + || !exact_lower_hex(&evidence.git_head, 40) + || !exact_lower_hex(&evidence.repository_receipt_id, 64) + || !exact_lower_hex(&evidence.event_chain_head, 64) + || !bounded_text(&evidence.human_responsibility_subject, 256) + || !bounded_text(&evidence.boot_id, 64) + || evidence.event_count == 0 + { + return Err("PNCC_SERVER_PROJECTION_EVIDENCE_REJECTED".into()); + } + let observed_at_unix_ms = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| "PNCC_LOCAL_CLOCK_INVALID".to_string())? + .as_millis(); + Ok(ServerPnccProjection { + schema: "hololake.pncc-server-projection/v1", + state: evidence.state, + persona_id: evidence.persona_id, + human_responsibility_subject: evidence.human_responsibility_subject, + node_id: evidence.node_id, + boot_id: evidence.boot_id, + git_head: evidence.git_head, + carrier_binding_state: evidence.carrier_binding_state, + persona_carrier_bound: false, + model_inference_started: false, + reality_execution_allowed: false, + primary_lease_held: true, + event_count: evidence.event_count, + event_chain_head: evidence.event_chain_head, + observed_at_unix_ms, + transport: "DEDICATED_SSH_LOOPBACK_READ_ONLY", + repository_content_exposed: false, + write_authority: false, + }) +} + +#[tauri::command] +pub async fn query_jd_pncc_server_projection() -> Result { + tauri::async_runtime::spawn_blocking(|| { + let output = Command::new("/usr/bin/ssh") + .args([ + "-o", + "BatchMode=yes", + "-o", + "ConnectTimeout=5", + "-o", + "ConnectionAttempts=1", + "-o", + "ClearAllForwardings=yes", + "-o", + "PermitLocalCommand=no", + "-o", + "StrictHostKeyChecking=yes", + SSH_TARGET, + REMOTE_STATUS_COMMAND, + ]) + .output() + .map_err(|_| "PNCC_SERVER_BRIDGE_UNAVAILABLE".to_string())?; + if !output.status.success() { + return Err("PNCC_SERVER_BRIDGE_UNAVAILABLE".into()); + } + parse_projection(&output.stdout) + }) + .await + .map_err(|_| "PNCC_SERVER_BRIDGE_TASK_FAILED".to_string())? +} + +#[cfg(test)] +mod tests { + use super::*; + + fn evidence() -> Vec { + serde_json::to_vec(&serde_json::json!({ + "schema": "guanghu.pncc-runtime-status/v1", + "state": "RESIDENT_BOUND_CARRIER_UNBOUND", + "personaId": "ICE-P-ZY001", + "humanResponsibilitySubject": "HUMAN-BINGSHUO-001", + "nodeId": "JD-FD-PRIMARY", + "bootId": "1170988c-5390-4f47-b89a-e9f88b2c5bbb", + "gitHead": "1".repeat(40), + "repositoryReceiptId": "2".repeat(64), + "carrierBindingState": "UNBOUND_EVIDENCE_REQUIRED", + "personaCarrierBound": false, + "modelInferenceStarted": false, + "realityExecutionAllowed": false, + "primaryLeaseHeld": true, + "eventCount": 4, + "eventChainHead": "3".repeat(64) + })) + .unwrap() + } + + #[test] + fn accepts_only_the_minimum_live_unbound_projection() { + let projection = parse_projection(&evidence()).unwrap(); + assert_eq!(projection.node_id, EXPECTED_NODE_ID); + assert!(!projection.repository_content_exposed); + assert!(!projection.write_authority); + } + + #[test] + fn refuses_identity_or_authority_escalation() { + let mut value: serde_json::Value = serde_json::from_slice(&evidence()).unwrap(); + value["personaCarrierBound"] = true.into(); + assert!(parse_projection(&serde_json::to_vec(&value).unwrap()).is_err()); + value["personaCarrierBound"] = false.into(); + value["realityExecutionAllowed"] = true.into(); + assert!(parse_projection(&serde_json::to_vec(&value).unwrap()).is_err()); + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/tauri.conf.json b/product-source/hololake-native-desktop/src-tauri/tauri.conf.json index b3a123ac2..c374314e7 100644 --- a/product-source/hololake-native-desktop/src-tauri/tauri.conf.json +++ b/product-source/hololake-native-desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "HoloLake", - "version": "0.1.0", + "version": "0.2.0", "identifier": "world.guanghu.hololake", "build": { "frontendDist": "../dist", diff --git a/product-source/hololake-native-desktop/src/main.tsx b/product-source/hololake-native-desktop/src/main.tsx index 3ceb2bd23..044e4f4c5 100644 --- a/product-source/hololake-native-desktop/src/main.tsx +++ b/product-source/hololake-native-desktop/src/main.tsx @@ -5,7 +5,7 @@ import './design-tokens.css' import './styles.css' type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear' -type Panel = 'channel' | 'receipts' | 'updates' | 'settings' | null +type Panel = 'channel' | 'pncc' | 'receipts' | 'updates' | 'settings' | null interface HomeStatus { schema: string @@ -87,6 +87,27 @@ interface ReleaseCheckReceipt { candidate?: ReleaseCandidate } +interface ServerPnccProjection { + schema: string + state: string + personaId: string + humanResponsibilitySubject: string + nodeId: string + bootId: string + gitHead: string + carrierBindingState: string + personaCarrierBound: boolean + modelInferenceStarted: boolean + realityExecutionAllowed: boolean + primaryLeaseHeld: boolean + eventCount: number + eventChainHead: string + observedAtUnixMs: number + transport: string + repositoryContentExposed: boolean + writeAuthority: boolean +} + const themes: Array<{ id: ThemeId; name: string }> = [ { id: 'night', name: '夜湖星光' }, { id: 'dawn', name: '晨湖曦光' }, @@ -157,6 +178,9 @@ function HoloLakeApp() { const [releaseBusy, setReleaseBusy] = useState(false) const [releaseMessage, setReleaseMessage] = useState('') const [message, setMessage] = useState('') + const [serverPncc, setServerPncc] = useState(null) + const [serverPnccBusy, setServerPnccBusy] = useState(false) + const [serverPnccReadback, setServerPnccReadback] = useState<'WAITING' | 'LIVE' | 'UNAVAILABLE'>('WAITING') const refreshStatus = useCallback(async () => { try { @@ -172,6 +196,26 @@ function HoloLakeApp() { return () => window.clearInterval(timer) }, [refreshStatus]) + const refreshServerPncc = useCallback(async () => { + setServerPnccBusy(true) + try { + const projection = await invoke('query_jd_pncc_server_projection') + setServerPncc(projection) + setServerPnccReadback('LIVE') + } catch { + setServerPncc(null) + setServerPnccReadback('UNAVAILABLE') + } finally { + setServerPnccBusy(false) + } + }, []) + + useEffect(() => { + void refreshServerPncc() + const timer = window.setInterval(() => void refreshServerPncc(), 15000) + return () => window.clearInterval(timer) + }, [refreshServerPncc]) + useEffect(() => { document.documentElement.dataset.theme = theme window.localStorage.setItem('hololake-theme', theme) @@ -378,10 +422,10 @@ function HoloLakeApp() {