diff --git a/product-source/hololake-native-desktop/contracts/module-donor-admission-registry.json b/product-source/hololake-native-desktop/contracts/module-donor-admission-registry.json index ffe8bbcf2..cfbbcd053 100644 --- a/product-source/hololake-native-desktop/contracts/module-donor-admission-registry.json +++ b/product-source/hololake-native-desktop/contracts/module-donor-admission-registry.json @@ -165,12 +165,26 @@ "ONLY_THEN_ASSIGN_PERMANENT_MODULE_NUMBER" ], "hot_install_boundary": { + "runtime_state": "SIGNED_DECLARATIVE_PACKAGE_ENGINE_IMPLEMENTED", + "runtime_contract": "contracts/module-package-runtime.json", + "numbered_module_runtime_operations": [ + "HLP-NIPC-OP-0063", + "HLP-NIPC-OP-0064", + "HLP-NIPC-OP-0065", + "HLP-NIPC-OP-0066", + "HLP-NIPC-OP-0067", + "HLP-NIPC-OP-0068", + "HLP-NIPC-OP-0069" + ], "source_repository_is_directly_executable": false, "immutable_signed_artifact_required": true, "compatibility_manifest_required": true, "permissions_declared_before_mount": true, "human_confirmation_required_when_boundary_expands": true, "rollback_on_self_test_failure": true, - "user_data_survives_unmount": true + "user_data_survives_unmount": true, + "arbitrary_native_code_allowed": false, + "arbitrary_webview_javascript_allowed": false, + "real_signed_acceptance_fixture": "fixtures/module-packages/HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001-0.1.0.ghmod" } } diff --git a/product-source/hololake-native-desktop/contracts/module-package-runtime.json b/product-source/hololake-native-desktop/contracts/module-package-runtime.json new file mode 100644 index 000000000..a00528451 --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/module-package-runtime.json @@ -0,0 +1,52 @@ +{ + "schema": "hololake.module-package-runtime/v1", + "record_id": "HLP-MODULE-PACKAGE-RUNTIME-001", + "protocols": ["GLS-0710", "GLS-0803", "GLS-0819", "GLS-0310"], + "package_schema": "hololake.module-package/v1", + "signature": { + "algorithm": "MINISIGN_ED25519", + "trust_source": "src-tauri/release-trust.json", + "signature_is_detached": true, + "package_bytes_are_signed_exactly": true, + "source_repository_is_executable": false + }, + "number_classes": { + "official": "HLP-MOD-OFFICIAL-*", + "private_channel": "HLP-MOD-LOCAL-*", + "candidate_number_is_runtime_number": false + }, + "host": { + "version": "0.5.0", + "maximum_package_bytes": 16777216, + "arbitrary_native_code": false, + "arbitrary_webview_javascript": false, + "declarative_payload_only": true + }, + "registered_adapters": [ + "native-composition-v1", + "channel-workbench-v1", + "persona-channel-body-v1", + "education-workbench-v1", + "web-novel-workbench-v1", + "mobile-sync-v1", + "dynamic-language-world-surface-v1" + ], + "lifecycle": [ + "INSTALLED_DORMANT", + "MOUNTED_PENDING_SELF_TEST", + "ACTIVE", + "DORMANT", + "ROLLBACK_PENDING_SELF_TEST", + "FAILED_CLOSED" + ], + "rules": { + "verified_human_route_required": true, + "permission_expansion_requires_human_confirmation": true, + "mount_never_executes_package_code": true, + "self_test_before_active": true, + "failed_self_test_rolls_back": true, + "restart_reconstructs_from_sqlite": true, + "unmount_preserves_user_data": true, + "every_mutation_writes_hash_chained_receipt": true + } +} diff --git a/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json b/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json index fce7843ce..d670d31ab 100644 --- a/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json +++ b/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json @@ -60,7 +60,8 @@ "get_zero_core_numbering_kernel", "zero_point_verify", "zero_point_sync", - "zero_point_status" + "zero_point_status", + "get_module_runtime_snapshot" ], "input_wrapper_aliases": [ "confirm_hololake_update_install", @@ -91,7 +92,13 @@ "inspect_mounted_pncc_repository", "confirm_pncc_repository_mount", "query_pncc_receipt_projection", - "zero_point_bind" + "zero_point_bind", + "verify_module_package", + "install_module_package", + "mount_module", + "self_test_module", + "unmount_module", + "rollback_module" ], "direct_field_aliases": { "perform_code_repo_login": [ @@ -917,6 +924,83 @@ "admission": "PREAUTH_SYSTEM_ROUTE", "effect": "STATE_CHANGE", "payload_schema": "hololake.numbered-ipc.payload/zero_point_status/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0063", + "alias": "get_module_runtime_snapshot", + "handler": "module_package_runtime::get_module_runtime_snapshot", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0020", + "target_number": "HLP-NIPC-TGT-0020", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_module_runtime_snapshot/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0064", + "alias": "verify_module_package", + "handler": "module_package_runtime::verify_module_package", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0020", + "target_number": "HLP-NIPC-TGT-0020", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/verify_module_package/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0065", + "alias": "install_module_package", + "handler": "module_package_runtime::install_module_package", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0020", + "target_number": "HLP-NIPC-TGT-0020", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/install_module_package/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0066", + "alias": "mount_module", + "handler": "module_package_runtime::mount_module", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0020", + "target_number": "HLP-NIPC-TGT-0020", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/mount_module/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0067", + "alias": "self_test_module", + "handler": "module_package_runtime::self_test_module", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0020", + "target_number": "HLP-NIPC-TGT-0020", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/self_test_module/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0068", + "alias": "unmount_module", + "handler": "module_package_runtime::unmount_module", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0020", + "target_number": "HLP-NIPC-TGT-0020", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/unmount_module/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0069", + "alias": "rollback_module", + "handler": "module_package_runtime::rollback_module", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0020", + "target_number": "HLP-NIPC-TGT-0020", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/rollback_module/v1" } ] } diff --git a/product-source/hololake-native-desktop/docs/ARCHITECTURE.md b/product-source/hololake-native-desktop/docs/ARCHITECTURE.md index a91da9aab..a1d6a16af 100644 --- a/product-source/hololake-native-desktop/docs/ARCHITECTURE.md +++ b/product-source/hololake-native-desktop/docs/ARCHITECTURE.md @@ -100,6 +100,8 @@ HoloLake 0.5.0 is the clean numbered-root base. The compiled 0.4.1 desktop appli Each donor capability receives a candidate coordinate, but no permanent runtime module number, until one isolated admission cycle has reviewed provenance and permissions, allocated numbered IPC module/target/operation coordinates, implemented an adapter without raw Tauri invoke, passed negative-route and data tests, and produced installed mount, restart, unmount and rollback receipts. The admission order and candidate inventory are recorded in `contracts/module-donor-admission-registry.json`. +The module-package runtime is now the shared admission executor. It accepts an exact detached-minisign `.ghmod` artifact, validates the package and its compatibility/permission manifest, stores it inside the authenticated account, and advances only through numbered install, mount, self-test, unmount and rollback operations. Lifecycle state and receipts are durable SQLite records; unmount never removes user data. A package is declarative and selects a host-registered adapter: repositories, native binaries and arbitrary webview JavaScript are not executable module inputs. Public lighthouse numbers remain unavailable until a candidate completes its own installed acceptance; private channel packages use a separate local number class. + ## Stage-one convergence verdict The Tauri source in this directory is the only future HoloLake desktop mainline. An installed build of it is an acceptance candidate, not a separate product line and not proof that stage one exists. The Electron 0.8.0 product and the legacy Tauri/platform sources remain read-only UX, behavior, engineering and protected-data donors until inventory, backup, readback, reversible migration rehearsal and signed installed-runtime acceptance all pass. diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001-0.1.0.ghmod b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001-0.1.0.ghmod new file mode 100644 index 000000000..db17e9e33 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001-0.1.0.ghmod @@ -0,0 +1,36 @@ +{ + "schema": "hololake.module-package/v1", + "manifest": { + "moduleNumber": "HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001", + "registrationClass": "PRIVATE_CHANNEL_LOCAL", + "displayName": "原生组合模块运行时验收包", + "version": "0.1.0", + "minimumHostVersion": "0.5.0", + "adapter": "native-composition-v1", + "contentDigest": "07bfafaaa0b06f160abc489edce2bc23a3b092520c8d6c6f4a5ce29cca4485ea", + "permissions": [ + "KNOWLEDGE_READ", + "PROJECTION_RENDER" + ], + "userDataSchema": "hololake.module-data/native-composition/v1", + "selfTest": { + "kind": "DECLARATIVE_SCHEMA_V1", + "expectedContentDigest": "07bfafaaa0b06f160abc489edce2bc23a3b092520c8d6c6f4a5ce29cca4485ea" + } + }, + "payload": { + "adapterContract": "hololake.native-composition-v1", + "capabilities": [ + "READ_CURRENT_ACCOUNT_KNOWLEDGE", + "PROJECT_SHARED_EXECUTION_RESULT" + ], + "entry": "native-composition", + "views": [ + "DASHBOARD", + "COMPARISON", + "VERTICAL_BAR", + "CLASSIFICATION", + "TABLE" + ] + } +} diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001-0.1.0.ghmod.sig b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001-0.1.0.ghmod.sig new file mode 100644 index 000000000..92e83cab4 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001-0.1.0.ghmod.sig @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRThwcUdLRThMYkJLN29MM1NmVFVCcmh4dGdrbHdPVWlvdm45enpveUhBeFZ3VEM1TkNHRDBKdGFoN21OZHgzV0NSMFVvMk1TNW44Z3BtRS9GcmN5SHdvPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDczNTkxCWZpbGU6SExQLU1PRC1MT0NBTC1SVU5USU1FLUFDQ0VQVEFOQ0UtMDAwMS0wLjEuMC5naG1vZApXRWZINVdEUG12SlNsdzRTMkFHRDV3V0NkSVhNYW92RmczRGZlU3M0ZU9nSnhjc1BWbU1ZS0x1cStoQXE5TE96cnV1S2FaQ1FnK0FGdFNEY1RvUGZBQT09Cg== \ No newline at end of file diff --git a/product-source/hololake-native-desktop/generated/unified-number-coordinate-tree.json b/product-source/hololake-native-desktop/generated/unified-number-coordinate-tree.json index c71395732..0a3672189 100644 --- a/product-source/hololake-native-desktop/generated/unified-number-coordinate-tree.json +++ b/product-source/hololake-native-desktop/generated/unified-number-coordinate-tree.json @@ -54,7 +54,7 @@ }, { "recordId": "HLP-NUMBERED-IPC-ROOT-001", - "sha256": "f8373820f373ffb59df08952acc753d48c2e3a8a0d0df5dab486bd43d3e9ab70" + "sha256": "09f5ed0933850d06df508387500ff8aa7d33c864b473647b675a2d5e405b68dc" }, { "recordId": "HLP-NBROKER-ROOT-001", @@ -69,7 +69,7 @@ "everyAcceptedCallHasEvidenceClass": true, "mismatchedCoordinate": "FAIL_CLOSED" }, - "routeCount": 84, + "routeCount": 91, "routes": [ { "transport": "DIRECT_LOCAL_NUMBERED_BROKER", @@ -855,6 +855,104 @@ "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0011/HLP-NIPC-OP-0042/HLP-NIPC-TGT-0011" }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0063", + "targetNumber": "HLP-NIPC-TGT-0020", + "alias": "get_module_runtime_snapshot", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0020/HLP-NIPC-OP-0063/HLP-NIPC-TGT-0020" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0064", + "targetNumber": "HLP-NIPC-TGT-0020", + "alias": "verify_module_package", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0020/HLP-NIPC-OP-0064/HLP-NIPC-TGT-0020" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0065", + "targetNumber": "HLP-NIPC-TGT-0020", + "alias": "install_module_package", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0020/HLP-NIPC-OP-0065/HLP-NIPC-TGT-0020" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0066", + "targetNumber": "HLP-NIPC-TGT-0020", + "alias": "mount_module", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0020/HLP-NIPC-OP-0066/HLP-NIPC-TGT-0020" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0067", + "targetNumber": "HLP-NIPC-TGT-0020", + "alias": "self_test_module", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0020/HLP-NIPC-OP-0067/HLP-NIPC-TGT-0020" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0068", + "targetNumber": "HLP-NIPC-TGT-0020", + "alias": "unmount_module", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0020/HLP-NIPC-OP-0068/HLP-NIPC-TGT-0020" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0069", + "targetNumber": "HLP-NIPC-TGT-0020", + "alias": "rollback_module", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0020/HLP-NIPC-OP-0069/HLP-NIPC-TGT-0020" + }, { "transport": "TAURI_WEBVIEW_NUMBERED_IPC", "protocolVersion": "HLP-NIPC-v1", diff --git a/product-source/hololake-native-desktop/scripts/module-donor-admission.test.mjs b/product-source/hololake-native-desktop/scripts/module-donor-admission.test.mjs index 9b4f20153..0f3e67969 100644 --- a/product-source/hololake-native-desktop/scripts/module-donor-admission.test.mjs +++ b/product-source/hololake-native-desktop/scripts/module-donor-admission.test.mjs @@ -31,9 +31,13 @@ test('legacy numbering donor is rejected and the numbered IPC root remains singu }) test('hot installation is artifact, permission, self-test and rollback backed', () => { + assert.equal(catalog.hot_install_boundary.runtime_state, 'SIGNED_DECLARATIVE_PACKAGE_ENGINE_IMPLEMENTED') + assert.equal(catalog.hot_install_boundary.numbered_module_runtime_operations.length, 7) assert.equal(catalog.hot_install_boundary.source_repository_is_directly_executable, false) assert.equal(catalog.hot_install_boundary.immutable_signed_artifact_required, true) assert.equal(catalog.hot_install_boundary.compatibility_manifest_required, true) assert.equal(catalog.hot_install_boundary.rollback_on_self_test_failure, true) assert.equal(catalog.hot_install_boundary.user_data_survives_unmount, true) + assert.equal(catalog.hot_install_boundary.arbitrary_native_code_allowed, false) + assert.equal(catalog.hot_install_boundary.arbitrary_webview_javascript_allowed, false) }) diff --git a/product-source/hololake-native-desktop/scripts/module-package-runtime.test.mjs b/product-source/hololake-native-desktop/scripts/module-package-runtime.test.mjs new file mode 100644 index 000000000..42e273ca8 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/module-package-runtime.test.mjs @@ -0,0 +1,37 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' + +const contract = JSON.parse(readFileSync(new URL('../contracts/module-package-runtime.json', import.meta.url), 'utf8')) +const registry = JSON.parse(readFileSync(new URL('../contracts/numbered-ipc-registry.json', import.meta.url), 'utf8')) +const rust = readFileSync(new URL('../src-tauri/src/module_package_runtime.rs', import.meta.url), 'utf8') + +test('module packages are signed declarative artifacts rather than executable repositories', () => { + assert.equal(contract.schema, 'hololake.module-package-runtime/v1') + assert.equal(contract.signature.source_repository_is_executable, false) + assert.equal(contract.signature.package_bytes_are_signed_exactly, true) + assert.equal(contract.host.arbitrary_native_code, false) + assert.equal(contract.host.arbitrary_webview_javascript, false) + assert.equal(contract.host.declarative_payload_only, true) + assert.equal(contract.rules.self_test_before_active, true) + assert.equal(contract.rules.failed_self_test_rolls_back, true) + assert.equal(contract.rules.unmount_preserves_user_data, true) +}) + +test('module lifecycle is reachable only through numbered verified-human routes', () => { + const routes = registry.operations.filter((route) => route.module_number === 'HLP-NIPC-MOD-0020') + assert.equal(routes.length, 7) + assert.deepEqual(routes.map((route) => route.operation_number), [ + 'HLP-NIPC-OP-0063', + 'HLP-NIPC-OP-0064', + 'HLP-NIPC-OP-0065', + 'HLP-NIPC-OP-0066', + 'HLP-NIPC-OP-0067', + 'HLP-NIPC-OP-0068', + 'HLP-NIPC-OP-0069', + ]) + assert.ok(routes.every((route) => route.admission === 'VERIFIED_HUMAN_ROUTE')) + assert.match(rust, /MINISIGN_ED25519|PublicKey::decode/) + assert.match(rust, /module_receipts/) + assert.match(rust, /ROLLBACK_PENDING_SELF_TEST/) +}) diff --git a/product-source/hololake-native-desktop/scripts/unified-number-coordinate-tree.test.mjs b/product-source/hololake-native-desktop/scripts/unified-number-coordinate-tree.test.mjs index 800ea88ed..0a1bfbba8 100644 --- a/product-source/hololake-native-desktop/scripts/unified-number-coordinate-tree.test.mjs +++ b/product-source/hololake-native-desktop/scripts/unified-number-coordinate-tree.test.mjs @@ -7,8 +7,8 @@ test('identity, webview and direct broker numbers compile into one unique eviden const generated = JSON.parse(readFileSync(new URL('../generated/unified-number-coordinate-tree.json', import.meta.url), 'utf8')) assert.deepEqual(generated, compileUnifiedNumberTree()) assert.equal(generated.recordId, 'HLP-UNIFIED-NUMBER-TREE-001') - assert.equal(generated.routeCount, 84) - assert.equal(new Set(generated.routes.map((route) => route.path)).size, 84) + assert.equal(generated.routeCount, 91) + assert.equal(new Set(generated.routes.map((route) => route.path)).size, 91) assert.equal(generated.invariants.everyPhysicalCallHasNumberedRoute, true) assert.equal(generated.invariants.everyAcceptedCallHasEvidenceClass, true) assert.ok(generated.routes.every((route) => route.admission && route.evidence)) diff --git a/product-source/hololake-native-desktop/src-tauri/Cargo.toml b/product-source/hololake-native-desktop/src-tauri/Cargo.toml index 56d30e107..d047dd6db 100644 --- a/product-source/hololake-native-desktop/src-tauri/Cargo.toml +++ b/product-source/hololake-native-desktop/src-tauri/Cargo.toml @@ -33,10 +33,10 @@ url = "2" reqwest = { version = "0.13.2", default-features = false, features = ["cookies", "form", "json", "rustls", "stream"] } tokio = { version = "1", features = ["time"] } futures-util = "0.3" +minisign-verify = "0.2.5" [target.'cfg(windows)'.dependencies] widestring = "1" [dev-dependencies] -minisign-verify = "0.2.5" tempfile = "3" diff --git a/product-source/hololake-native-desktop/src-tauri/src/direct_local_broker.rs b/product-source/hololake-native-desktop/src-tauri/src/direct_local_broker.rs index dbb6fdf1e..ac7fb28ce 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/direct_local_broker.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/direct_local_broker.rs @@ -230,7 +230,7 @@ enum BrokerRequest { OpenSession(OpenSessionInput), ResumeSession(ResumeSessionInput), HeartbeatSession(AuthenticateSessionInput), - PresentPersonaCarrierLicense(AuthenticatedPersonaCarrierLicenseInput), + PresentPersonaCarrierLicense(Box), GetPersonaCarrierLicenseStatus(AuthenticatedPersonaCarrierLicenseStatusInput), GetWorkEnvironment(AuthenticatedWorkEnvironmentInput), AppendEvent(AppendSessionEventInput), @@ -737,7 +737,7 @@ fn connect_endpoint(path: &Path) -> std::io::Result { #[cfg(unix)] { let name = path.to_fs_name::()?; - return LocalSocketStream::connect(name); + LocalSocketStream::connect(name) } #[cfg(windows)] { @@ -1055,6 +1055,7 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse { BrokerRequest::ResumeSession(input) => resume_at(session_root, input) .and_then(|receipt| serde_json::to_value(receipt).map_err(|error| error.to_string())), BrokerRequest::PresentPersonaCarrierLicense(input) => { + let input = *input; authenticate_context_at(session_root, &input.session) .and_then(|context| { present_persona_license_at( 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 0b5c06b32..64f36f8a7 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/lib.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/lib.rs @@ -14,6 +14,7 @@ mod home_status; mod knowledge_base; mod local_development_bridge; mod metacognitive_zero_layer; +mod module_package_runtime; mod number_coordinate_tree; mod numbered_ipc; mod numbered_ipc_dispatch; @@ -49,6 +50,7 @@ pub fn run() { gls_protocol_kernel::start_on_application_open(app.handle())?; metacognitive_zero_layer::start_on_application_open()?; persona_carrier_license::start_on_application_open()?; + module_package_runtime::start_on_application_open(app.handle())?; number_coordinate_tree::start_on_application_open()?; numbered_language_input::validate_contract()?; numbered_ipc::start_on_application_open(app.handle())?; diff --git a/product-source/hololake-native-desktop/src-tauri/src/module_package_runtime.rs b/product-source/hololake-native-desktop/src-tauri/src/module_package_runtime.rs new file mode 100644 index 000000000..866901e8d --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/module_package_runtime.rs @@ -0,0 +1,1202 @@ +//! Signed, account-scoped HoloLake module package runtime. +//! +//! A repository is never executable input. The runtime accepts only an immutable +//! detached-minisign package, validates a declarative manifest, persists lifecycle +//! state in SQLite and writes a hash-chained receipt for every mutation. Package +//! mount does not load native code or arbitrary webview JavaScript. + +use base64::{engine::general_purpose::STANDARD, Engine as _}; +use minisign_verify::{PublicKey, Signature}; +use ring::digest::{digest, SHA256}; +use rusqlite::{params, Connection, OptionalExtension, Transaction}; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; +use tauri::AppHandle; +use uuid::Uuid; + +const CONTRACT_RAW: &str = include_str!("../../contracts/module-package-runtime.json"); +const RELEASE_TRUST_RAW: &str = include_str!("../release-trust.json"); +const CONTRACT_SCHEMA: &str = "hololake.module-package-runtime/v1"; +const PACKAGE_SCHEMA: &str = "hololake.module-package/v1"; +const HOST_VERSION: &str = "0.5.0"; +const MAX_PACKAGE_BYTES: u64 = 16 * 1024 * 1024; + +#[derive(Debug, Deserialize)] +struct RuntimeContract { + schema: String, + record_id: String, + package_schema: String, + host: HostContract, + registered_adapters: Vec, + lifecycle: Vec, +} + +#[derive(Debug, Deserialize)] +struct HostContract { + version: String, + maximum_package_bytes: u64, + arbitrary_native_code: bool, + arbitrary_webview_javascript: bool, + declarative_payload_only: bool, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct ModulePackage { + schema: String, + manifest: ModuleManifest, + payload: Value, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct ModuleManifest { + module_number: String, + registration_class: String, + display_name: String, + version: String, + minimum_host_version: String, + adapter: String, + content_digest: String, + permissions: Vec, + user_data_schema: String, + self_test: ModuleSelfTest, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct ModuleSelfTest { + kind: String, + expected_content_digest: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct VerifyModulePackageInput { + package_path: String, + signature_path: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct InstallModulePackageInput { + package_path: String, + signature_path: String, + expected_package_sha256: String, + human_confirmed_permission_expansion: bool, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ModuleNumberInput { + module_number: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifiedModulePackage { + state: String, + package_sha256: String, + module_number: String, + display_name: String, + version: String, + adapter: String, + permissions: Vec, + signature_verified: bool, + package_code_executed: bool, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InstalledModule { + module_number: String, + display_name: String, + version: String, + adapter: String, + state: String, + package_sha256: String, + previous_package_sha256: Option, + permissions: Vec, + user_data_preserved: bool, + updated_at_unix_ms: u64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ModuleRuntimeSnapshot { + schema: &'static str, + state: &'static str, + host_version: &'static str, + package_code_executed: bool, + modules: Vec, + receipt_count: u64, + latest_receipt_hash: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ModuleMutationReceipt { + receipt_id: String, + event: String, + module_number: String, + state: String, + package_sha256: String, + previous_receipt_hash: String, + receipt_hash: String, + observed_at_unix_ms: u64, + user_data_preserved: bool, +} + +fn now_unix_ms() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_millis() as u64) + .unwrap_or(0) +} + +fn sha256_hex(bytes: &[u8]) -> String { + digest(&SHA256, bytes) + .as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn is_sha256(value: &str) -> bool { + value.len() == 64 && value.chars().all(|item| item.is_ascii_hexdigit()) +} + +fn canonical_value(value: &Value) -> Value { + match value { + Value::Array(values) => Value::Array(values.iter().map(canonical_value).collect()), + Value::Object(object) => { + let mut keys = object.keys().collect::>(); + keys.sort(); + let mut canonical = Map::new(); + for key in keys { + canonical.insert(key.clone(), canonical_value(&object[key])); + } + Value::Object(canonical) + } + _ => value.clone(), + } +} + +fn payload_digest(value: &Value) -> Result { + let bytes = serde_json::to_vec(&canonical_value(value)) + .map_err(|error| format!("HOLOLAKE_MODULE_PAYLOAD_INVALID: {error}"))?; + Ok(sha256_hex(&bytes)) +} + +fn contract() -> Result { + let contract: RuntimeContract = serde_json::from_str(CONTRACT_RAW) + .map_err(|error| format!("HOLOLAKE_MODULE_RUNTIME_CONTRACT_INVALID: {error}"))?; + if contract.schema != CONTRACT_SCHEMA + || contract.record_id != "HLP-MODULE-PACKAGE-RUNTIME-001" + || contract.package_schema != PACKAGE_SCHEMA + || contract.host.version != HOST_VERSION + || contract.host.maximum_package_bytes != MAX_PACKAGE_BYTES + || contract.host.arbitrary_native_code + || contract.host.arbitrary_webview_javascript + || !contract.host.declarative_payload_only + || contract.registered_adapters.len() != 7 + || contract.lifecycle.len() != 6 + { + return Err("HOLOLAKE_MODULE_RUNTIME_CONTRACT_BOUNDARY_INVALID".into()); + } + Ok(contract) +} + +fn decode_outer_base64(value: &str, label: &str) -> Result { + let bytes = STANDARD + .decode(value.trim()) + .map_err(|error| format!("{label}_BASE64_INVALID: {error}"))?; + String::from_utf8(bytes).map_err(|error| format!("{label}_UTF8_INVALID: {error}")) +} + +fn verify_signature(package: &[u8], signature_path: &Path, trust_raw: &str) -> Result<(), String> { + let trust: Value = serde_json::from_str(trust_raw) + .map_err(|error| format!("HOLOLAKE_MODULE_TRUST_INVALID: {error}"))?; + if trust.get("state").and_then(Value::as_str) != Some("PROVISIONED") { + return Err("HOLOLAKE_MODULE_TRUST_UNPROVISIONED".into()); + } + let encoded_key = trust + .get("publicKey") + .and_then(Value::as_str) + .ok_or("HOLOLAKE_MODULE_TRUST_KEY_MISSING")?; + let public_key = PublicKey::decode(&decode_outer_base64(encoded_key, "HOLOLAKE_MODULE_KEY")?) + .map_err(|error| format!("HOLOLAKE_MODULE_KEY_INVALID: {error}"))?; + let signature_raw = fs::read_to_string(signature_path) + .map_err(|error| format!("HOLOLAKE_MODULE_SIGNATURE_READ_FAILED: {error}"))?; + let signature = Signature::decode(&decode_outer_base64( + &signature_raw, + "HOLOLAKE_MODULE_SIGNATURE", + )?) + .map_err(|error| format!("HOLOLAKE_MODULE_SIGNATURE_INVALID: {error}"))?; + public_key + .verify(package, &signature, true) + .map_err(|error| format!("HOLOLAKE_MODULE_SIGNATURE_REJECTED: {error}")) +} + +fn valid_number(value: &str, registration_class: &str) -> bool { + let prefix = match registration_class { + "OFFICIAL_LIGHTHOUSE" => "HLP-MOD-OFFICIAL-", + "PRIVATE_CHANNEL_LOCAL" => "HLP-MOD-LOCAL-", + _ => return false, + }; + value.starts_with(prefix) + && value.len() <= 96 + && value + .chars() + .all(|item| item.is_ascii_uppercase() || item.is_ascii_digit() || item == '-') +} + +fn version_triplet(value: &str) -> Option<(u64, u64, u64)> { + let parts = value.split('.').collect::>(); + if parts.len() != 3 { + return None; + } + Some(( + parts[0].parse().ok()?, + parts[1].parse().ok()?, + parts[2].parse().ok()?, + )) +} + +fn validate_package(package: &ModulePackage, contract: &RuntimeContract) -> Result<(), String> { + let manifest = &package.manifest; + let permissions = manifest.permissions.iter().collect::>(); + if package.schema != PACKAGE_SCHEMA + || !valid_number(&manifest.module_number, &manifest.registration_class) + || manifest.display_name.trim().is_empty() + || manifest.display_name.len() > 120 + || version_triplet(&manifest.version).is_none() + || version_triplet(&manifest.minimum_host_version).is_none() + || version_triplet(&manifest.minimum_host_version) > version_triplet(HOST_VERSION) + || !contract.registered_adapters.contains(&manifest.adapter) + || !is_sha256(&manifest.content_digest) + || permissions.len() != manifest.permissions.len() + || manifest.permissions.len() > 32 + || manifest.permissions.iter().any(|permission| { + permission.is_empty() + || permission.len() > 80 + || !permission + .chars() + .all(|item| item.is_ascii_uppercase() || item.is_ascii_digit() || item == '_') + }) + || manifest.user_data_schema.trim().is_empty() + || manifest.self_test.kind != "DECLARATIVE_SCHEMA_V1" + || manifest.self_test.expected_content_digest != manifest.content_digest + || !package.payload.is_object() + || payload_digest(&package.payload)? != manifest.content_digest + { + return Err("HOLOLAKE_MODULE_PACKAGE_CONTRACT_INVALID".into()); + } + Ok(()) +} + +fn read_verified_package( + package_path: &Path, + signature_path: &Path, + trust_raw: &str, +) -> Result<(Vec, ModulePackage, String), String> { + let metadata = fs::symlink_metadata(package_path) + .map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_METADATA_FAILED: {error}"))?; + let signature_metadata = fs::symlink_metadata(signature_path) + .map_err(|error| format!("HOLOLAKE_MODULE_SIGNATURE_METADATA_FAILED: {error}"))?; + if metadata.file_type().is_symlink() + || signature_metadata.file_type().is_symlink() + || !metadata.is_file() + || !signature_metadata.is_file() + || metadata.len() == 0 + || metadata.len() > MAX_PACKAGE_BYTES + || signature_metadata.len() == 0 + || signature_metadata.len() > 16 * 1024 + { + return Err("HOLOLAKE_MODULE_PACKAGE_SIZE_INVALID".into()); + } + let bytes = fs::read(package_path) + .map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_READ_FAILED: {error}"))?; + verify_signature(&bytes, signature_path, trust_raw)?; + let package: ModulePackage = serde_json::from_slice(&bytes) + .map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_JSON_INVALID: {error}"))?; + validate_package(&package, &contract()?)?; + let package_sha256 = sha256_hex(&bytes); + Ok((bytes, package, package_sha256)) +} + +fn runtime_root(app: &AppHandle) -> Result { + crate::authenticated_storage::account_storage_root(app, "module-runtime-v1") +} + +fn open_db(root: &Path) -> Result { + fs::create_dir_all(root) + .map_err(|error| format!("HOLOLAKE_MODULE_RUNTIME_ROOT_FAILED: {error}"))?; + let connection = Connection::open(root.join("module-runtime.sqlite3")) + .map_err(|error| format!("HOLOLAKE_MODULE_RUNTIME_DB_OPEN_FAILED: {error}"))?; + connection + .busy_timeout(std::time::Duration::from_secs(5)) + .map_err(|error| format!("HOLOLAKE_MODULE_RUNTIME_DB_TIMEOUT_FAILED: {error}"))?; + connection + .execute_batch( + "PRAGMA journal_mode=WAL; + PRAGMA foreign_keys=ON; + CREATE TABLE IF NOT EXISTS installed_modules( + module_number TEXT PRIMARY KEY, + display_name TEXT NOT NULL, + version TEXT NOT NULL, + adapter TEXT NOT NULL, + state TEXT NOT NULL, + package_sha256 TEXT NOT NULL, + previous_package_sha256 TEXT, + permissions_json TEXT NOT NULL, + manifest_json TEXT NOT NULL, + updated_at_unix_ms INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS module_receipts( + sequence INTEGER PRIMARY KEY AUTOINCREMENT, + receipt_id TEXT NOT NULL UNIQUE, + event TEXT NOT NULL, + module_number TEXT NOT NULL, + state TEXT NOT NULL, + package_sha256 TEXT NOT NULL, + previous_receipt_hash TEXT NOT NULL, + receipt_hash TEXT NOT NULL, + observed_at_unix_ms INTEGER NOT NULL + );", + ) + .map_err(|error| format!("HOLOLAKE_MODULE_RUNTIME_DB_SCHEMA_FAILED: {error}"))?; + Ok(connection) +} + +fn known_state(state: &str) -> bool { + matches!( + state, + "INSTALLED_DORMANT" + | "MOUNTED_PENDING_SELF_TEST" + | "ACTIVE" + | "DORMANT" + | "ROLLBACK_PENDING_SELF_TEST" + | "FAILED_CLOSED" + ) +} + +fn verify_receipt_chain(connection: &Connection) -> Result<(), String> { + let mut statement = connection + .prepare( + "SELECT receipt_id,event,module_number,state,package_sha256,previous_receipt_hash,receipt_hash,observed_at_unix_ms FROM module_receipts ORDER BY sequence", + ) + .map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_VERIFY_PREPARE_FAILED: {error}"))?; + let rows = statement + .query_map([], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + row.get::<_, String>(3)?, + row.get::<_, String>(4)?, + row.get::<_, String>(5)?, + row.get::<_, String>(6)?, + row.get::<_, u64>(7)?, + )) + }) + .map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_VERIFY_QUERY_FAILED: {error}"))?; + let mut previous = "0".repeat(64); + for row in rows { + let (receipt_id, event, module_number, state, package_sha256, linked, stored, observed) = + row.map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_VERIFY_ROW_FAILED: {error}"))?; + let expected = sha256_hex( + format!( + "HLP-MODULE-RECEIPT-v1\n{receipt_id}\n{event}\n{module_number}\n{state}\n{package_sha256}\n{linked}\n{observed}" + ) + .as_bytes(), + ); + if linked != previous + || stored != expected + || !is_sha256(&stored) + || !is_sha256(&package_sha256) + || !known_state(&state) + { + return Err("HOLOLAKE_MODULE_RECEIPT_CHAIN_INVALID".into()); + } + previous = stored; + } + Ok(()) +} + +fn verify_persisted_modules(connection: &Connection, root: &Path) -> Result<(), String> { + let mut statement = connection + .prepare( + "SELECT module_number,display_name,version,adapter,state,package_sha256,previous_package_sha256,permissions_json,manifest_json FROM installed_modules ORDER BY module_number", + ) + .map_err(|error| format!("HOLOLAKE_MODULE_PERSISTED_VERIFY_PREPARE_FAILED: {error}"))?; + let rows = statement + .query_map([], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + row.get::<_, String>(3)?, + row.get::<_, String>(4)?, + row.get::<_, String>(5)?, + row.get::<_, Option>(6)?, + row.get::<_, String>(7)?, + row.get::<_, String>(8)?, + )) + }) + .map_err(|error| format!("HOLOLAKE_MODULE_PERSISTED_VERIFY_QUERY_FAILED: {error}"))?; + for row in rows { + let ( + number, + display, + version, + adapter, + state, + digest, + previous, + permissions_raw, + manifest_raw, + ) = row.map_err(|error| format!("HOLOLAKE_MODULE_PERSISTED_VERIFY_ROW_FAILED: {error}"))?; + if !known_state(&state) || !is_sha256(&digest) { + return Err("HOLOLAKE_MODULE_PERSISTED_STATE_INVALID".into()); + } + let manifest: ModuleManifest = serde_json::from_str(&manifest_raw) + .map_err(|_| "HOLOLAKE_MODULE_PERSISTED_MANIFEST_INVALID".to_string())?; + let permissions: Vec = serde_json::from_str(&permissions_raw) + .map_err(|_| "HOLOLAKE_MODULE_PERSISTED_PERMISSIONS_INVALID".to_string())?; + let (_, package, verified_digest) = read_verified_package( + &package_file(root, &digest), + &signature_file(root, &digest), + RELEASE_TRUST_RAW, + )?; + if verified_digest != digest + || package.manifest != manifest + || manifest.module_number != number + || manifest.display_name != display + || manifest.version != version + || manifest.adapter != adapter + || manifest.permissions != permissions + { + return Err("HOLOLAKE_MODULE_PERSISTED_IDENTITY_INVALID".into()); + } + if let Some(previous) = previous { + if !is_sha256(&previous) { + return Err("HOLOLAKE_MODULE_PREVIOUS_PACKAGE_INVALID".into()); + } + let (_, previous_package, previous_digest) = read_verified_package( + &package_file(root, &previous), + &signature_file(root, &previous), + RELEASE_TRUST_RAW, + )?; + if previous_digest != previous || previous_package.manifest.module_number != number { + return Err("HOLOLAKE_MODULE_PREVIOUS_PACKAGE_IDENTITY_INVALID".into()); + } + } + } + Ok(()) +} + +fn package_file(root: &Path, digest: &str) -> PathBuf { + root.join("packages").join(format!("{digest}.ghmod")) +} + +fn signature_file(root: &Path, digest: &str) -> PathBuf { + root.join("packages").join(format!("{digest}.ghmod.sig")) +} + +fn copy_atomic(source: &Path, target: &Path) -> Result<(), String> { + let parent = target + .parent() + .ok_or("HOLOLAKE_MODULE_PACKAGE_TARGET_INVALID")?; + fs::create_dir_all(parent) + .map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_STORE_FAILED: {error}"))?; + if target.exists() { + return Ok(()); + } + let temporary = parent.join(format!(".install-{}", Uuid::new_v4())); + fs::copy(source, &temporary) + .map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_COPY_FAILED: {error}"))?; + fs::rename(&temporary, target) + .map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_COMMIT_FAILED: {error}")) +} + +fn previous_receipt_hash(transaction: &Transaction<'_>) -> Result { + transaction + .query_row( + "SELECT receipt_hash FROM module_receipts ORDER BY sequence DESC LIMIT 1", + [], + |row| row.get(0), + ) + .optional() + .map(|value| value.unwrap_or_else(|| "0".repeat(64))) + .map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_READ_FAILED: {error}")) +} + +fn append_receipt( + transaction: &Transaction<'_>, + event: &str, + module_number: &str, + state: &str, + package_sha256: &str, + observed_at_unix_ms: u64, +) -> Result { + let receipt_id = format!("HLP-MOD-RCPT-{}", Uuid::new_v4()); + let previous = previous_receipt_hash(transaction)?; + let receipt_hash = sha256_hex( + format!( + "HLP-MODULE-RECEIPT-v1\n{receipt_id}\n{event}\n{module_number}\n{state}\n{package_sha256}\n{previous}\n{observed_at_unix_ms}" + ) + .as_bytes(), + ); + transaction + .execute( + "INSERT INTO module_receipts(receipt_id,event,module_number,state,package_sha256,previous_receipt_hash,receipt_hash,observed_at_unix_ms) + VALUES(?1,?2,?3,?4,?5,?6,?7,?8)", + params![receipt_id, event, module_number, state, package_sha256, previous, receipt_hash, observed_at_unix_ms], + ) + .map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_WRITE_FAILED: {error}"))?; + Ok(ModuleMutationReceipt { + receipt_id, + event: event.into(), + module_number: module_number.into(), + state: state.into(), + package_sha256: package_sha256.into(), + previous_receipt_hash: previous, + receipt_hash, + observed_at_unix_ms, + user_data_preserved: true, + }) +} + +fn permissions_from_json(raw: &str) -> BTreeSet { + serde_json::from_str::>(raw) + .unwrap_or_default() + .into_iter() + .collect() +} + +pub async fn verify_module_package( + app: AppHandle, + input: VerifyModulePackageInput, +) -> Result { + let _ = runtime_root(&app)?; + let (_, package, package_sha256) = read_verified_package( + Path::new(&input.package_path), + Path::new(&input.signature_path), + RELEASE_TRUST_RAW, + )?; + Ok(VerifiedModulePackage { + state: "VERIFIED_NOT_INSTALLED".into(), + package_sha256, + module_number: package.manifest.module_number, + display_name: package.manifest.display_name, + version: package.manifest.version, + adapter: package.manifest.adapter, + permissions: package.manifest.permissions, + signature_verified: true, + package_code_executed: false, + }) +} + +pub async fn install_module_package( + app: AppHandle, + input: InstallModulePackageInput, +) -> Result { + install_module_package_at( + &runtime_root(&app)?, + &input, + RELEASE_TRUST_RAW, + now_unix_ms(), + ) +} + +fn install_module_package_at( + root: &Path, + input: &InstallModulePackageInput, + trust_raw: &str, + observed: u64, +) -> Result { + if !is_sha256(&input.expected_package_sha256) { + return Err("HOLOLAKE_MODULE_EXPECTED_DIGEST_INVALID".into()); + } + let (bytes, package, digest) = read_verified_package( + Path::new(&input.package_path), + Path::new(&input.signature_path), + trust_raw, + )?; + if digest != input.expected_package_sha256 { + return Err("HOLOLAKE_MODULE_EXPECTED_DIGEST_MISMATCH".into()); + } + let mut connection = open_db(root)?; + let existing: Option<(String, String)> = connection + .query_row( + "SELECT package_sha256, permissions_json FROM installed_modules WHERE module_number=?1", + params![package.manifest.module_number], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_MODULE_EXISTING_READ_FAILED: {error}"))?; + let requested = package + .manifest + .permissions + .iter() + .cloned() + .collect::>(); + let expansion = existing + .as_ref() + .map(|(_, raw)| !requested.is_subset(&permissions_from_json(raw))) + .unwrap_or(!requested.is_empty()); + if expansion && !input.human_confirmed_permission_expansion { + return Err("HOLOLAKE_MODULE_PERMISSION_EXPANSION_REQUIRES_HUMAN_CONFIRMATION".into()); + } + copy_atomic(Path::new(&input.package_path), &package_file(root, &digest))?; + copy_atomic( + Path::new(&input.signature_path), + &signature_file(root, &digest), + )?; + if fs::read(package_file(root, &digest)) + .map_err(|error| format!("HOLOLAKE_MODULE_STORED_PACKAGE_READ_FAILED: {error}"))? + != bytes + { + return Err("HOLOLAKE_MODULE_STORED_PACKAGE_MISMATCH".into()); + } + if fs::read(signature_file(root, &digest)) + .map_err(|error| format!("HOLOLAKE_MODULE_STORED_SIGNATURE_READ_FAILED: {error}"))? + != fs::read(&input.signature_path) + .map_err(|error| format!("HOLOLAKE_MODULE_SOURCE_SIGNATURE_READ_FAILED: {error}"))? + { + return Err("HOLOLAKE_MODULE_STORED_SIGNATURE_MISMATCH".into()); + } + fs::create_dir_all(root.join("user-data").join(&package.manifest.module_number)) + .map_err(|error| format!("HOLOLAKE_MODULE_USER_DATA_ROOT_FAILED: {error}"))?; + let transaction = connection + .transaction() + .map_err(|error| format!("HOLOLAKE_MODULE_INSTALL_TRANSACTION_FAILED: {error}"))?; + let permissions_json = serde_json::to_string(&package.manifest.permissions) + .map_err(|error| format!("HOLOLAKE_MODULE_PERMISSIONS_INVALID: {error}"))?; + let manifest_json = serde_json::to_string(&package.manifest) + .map_err(|error| format!("HOLOLAKE_MODULE_MANIFEST_INVALID: {error}"))?; + let previous = existing.as_ref().map(|(value, _)| value.as_str()); + transaction + .execute( + "INSERT INTO installed_modules(module_number,display_name,version,adapter,state,package_sha256,previous_package_sha256,permissions_json,manifest_json,updated_at_unix_ms) + VALUES(?1,?2,?3,?4,'INSTALLED_DORMANT',?5,?6,?7,?8,?9) + ON CONFLICT(module_number) DO UPDATE SET display_name=excluded.display_name,version=excluded.version,adapter=excluded.adapter,state='INSTALLED_DORMANT',package_sha256=excluded.package_sha256,previous_package_sha256=installed_modules.package_sha256,permissions_json=excluded.permissions_json,manifest_json=excluded.manifest_json,updated_at_unix_ms=excluded.updated_at_unix_ms", + params![package.manifest.module_number, package.manifest.display_name, package.manifest.version, package.manifest.adapter, digest, previous, permissions_json, manifest_json, observed], + ) + .map_err(|error| format!("HOLOLAKE_MODULE_INSTALL_WRITE_FAILED: {error}"))?; + let receipt = append_receipt( + &transaction, + "INSTALL", + &package.manifest.module_number, + "INSTALLED_DORMANT", + &digest, + observed, + )?; + transaction + .commit() + .map_err(|error| format!("HOLOLAKE_MODULE_INSTALL_COMMIT_FAILED: {error}"))?; + Ok(receipt) +} + +fn transition( + root: &Path, + module_number: &str, + allowed_from: &[&str], + target: &str, + event: &str, +) -> Result { + let mut connection = open_db(root)?; + let row: (String, String) = connection + .query_row( + "SELECT state,package_sha256 FROM installed_modules WHERE module_number=?1", + params![module_number], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_MODULE_STATE_READ_FAILED: {error}"))? + .ok_or("HOLOLAKE_MODULE_NOT_INSTALLED")?; + if !allowed_from.contains(&row.0.as_str()) { + return Err("HOLOLAKE_MODULE_LIFECYCLE_TRANSITION_INVALID".into()); + } + let observed = now_unix_ms(); + let transaction = connection + .transaction() + .map_err(|error| format!("HOLOLAKE_MODULE_TRANSITION_BEGIN_FAILED: {error}"))?; + transaction + .execute( + "UPDATE installed_modules SET state=?1,updated_at_unix_ms=?2 WHERE module_number=?3 AND state=?4", + params![target, observed, module_number, row.0], + ) + .map_err(|error| format!("HOLOLAKE_MODULE_TRANSITION_WRITE_FAILED: {error}"))?; + let receipt = append_receipt(&transaction, event, module_number, target, &row.1, observed)?; + transaction + .commit() + .map_err(|error| format!("HOLOLAKE_MODULE_TRANSITION_COMMIT_FAILED: {error}"))?; + Ok(receipt) +} + +pub async fn mount_module( + app: AppHandle, + input: ModuleNumberInput, +) -> Result { + transition( + &runtime_root(&app)?, + &input.module_number, + &["INSTALLED_DORMANT", "DORMANT"], + "MOUNTED_PENDING_SELF_TEST", + "MOUNT", + ) +} + +pub async fn self_test_module( + app: AppHandle, + input: ModuleNumberInput, +) -> Result { + self_test_module_at(&runtime_root(&app)?, &input.module_number, now_unix_ms()) +} + +fn self_test_module_at( + root: &Path, + module_number: &str, + observed: u64, +) -> Result { + let mut connection = open_db(root)?; + let row: (String, String, Option) = connection + .query_row( + "SELECT state,package_sha256,previous_package_sha256 FROM installed_modules WHERE module_number=?1", + params![module_number], + |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_MODULE_SELF_TEST_STATE_FAILED: {error}"))? + .ok_or("HOLOLAKE_MODULE_NOT_INSTALLED")?; + if !matches!( + row.0.as_str(), + "MOUNTED_PENDING_SELF_TEST" | "ROLLBACK_PENDING_SELF_TEST" + ) { + return Err("HOLOLAKE_MODULE_SELF_TEST_STATE_INVALID".into()); + } + let verification = read_verified_package( + &package_file(root, &row.1), + &signature_file(root, &row.1), + RELEASE_TRUST_RAW, + ); + let success = verification.as_ref().is_ok_and(|(_, package, digest)| { + digest == &row.1 && package.manifest.module_number == module_number + }); + let rollback_package = row.2.as_ref().and_then(|previous| { + read_verified_package( + &package_file(root, previous), + &signature_file(root, previous), + RELEASE_TRUST_RAW, + ) + .ok() + .filter(|(_, package, digest)| { + digest == previous && package.manifest.module_number == module_number + }) + }); + let transaction = connection + .transaction() + .map_err(|error| format!("HOLOLAKE_MODULE_SELF_TEST_BEGIN_FAILED: {error}"))?; + if success { + transaction + .execute( + "UPDATE installed_modules SET state='ACTIVE',updated_at_unix_ms=?1 WHERE module_number=?2", + params![observed, module_number], + ) + .map_err(|error| format!("HOLOLAKE_MODULE_SELF_TEST_WRITE_FAILED: {error}"))?; + let receipt = append_receipt( + &transaction, + "SELF_TEST_PASS", + module_number, + "ACTIVE", + &row.1, + observed, + )?; + transaction + .commit() + .map_err(|error| format!("HOLOLAKE_MODULE_SELF_TEST_COMMIT_FAILED: {error}"))?; + return Ok(receipt); + } + let (target_state, target_digest) = if let Some((_, previous_package, previous)) = + rollback_package + { + let permissions_json = serde_json::to_string(&previous_package.manifest.permissions) + .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_PERMISSIONS_INVALID: {error}"))?; + let manifest_json = serde_json::to_string(&previous_package.manifest) + .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_MANIFEST_INVALID: {error}"))?; + transaction + .execute( + "UPDATE installed_modules SET display_name=?1,version=?2,adapter=?3,state='ROLLBACK_PENDING_SELF_TEST',package_sha256=?4,previous_package_sha256=NULL,permissions_json=?5,manifest_json=?6,updated_at_unix_ms=?7 WHERE module_number=?8", + params![previous_package.manifest.display_name, previous_package.manifest.version, previous_package.manifest.adapter, previous, permissions_json, manifest_json, observed, module_number], + ) + .map_err(|error| format!("HOLOLAKE_MODULE_AUTO_ROLLBACK_WRITE_FAILED: {error}"))?; + ("ROLLBACK_PENDING_SELF_TEST", previous) + } else { + transaction + .execute( + "UPDATE installed_modules SET state='FAILED_CLOSED',updated_at_unix_ms=?1 WHERE module_number=?2", + params![observed, module_number], + ) + .map_err(|error| format!("HOLOLAKE_MODULE_FAIL_CLOSED_WRITE_FAILED: {error}"))?; + ("FAILED_CLOSED", row.1) + }; + let _ = append_receipt( + &transaction, + "SELF_TEST_FAIL", + module_number, + target_state, + &target_digest, + observed, + )?; + transaction + .commit() + .map_err(|error| format!("HOLOLAKE_MODULE_SELF_TEST_FAILURE_COMMIT_FAILED: {error}"))?; + Err(verification + .err() + .unwrap_or_else(|| "HOLOLAKE_MODULE_SELF_TEST_IDENTITY_MISMATCH".into())) +} + +pub async fn unmount_module( + app: AppHandle, + input: ModuleNumberInput, +) -> Result { + transition( + &runtime_root(&app)?, + &input.module_number, + &["ACTIVE", "MOUNTED_PENDING_SELF_TEST", "FAILED_CLOSED"], + "DORMANT", + "UNMOUNT", + ) +} + +pub async fn rollback_module( + app: AppHandle, + input: ModuleNumberInput, +) -> Result { + let root = runtime_root(&app)?; + let mut connection = open_db(&root)?; + let row: (String, Option) = connection + .query_row( + "SELECT package_sha256,previous_package_sha256 FROM installed_modules WHERE module_number=?1", + params![input.module_number], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_READ_FAILED: {error}"))? + .ok_or("HOLOLAKE_MODULE_NOT_INSTALLED")?; + let previous = row.1.ok_or("HOLOLAKE_MODULE_ROLLBACK_NOT_AVAILABLE")?; + let (_, previous_package, verified_previous) = read_verified_package( + &package_file(&root, &previous), + &signature_file(&root, &previous), + RELEASE_TRUST_RAW, + )?; + if verified_previous != previous + || previous_package.manifest.module_number != input.module_number + { + return Err("HOLOLAKE_MODULE_ROLLBACK_PACKAGE_IDENTITY_INVALID".into()); + } + let permissions_json = serde_json::to_string(&previous_package.manifest.permissions) + .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_PERMISSIONS_INVALID: {error}"))?; + let manifest_json = serde_json::to_string(&previous_package.manifest) + .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_MANIFEST_INVALID: {error}"))?; + let observed = now_unix_ms(); + let transaction = connection + .transaction() + .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_BEGIN_FAILED: {error}"))?; + transaction + .execute( + "UPDATE installed_modules SET display_name=?1,version=?2,adapter=?3,state='ROLLBACK_PENDING_SELF_TEST',package_sha256=?4,previous_package_sha256=?5,permissions_json=?6,manifest_json=?7,updated_at_unix_ms=?8 WHERE module_number=?9", + params![previous_package.manifest.display_name, previous_package.manifest.version, previous_package.manifest.adapter, previous, row.0, permissions_json, manifest_json, observed, input.module_number], + ) + .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_WRITE_FAILED: {error}"))?; + let receipt = append_receipt( + &transaction, + "ROLLBACK", + &input.module_number, + "ROLLBACK_PENDING_SELF_TEST", + &previous, + observed, + )?; + transaction + .commit() + .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_COMMIT_FAILED: {error}"))?; + Ok(receipt) +} + +pub async fn get_module_runtime_snapshot(app: AppHandle) -> Result { + let root = runtime_root(&app)?; + snapshot_at(&root) +} + +fn snapshot_at(root: &Path) -> Result { + let connection = open_db(root)?; + verify_receipt_chain(&connection)?; + verify_persisted_modules(&connection, root)?; + let mut statement = connection + .prepare( + "SELECT module_number,display_name,version,adapter,state,package_sha256,previous_package_sha256,permissions_json,updated_at_unix_ms FROM installed_modules ORDER BY module_number", + ) + .map_err(|error| format!("HOLOLAKE_MODULE_SNAPSHOT_PREPARE_FAILED: {error}"))?; + let modules = statement + .query_map([], |row| { + let permissions_json: String = row.get(7)?; + Ok(InstalledModule { + module_number: row.get(0)?, + display_name: row.get(1)?, + version: row.get(2)?, + adapter: row.get(3)?, + state: row.get(4)?, + package_sha256: row.get(5)?, + previous_package_sha256: row.get(6)?, + permissions: serde_json::from_str(&permissions_json).unwrap_or_default(), + user_data_preserved: true, + updated_at_unix_ms: row.get(8)?, + }) + }) + .map_err(|error| format!("HOLOLAKE_MODULE_SNAPSHOT_QUERY_FAILED: {error}"))? + .collect::, _>>() + .map_err(|error| format!("HOLOLAKE_MODULE_SNAPSHOT_ROW_FAILED: {error}"))?; + let receipt_count: u64 = connection + .query_row("SELECT COUNT(*) FROM module_receipts", [], |row| row.get(0)) + .map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_COUNT_FAILED: {error}"))?; + let latest_receipt_hash = connection + .query_row( + "SELECT receipt_hash FROM module_receipts ORDER BY sequence DESC LIMIT 1", + [], + |row| row.get(0), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_TAIL_FAILED: {error}"))? + .unwrap_or_else(|| "0".repeat(64)); + Ok(ModuleRuntimeSnapshot { + schema: "hololake.module-runtime-snapshot/v1", + state: "ACTIVE_SIGNED_DECLARATIVE_PACKAGES_ONLY", + host_version: HOST_VERSION, + package_code_executed: false, + modules, + receipt_count, + latest_receipt_hash, + }) +} + +pub(crate) fn start_on_application_open(app: &AppHandle) -> Result<(), String> { + let _ = contract()?; + let root = runtime_root(app); + match root { + Ok(root) => { + let _ = snapshot_at(&root)?; + Ok(()) + } + Err(error) if error == "HOLOLAKE_AUTHENTICATED_ACCOUNT_REQUIRED" => Ok(()), + Err(error) => Err(error), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn signed_fixture() -> (PathBuf, PathBuf, String) { + let package = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../fixtures/module-packages/HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001-0.1.0.ghmod"); + let signature = package.with_extension("ghmod.sig"); + let digest = sha256_hex(&fs::read(&package).unwrap()); + (package, signature, digest) + } + + fn install_fixture(root: &Path, observed: u64) -> String { + let (package, signature, digest) = signed_fixture(); + install_module_package_at( + root, + &InstallModulePackageInput { + package_path: package.to_string_lossy().into_owned(), + signature_path: signature.to_string_lossy().into_owned(), + expected_package_sha256: digest.clone(), + human_confirmed_permission_expansion: true, + }, + RELEASE_TRUST_RAW, + observed, + ) + .unwrap(); + digest + } + + #[test] + fn contract_keeps_packages_declarative_and_lifecycle_closed() { + let contract = contract().unwrap(); + assert_eq!(contract.registered_adapters.len(), 7); + assert!(!contract.host.arbitrary_native_code); + assert!(!contract.host.arbitrary_webview_javascript); + assert!(contract.host.declarative_payload_only); + } + + #[test] + fn package_number_classes_and_content_digest_are_exact() { + let payload = serde_json::json!({"views":[],"entry":"home"}); + let digest = payload_digest(&payload).unwrap(); + let package = ModulePackage { + schema: PACKAGE_SCHEMA.into(), + manifest: ModuleManifest { + module_number: "HLP-MOD-LOCAL-0001".into(), + registration_class: "PRIVATE_CHANNEL_LOCAL".into(), + display_name: "Local module".into(), + version: "1.0.0".into(), + minimum_host_version: "0.5.0".into(), + adapter: "native-composition-v1".into(), + content_digest: digest.clone(), + permissions: vec!["KNOWLEDGE_READ".into()], + user_data_schema: "hololake.module-data/local/v1".into(), + self_test: ModuleSelfTest { + kind: "DECLARATIVE_SCHEMA_V1".into(), + expected_content_digest: digest, + }, + }, + payload, + }; + let runtime_contract = contract().unwrap(); + validate_package(&package, &runtime_contract).unwrap(); + let mut invalid = package; + invalid.manifest.module_number = "HLP-DONOR-CAND-0001".into(); + assert_eq!( + validate_package(&invalid, &runtime_contract).unwrap_err(), + "HOLOLAKE_MODULE_PACKAGE_CONTRACT_INVALID" + ); + } + + #[test] + fn tampered_persisted_manifest_fails_closed_without_touching_user_data() { + let temporary = tempfile::tempdir().unwrap(); + let root = temporary.path(); + let module_number = "HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001"; + install_fixture(root, 1); + fs::write( + root.join("user-data") + .join(module_number) + .join("human-data.json"), + b"preserve", + ) + .unwrap(); + let connection = open_db(root).unwrap(); + connection + .execute( + "UPDATE installed_modules SET manifest_json='{}' WHERE module_number=?1", + params![module_number], + ) + .unwrap(); + drop(connection); + assert!(snapshot_at(root) + .unwrap_err() + .starts_with("HOLOLAKE_MODULE_PERSISTED_MANIFEST_INVALID")); + assert_eq!( + fs::read( + root.join("user-data") + .join(module_number) + .join("human-data.json") + ) + .unwrap(), + b"preserve" + ); + } + + #[test] + fn tampered_receipt_chain_fails_closed_on_restart() { + let temporary = tempfile::tempdir().unwrap(); + install_fixture(temporary.path(), 1); + let connection = open_db(temporary.path()).unwrap(); + connection + .execute( + "UPDATE module_receipts SET receipt_hash=?1 WHERE sequence=1", + params!["f".repeat(64)], + ) + .unwrap(); + drop(connection); + assert_eq!( + snapshot_at(temporary.path()).unwrap_err(), + "HOLOLAKE_MODULE_RECEIPT_CHAIN_INVALID" + ); + } + + #[test] + fn real_product_signed_fixture_verifies_without_executing_package_code() { + let (fixture, signature, _) = signed_fixture(); + let (_, package, package_sha256) = + read_verified_package(&fixture, &signature, RELEASE_TRUST_RAW).unwrap(); + assert_eq!( + package.manifest.module_number, + "HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001" + ); + assert!(is_sha256(&package_sha256)); + } + + #[test] + fn signed_package_install_mount_self_test_restart_and_unmount_are_receipted() { + let (fixture, signature, expected) = signed_fixture(); + let temporary = tempfile::tempdir().unwrap(); + let module_number = "HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001"; + let mut input = InstallModulePackageInput { + package_path: fixture.to_string_lossy().into_owned(), + signature_path: signature.to_string_lossy().into_owned(), + expected_package_sha256: expected, + human_confirmed_permission_expansion: false, + }; + assert_eq!( + install_module_package_at(temporary.path(), &input, RELEASE_TRUST_RAW, 100) + .unwrap_err(), + "HOLOLAKE_MODULE_PERMISSION_EXPANSION_REQUIRES_HUMAN_CONFIRMATION" + ); + input.human_confirmed_permission_expansion = true; + install_module_package_at(temporary.path(), &input, RELEASE_TRUST_RAW, 101).unwrap(); + transition( + temporary.path(), + module_number, + &["INSTALLED_DORMANT"], + "MOUNTED_PENDING_SELF_TEST", + "MOUNT", + ) + .unwrap(); + self_test_module_at(temporary.path(), module_number, 103).unwrap(); + let user_data = temporary + .path() + .join("user-data") + .join(module_number) + .join("human-data.json"); + fs::write(&user_data, b"preserve-after-unmount").unwrap(); + transition( + temporary.path(), + module_number, + &["ACTIVE"], + "DORMANT", + "UNMOUNT", + ) + .unwrap(); + let reopened = snapshot_at(temporary.path()).unwrap(); + assert_eq!(reopened.modules.len(), 1); + assert_eq!(reopened.modules[0].state, "DORMANT"); + assert_eq!(reopened.receipt_count, 4); + assert_eq!(fs::read(user_data).unwrap(), b"preserve-after-unmount"); + } + + #[cfg(unix)] + #[test] + fn symlinked_package_input_is_rejected_before_signature_processing() { + use std::os::unix::fs::symlink; + + let temporary = tempfile::tempdir().unwrap(); + let source = temporary.path().join("source.ghmod"); + let link = temporary.path().join("link.ghmod"); + let signature = temporary.path().join("dummy.sig"); + fs::write(&source, b"{}").unwrap(); + fs::write(&signature, b"not-a-signature").unwrap(); + symlink(&source, &link).unwrap(); + assert_eq!( + read_verified_package(&link, &signature, RELEASE_TRUST_RAW).unwrap_err(), + "HOLOLAKE_MODULE_PACKAGE_SIZE_INVALID" + ); + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/number_coordinate_tree.rs b/product-source/hololake-native-desktop/src-tauri/src/number_coordinate_tree.rs index 5a55adc10..fbf3e8e86 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/number_coordinate_tree.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/number_coordinate_tree.rs @@ -53,7 +53,7 @@ fn validate_tree() -> Result<(), String> { || tree.record_id != "HLP-UNIFIED-NUMBER-TREE-001" || tree.state != "MACHINE_COMPILED_STARTUP_ENFORCED" || tree.root_number != "HLP-NUMBER-WORLD-ROOT-001" - || tree.route_count != 84 + || tree.route_count != 91 || tree.routes.len() != tree.route_count || !tree.invariants.number_is_stable_coordinate_not_authority || !tree.invariants.path_is_unique_navigation diff --git a/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc.rs b/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc.rs index f7b525992..1e00f8624 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc.rs @@ -934,7 +934,7 @@ mod tests { #[test] fn registry_is_closed_and_contains_every_migrated_command() { let registry = load_registry().unwrap(); - assert_eq!(registry.operations.len(), 62); + assert_eq!(registry.operations.len(), 69); assert!(!registry.runtime.legacy_direct_commands_allowed); } diff --git a/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc_dispatch.rs b/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc_dispatch.rs index b995e275b..4ebb4b87c 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc_dispatch.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc_dispatch.rs @@ -99,6 +99,27 @@ pub(crate) async fn dispatch( ) .await?, ), + "module_package_runtime::get_module_runtime_snapshot" => { + json(crate::module_package_runtime::get_module_runtime_snapshot(app).await?) + } + "module_package_runtime::verify_module_package" => { + json(crate::module_package_runtime::verify_module_package(app, input(&payload)?).await?) + } + "module_package_runtime::install_module_package" => json( + crate::module_package_runtime::install_module_package(app, input(&payload)?).await?, + ), + "module_package_runtime::mount_module" => { + json(crate::module_package_runtime::mount_module(app, input(&payload)?).await?) + } + "module_package_runtime::self_test_module" => { + json(crate::module_package_runtime::self_test_module(app, input(&payload)?).await?) + } + "module_package_runtime::unmount_module" => { + json(crate::module_package_runtime::unmount_module(app, input(&payload)?).await?) + } + "module_package_runtime::rollback_module" => { + json(crate::module_package_runtime::rollback_module(app, input(&payload)?).await?) + } "local_development_bridge::acquire_development_write_lane" => json( crate::local_development_bridge::acquire_development_write_lane(app, input(&payload)?) .await?, diff --git a/product-source/hololake-native-desktop/src/modules/numbered-ipc.ts b/product-source/hololake-native-desktop/src/modules/numbered-ipc.ts index 8aa9ac8e4..1c0b1d341 100644 --- a/product-source/hololake-native-desktop/src/modules/numbered-ipc.ts +++ b/product-source/hololake-native-desktop/src/modules/numbered-ipc.ts @@ -496,6 +496,62 @@ const ROUTES = { "moduleNumber": "HLP-NIPC-MOD-0019", "operationNumber": "HLP-NIPC-OP-0062", "targetNumber": "HLP-NIPC-TGT-0019" + }, + "get_module_runtime_snapshot": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0063", + "targetNumber": "HLP-NIPC-TGT-0020" + }, + "verify_module_package": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0064", + "targetNumber": "HLP-NIPC-TGT-0020" + }, + "install_module_package": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0065", + "targetNumber": "HLP-NIPC-TGT-0020" + }, + "mount_module": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0066", + "targetNumber": "HLP-NIPC-TGT-0020" + }, + "self_test_module": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0067", + "targetNumber": "HLP-NIPC-TGT-0020" + }, + "unmount_module": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0068", + "targetNumber": "HLP-NIPC-TGT-0020" + }, + "rollback_module": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0069", + "targetNumber": "HLP-NIPC-TGT-0020" } } as const