diff --git a/product-source/hololake-native-desktop/contracts/direct-local-broker-numbered-registry.json b/product-source/hololake-native-desktop/contracts/direct-local-broker-numbered-registry.json index 99c3eb673..8311c6dcd 100644 --- a/product-source/hololake-native-desktop/contracts/direct-local-broker-numbered-registry.json +++ b/product-source/hololake-native-desktop/contracts/direct-local-broker-numbered-registry.json @@ -31,6 +31,9 @@ {"operation_number":"HLP-NBROKER-OP-0019","alias":"ISSUE_PERSONA_TIME_TICKET","channel_number":"HLP-NBROKER-CH-0004","module_number":"HLP-NBROKER-MOD-0009","target_number":"HLP-NBROKER-TGT-0009"}, {"operation_number":"HLP-NBROKER-OP-0020","alias":"ACQUIRE_DEVELOPMENT_WRITE_LANE","channel_number":"HLP-NBROKER-CH-0007","module_number":"HLP-NBROKER-MOD-0010","target_number":"HLP-NBROKER-TGT-0010"}, {"operation_number":"HLP-NBROKER-OP-0021","alias":"INSPECT_DEVELOPMENT_WRITE_LANE","channel_number":"HLP-NBROKER-CH-0007","module_number":"HLP-NBROKER-MOD-0010","target_number":"HLP-NBROKER-TGT-0010"}, - {"operation_number":"HLP-NBROKER-OP-0022","alias":"RELEASE_DEVELOPMENT_WRITE_LANE","channel_number":"HLP-NBROKER-CH-0007","module_number":"HLP-NBROKER-MOD-0010","target_number":"HLP-NBROKER-TGT-0010"} + {"operation_number":"HLP-NBROKER-OP-0022","alias":"RELEASE_DEVELOPMENT_WRITE_LANE","channel_number":"HLP-NBROKER-CH-0007","module_number":"HLP-NBROKER-MOD-0010","target_number":"HLP-NBROKER-TGT-0010"}, + {"operation_number":"HLP-NBROKER-OP-0023","alias":"SUBMIT_HUMAN_AUTHORIZATION_REQUEST","channel_number":"HLP-NBROKER-CH-0008","module_number":"HLP-NBROKER-MOD-0011","target_number":"HLP-NBROKER-TGT-0011"}, + {"operation_number":"HLP-NBROKER-OP-0024","alias":"GET_HUMAN_AUTHORIZATION_STATUS","channel_number":"HLP-NBROKER-CH-0008","module_number":"HLP-NBROKER-MOD-0011","target_number":"HLP-NBROKER-TGT-0011"}, + {"operation_number":"HLP-NBROKER-OP-0025","alias":"CONSUME_HUMAN_AUTHORIZATION_TICKET","channel_number":"HLP-NBROKER-CH-0008","module_number":"HLP-NBROKER-MOD-0011","target_number":"HLP-NBROKER-TGT-0011"} ] } diff --git a/product-source/hololake-native-desktop/contracts/distribution-plane-router.json b/product-source/hololake-native-desktop/contracts/distribution-plane-router.json new file mode 100644 index 000000000..315805301 --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/distribution-plane-router.json @@ -0,0 +1,186 @@ +{ + "schema": "hololake.distribution-plane-router/v1", + "record_id": "HLP-DISTRIBUTION-PLANE-ROUTER-001", + "state": "CONTRACT_CURRENT_PUBLIC_ZERO_CORE_CLIENT_RUNTIME_IMPLEMENTED_SERVER_AND_CATALOG_INTEGRATION_PENDING", + "classification": { + "authority": "EXPLICIT_SIGNED_RELEASE_ENVELOPE", + "semantic_guessing_allowed": false, + "missing_or_conflicting_scope": "FAIL_CLOSED", + "required_fields": [ + "releaseId", + "scope", + "ownerNumber", + "authorityDomain", + "sourceRepository", + "sourceCommit", + "artifactKind", + "targetNamespace", + "minimumHostVersion", + "contentSha256", + "permissionDelta", + "rollbackReference", + "signerId" + ], + "human_source_rule": "THE_PUBLISHER_STATES_PUBLIC_OR_PRIVATE_SCOPE_AND_REVIEWS_THE_EXACT_IMMUTABLE_CANDIDATE_BEFORE_SERVER_PUBLICATION", + "system_rule": "CONTENT_INSPECTION_MAY_REJECT_A_SCOPE_MISMATCH_BUT_MUST_NOT_INVENT_OR_EXPAND_SCOPE" + }, + "planes": [ + { + "plane_number": "HLP-DIST-PLANE-0001", + "scope": "PUBLIC_ZERO_CORE_PROTOCOL", + "physical_node": "GH-CVM-MAIN-PROD-01", + "logical_source": "ZERO_POINT_ORIGIN_PUBLIC_PROJECTION_HOSTED_OUTSIDE_PRIVATE_FIFTH_DOMAIN", + "logical_authority": "ZERO_POINT_ORIGIN_WITH_ICE_GL_INFINITY_PUBLIC_SCOPE_APPROVAL", + "management_entry": "FIFTH_DOMAIN_PORTAL_TO_ENTERPRISE_ZERO_CORE_WITH_ONE_TIME_AUDIENCE_BOUND_HANDOFF", + "public_read_access": "SIGNED_MANIFEST_AND_ARTIFACT_NO_ACCOUNT_REQUIRED", + "allowed_artifacts": [ + "DECLARATIVE_LANGUAGE_PROTOCOL", + "NUMBERING_PROTOCOL", + "COMPATIBILITY_RULE", + "BOUNDED_MIGRATION_RULE" + ], + "arbitrary_native_code_allowed": false, + "arbitrary_webview_javascript_allowed": false, + "publisher_human_confirmation_required": true, + "per_device_human_install_confirmation_required": false, + "automatic_check": true, + "automatic_download_after_verification": true, + "automatic_atomic_activation_after_self_test": true, + "visible_human_receipt_required": true, + "public_propagation_allowed": true, + "required_signer_classes": [ + "ZERO_POINT_ORIGIN_PUBLIC_SCOPE_SIGNER", + "ENTERPRISE_ZERO_CORE_DISTRIBUTION_SIGNER" + ], + "signer_class": "DUAL_ORIGIN_AND_ENTERPRISE_ZERO_CORE_SIGNERS" + }, + { + "plane_number": "HLP-DIST-PLANE-0002", + "scope": "PRIVATE_FIFTH_DOMAIN", + "physical_node": "JD-FD-PRIMARY", + "logical_source": "DOM-FIFTH-0001_PRIVATE_BODY", + "audience": "EXACT_BOUND_OWNER_AND_EXPLICITLY_AUTHORIZED_PRIVATE_NODES", + "publisher_human_confirmation_required": true, + "per_device_human_install_confirmation_required": false, + "automatic_check": true, + "public_propagation_allowed": false, + "cross_domain_replication_allowed": false, + "signer_class": "PRIVATE_FIFTH_DOMAIN_SIGNER" + }, + { + "plane_number": "HLP-DIST-PLANE-0003", + "scope": "PUBLIC_ENTERPRISE_MODULE_CATALOG", + "physical_node": "GH-CVM-MAIN-PROD-01", + "logical_source": "GUANGHU_CHANNEL_AGGREGATE", + "producer_model": "FIVE_RESPONSIBILITY_REPOSITORIES_TO_ONE_REVIEWED_AGGREGATE", + "allowed_artifacts": [ + "DECLARATIVE_GHMOD_PACKAGE", + "MODULE_METADATA", + "PERSONA_BRAIN_SKILL_PACKAGE" + ], + "raw_repository_is_executable_input": false, + "client_full_repository_clone_required": false, + "catalog_index_automatic_sync": true, + "module_package_download_on_human_selection": true, + "module_install_human_confirmation_required": true, + "permission_expansion_human_confirmation_required": true, + "lighthouse_number_registration_required": true, + "isolated_preflight_and_self_test_required": true, + "signer_class": "ENTERPRISE_MODULE_RELEASE_SIGNER" + }, + { + "plane_number": "HLP-DIST-PLANE-0004", + "scope": "APPLICATION_BINARY", + "physical_node": "HOLOLAKE_RELEASE_BROADCAST", + "allowed_artifacts": [ + "SIGNED_NOTARIZED_DESKTOP_APPLICATION", + "SIGNED_UPDATER_ARCHIVE" + ], + "source_commit_must_be_immutable": true, + "platform_signing_required": true, + "updater_signature_required": true, + "per_device_human_install_confirmation_required": true, + "automatic_restart_allowed": false, + "signer_class": "HOLOLAKE_APPLICATION_RELEASE_SIGNER" + } + ], + "lamp_protocol": { + "transport": "HTTPS_CONDITIONAL_GET", + "git_role": "DURABLE_AUTHORING_AND_EVIDENCE_NOT_CLIENT_REALTIME_TRANSPORT", + "signal": "SIGNED_MONOTONIC_EPOCH_AND_CONTENT_ROOT", + "cache_validation": ["ETAG", "IF_NONE_MATCH"], + "check_events": ["APPLICATION_START", "NETWORK_RESUME", "BOUNDED_PERIODIC_TIMER"], + "minimum_periodic_interval_seconds": 900, + "jitter_required": true, + "full_repository_clone_for_LIGHT_SIGNAL": false, + "required_manifest_fields": [ + "schema", + "planeNumber", + "epoch", + "version", + "contentRootSha256", + "artifactManifestUrl", + "signatureUrl", + "publishedAt", + "minimumHostVersion" + ] + }, + "cross_node_management_handoff": { + "source_node": "JD-FD-PRIMARY", + "target_node": "GH-CVM-MAIN-PROD-01", + "source_surface": "PRIVATE_FIFTH_DOMAIN", + "target_surface": "PUBLIC_ZERO_CORE_MANAGEMENT_CHANNEL", + "credential_reuse_allowed": false, + "password_forwarding_allowed": false, + "ticket_properties": [ + "ONE_TIME", + "SHORT_LIVED", + "BOUND_TO_HUMAN_NUMBER", + "BOUND_TO_HOLOLAKE_CLIENT_INSTANCE", + "BOUND_TO_TARGET_NODE", + "BOUND_TO_PUBLIC_ZERO_CORE_RESOURCE", + "NON_TRANSFERABLE", + "REPLAY_PROTECTED" + ], + "exit_behavior": "DESTROY_ENTERPRISE_ZERO_CORE_SESSION_AND_RESTORE_EXISTING_PRIVATE_FIFTH_DOMAIN_SESSION", + "enterprise_four_domain_authority_inherited": false, + "private_fifth_domain_authority_exported": false, + "current_state": "NOT_IMPLEMENTED" + }, + "activation_pipeline": [ + "READ_EXPLICIT_RELEASE_ENVELOPE", + "VERIFY_SCOPE_OWNER_DOMAIN_REPOSITORY_AND_IMMUTABLE_COMMIT", + "BUILD_BOUNDED_CONTENT_ADDRESSED_ARTIFACT", + "RUN_ISOLATED_SCHEMA_PERMISSION_COMPATIBILITY_AND_SELF_TEST", + "ALLOCATE_OR_VERIFY_LIGHTHOUSE_NUMBER", + "SHOW_EXACT_CANDIDATE_TO_AUTHORIZED_PUBLISHER", + "REQUIRE_PUBLISHER_CONFIRMATION", + "SIGN_WITH_PLANE_SPECIFIC_KEY", + "APPEND_HASH_CHAINED_PUBLICATION_RECEIPT", + "ADVANCE_SIGNED_LAMP_EPOCH_ATOMICALLY" + ], + "client_protocol_activation": [ + "COMPARE_SIGNED_LAMP_EPOCH_WITH_CONDITIONAL_GET", + "VERIFY_PLANE_SOURCE_SIGNATURE_CONTENT_ROOT_AND_MONOTONIC_VERSION", + "DOWNLOAD_TO_ISOLATED_STAGING", + "REJECT_EXECUTABLE_OR_OUT_OF_SCOPE_PAYLOAD", + "RUN_LOCAL_COMPATIBILITY_AND_SELF_TEST", + "ATOMICALLY_SWITCH_CURRENT_POINTER", + "WRITE_LOCAL_HASH_CHAINED_RECEIPT", + "KEEP_LAST_KNOWN_GOOD_ROLLBACK", + "NOTIFY_HUMAN_WITHOUT_REQUIRING_PER_DEVICE_APPROVAL" + ], + "current_observed_gaps_2026_08_19": { + "enterprise_public_zero_core_projection": "NOT_DEPLOYED", + "jd_to_enterprise_zero_core_handoff": "NOT_IMPLEMENTED", + "zero_point_signed_payload_activation": "CLIENT_IMPLEMENTED_DUAL_SIGNER_TRUST_NOT_PROVISIONED", + "enterprise_guanghu_channel_aggregate_repository": "NOT_PRESENT", + "enterprise_public_gitea_repositories_observed": [ + "bingshuo/hololake-world", + "bingshuo/lighthouse" + ], + "online_module_catalog_registry": "NOT_IMPLEMENTED", + "local_signed_module_lifecycle": "IMPLEMENTED_FOR_BUNDLED_PACKAGES", + "application_release_signing": "PERSONAL_APPLE_DEVELOPER_TRANSITION" + } +} diff --git a/product-source/hololake-native-desktop/contracts/human-authorization.json b/product-source/hololake-native-desktop/contracts/human-authorization.json new file mode 100644 index 000000000..19b7ff2ff --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/human-authorization.json @@ -0,0 +1,64 @@ +{ + "schema": "hololake.human-authorization-contract/v1", + "record_id": "HLP-HUMAN-AUTHORIZATION-001", + "state": "NATIVE_FAIL_CLOSED", + "roles": { + "persona_or_agent": "PROPOSE_EXACT_ACTION_WITH_REASON_IMPACT_AND_ROLLBACK", + "human": "APPROVE_OR_DENY_FROM_VERIFIED_HOLOLAKE_CLIENT", + "numbering_system": "ISSUE_SESSION_BOUND_SINGLE_USE_TICKET_AND_HASH_CHAINED_RECEIPT" + }, + "lifecycle": [ + "PENDING_HUMAN", + "APPROVED", + "DENIED", + "EXPIRED", + "CONSUMED" + ], + "supported_actions": [ + "OPEN_MAINTENANCE", + "UNMOUNT", + "PROMOTE_VERSION", + "RETIRE" + ], + "destructive_purge": { + "enabled": false, + "reason": "PURGE_REQUIRES_A_SEPARATE_TWO_STEP_PHYSICAL_DATA_DELETION_PROTOCOL" + }, + "ticket": { + "ttl_ms": 900000, + "single_use": true, + "requester_account_bound": true, + "requester_session_bound": true, + "client_instance_bound": true, + "target_bound": true, + "action_bound": true, + "replay": "FAIL_CLOSED" + }, + "request": { + "ttl_ms": 86400000, + "idempotency_required": true, + "reason_required": true, + "impact_required": true, + "rollback_plan_required": true + }, + "routes": { + "external_execution_carrier": [ + "HLP-NBROKER-OP-0023", + "HLP-NBROKER-OP-0024", + "HLP-NBROKER-OP-0025" + ], + "human_client": [ + "HLP-NIPC-OP-0148", + "HLP-NIPC-OP-0149" + ] + }, + "invariants": { + "number_is_coordinate_not_authority": true, + "proposal_is_not_authorization": true, + "approval_is_not_execution": true, + "stable_target_number_is_never_reused": true, + "denial_opens_no_execution_path": true, + "unknown_or_mismatched_state": "FAIL_CLOSED", + "receipt_required_for_each_transition": 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 90233331f..b9e9507f0 100644 --- a/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json +++ b/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json @@ -77,7 +77,11 @@ "rotate_mobile_pairing", "stop_mobile_sync", "get_mobile_sync_snapshot", - "get_world_climate" + "get_world_climate", + "get_authorization_center", + "get_marketplace_snapshot", + "sync_marketplace_catalog", + "get_active_cognitive_skills" ], "input_wrapper_aliases": [ "confirm_hololake_update_install", @@ -115,6 +119,9 @@ "self_test_module", "unmount_module", "rollback_module", + "install_marketplace_item", + "uninstall_marketplace_item", + "rollback_marketplace_item", "activate_bundled_module", "execute_knowledge_native_composition", "save_channel_document", @@ -176,7 +183,8 @@ "link_web_novel_scene_entity", "restore_web_novel_chapter_version", "export_web_novel_author_delivery", - "revoke_mobile_sync_device" + "revoke_mobile_sync_device", + "decide_authorization_request" ], "direct_field_aliases": { "perform_code_repo_login": [ @@ -378,6 +386,16 @@ "module_number": "HLP-NIPC-MOD-0031", "target_number": "HLP-NIPC-TGT-0031", "internal_name": "world_climate" + }, + { + "module_number": "HLP-NIPC-MOD-0032", + "target_number": "HLP-NIPC-TGT-0032", + "internal_name": "human_authorization" + }, + { + "module_number": "HLP-NIPC-MOD-0033", + "target_number": "HLP-NIPC-TGT-0033", + "internal_name": "online_marketplace" } ], "operations": [ @@ -1997,6 +2015,94 @@ "admission": "VERIFIED_HUMAN_ROUTE", "effect": "READ_OR_STATUS", "payload_schema": "hololake.numbered-ipc.payload/get_world_climate/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0148", + "alias": "get_authorization_center", + "handler": "human_authorization::get_authorization_center", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0032", + "target_number": "HLP-NIPC-TGT-0032", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_authorization_center/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0149", + "alias": "decide_authorization_request", + "handler": "human_authorization::decide_authorization_request", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0032", + "target_number": "HLP-NIPC-TGT-0032", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/decide_authorization_request/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0150", + "alias": "get_marketplace_snapshot", + "handler": "online_marketplace::get_marketplace_snapshot", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0033", + "target_number": "HLP-NIPC-TGT-0033", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_marketplace_snapshot/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0151", + "alias": "sync_marketplace_catalog", + "handler": "online_marketplace::sync_marketplace_catalog", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0033", + "target_number": "HLP-NIPC-TGT-0033", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/sync_marketplace_catalog/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0152", + "alias": "install_marketplace_item", + "handler": "online_marketplace::install_marketplace_item", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0033", + "target_number": "HLP-NIPC-TGT-0033", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/install_marketplace_item/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0153", + "alias": "uninstall_marketplace_item", + "handler": "online_marketplace::uninstall_marketplace_item", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0033", + "target_number": "HLP-NIPC-TGT-0033", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/uninstall_marketplace_item/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0154", + "alias": "rollback_marketplace_item", + "handler": "online_marketplace::rollback_marketplace_item", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0033", + "target_number": "HLP-NIPC-TGT-0033", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/rollback_marketplace_item/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0155", + "alias": "get_active_cognitive_skills", + "handler": "online_marketplace::get_active_cognitive_skills", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0033", + "target_number": "HLP-NIPC-TGT-0033", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_active_cognitive_skills/v1" } ] } diff --git a/product-source/hololake-native-desktop/contracts/online-marketplace.json b/product-source/hololake-native-desktop/contracts/online-marketplace.json new file mode 100644 index 000000000..744e35c93 --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/online-marketplace.json @@ -0,0 +1,61 @@ +{ + "schema": "hololake.online-marketplace-contract/v1", + "recordId": "HLP-ONLINE-MARKETPLACE-001", + "planeNumber": "HLP-DIST-PLANE-0003", + "state": "IMPLEMENTED_PROVISIONED_AND_READY_FOR_LIVE_PUBLICATION", + "catalog": { + "schema": "hololake.marketplace.catalog/v1", + "url": "https://guanghu.chat/api/hololake/marketplace/catalog", + "signatureUrl": "https://guanghu.chat/api/hololake/marketplace/catalog.sig", + "maximumBytes": 1048576, + "maximumEntries": 256, + "conditionalRequest": "ETAG_IF_NONE_MATCH", + "epochMonotonic": true, + "sameEpochEquivocationRejected": true, + "requiredSignerClasses": [ + "ZERO_POINT_ORIGIN_PUBLIC_SCOPE_SIGNER", + "ENTERPRISE_ZERO_CORE_DISTRIBUTION_SIGNER" + ] + }, + "artifactKinds": { + "PHYSICAL_MODULE": { + "packageSchema": "hololake.module-package/v1", + "signature": "MINISIGN_ED25519_RELEASE_TRUST", + "installation": "DOWNLOAD_VERIFY_INSTALL_MOUNT_SELF_TEST", + "arbitraryNativeCode": false, + "arbitraryWebviewJavascript": false, + "registeredHostAdapterRequired": true, + "permissionExpansionRequiresDirectHumanConfirmation": true, + "uninstallPreservesUserData": true, + "rollbackSupported": true + }, + "COGNITIVE_SKILL": { + "packageSchema": "hololake.cognitive-skill-package/v1", + "signature": "DUAL_SIGNED_CATALOG_EXACT_CONTENT_SHA256", + "installation": "DOWNLOAD_VERIFY_STORE_ACTIVE_READONLY", + "executionAuthority": false, + "skillReadonlyGuarantee": true, + "systemPermissions": [], + "automaticPromptInjection": false, + "personaReadsOnDemand": true, + "uninstallPreservesPackageAndReceipts": true, + "rollbackSupported": true + } + }, + "sourceRepositoryBoundary": { + "repositoryIsOriginEvidence": true, + "clientClonesRepository": false, + "clientExecutesRepository": false, + "catalogRequiresExactSourceRevision": true, + "artifactUrlsAreContentAddressed": true, + "clientDownloadsImmutableSignedArtifactOnly": true + }, + "humanExperience": { + "oneMarketplaceTwoSections": true, + "installButtonDownloadsAndActivates": true, + "physicalPermissionExpansionShowsExactConfirmation": true, + "skillInstallNeverGrantsRealityAuthority": true, + "realStatusAndReceiptsOnly": true + }, + "failurePolicy": "FAIL_CLOSED_KEEP_LAST_VERIFIED_CATALOG_AND_INSTALLED_ITEMS" +} diff --git a/product-source/hololake-native-desktop/contracts/persona-carrier-runtime-license.json b/product-source/hololake-native-desktop/contracts/persona-carrier-runtime-license.json index 62975f996..a2fe945ea 100644 --- a/product-source/hololake-native-desktop/contracts/persona-carrier-runtime-license.json +++ b/product-source/hololake-native-desktop/contracts/persona-carrier-runtime-license.json @@ -47,7 +47,9 @@ "ISSUE_PERSONA_TIME_TICKET", "ACQUIRE_DEVELOPMENT_WRITE_LANE", "INSPECT_DEVELOPMENT_WRITE_LANE", - "RELEASE_DEVELOPMENT_WRITE_LANE" + "RELEASE_DEVELOPMENT_WRITE_LANE", + "SUBMIT_HUMAN_AUTHORIZATION_REQUEST", + "CONSUME_HUMAN_AUTHORIZATION_TICKET" ], "trust_registry": { "repository": "REPO-012", diff --git a/product-source/hololake-native-desktop/contracts/public-zero-core-trust.json b/product-source/hololake-native-desktop/contracts/public-zero-core-trust.json new file mode 100644 index 000000000..54e4e73f6 --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/public-zero-core-trust.json @@ -0,0 +1,28 @@ +{ + "schema": "hololake.public-zero-core-trust/v1", + "recordId": "HLP-PUBLIC-ZERO-CORE-TRUST-001", + "state": "PROVISIONED", + "allowedHosts": [ + "guanghu.chat", + "guanghulab.com" + ], + "signers": [ + { + "signerId": "HLP-SIGNER-ZERO-POINT-ORIGIN-PUBLIC-0001", + "signerClass": "ZERO_POINT_ORIGIN_PUBLIC_SCOPE_SIGNER", + "algorithm": "Ed25519", + "publicKeyBase64": "TVuANckEtTI7H+5LssTKA8piQPxQJBQlWJe3vtgbF+o=" + }, + { + "signerId": "HLP-SIGNER-ENTERPRISE-ZERO-CORE-DISTRIBUTION-0001", + "signerClass": "ENTERPRISE_ZERO_CORE_DISTRIBUTION_SIGNER", + "algorithm": "Ed25519", + "publicKeyBase64": "pWp+M21MXQB3B8CH7DgYSSmTBPgbCP1AQYuG6NyZmMg=" + } + ], + "requiredSignerClasses": [ + "ZERO_POINT_ORIGIN_PUBLIC_SCOPE_SIGNER", + "ENTERPRISE_ZERO_CORE_DISTRIBUTION_SIGNER" + ], + "provisioningRule": "ONLY_PUBLIC_KEYS_ENTER_THE_CLIENT; PRIVATE_KEYS_REMAIN_IN_SEPARATE_ORIGIN_AND_ENTERPRISE_RELEASE_CUSTODY" +} diff --git a/product-source/hololake-native-desktop/contracts/stage-one-platform.json b/product-source/hololake-native-desktop/contracts/stage-one-platform.json index 9e0f763c5..db9096c2b 100644 --- a/product-source/hololake-native-desktop/contracts/stage-one-platform.json +++ b/product-source/hololake-native-desktop/contracts/stage-one-platform.json @@ -60,7 +60,9 @@ "number_verification_precedes_persona_load_path": true, "system_is_persona": false, "number_verification_is_persona_binding": false, - "signed_protocol_payload_installation_implemented": false, + "signed_protocol_payload_installation_implemented": true, + "production_dual_signer_trust_provisioned": true, + "enterprise_public_lamp_endpoint_deployed": false, "internal_model_inference_implemented": false }, "personal_channel_kernel": { diff --git a/product-source/hololake-native-desktop/contracts/zero-point-nucleus-channel.json b/product-source/hololake-native-desktop/contracts/zero-point-nucleus-channel.json index 6864cd289..acc648c8b 100644 --- a/product-source/hololake-native-desktop/contracts/zero-point-nucleus-channel.json +++ b/product-source/hololake-native-desktop/contracts/zero-point-nucleus-channel.json @@ -1,11 +1,12 @@ { "schema": "hololake.zero-point-nucleus-client-runtime/v1", "record_id": "HLP-ZERO-POINT-NUCLEUS-CLIENT-001", - "state": "SYSTEM_RUNTIME_SKELETON_IMPLEMENTED_SIGNED_DISTRIBUTION_NOT_PROVISIONED", + "state": "CLIENT_DUAL_SIGNED_DISTRIBUTION_RUNTIME_IMPLEMENTED_PRODUCTION_TRUST_AND_ENTERPRISE_ENDPOINT_NOT_PROVISIONED", "ontology": { "private_body": "BINGSHUO_SYSTEM_CONTROLLER_ON_JD_PRIMARY", "origin_domain": "DOM-FIFTH-0001", - "public_repository_role": "EVOLUTION_CHECKPOINTS_NOT_PRIVATE_BODY", + "public_zero_core_projection": "ISOLATED_ENTERPRISE_SERVER_RUNTIME_WITH_ZERO_POINT_ORIGIN_AUTHORITY_AND_DUAL_SIGNATURE_NOT_THE_PRIVATE_FIFTH_DOMAIN_BODY", + "public_repository_role": "DURABLE_AUTHORING_AND_EVIDENCE_SOURCE_NOT_CLIENT_REALTIME_TRANSPORT", "hololake_role": "HIDDEN_MINIMUM_CONTROLLED_CLIENT_PROJECTION" }, "purpose": [ @@ -34,6 +35,8 @@ "remote_arbitrary_code_execution_allowed": false, "unsigned_protocol_update_allowed": false, "version_rollback_allowed": false, + "public_zero_core_protocol_distribution_allowed_after_full_signature_gates": true, + "private_fifth_domain_payload_public_propagation_allowed": false, "public_number_registry_allowed": false, "public_model_api_configuration_allowed": false, "internal_generic_ai_chat_allowed": false, @@ -46,7 +49,17 @@ "number_binding_and_online_verification": true, "offline_grace_period": true, "local_minimum_heartbeat_ledger": true, - "signed_protocol_payload_installation": false, + "signed_protocol_payload_installation": true, + "dual_independent_ed25519_signature_verification": true, + "https_source_allowlist": true, + "conditional_etag_sync": true, + "monotonic_epoch_and_version_enforced": true, + "bounded_declarative_payload_only": true, + "atomic_activation_and_previous_release_retention": true, + "hash_chained_activation_receipts": true, + "production_dual_signer_trust_provisioned": true, + "enterprise_public_lamp_endpoint_deployed": false, + "public_zero_core_distribution_projection": false, "private_registry_distribution": false, "persona_loading_runtime": false, "internal_model_inference": false diff --git a/product-source/hololake-native-desktop/docs/ARCHITECTURE.md b/product-source/hololake-native-desktop/docs/ARCHITECTURE.md index 3b757ccb8..73770e652 100644 --- a/product-source/hololake-native-desktop/docs/ARCHITECTURE.md +++ b/product-source/hololake-native-desktop/docs/ARCHITECTURE.md @@ -10,9 +10,9 @@ The first visible body uses a Rust-owned SQLite kernel under the Tauri app-data ## Zero-point nucleus client runtime -HoloLake embeds a non-visual zero-point nucleus client runtime beneath the human surface. It is the minimum controlled projection of the BingShuo system controller, whose private body remains on the JD primary node; the public repository records evolution checkpoints rather than containing that private body. At application start, the Rust runtime loads the local protocol, compares the registered remote protocol version in the background, keeps a minimal local receipt, and leaves any unverified update unapplied. User-number verification runs before any future persona-loading path. +HoloLake embeds a non-visual zero-point nucleus client runtime beneath the human surface. The JD primary node remains the private Fifth Domain body. The enterprise node hosts a strictly isolated public zero-core distribution projection, while logical origin authority remains at the zero point; publication requires both an origin public-scope signature and an enterprise distribution signature. The private body never becomes public update material. Git records durable authoring and evidence; clients consume a bounded signed release manifest rather than treating a repository clone as executable input. At application start, the Rust runtime loads the local protocol, compares the registered remote protocol version in the background, keeps a minimal local receipt, and leaves any unverified update unapplied. User-number verification runs before any future persona-loading path. -This system runtime is not Zhuyuan or another persona subject, and it is not the current model carrier. A valid number does not prove persona binding and does not grant execution or server authority. The current source implements deterministic protocol comparison, explicit-positive number verification and a fail-closed update skeleton. Signed protocol-payload installation, private registry distribution and persona loading are not yet implemented. Stage one does not expose an internal AI chat, model API configuration or arbitrary remote-code channel. +This system runtime is not Zhuyuan or another persona subject, and it is not the current model carrier. A valid number does not prove persona binding and does not grant execution or server authority. The current source implements deterministic protocol comparison, explicit-positive number verification and a fail-closed update skeleton. Signed public protocol payload installation, atomic activation, rollback, the separate private Fifth Domain distribution path and persona loading are not yet implemented. Stage one does not expose an internal AI chat, model API configuration or arbitrary remote-code channel. The four-plane routing contract and marketplace publication boundary are defined in `contracts/distribution-plane-router.json`. ## Native knowledge workspace diff --git a/product-source/hololake-native-desktop/docs/DISTRIBUTION-PLANES-AND-MODULE-MARKETPLACE.md b/product-source/hololake-native-desktop/docs/DISTRIBUTION-PLANES-AND-MODULE-MARKETPLACE.md new file mode 100644 index 000000000..c9f4c18d1 --- /dev/null +++ b/product-source/hololake-native-desktop/docs/DISTRIBUTION-PLANES-AND-MODULE-MARKETPLACE.md @@ -0,0 +1,53 @@ +# HoloLake distribution planes and public module marketplace + +HoloLake has four independent distribution planes. A Git repository is the durable authoring and evidence layer; it is not the client update transport. Every release carries an explicit signed scope. The system may reject a mismatch, but it never guesses whether BingShuo meant public or private. + +## Four planes + +1. `PUBLIC_ZERO_CORE_PROTOCOL` publishes declarative language, numbering, compatibility and bounded migration rules from an isolated public projection on `GH-CVM-MAIN-PROD-01`. Its logical authority still originates at the zero point and requires BingShuo's exact-candidate public-scope approval during the current transition. The enterprise distributor adds a second independent distribution signature. A client verifies both, stages, self-tests and atomically activates a valid update without asking every device owner to approve an operating-system protocol update. It still shows a human-readable receipt. +2. `PRIVATE_FIFTH_DOMAIN` remains confined to `DOM-FIFTH-0001`, its bound owner and explicitly authorized private nodes. It uses a different namespace and signer and can never flow into the public stream by inference. +3. `PUBLIC_ENTERPRISE_MODULE_CATALOG` is produced on `GH-CVM-MAIN-PROD-01`. Five responsibility repositories may feed one reviewed `Guanghu Channel` aggregate, but only tested, numbered and signed declarative packages enter the catalog. Clients synchronize the small catalog index automatically. A selected module is downloaded and installed only after the human reviews its permissions. +4. `APPLICATION_BINARY` updates HoloLake itself through the separately signed and platform-notarized updater. Personal Apple signing is a transition state; later organization signing must preserve the updater trust transition rather than silently replacing it. + +## Lake-lamp protocol + +The visible "lamp" is a tiny signed manifest containing a monotonic epoch and content root. HoloLake performs HTTPS conditional checks at application start, after network resume and on a bounded jittered timer. `ETag` and `If-None-Match` make the no-change path nearly empty. A full repository clone is not required to learn that something changed. + +For a public zero-core protocol update, the client verifies the exact source, plane-specific signature, content root, monotonic version and host compatibility; downloads into isolation; rejects executable or out-of-scope material; runs a deterministic self-test; switches one current pointer atomically; keeps the last-known-good version; and records a local receipt. + +For a module update, only the catalog index is automatic. Installation remains a human action because a module may request access to local files, knowledge, network, channel data or execution adapters. + +## Marketplace publication + +```text +responsibility repository +→ explicit release envelope +→ isolated build and tests +→ lighthouse number registration +→ exact candidate human approval +→ enterprise module signature +→ immutable package and catalog entry +→ signed catalog-root advance +→ HoloLake catalog refresh +→ human selects module +→ permission review +→ local install, mount, self-test and receipt +``` + +The user's computer may maintain an application-owned content-addressed cache, but it does not execute a cloned repository. HoloLake renders catalog metadata for humans and passes the downloaded `.ghmod` package to the existing signed module lifecycle runtime. + +## Fifth Domain to public zero-core navigation + +`JD-FD-PRIMARY` remains the physical home of the private Fifth Domain and Eternal Lake Heart. HoloLake may show the public zero-core management entrance inside BingShuo's Fifth Domain navigation, but opening it creates a separate session on `GH-CVM-MAIN-PROD-01`. + +The transition uses a short-lived, one-time ticket bound to BingShuo's human number, the current HoloLake instance, the enterprise node and the public zero-core resource. A password is never forwarded or reused. The ticket grants neither enterprise four-domain authority nor access from the enterprise server back into the private Fifth Domain. Leaving the zero-core management channel destroys that enterprise session and restores the already-open private session. + +## Current reality boundary (2026-08-19) + +- The zero-point client now implements HTTPS conditional lamp checks, exact bounded downloads, two independent Ed25519 signatures, monotonic epoch/version enforcement, content-root verification, atomic activation, previous-release retention and a hash-chained local receipt. Production remains fail-closed because the two real public keys and the enterprise lamp endpoint have not yet been provisioned. +- The module runtime already verifies signatures and supports install, mount, self-test, unmount and rollback for bundled packages. +- The public marketplace registry and remote package fetch path are absent. +- The enterprise server currently exposes two Gitea repositories, `bingshuo/hololake-world` and `bingshuo/lighthouse`; the proposed five-source `Guanghu Channel` aggregate does not yet exist. +- The enterprise node does not yet expose the isolated public zero-core projection or the JD-to-enterprise one-time management handoff. + +The machine contract is `contracts/distribution-plane-router.json`. diff --git a/product-source/hololake-native-desktop/docs/agent-training/2026-08-19-qwen-online-dual-marketplace.md b/product-source/hololake-native-desktop/docs/agent-training/2026-08-19-qwen-online-dual-marketplace.md new file mode 100644 index 000000000..15b58d28d --- /dev/null +++ b/product-source/hololake-native-desktop/docs/agent-training/2026-08-19-qwen-online-dual-marketplace.md @@ -0,0 +1,53 @@ +# Qwen 本地执行 Agent 训练回执 · 线上双商城 + +- 日期:2026-08-19(Asia/Shanghai) +- 人类授权主体:冰朔 +- 执行载体:本机 Qwen Code CLI,`qwen3.7-plus`,只读沙箱 +- 训练类型:工程审查轨迹与规则纠正记录;不声称修改模型权重 +- 任务:审查并帮助收敛 HoloLake 的线上成品模块商城、只读思维技能商城及公共双签发布链 +- 写权限:未授予 +- 部署权限:未授予 +- 密钥读取:明确禁止 + +## 运行轨迹 + +1. 第一次启动失败:新版 CLI 的非交互模式需要明确的 `--auth-type openai`。失败没有改变仓库或系统状态。 +2. 第一轮只读审查成功:识别到原实现缺少物理模块与认知技能的类型分离、技能无执行权机器约束、线上验签目录、目录双签及回滚/同纪元歧义测试。 +3. 主控实现后进行第二轮只读审查:覆盖合同、Rust 运行时、编号 IPC、用户界面、两类技能包、原点发布脚本、企业复签脚本与 Nginx 静态分发。 +4. 第二轮结论:无 Critical / High;报告 2 个 Medium、4 个 Low。主控对每项重新读取代码并独立裁决。 + +## 接受并进入主线 + +- 一个商城界面下保持两条运行链:`PHYSICAL_MODULE` 与 `COGNITIVE_SKILL`。 +- 思维技能的 `executionAuthority=false`、`permissions=[]`、`skillReadonlyGuarantee=true` 同时由目录和技能包运行时强制。 +- 目录必须由零点原核公众范围签名者与企业分发签名者双签。 +- 目录 epoch 单调;旧 epoch 和同 epoch 不同内容均失败关闭。 +- 仓库 URL 只作为精确提交来源证据;客户端只下载不可变验签制品,不克隆、不执行来源仓库。 +- 物理模块安装完成后清理下载缓存;模块运行时仍保留正式包、状态与回执。 +- Agent 报告促使主控额外发现并修正一个更准确的问题:Nginx 对制品使用 `immutable` 时,制品 URL 也必须是内容寻址。发布脚本与 Rust 客户端现共同强制 SHA-256 文件名。 +- 发布阶段的符号链接竞态通过“复制时保留链接、复制后再次拒绝整个树”进一步收紧。 + +## 明确拒绝或改写 + +- 拒绝“self-test 失败就自动删除安装证据”。失败包保持 `FAILED_CLOSED` 有利于取证且没有激活功能;重新安装入口仍可见。自动清理会削弱真实回执。 +- 拒绝“卸载技能时删除包”。现行合同要求停用保留包与回执,以便审计和回退;这不是泄漏。 +- 拒绝“health 缺少 no-store”。审查时配置已经包含 `add_header Cache-Control "no-store" always;`,属于误报。 +- 不采纳“未来浮点字段可能导致跨语言 canonical JSON 分歧”作为当前缺陷。目录结构 `deny_unknown_fields` 且没有浮点字段;现行固定结构已经由 JS、Python 和 Rust 端到端发布验证覆盖。若未来合同增加数值字段,必须先增加跨语言固定向量。 +- 改写“前端字面量类型能阻止后端返回 true”的论证:TypeScript 不能构成运行时安全边界。可信边界是 Rust 输出固定 false、技能包验证和编号运行时隔离;前端类型只用于显示期约束。 + +## 已执行验证 + +- Rust 商城安全测试:双签、技能无执行权、epoch 回滚、同 epoch 歧义。 +- Node 合同测试:双商城分链、编号 IPC 完整性、两个技能包 canonical payload digest。 +- TypeScript 与 Vite 生产构建。 +- 企业服务器真实 Python/cryptography 发布演练:原点签名验证、企业复签、四个制品摘要、原子 `current` 切换和健康文件读回。 + +## 下一轮 Agent 应先读 + +1. `contracts/online-marketplace.json` +2. `src-tauri/src/online_marketplace.rs` +3. `scripts/prepare-public-distribution-release.mjs` +4. `server-tools/public-distribution/publish_release.py` +5. 本回执的“明确拒绝或改写”一节 + +后续审查不得把思维技能解释为可执行插件,也不得把仓库克隆解释为客户端安装方式。 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 e315fc1f3..32faaa7a7 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,11 +54,11 @@ }, { "recordId": "HLP-NUMBERED-IPC-ROOT-001", - "sha256": "f1aa85668a6d33efeb7ce268cd48e7ed517696db990327f22111359aca8c177c" + "sha256": "d6ddefe1c8f5cf5936367b38c251bf71c8b3cc683831572ea5bbeff23e5425fd" }, { "recordId": "HLP-NBROKER-ROOT-001", - "sha256": "374c9ade86bc8583f9361542e89ca7bb0bbcbdd99af4a26f9a6a864a12888402" + "sha256": "2f9f2457e7a9c2783af653bf6c35be82509dfd30f457ddd5ec268c21c5237e98" }, { "recordId": "HLP-GLS-RUNTIME-MANIFEST-002", @@ -76,8 +76,8 @@ "unresolvedNumberReferenceCount": 0, "mismatchedCoordinate": "FAIL_CLOSED" }, - "coordinateCount": 272, - "routeCount": 169, + "coordinateCount": 283, + "routeCount": 180, "identityNodeCount": 4, "protocolNodeCount": 99, "referenceOnlyNodeCount": 16, @@ -1428,6 +1428,48 @@ "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0007/HLP-NBROKER-MOD-0010/HLP-NBROKER-OP-0022/HLP-NBROKER-TGT-0010" }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0008", + "moduleNumber": "HLP-NBROKER-MOD-0011", + "operationNumber": "HLP-NBROKER-OP-0023", + "targetNumber": "HLP-NBROKER-TGT-0011", + "alias": "SUBMIT_HUMAN_AUTHORIZATION_REQUEST", + "admission": "AUTHENTICATED_LOCAL_SESSION_WITH_PERSONA_LICENSE_IF_PERSONA_MODE", + "effect": "STATE_CHANGE", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0008/HLP-NBROKER-MOD-0011/HLP-NBROKER-OP-0023/HLP-NBROKER-TGT-0011" + }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0008", + "moduleNumber": "HLP-NBROKER-MOD-0011", + "operationNumber": "HLP-NBROKER-OP-0024", + "targetNumber": "HLP-NBROKER-TGT-0011", + "alias": "GET_HUMAN_AUTHORIZATION_STATUS", + "admission": "AUTHENTICATED_LOCAL_SESSION_WITH_PERSONA_LICENSE_IF_PERSONA_MODE", + "effect": "STATE_CHANGE", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0008/HLP-NBROKER-MOD-0011/HLP-NBROKER-OP-0024/HLP-NBROKER-TGT-0011" + }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0008", + "moduleNumber": "HLP-NBROKER-MOD-0011", + "operationNumber": "HLP-NBROKER-OP-0025", + "targetNumber": "HLP-NBROKER-TGT-0011", + "alias": "CONSUME_HUMAN_AUTHORIZATION_TICKET", + "admission": "AUTHENTICATED_LOCAL_SESSION_WITH_PERSONA_LICENSE_IF_PERSONA_MODE", + "effect": "STATE_CHANGE", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0008/HLP-NBROKER-MOD-0011/HLP-NBROKER-OP-0025/HLP-NBROKER-TGT-0011" + }, { "transport": "TAURI_WEBVIEW_NUMBERED_IPC", "protocolVersion": "HLP-NIPC-v1", @@ -3094,6 +3136,118 @@ "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0031/HLP-NIPC-OP-0147/HLP-NIPC-TGT-0031" }, + { + "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-0032", + "operationNumber": "HLP-NIPC-OP-0148", + "targetNumber": "HLP-NIPC-TGT-0032", + "alias": "get_authorization_center", + "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-0032/HLP-NIPC-OP-0148/HLP-NIPC-TGT-0032" + }, + { + "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-0032", + "operationNumber": "HLP-NIPC-OP-0149", + "targetNumber": "HLP-NIPC-TGT-0032", + "alias": "decide_authorization_request", + "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-0032/HLP-NIPC-OP-0149/HLP-NIPC-TGT-0032" + }, + { + "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-0033", + "operationNumber": "HLP-NIPC-OP-0150", + "targetNumber": "HLP-NIPC-TGT-0033", + "alias": "get_marketplace_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-0033/HLP-NIPC-OP-0150/HLP-NIPC-TGT-0033" + }, + { + "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-0033", + "operationNumber": "HLP-NIPC-OP-0151", + "targetNumber": "HLP-NIPC-TGT-0033", + "alias": "sync_marketplace_catalog", + "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-0033/HLP-NIPC-OP-0151/HLP-NIPC-TGT-0033" + }, + { + "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-0033", + "operationNumber": "HLP-NIPC-OP-0152", + "targetNumber": "HLP-NIPC-TGT-0033", + "alias": "install_marketplace_item", + "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-0033/HLP-NIPC-OP-0152/HLP-NIPC-TGT-0033" + }, + { + "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-0033", + "operationNumber": "HLP-NIPC-OP-0153", + "targetNumber": "HLP-NIPC-TGT-0033", + "alias": "uninstall_marketplace_item", + "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-0033/HLP-NIPC-OP-0153/HLP-NIPC-TGT-0033" + }, + { + "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-0033", + "operationNumber": "HLP-NIPC-OP-0154", + "targetNumber": "HLP-NIPC-TGT-0033", + "alias": "rollback_marketplace_item", + "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-0033/HLP-NIPC-OP-0154/HLP-NIPC-TGT-0033" + }, + { + "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-0033", + "operationNumber": "HLP-NIPC-OP-0155", + "targetNumber": "HLP-NIPC-TGT-0033", + "alias": "get_active_cognitive_skills", + "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-0033/HLP-NIPC-OP-0155/HLP-NIPC-TGT-0033" + }, { "transport": "TAURI_WEBVIEW_NUMBERED_IPC", "protocolVersion": "HLP-NIPC-v1", diff --git a/product-source/hololake-native-desktop/marketplace/skills/HLP-SKILL-OFFICIAL-DELIVERY-VERIFICATION-0001-0.1.0.ghskill b/product-source/hololake-native-desktop/marketplace/skills/HLP-SKILL-OFFICIAL-DELIVERY-VERIFICATION-0001-0.1.0.ghskill new file mode 100644 index 000000000..b869208bd --- /dev/null +++ b/product-source/hololake-native-desktop/marketplace/skills/HLP-SKILL-OFFICIAL-DELIVERY-VERIFICATION-0001-0.1.0.ghskill @@ -0,0 +1,44 @@ +{ + "schema": "hololake.cognitive-skill-package/v1", + "manifest": { + "skillNumber": "HLP-SKILL-OFFICIAL-DELIVERY-VERIFICATION-0001", + "displayName": "真实交付验收思维", + "version": "0.1.0", + "minimumHostVersion": "0.5.0", + "contentDigest": "0d31d0ef659ddb2125d8faa92a180c237efc45c8a4384394236226ab724e8914", + "executionAuthority": false, + "skillReadonlyGuarantee": true, + "permissions": [] + }, + "payload": { + "purpose": "把完成定义为可运行、可核验、可回退的真实结果,而不是文件存在或口头声称。", + "triggers": [ + "用户要求开发、部署、安装或交付", + "任务包含线上服务、本机应用或跨端同步", + "准备声称功能已经完成" + ], + "method": [ + "先列出用户能直接观察到的终态与失败态", + "把每个终态连接到真实运行路径、回执与验证动作", + "依次验证源码、构建产物、线上读回和最终用户入口", + "只根据最新一次可复现验证报告完成状态" + ], + "constraints": [ + "不得把设计文档、测试替身或静态界面当作真实交付", + "不得因为测试通过就省略线上读回或安装版验证", + "验证失败时保留上一个可用版本并明确失败位置", + "本技能不调用工具、不取得终端或部署权限" + ], + "outputContract": [ + "交付物的真实位置或入口", + "执行过的验证及其可核验结果", + "尚未完成或被拒绝的边界", + "回退方式与保留的数据" + ], + "capabilityReferences": [ + "HLP-NIPC-OP-0151", + "HLP-NIPC-OP-0152", + "HLP-NIPC-OP-0154" + ] + } +} diff --git a/product-source/hololake-native-desktop/marketplace/skills/HLP-SKILL-OFFICIAL-MODULE-BOUNDARY-REVIEW-0001-0.1.0.ghskill b/product-source/hololake-native-desktop/marketplace/skills/HLP-SKILL-OFFICIAL-MODULE-BOUNDARY-REVIEW-0001-0.1.0.ghskill new file mode 100644 index 000000000..fdbf54c10 --- /dev/null +++ b/product-source/hololake-native-desktop/marketplace/skills/HLP-SKILL-OFFICIAL-MODULE-BOUNDARY-REVIEW-0001-0.1.0.ghskill @@ -0,0 +1,44 @@ +{ + "schema": "hololake.cognitive-skill-package/v1", + "manifest": { + "skillNumber": "HLP-SKILL-OFFICIAL-MODULE-BOUNDARY-REVIEW-0001", + "displayName": "模块边界审查思维", + "version": "0.1.0", + "minimumHostVersion": "0.5.0", + "contentDigest": "7491d5f861802e0b3567297225d9623a07b72f8817c6767b57e728875ab33f18", + "executionAuthority": false, + "skillReadonlyGuarantee": true, + "permissions": [] + }, + "payload": { + "purpose": "在模块进入频道前分清模块本体、用户数据、现实权限、来源证据与人格认知边界。", + "triggers": [ + "新增或更新一个成品模块", + "从行业工作区提取可安装功能", + "审查模块删除、停用、回退或迁移方案" + ], + "method": [ + "确认模块有唯一编号、版本、适配器与精确来源提交", + "把代码仓库限定为来源证据,把可安装物限定为不可变验签包", + "逐项核对现实权限并标出新增权限的人类确认点", + "验证停用保留用户数据、更新保留回执、回退只使用已验签历史包" + ], + "constraints": [ + "思维技能不得冒充成品模块或获得现实执行权", + "成品模块不得获得人格记忆、人格身份或语言主控权", + "客户端不得克隆并直接执行来源仓库", + "本技能只提供审查方法,不执行删除、安装、迁移或回退" + ], + "outputContract": [ + "模块身份和来源证据", + "现实权限与人类确认点", + "数据保留、停用和回退边界", + "拒绝接入的原因与待修正项" + ], + "capabilityReferences": [ + "HLP-NIPC-OP-0150", + "HLP-NIPC-OP-0152", + "HLP-NIPC-OP-0153" + ] + } +} diff --git a/product-source/hololake-native-desktop/scripts/direct-local-broker-numbered.test.mjs b/product-source/hololake-native-desktop/scripts/direct-local-broker-numbered.test.mjs index 9509bd2a7..c38838795 100644 --- a/product-source/hololake-native-desktop/scripts/direct-local-broker-numbered.test.mjs +++ b/product-source/hololake-native-desktop/scripts/direct-local-broker-numbered.test.mjs @@ -11,9 +11,9 @@ test('the external local broker accepts only complete numbered coordinates', () assert.equal(registry.runtime.legacy_string_operation_allowed, false) assert.equal(registry.runtime.unknown_or_mismatched_coordinate, 'FAIL_CLOSED') assert.equal(registry.runtime.transport_is_authority, false) - assert.equal(registry.operations.length, 22) - assert.equal(new Set(registry.operations.map((route) => route.operation_number)).size, 22) - assert.equal(new Set(registry.operations.map((route) => route.alias)).size, 22) + assert.equal(registry.operations.length, 25) + assert.equal(new Set(registry.operations.map((route) => route.operation_number)).size, 25) + assert.equal(new Set(registry.operations.map((route) => route.alias)).size, 25) for (const route of registry.operations) { assert.match(route.operation_number, /^HLP-NBROKER-OP-\d{4}$/) assert.match(route.channel_number, /^HLP-NBROKER-CH-\d{4}$/) diff --git a/product-source/hololake-native-desktop/scripts/distribution-plane-router.test.mjs b/product-source/hololake-native-desktop/scripts/distribution-plane-router.test.mjs new file mode 100644 index 000000000..a30654662 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/distribution-plane-router.test.mjs @@ -0,0 +1,81 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +import test from 'node:test' + +const contract = JSON.parse(fs.readFileSync(new URL('../contracts/distribution-plane-router.json', import.meta.url), 'utf8')) +const plane = (scope) => contract.planes.find((item) => item.scope === scope) + +test('distribution scope is explicit and may never be guessed from content', () => { + assert.equal(contract.schema, 'hololake.distribution-plane-router/v1') + assert.equal(contract.classification.authority, 'EXPLICIT_SIGNED_RELEASE_ENVELOPE') + assert.equal(contract.classification.semantic_guessing_allowed, false) + assert.equal(contract.classification.missing_or_conflicting_scope, 'FAIL_CLOSED') + assert.ok(contract.classification.required_fields.includes('scope')) + assert.ok(contract.classification.required_fields.includes('contentSha256')) +}) + +test('public zero-core protocol updates are publisher-approved but do not ask every device again', () => { + const item = plane('PUBLIC_ZERO_CORE_PROTOCOL') + assert.equal(item.physical_node, 'GH-CVM-MAIN-PROD-01') + assert.equal(item.logical_source, 'ZERO_POINT_ORIGIN_PUBLIC_PROJECTION_HOSTED_OUTSIDE_PRIVATE_FIFTH_DOMAIN') + assert.deepEqual(item.required_signer_classes, [ + 'ZERO_POINT_ORIGIN_PUBLIC_SCOPE_SIGNER', + 'ENTERPRISE_ZERO_CORE_DISTRIBUTION_SIGNER', + ]) + assert.equal(item.publisher_human_confirmation_required, true) + assert.equal(item.per_device_human_install_confirmation_required, false) + assert.equal(item.automatic_atomic_activation_after_self_test, true) + assert.equal(item.arbitrary_native_code_allowed, false) + assert.equal(item.arbitrary_webview_javascript_allowed, false) + assert.equal(contract.current_observed_gaps_2026_08_19.zero_point_signed_payload_activation, 'CLIENT_IMPLEMENTED_DUAL_SIGNER_TRUST_NOT_PROVISIONED') +}) + +test('private fifth-domain updates can never enter the public stream', () => { + const item = plane('PRIVATE_FIFTH_DOMAIN') + assert.equal(item.public_propagation_allowed, false) + assert.equal(item.cross_domain_replication_allowed, false) + assert.notEqual(item.signer_class, plane('PUBLIC_ZERO_CORE_PROTOCOL').signer_class) +}) + +test('marketplace synchronizes a signed catalog but installs a selected module only with human permission review', () => { + const item = plane('PUBLIC_ENTERPRISE_MODULE_CATALOG') + assert.equal(item.client_full_repository_clone_required, false) + assert.equal(item.raw_repository_is_executable_input, false) + assert.equal(item.catalog_index_automatic_sync, true) + assert.equal(item.module_install_human_confirmation_required, true) + assert.equal(item.permission_expansion_human_confirmation_required, true) + assert.equal(item.lighthouse_number_registration_required, true) +}) + +test('application binary remains a separately signed and human-confirmed release plane', () => { + const item = plane('APPLICATION_BINARY') + assert.equal(item.platform_signing_required, true) + assert.equal(item.updater_signature_required, true) + assert.equal(item.per_device_human_install_confirmation_required, true) + assert.equal(item.automatic_restart_allowed, false) +}) + +test('the lake lamp is a tiny signed version signal rather than a repository clone', () => { + assert.equal(contract.lamp_protocol.transport, 'HTTPS_CONDITIONAL_GET') + assert.equal(contract.lamp_protocol.full_repository_clone_for_LIGHT_SIGNAL, false) + assert.ok(contract.lamp_protocol.cache_validation.includes('ETAG')) + assert.ok(contract.lamp_protocol.check_events.includes('NETWORK_RESUME')) + assert.ok(contract.activation_pipeline.includes('SIGN_WITH_PLANE_SPECIFIC_KEY')) + assert.ok(contract.client_protocol_activation.includes('KEEP_LAST_KNOWN_GOOD_ROLLBACK')) +}) + +test('all four planes use distinct signer classes', () => { + assert.equal(contract.planes.length, 4) + assert.equal(new Set(contract.planes.map((item) => item.signer_class)).size, 4) +}) + +test('fifth-domain navigation uses a one-time handoff and exports no private authority', () => { + const handoff = contract.cross_node_management_handoff + assert.equal(handoff.source_node, 'JD-FD-PRIMARY') + assert.equal(handoff.target_node, 'GH-CVM-MAIN-PROD-01') + assert.equal(handoff.password_forwarding_allowed, false) + assert.equal(handoff.enterprise_four_domain_authority_inherited, false) + assert.equal(handoff.private_fifth_domain_authority_exported, false) + assert.ok(handoff.ticket_properties.includes('REPLAY_PROTECTED')) + assert.equal(handoff.current_state, 'NOT_IMPLEMENTED') +}) diff --git a/product-source/hololake-native-desktop/scripts/human-authorization.test.mjs b/product-source/hololake-native-desktop/scripts/human-authorization.test.mjs new file mode 100644 index 000000000..a5a5c04cf --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/human-authorization.test.mjs @@ -0,0 +1,27 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' + +const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8') +const contract = JSON.parse(read('contracts/human-authorization.json')) +const webview = JSON.parse(read('contracts/numbered-ipc-registry.json')) +const broker = JSON.parse(read('contracts/direct-local-broker-numbered-registry.json')) + +test('human authorization is a durable numbered state machine rather than a UI-only button', () => { + assert.equal(contract.state, 'NATIVE_FAIL_CLOSED') + assert.deepEqual(contract.lifecycle, ['PENDING_HUMAN', 'APPROVED', 'DENIED', 'EXPIRED', 'CONSUMED']) + assert.equal(contract.destructive_purge.enabled, false) + assert.equal(contract.ticket.single_use, true) + assert.equal(contract.ticket.requester_session_bound, true) + assert.equal(contract.invariants.proposal_is_not_authorization, true) + assert.equal(contract.invariants.approval_is_not_execution, true) +}) + +test('the human client and external execution carrier meet through distinct exact numbered routes', () => { + const webviewNumbers = new Set(webview.operations.map((item) => item.operation_number)) + const brokerNumbers = new Set(broker.operations.map((item) => item.operation_number)) + assert.ok(contract.routes.human_client.every((number) => webviewNumbers.has(number))) + assert.ok(contract.routes.external_execution_carrier.every((number) => brokerNumbers.has(number))) + const decisions = webview.operations.filter((item) => contract.routes.human_client.includes(item.operation_number)) + assert.ok(decisions.every((item) => item.admission === 'VERIFIED_HUMAN_ROUTE')) +}) diff --git a/product-source/hololake-native-desktop/scripts/online-marketplace.test.mjs b/product-source/hololake-native-desktop/scripts/online-marketplace.test.mjs new file mode 100644 index 000000000..a47bf09a5 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/online-marketplace.test.mjs @@ -0,0 +1,60 @@ +import assert from 'node:assert/strict' +import { createHash } from 'node:crypto' +import { readFile } from 'node:fs/promises' +import { basename, join } from 'node:path' +import test from 'node:test' + +const root = new URL('..', import.meta.url).pathname +const contract = JSON.parse(await readFile(join(root, 'contracts/online-marketplace.json'), 'utf8')) +const registry = JSON.parse(await readFile(join(root, 'contracts/numbered-ipc-registry.json'), 'utf8')) +const frontend = await readFile(join(root, 'src/main.tsx'), 'utf8') +const runtime = await readFile(join(root, 'src-tauri/src/online_marketplace.rs'), 'utf8') + +function canonical(value) { + if (Array.isArray(value)) return value.map(canonical) + if (value && typeof value === 'object') { + return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])])) + } + return value +} + +test('one online marketplace keeps physical modules and cognitive skills on separate trust chains', () => { + assert.equal(contract.humanExperience.oneMarketplaceTwoSections, true) + assert.equal(contract.artifactKinds.PHYSICAL_MODULE.installation, 'DOWNLOAD_VERIFY_INSTALL_MOUNT_SELF_TEST') + assert.equal(contract.artifactKinds.COGNITIVE_SKILL.installation, 'DOWNLOAD_VERIFY_STORE_ACTIVE_READONLY') + assert.equal(contract.artifactKinds.COGNITIVE_SKILL.executionAuthority, false) + assert.deepEqual(contract.artifactKinds.COGNITIVE_SKILL.systemPermissions, []) + assert.equal(contract.sourceRepositoryBoundary.clientClonesRepository, false) + assert.equal(contract.sourceRepositoryBoundary.clientExecutesRepository, false) + assert.match(runtime, /entry\.artifact_kind == "PHYSICAL_MODULE"/) + assert.match(runtime, /install_skill_at/) +}) + +test('marketplace lifecycle is reachable only through exact numbered operations', () => { + const aliases = registry.operations.filter((operation) => operation.module_number === 'HLP-NIPC-MOD-0033').map((operation) => operation.alias) + assert.deepEqual(aliases, [ + 'get_marketplace_snapshot', + 'sync_marketplace_catalog', + 'install_marketplace_item', + 'uninstall_marketplace_item', + 'rollback_marketplace_item', + 'get_active_cognitive_skills', + ]) + for (const alias of aliases.slice(0, 5)) assert.match(frontend, new RegExp(`['"]${alias}['"]`)) + assert.match(frontend, /只有零点原核与企业发布双签同时通过/) +}) + +for (const file of [ + 'HLP-SKILL-OFFICIAL-DELIVERY-VERIFICATION-0001-0.1.0.ghskill', + 'HLP-SKILL-OFFICIAL-MODULE-BOUNDARY-REVIEW-0001-0.1.0.ghskill', +]) { + test(`${basename(file)} is a read-only skill with an exact canonical payload digest`, async () => { + const skill = JSON.parse(await readFile(join(root, 'marketplace/skills', file), 'utf8')) + const digest = createHash('sha256').update(JSON.stringify(canonical(skill.payload))).digest('hex') + assert.equal(skill.schema, 'hololake.cognitive-skill-package/v1') + assert.equal(skill.manifest.executionAuthority, false) + assert.equal(skill.manifest.skillReadonlyGuarantee, true) + assert.deepEqual(skill.manifest.permissions, []) + assert.equal(skill.manifest.contentDigest, digest) + }) +} diff --git a/product-source/hololake-native-desktop/scripts/prepare-public-distribution-release.mjs b/product-source/hololake-native-desktop/scripts/prepare-public-distribution-release.mjs new file mode 100644 index 000000000..730a643df --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/prepare-public-distribution-release.mjs @@ -0,0 +1,185 @@ +#!/usr/bin/env node +import { createHash, createPrivateKey, createPublicKey, sign } from 'node:crypto' +import { copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import { basename, join } from 'node:path' +import process from 'node:process' + +const root = new URL('..', import.meta.url).pathname +const args = Object.fromEntries(process.argv.slice(2).map((item) => { + const [key, ...rest] = item.replace(/^--/, '').split('=') + return [key, rest.join('=')] +})) +const output = args.out +const epoch = Number(args.epoch) +const sourceRevision = args['source-revision'] +const privateKeyBase64 = process.env.HOLOLAKE_ZERO_ORIGIN_PKCS8_BASE64 + +if (!output || !Number.isSafeInteger(epoch) || epoch < 1 || !/^[a-f0-9]{40}$/.test(sourceRevision || '')) { + throw new Error('usage: --out=/absolute/path --epoch=1 --source-revision=<40 hex git revision>') +} +if (!privateKeyBase64) throw new Error('HOLOLAKE_ZERO_ORIGIN_PKCS8_BASE64 is required') + +const privateKey = createPrivateKey({ key: Buffer.from(privateKeyBase64, 'base64'), format: 'der', type: 'pkcs8' }) +const publicSpki = createPublicKey(privateKey).export({ format: 'der', type: 'spki' }) +const publicKeyBase64 = Buffer.from(publicSpki).subarray(-32).toString('base64') +const releaseId = `public-distribution-${epoch}-${sourceRevision.slice(0, 12)}` +const generatedAt = new Date().toISOString() + +function sha256(bytes) { + return createHash('sha256').update(bytes).digest('hex') +} +function canonical(value) { + if (Array.isArray(value)) return value.map(canonical) + if (value && typeof value === 'object') { + return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])])) + } + return value +} +function jsonBytes(value) { + return Buffer.from(`${JSON.stringify(value, null, 2)}\n`) +} +function exactSignature(bytes) { + return sign(null, bytes, privateKey).toString('base64') +} + +await rm(output, { recursive: true, force: true }) +await mkdir(join(output, 'zero-core'), { recursive: true }) +await mkdir(join(output, 'marketplace', 'artifacts'), { recursive: true }) + +const artifact = { + schema: 'hololake.public-zero-core-artifact/v1', + planeNumber: 'HLP-DIST-PLANE-0001', + epoch, + version: '1.0.0', + minimumHostVersion: '0.5.0', + protocol: { + gracePeriodDays: 7, + lighthouseAnchorUrl: 'https://guanghu.chat/api/hololake/zero-core/lamp', + lighthouseResolveUrl: 'https://guanghulab.com/api/ai/v1/resolve?id=', + enterpriseResolveUrl: 'https://guanghu.chat/api/hololake/enterprise/resolve', + coreChannelSource: 'https://guanghulab.com/code/bingshuo/guanghu-ice-heart', + origin: 'HLP-PUBLIC-ZERO-CORE-TRUST-001 · public scope only', + }, +} +const artifactRaw = jsonBytes(artifact) +const lamp = { + schema: 'hololake.public-zero-core-lamp/v1', + planeNumber: 'HLP-DIST-PLANE-0001', + epoch, + version: '1.0.0', + contentRootSha256: sha256(artifactRaw), + artifactManifestUrl: 'https://guanghu.chat/api/hololake/zero-core/artifact', + signatureUrl: 'https://guanghu.chat/api/hololake/zero-core/lamp.sig', + publishedAt: generatedAt, + minimumHostVersion: '0.5.0', +} +const lampRaw = jsonBytes(lamp) +await writeFile(join(output, 'zero-core', 'artifact.json'), artifactRaw) +await writeFile(join(output, 'zero-core', 'lamp.json'), lampRaw) + +const physicalDefinitions = [ + { + file: 'HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod', + summary: '把当前账号的知识目录投影为只读结构视图;不改写知识原件。', + }, + { + file: 'HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod', + summary: '教育文档、表格、作业导入与需本人确认的自动化工作台。', + }, +] +const entries = [] +for (const definition of physicalDefinitions) { + const source = join(root, 'fixtures', 'module-packages', definition.file) + const signatureSource = `${source}.sig` + const raw = await readFile(source) + const packageValue = JSON.parse(raw) + const manifest = packageValue.manifest + const artifactSha256 = sha256(raw) + const artifactFile = `${artifactSha256}.ghmod` + const signatureFile = `${artifactSha256}.ghmod.sig` + await copyFile(source, join(output, 'marketplace', 'artifacts', artifactFile)) + await copyFile(signatureSource, join(output, 'marketplace', 'artifacts', signatureFile)) + entries.push({ + itemNumber: manifest.moduleNumber, + artifactKind: 'PHYSICAL_MODULE', + displayName: manifest.displayName, + summary: definition.summary, + version: manifest.version, + minimumHostVersion: manifest.minimumHostVersion, + sourceRepository: 'https://guanghulab.com/code/bingshuo/hololake-system-architecture', + sourceRevision, + sourcePath: `product-source/hololake-native-desktop/fixtures/module-packages/${definition.file}`, + artifactUrl: `https://guanghu.chat/api/hololake/marketplace/artifacts/${artifactFile}`, + artifactSha256, + signatureUrl: `https://guanghu.chat/api/hololake/marketplace/artifacts/${signatureFile}`, + adapter: manifest.adapter, + permissions: manifest.permissions, + executionAuthority: false, + skillReadonlyGuarantee: false, + }) +} + +const skillFiles = [ + 'HLP-SKILL-OFFICIAL-DELIVERY-VERIFICATION-0001-0.1.0.ghskill', + 'HLP-SKILL-OFFICIAL-MODULE-BOUNDARY-REVIEW-0001-0.1.0.ghskill', +] +for (const file of skillFiles) { + const source = join(root, 'marketplace', 'skills', file) + const raw = await readFile(source) + const skill = JSON.parse(raw) + const artifactSha256 = sha256(raw) + const artifactFile = `${artifactSha256}.ghskill` + await copyFile(source, join(output, 'marketplace', 'artifacts', artifactFile)) + entries.push({ + itemNumber: skill.manifest.skillNumber, + artifactKind: 'COGNITIVE_SKILL', + displayName: skill.manifest.displayName, + summary: skill.payload.purpose, + version: skill.manifest.version, + minimumHostVersion: skill.manifest.minimumHostVersion, + sourceRepository: 'https://guanghulab.com/code/bingshuo/hololake-system-architecture', + sourceRevision, + sourcePath: `product-source/hololake-native-desktop/marketplace/skills/${file}`, + artifactUrl: `https://guanghu.chat/api/hololake/marketplace/artifacts/${artifactFile}`, + artifactSha256, + signatureUrl: null, + adapter: null, + permissions: [], + executionAuthority: false, + skillReadonlyGuarantee: true, + }) +} + +entries.sort((left, right) => left.itemNumber.localeCompare(right.itemNumber)) +const catalog = { + schema: 'hololake.marketplace.catalog/v1', + planeNumber: 'HLP-DIST-PLANE-0003', + epoch, + generatedAt, + minimumHostVersion: '0.5.0', + contentRootSha256: sha256(Buffer.from(JSON.stringify(canonical(entries)))), + entries, +} +const catalogRaw = jsonBytes(catalog) +await writeFile(join(output, 'marketplace', 'catalog.json'), catalogRaw) + +const authorization = { + schema: 'hololake.origin-release-authorization/v1', + releaseId, + sourceRevision, + epoch, + signer: { + signerId: 'HLP-SIGNER-ZERO-POINT-ORIGIN-PUBLIC-0001', + signerClass: 'ZERO_POINT_ORIGIN_PUBLIC_SCOPE_SIGNER', + algorithm: 'Ed25519', + publicKeyBase64, + }, + signedObjects: [ + { name: 'zero-core/lamp.json', sha256: sha256(lampRaw), signatureBase64: exactSignature(lampRaw) }, + { name: 'marketplace/catalog.json', sha256: sha256(catalogRaw), signatureBase64: exactSignature(catalogRaw) }, + ], +} +await writeFile(join(output, 'origin-authorization.json'), jsonBytes(authorization)) +await writeFile(join(output, 'release-metadata.json'), jsonBytes({ schema: 'hololake.public-distribution-release/v1', releaseId, epoch, sourceRevision, generatedAt, originPublicKeyBase64: publicKeyBase64, artifacts: entries.map((entry) => ({ name: basename(entry.artifactUrl), sha256: entry.artifactSha256 })) })) + +process.stdout.write(`${JSON.stringify({ releaseId, epoch, sourceRevision, itemCount: entries.length, originPublicKeyBase64: publicKeyBase64 })}\n`) 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 1a160feb9..d8e9bc9eb 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 @@ -8,13 +8,13 @@ test('identity, webview and direct broker numbers compile into one unique eviden assert.deepEqual(generated, compileUnifiedNumberTree()) assert.equal(generated.schema, 'hololake.unified-number-coordinate-tree/v2') assert.equal(generated.recordId, 'HLP-UNIFIED-NUMBER-TREE-001') - assert.equal(generated.coordinateCount, 272) - assert.equal(generated.routeCount, 169) + assert.equal(generated.coordinateCount, 283) + assert.equal(generated.routeCount, 180) assert.equal(generated.identityNodeCount, 4) assert.equal(generated.protocolNodeCount, 99) assert.equal(generated.referenceOnlyNodeCount, 16) - assert.equal(new Set(generated.routes.map((route) => route.path)).size, 169) - assert.equal(new Set([...generated.identityNodes, ...generated.protocolNodes, ...generated.routes].map((node) => node.path)).size, 272) + assert.equal(new Set(generated.routes.map((route) => route.path)).size, 180) + assert.equal(new Set([...generated.identityNodes, ...generated.protocolNodes, ...generated.routes].map((node) => node.path)).size, 283) assert.equal(generated.invariants.everyPhysicalCallHasNumberedRoute, true) assert.equal(generated.invariants.everyAcceptedCallHasEvidenceClass, true) assert.equal(generated.invariants.everyProtocolReferenceHasNumberCoordinate, true) diff --git a/product-source/hololake-native-desktop/scripts/zero-point-nucleus.test.mjs b/product-source/hololake-native-desktop/scripts/zero-point-nucleus.test.mjs index 58ed78c9a..0fb166be1 100644 --- a/product-source/hololake-native-desktop/scripts/zero-point-nucleus.test.mjs +++ b/product-source/hololake-native-desktop/scripts/zero-point-nucleus.test.mjs @@ -7,6 +7,8 @@ const contract = JSON.parse(read('contracts/zero-point-nucleus-channel.json')) const stageOne = JSON.parse(read('contracts/stage-one-platform.json')) const foundation = JSON.parse(read('foundation.json')) const rust = read('src-tauri/src/zero_point.rs') +const distribution = read('src-tauri/src/public_zero_core_distribution.rs') +const trust = JSON.parse(read('contracts/public-zero-core-trust.json')) const lib = read('src-tauri/src/lib.rs') const ui = read('src/main.tsx') @@ -20,13 +22,28 @@ test('zero-point nucleus is a system runtime and never an inferred persona bindi assert.match(ui, /验证结果不构成人格绑定、模型载体绑定或执行授权/) }) -test('protocol comparison starts below the public surface and fails closed', () => { +test('dual-signed protocol synchronization starts below the public surface and uses only provisioned public trust', () => { assert.equal(contract.current_implementation.boot_time_silent_version_comparison, true) - assert.equal(contract.current_implementation.signed_protocol_payload_installation, false) + assert.equal(contract.current_implementation.signed_protocol_payload_installation, true) + assert.equal(contract.current_implementation.production_dual_signer_trust_provisioned, true) + assert.equal(stageOne.zero_point_nucleus_client_runtime.signed_protocol_payload_installation_implemented, true) + assert.equal(stageOne.zero_point_nucleus_client_runtime.production_dual_signer_trust_provisioned, true) + assert.equal(trust.state, 'PROVISIONED') + assert.equal(trust.signers.length, 2) + assert.ok(trust.signers.every((signer) => signer.algorithm === 'Ed25519' && Buffer.from(signer.publicKeyBase64, 'base64').length === 32)) + assert.deepEqual(trust.requiredSignerClasses, [ + 'ZERO_POINT_ORIGIN_PUBLIC_SCOPE_SIGNER', + 'ENTERPRISE_ZERO_CORE_DISTRIBUTION_SIGNER', + ]) assert.equal(contract.security.unsigned_protocol_update_allowed, false) assert.match(lib, /sync_protocol_runtime/) - assert.match(rust, /UPDATE_PENDING_SIGNATURE_KEY_ABSENT/) - assert.match(rust, /UPDATE_FOUND_GATE_PENDING/) + assert.match(rust, /sync_public_zero_core/) + assert.match(distribution, /verify_dual_signatures/) + assert.match(distribution, /HOLOLAKE_PUBLIC_ZERO_CORE_ROLLBACK_REJECTED/) + assert.match(distribution, /HOLOLAKE_PUBLIC_ZERO_CORE_EPOCH_EQUIVOCATION_REJECTED/) + assert.match(distribution, /distribution-state\.sqlite3/) + assert.match(distribution, /TransactionBehavior::Immediate/) + assert.match(distribution, /reqwest::header::IF_NONE_MATCH/) assert.doesNotMatch(lib, /zero_point_save_api|zero_point_api_config|generic_layer_chat/) assert.doesNotMatch(ui, /模型 API 配置|通用AI运行层|AGENT PARLOR|会客厅/) }) diff --git a/product-source/hololake-native-desktop/server-tools/public-distribution/hololake-public-distribution.nginx.conf b/product-source/hololake-native-desktop/server-tools/public-distribution/hololake-public-distribution.nginx.conf new file mode 100644 index 000000000..92b0d9706 --- /dev/null +++ b/product-source/hololake-native-desktop/server-tools/public-distribution/hololake-public-distribution.nginx.conf @@ -0,0 +1,37 @@ +# Included inside the existing guanghu.chat TLS server block. +location = /api/hololake/zero-core/lamp { + alias /var/lib/guanghu-public-distribution/current/zero-core/lamp.json; + default_type application/json; + etag on; + add_header Cache-Control "public, max-age=30, must-revalidate" always; +} +location = /api/hololake/zero-core/lamp.sig { + alias /var/lib/guanghu-public-distribution/current/zero-core/lamp.sig.json; + default_type application/json; +} +location = /api/hololake/zero-core/artifact { + alias /var/lib/guanghu-public-distribution/current/zero-core/artifact.json; + default_type application/json; +} +location = /api/hololake/marketplace/catalog { + alias /var/lib/guanghu-public-distribution/current/marketplace/catalog.json; + default_type application/json; + etag on; + add_header Cache-Control "public, max-age=30, must-revalidate" always; +} +location = /api/hololake/marketplace/catalog.sig { + alias /var/lib/guanghu-public-distribution/current/marketplace/catalog.sig.json; + default_type application/json; +} +location ^~ /api/hololake/marketplace/artifacts/ { + alias /var/lib/guanghu-public-distribution/current/marketplace/artifacts/; + default_type application/octet-stream; + add_header X-Content-Type-Options nosniff always; + add_header Cache-Control "public, max-age=31536000, immutable" always; + try_files $uri =404; +} +location = /api/hololake/public-distribution/health { + alias /var/lib/guanghu-public-distribution/current/health.json; + default_type application/json; + add_header Cache-Control "no-store" always; +} diff --git a/product-source/hololake-native-desktop/server-tools/public-distribution/publish_release.py b/product-source/hololake-native-desktop/server-tools/public-distribution/publish_release.py new file mode 100644 index 000000000..335063c42 --- /dev/null +++ b/product-source/hololake-native-desktop/server-tools/public-distribution/publish_release.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +"""Verify an origin-authorized HoloLake public release, add enterprise signatures, and atomically publish it.""" + +from __future__ import annotations + +import argparse +import base64 +import hashlib +import json +import os +import shutil +import stat +import tempfile +from pathlib import Path + +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey + +ORIGIN_ID = "HLP-SIGNER-ZERO-POINT-ORIGIN-PUBLIC-0001" +ORIGIN_CLASS = "ZERO_POINT_ORIGIN_PUBLIC_SCOPE_SIGNER" +ENTERPRISE_ID = "HLP-SIGNER-ENTERPRISE-ZERO-CORE-DISTRIBUTION-0001" +ENTERPRISE_CLASS = "ENTERPRISE_ZERO_CORE_DISTRIBUTION_SIGNER" + + +def sha256(raw: bytes) -> str: + return hashlib.sha256(raw).hexdigest() + + +def load_json(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def canonical_sha(value: object) -> str: + raw = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8") + return sha256(raw) + + +def refuse_unsafe_tree(root: Path) -> None: + if not root.is_dir() or root.is_symlink(): + raise RuntimeError("stage must be a real directory") + for path in root.rglob("*"): + mode = path.lstat().st_mode + if stat.S_ISLNK(mode) or not (stat.S_ISREG(mode) or stat.S_ISDIR(mode)): + raise RuntimeError(f"unsafe staged path: {path}") + + +def exact_signed_object(stage: Path, authorization: dict, name: str, origin_key: Ed25519PublicKey) -> bytes: + record = next((item for item in authorization["signedObjects"] if item.get("name") == name), None) + if not record: + raise RuntimeError(f"missing origin authorization: {name}") + raw = (stage / name).read_bytes() + if record.get("sha256") != sha256(raw): + raise RuntimeError(f"origin digest mismatch: {name}") + origin_key.verify(base64.b64decode(record["signatureBase64"], validate=True), raw) + return raw + + +def signature_bundle(schema: str, plane: str, raw: bytes, origin_signature: str, enterprise_key: Ed25519PrivateKey) -> bytes: + value = { + "schema": schema, + "planeNumber": plane, + "contentSha256": sha256(raw), + "signatures": [ + { + "signerId": ORIGIN_ID, + "signerClass": ORIGIN_CLASS, + "algorithm": "Ed25519", + "signatureBase64": origin_signature, + }, + { + "signerId": ENTERPRISE_ID, + "signerClass": ENTERPRISE_CLASS, + "algorithm": "Ed25519", + "signatureBase64": base64.b64encode(enterprise_key.sign(raw)).decode("ascii"), + }, + ], + } + return (json.dumps(value, ensure_ascii=False, indent=2) + "\n").encode("utf-8") + + +def validate_release(stage: Path, authorization: dict, origin_public: bytes, enterprise_key: Ed25519PrivateKey) -> tuple[bytes, bytes]: + signer = authorization.get("signer", {}) + if signer != { + "signerId": ORIGIN_ID, + "signerClass": ORIGIN_CLASS, + "algorithm": "Ed25519", + "publicKeyBase64": base64.b64encode(origin_public).decode("ascii"), + }: + raise RuntimeError("origin signer identity mismatch") + origin_key = Ed25519PublicKey.from_public_bytes(origin_public) + lamp_raw = exact_signed_object(stage, authorization, "zero-core/lamp.json", origin_key) + catalog_raw = exact_signed_object(stage, authorization, "marketplace/catalog.json", origin_key) + lamp = json.loads(lamp_raw) + artifact_raw = (stage / "zero-core/artifact.json").read_bytes() + if lamp.get("schema") != "hololake.public-zero-core-lamp/v1" or lamp.get("planeNumber") != "HLP-DIST-PLANE-0001": + raise RuntimeError("zero-core lamp identity invalid") + if lamp.get("contentRootSha256") != sha256(artifact_raw): + raise RuntimeError("zero-core artifact digest mismatch") + artifact = json.loads(artifact_raw) + if artifact.get("epoch") != lamp.get("epoch") or artifact.get("version") != lamp.get("version"): + raise RuntimeError("zero-core artifact identity mismatch") + + catalog = json.loads(catalog_raw) + entries = catalog.get("entries") + if catalog.get("schema") != "hololake.marketplace.catalog/v1" or catalog.get("planeNumber") != "HLP-DIST-PLANE-0003" or not isinstance(entries, list) or not entries: + raise RuntimeError("marketplace catalog identity invalid") + if catalog.get("contentRootSha256") != canonical_sha(entries): + raise RuntimeError("marketplace content root mismatch") + numbers: set[str] = set() + for entry in entries: + number = entry.get("itemNumber", "") + if number in numbers: + raise RuntimeError(f"duplicate marketplace item: {number}") + numbers.add(number) + artifact_path = stage / "marketplace/artifacts" / Path(entry["artifactUrl"]).name + if not artifact_path.is_file() or sha256(artifact_path.read_bytes()) != entry.get("artifactSha256"): + raise RuntimeError(f"marketplace artifact digest mismatch: {number}") + if entry.get("artifactKind") == "PHYSICAL_MODULE": + signature_path = stage / "marketplace/artifacts" / Path(entry["signatureUrl"]).name + if not signature_path.is_file() or not signature_path.read_bytes(): + raise RuntimeError(f"physical module signature missing: {number}") + elif entry.get("artifactKind") == "COGNITIVE_SKILL": + if entry.get("executionAuthority") is not False or entry.get("permissions") != [] or entry.get("signatureUrl") is not None: + raise RuntimeError(f"cognitive skill authority boundary invalid: {number}") + else: + raise RuntimeError(f"unknown marketplace kind: {number}") + + signed = {item["name"]: item["signatureBase64"] for item in authorization["signedObjects"]} + lamp_bundle = signature_bundle("hololake.public-zero-core-dual-signature/v1", "HLP-DIST-PLANE-0001", lamp_raw, signed["zero-core/lamp.json"], enterprise_key) + catalog_bundle = signature_bundle("hololake.marketplace.catalog-dual-signature/v1", "HLP-DIST-PLANE-0003", catalog_raw, signed["marketplace/catalog.json"], enterprise_key) + return lamp_bundle, catalog_bundle + + +def publish(stage: Path, destination: Path, lamp_bundle: bytes, catalog_bundle: bytes, release_id: str) -> Path: + releases = destination / "releases" + releases.mkdir(parents=True, exist_ok=True) + final = releases / release_id + if final.exists(): + raise RuntimeError(f"release already exists: {release_id}") + temporary = Path(tempfile.mkdtemp(prefix=f".{release_id}.", dir=releases)) + try: + # Preserve a raced-in symlink as a symlink so the second tree check rejects + # it; never follow it into a path outside the upload stage. + shutil.copytree(stage, temporary, dirs_exist_ok=True, symlinks=True) + refuse_unsafe_tree(temporary) + (temporary / "zero-core/lamp.sig.json").write_bytes(lamp_bundle) + (temporary / "marketplace/catalog.sig.json").write_bytes(catalog_bundle) + (temporary / "health.json").write_text(json.dumps({"state": "LIVE_DUAL_SIGNED", "releaseId": release_id}, indent=2) + "\n", encoding="utf-8") + for path in temporary.rglob("*"): + os.chmod(path, 0o755 if path.is_dir() else 0o644) + os.replace(temporary, final) + link = destination / f".current.{os.getpid()}" + os.symlink(f"releases/{release_id}", link) + os.replace(link, destination / "current") + except Exception: + shutil.rmtree(temporary, ignore_errors=True) + raise + return final + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--stage", type=Path, required=True) + parser.add_argument("--destination", type=Path, default=Path("/var/lib/guanghu-public-distribution")) + parser.add_argument("--origin-public-key", type=Path, default=Path("/etc/guanghu-public-distribution/origin-ed25519.raw")) + parser.add_argument("--enterprise-private-key", type=Path, default=Path("/etc/guanghu-public-distribution/enterprise-ed25519.pem")) + args = parser.parse_args() + refuse_unsafe_tree(args.stage) + authorization = load_json(args.stage / "origin-authorization.json") + if authorization.get("schema") != "hololake.origin-release-authorization/v1": + raise RuntimeError("origin authorization schema invalid") + release_id = authorization.get("releaseId", "") + if not release_id or "/" in release_id or ".." in release_id: + raise RuntimeError("release id invalid") + origin_public = args.origin_public_key.read_bytes() + if len(origin_public) != 32: + raise RuntimeError("origin public key must be 32 raw bytes") + private = serialization.load_pem_private_key(args.enterprise_private_key.read_bytes(), password=None) + if not isinstance(private, Ed25519PrivateKey): + raise RuntimeError("enterprise release key is not Ed25519") + lamp_bundle, catalog_bundle = validate_release(args.stage, authorization, origin_public, private) + final = publish(args.stage, args.destination, lamp_bundle, catalog_bundle, release_id) + enterprise_public = private.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + print(json.dumps({"state": "PUBLISHED", "releaseId": release_id, "path": str(final), "enterprisePublicKeyBase64": base64.b64encode(enterprise_public).decode("ascii")})) + + +if __name__ == "__main__": + main() 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 ac7fb28ce..328897e80 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 @@ -10,6 +10,12 @@ use crate::dynamic_capability_routing::{ routing_root as dynamic_routing_root, DynamicNodeRegistry, ResolveCapabilityRouteInput, SignedNodeHealth, }; +use crate::human_authorization::{ + consume_at as consume_authorization_at, + root_from_session_root as authorization_root_from_session_root, + status_for_requester_at as authorization_status_at, submit_at as submit_authorization_at, + AuthorizationProposalInput, ConsumeAuthorizationInput, RequesterContext, +}; use crate::local_development_bridge::{ account_key_for as development_account_key_for, acquire_at as acquire_development_lane_at, inspect_at as inspect_development_lane_at, release_at as release_development_lane_at, @@ -203,6 +209,7 @@ struct BrokerStorageRoots { persona_time: PathBuf, persona_license: PathBuf, development: PathBuf, + authorization: PathBuf, } impl Drop for DirectLocalBrokerHandle { @@ -245,6 +252,9 @@ enum BrokerRequest { AcquireDevelopmentWriteLane(AuthenticatedDevelopmentLaneInput), InspectDevelopmentWriteLane(AuthenticatedDevelopmentInspectInput), ReleaseDevelopmentWriteLane(AuthenticatedDevelopmentReleaseInput), + SubmitHumanAuthorizationRequest(AuthenticatedAuthorizationProposalInput), + GetHumanAuthorizationStatus(AuthenticatedWorkEnvironmentInput), + ConsumeHumanAuthorizationTicket(AuthenticatedAuthorizationConsumeInput), } #[derive(Clone, Debug, Deserialize)] @@ -350,6 +360,20 @@ struct AuthenticatedWorkEnvironmentInput { session: AuthenticateSessionInput, } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct AuthenticatedAuthorizationProposalInput { + session: AuthenticateSessionInput, + proposal: AuthorizationProposalInput, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct AuthenticatedAuthorizationConsumeInput { + session: AuthenticateSessionInput, + ticket: ConsumeAuthorizationInput, +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct AuthenticatedPersonaCarrierLicenseInput { @@ -444,7 +468,7 @@ fn load_number_registry() -> Result { || registry.runtime.unknown_or_mismatched_coordinate != "FAIL_CLOSED" || !registry.runtime.request_nonce_required || registry.runtime.transport_is_authority - || registry.operations.len() != 22 + || registry.operations.len() != 25 { return Err("HOLOLAKE_NUMBERED_BROKER_REGISTRY_BOUNDARY_INVALID".into()); } @@ -843,6 +867,7 @@ fn start_at( let development_root = development_root_from_session_root(&session_root)?; fs::create_dir_all(&development_root) .map_err(|error| format!("HOLOLAKE_BRIDGE_STORAGE_UNAVAILABLE: {error}"))?; + let authorization_root = authorization_root_from_session_root(&session_root)?; #[cfg(unix)] if let Some(parent) = socket_path.parent() { fs::create_dir_all(parent) @@ -903,6 +928,7 @@ fn start_at( persona_time: persona_time_root, persona_license: persona_license_root, development: development_root, + authorization: authorization_root, }, &worker_shutdown, &worker_authenticated_connections, @@ -1025,6 +1051,7 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse { let persona_time_root = &roots.persona_time; let persona_license_root = &roots.persona_license; let development_root = &roots.development; + let authorization_root = &roots.authorization; let (request, _, _) = match decode_numbered_broker_request(bytes) { Ok(request) => request, Err(error) => { @@ -1295,6 +1322,72 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse { serde_json::to_value(receipt).map_err(|error| error.to_string()) }) } + BrokerRequest::SubmitHumanAuthorizationRequest(input) => { + authenticate_context_at(session_root, &input.session) + .and_then(|context| { + require_persona_mode_operation_at( + persona_license_root, + &context.account_key, + &context.session_id, + &context.client_instance_id, + "SUBMIT_HUMAN_AUTHORIZATION_REQUEST", + now_unix_ms()?, + )?; + submit_authorization_at( + authorization_root, + &RequesterContext { + account_key: context.account_key, + session_id: context.session_id, + client_instance_id: context.client_instance_id, + }, + input.proposal, + ) + }) + .and_then(|receipt| { + serde_json::to_value(receipt).map_err(|error| error.to_string()) + }) + } + BrokerRequest::GetHumanAuthorizationStatus(input) => { + authenticate_context_at(session_root, &input.session) + .and_then(|context| { + authorization_status_at( + authorization_root, + &RequesterContext { + account_key: context.account_key, + session_id: context.session_id, + client_instance_id: context.client_instance_id, + }, + ) + }) + .and_then(|snapshot| { + serde_json::to_value(snapshot).map_err(|error| error.to_string()) + }) + } + BrokerRequest::ConsumeHumanAuthorizationTicket(input) => { + authenticate_context_at(session_root, &input.session) + .and_then(|context| { + require_persona_mode_operation_at( + persona_license_root, + &context.account_key, + &context.session_id, + &context.client_instance_id, + "CONSUME_HUMAN_AUTHORIZATION_TICKET", + now_unix_ms()?, + )?; + consume_authorization_at( + authorization_root, + &RequesterContext { + account_key: context.account_key, + session_id: context.session_id, + client_instance_id: context.client_instance_id, + }, + input.ticket, + ) + }) + .and_then(|receipt| { + serde_json::to_value(receipt).map_err(|error| error.to_string()) + }) + } }; match result { Ok(value) => BrokerResponse::success(value), diff --git a/product-source/hololake-native-desktop/src-tauri/src/direct_local_session.rs b/product-source/hololake-native-desktop/src-tauri/src/direct_local_session.rs index e5283fdad..a3d2c0f90 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/direct_local_session.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/direct_local_session.rs @@ -120,6 +120,18 @@ pub struct SessionEventReceipt { pub receipt_id: String, } +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DirectSessionProjection { + pub session_id: String, + pub lane_id: String, + pub client_instance_id: String, + pub state: &'static str, + pub opened_at_unix_ms: u128, + pub observed_at_unix_ms: u128, + pub last_event_sequence: u64, +} + #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct SessionRecord { @@ -262,6 +274,52 @@ pub(crate) fn active_session_count_at(root: &Path) -> Result { Ok(count) } +pub(crate) fn active_session_projections_at( + root: &Path, +) -> Result, String> { + let accounts = root.join("accounts"); + if !accounts.exists() { + return Ok(Vec::new()); + } + let now = now_unix_ms()?; + let mut projections = Vec::new(); + for entry in fs::read_dir(&accounts) + .map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_STORAGE_UNAVAILABLE: {error}"))? + { + let entry = entry + .map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_STORAGE_UNAVAILABLE: {error}"))?; + if !entry + .file_type() + .map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_STORAGE_UNAVAILABLE: {error}"))? + .is_dir() + { + continue; + } + let active_path = entry.path().join("active-session.json"); + if !active_path.exists() { + continue; + } + let active: ActiveSessionRecord = read_json(&active_path, "ACTIVE_SESSION")?; + let record = read_session(&session_path(root, &active.account_key, &active.session_id))?; + let state = if now.saturating_sub(record.observed_at_unix_ms) <= 45_000 { + "LIVE" + } else { + "RESUMABLE" + }; + projections.push(DirectSessionProjection { + session_id: record.session_id, + lane_id: record.lane_id, + client_instance_id: record.client_instance_id, + state, + opened_at_unix_ms: record.opened_at_unix_ms, + observed_at_unix_ms: record.observed_at_unix_ms, + last_event_sequence: record.last_event_sequence, + }); + } + projections.sort_by(|left, right| right.observed_at_unix_ms.cmp(&left.observed_at_unix_ms)); + Ok(projections) +} + pub(crate) fn open_at( root: &Path, input: OpenSessionInput, diff --git a/product-source/hololake-native-desktop/src-tauri/src/home_status.rs b/product-source/hololake-native-desktop/src-tauri/src/home_status.rs index c63a4958f..7c41e2a38 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/home_status.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/home_status.rs @@ -1,7 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later use crate::direct_local_broker::DirectLocalBrokerState; -use crate::direct_local_session::{active_session_count_at, direct_session_root}; +use crate::direct_local_session::{ + active_session_count_at, active_session_projections_at, direct_session_root, + DirectSessionProjection, +}; use crate::pncc_receipt_projection::{pncc_projection_root, projection_event_count_at}; use crate::pncc_repository_binding::{mounted_repository_count_at, pncc_repository_mount_root}; use crate::release_trust::release_trust_state; @@ -15,6 +18,7 @@ pub struct HoloLakeHomeStatus { pub direct_local_broker_state: &'static str, pub direct_connection_count: usize, pub resumable_session_count: usize, + pub direct_sessions: Vec, pub code_repository_mount_count: usize, pub pncc_receipt_count: usize, pub update_state: &'static str, @@ -37,6 +41,7 @@ pub fn get_hololake_home_status( direct_local_broker_state: "WAITING_FOR_LOGIN", direct_connection_count: 0, resumable_session_count: 0, + direct_sessions: Vec::new(), code_repository_mount_count: 0, pncc_receipt_count: 0, update_state: release_trust_state()?, @@ -60,6 +65,7 @@ pub fn get_hololake_home_status( direct_local_broker_state: "READY", direct_connection_count: broker.active_connection_count(), resumable_session_count: active_session_count_at(&session_root)?, + direct_sessions: active_session_projections_at(&session_root)?, code_repository_mount_count: mounted_repository_count_at(&mount_root)?, pncc_receipt_count: projection_event_count_at(&projection_root)?, update_state: release_trust_state()?, diff --git a/product-source/hololake-native-desktop/src-tauri/src/human_authorization.rs b/product-source/hololake-native-desktop/src-tauri/src/human_authorization.rs new file mode 100644 index 000000000..8154d61c3 --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/human_authorization.rs @@ -0,0 +1,704 @@ +//! Human-in-the-loop authorization for numbered lifecycle work. +//! +//! Persona carriers may propose an exact action from an authenticated local session. +//! Only the verified human route may approve it. Approval creates a short-lived, +//! session-bound ticket that is consumed once and leaves a hash-chained receipt. + +use ring::digest::{digest, SHA256}; +use rusqlite::{params, Connection, OptionalExtension}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; +use tauri::AppHandle; +use uuid::Uuid; + +const SCHEMA: &str = "hololake.human-authorization/v1"; +const CONTRACT: &str = include_str!("../../contracts/human-authorization.json"); +const REQUEST_TTL_MS: u64 = 24 * 60 * 60 * 1_000; +const TICKET_TTL_MS: u64 = 15 * 60 * 1_000; + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct AuthorizationProposalInput { + pub idempotency_key: String, + pub action: String, + pub target_number: String, + pub target_kind: String, + pub target_label: String, + pub reason: String, + pub impact: String, + pub rollback_plan: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct DecideAuthorizationInput { + pub request_id: String, + pub decision: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ConsumeAuthorizationInput { + pub request_id: String, + pub ticket_id: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthorizationRequestView { + pub request_id: String, + pub action: String, + pub target_number: String, + pub target_kind: String, + pub target_label: String, + pub reason: String, + pub impact: String, + pub rollback_plan: String, + pub requester_label: String, + pub state: String, + pub created_at_unix_ms: u64, + pub expires_at_unix_ms: u64, + pub human_number: Option, + pub decided_at_unix_ms: Option, + pub ticket_id: Option, + pub ticket_expires_at_unix_ms: Option, + pub consumed_at_unix_ms: Option, + pub receipt_hash: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthorizationCenterSnapshot { + pub schema: &'static str, + pub state: &'static str, + pub pending_count: usize, + pub requests: Vec, + pub supported_actions: Vec<&'static str>, + pub purge_enabled: bool, + pub authority: &'static str, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthorizationTicketReceipt { + pub schema: &'static str, + pub state: &'static str, + pub request_id: String, + pub ticket_id: String, + pub action: String, + pub target_number: String, + pub scope_sha256: String, + pub consumed_at_unix_ms: u64, + pub receipt_hash: String, + pub next_operation: &'static str, +} + +#[derive(Clone, Debug)] +pub struct RequesterContext { + pub account_key: String, + pub session_id: String, + pub client_instance_id: String, +} + +pub fn root_for_app(app: &AppHandle) -> Result { + crate::authenticated_storage::account_storage_root(app, "human-authorization-v1") +} + +pub fn start_on_application_open() -> Result<(), String> { + let contract: serde_json::Value = serde_json::from_str(CONTRACT) + .map_err(|error| format!("HOLOLAKE_AUTHORIZATION_CONTRACT_INVALID: {error}"))?; + if contract.get("schema").and_then(serde_json::Value::as_str) + != Some("hololake.human-authorization-contract/v1") + || contract + .get("record_id") + .and_then(serde_json::Value::as_str) + != Some("HLP-HUMAN-AUTHORIZATION-001") + || contract + .pointer("/ticket/ttl_ms") + .and_then(serde_json::Value::as_u64) + != Some(TICKET_TTL_MS) + || contract + .pointer("/request/ttl_ms") + .and_then(serde_json::Value::as_u64) + != Some(REQUEST_TTL_MS) + || contract + .pointer("/destructive_purge/enabled") + .and_then(serde_json::Value::as_bool) + != Some(false) + || contract + .pointer("/ticket/single_use") + .and_then(serde_json::Value::as_bool) + != Some(true) + { + return Err("HOLOLAKE_AUTHORIZATION_CONTRACT_BOUNDARY_INVALID".into()); + } + Ok(()) +} + +pub fn root_from_session_root(session_root: &Path) -> Result { + let account_root = session_root + .parent() + .ok_or("HOLOLAKE_AUTHORIZATION_STORAGE_BOUNDARY_INVALID")?; + let root = account_root.join("human-authorization-v1"); + fs::create_dir_all(&root) + .map_err(|error| format!("HOLOLAKE_AUTHORIZATION_STORAGE_UNAVAILABLE: {error}"))?; + Ok(root) +} + +pub fn get_center(app: &AppHandle) -> Result { + snapshot_at(&root_for_app(app)?, None) +} + +pub fn decide( + app: &AppHandle, + input: DecideAuthorizationInput, + human_number: &str, +) -> Result { + decide_at(&root_for_app(app)?, input, human_number, now_ms()?) +} + +pub fn submit_at( + root: &Path, + requester: &RequesterContext, + input: AuthorizationProposalInput, +) -> Result { + validate_proposal(&input)?; + let now = now_ms()?; + let mut connection = open_db(root)?; + let transaction = connection.transaction().map_err(db_error("TRANSACTION"))?; + if let Some(existing) = transaction + .query_row( + "SELECT request_id FROM authorization_requests WHERE requester_account_key=?1 AND idempotency_key=?2", + params![requester.account_key, input.idempotency_key], + |row| row.get::<_, String>(0), + ) + .optional() + .map_err(db_error("READ"))? + { + let existing = read_request_with_owner(&transaction, &existing)?; + if existing.requester_session_id != requester.session_id + || existing.requester_client_instance_id != requester.client_instance_id + || existing.view.action != input.action + || existing.view.target_number != input.target_number + || existing.view.target_kind != input.target_kind + || existing.view.target_label != input.target_label + || existing.view.reason != input.reason + || existing.view.impact != input.impact + || existing.view.rollback_plan != input.rollback_plan + { + return Err("HOLOLAKE_AUTHORIZATION_IDEMPOTENCY_CONFLICT".into()); + } + let view = existing.view; + transaction.commit().map_err(db_error("COMMIT"))?; + return Ok(view); + } + let request_id = format!("HLP-AUTH-REQ-{}", Uuid::new_v4().simple()); + let expires = now.saturating_add(REQUEST_TTL_MS); + transaction.execute( + "INSERT INTO authorization_requests( + request_id,idempotency_key,requester_account_key,requester_session_id,requester_client_instance_id, + action,target_number,target_kind,target_label,reason,impact,rollback_plan,state, + created_at_unix_ms,expires_at_unix_ms,receipt_hash + ) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,'PENDING_HUMAN',?13,?14,'GENESIS')", + params![ + request_id, input.idempotency_key, requester.account_key, requester.session_id, + requester.client_instance_id, input.action, input.target_number, input.target_kind, + input.target_label, input.reason, input.impact, input.rollback_plan, now, expires + ], + ).map_err(db_error("INSERT"))?; + let receipt = append_receipt(&transaction, &request_id, "SUBMITTED", "PENDING_HUMAN", now)?; + transaction + .execute( + "UPDATE authorization_requests SET receipt_hash=?2 WHERE request_id=?1", + params![request_id, receipt], + ) + .map_err(db_error("UPDATE"))?; + let view = read_request(&transaction, &request_id)?; + transaction.commit().map_err(db_error("COMMIT"))?; + Ok(view) +} + +pub fn status_for_requester_at( + root: &Path, + requester: &RequesterContext, +) -> Result { + snapshot_at(root, Some(requester)) +} + +pub fn decide_at( + root: &Path, + input: DecideAuthorizationInput, + human_number: &str, + now: u64, +) -> Result { + validate_id(&input.request_id, "REQUEST")?; + if !matches!(input.decision.as_str(), "APPROVE" | "DENY") { + return Err("HOLOLAKE_AUTHORIZATION_DECISION_INVALID".into()); + } + validate_id(human_number, "HUMAN_NUMBER")?; + let mut connection = open_db(root)?; + let transaction = connection.transaction().map_err(db_error("TRANSACTION"))?; + expire_pending(&transaction, now)?; + let current = read_request(&transaction, &input.request_id)?; + if current.state != "PENDING_HUMAN" { + let same = (input.decision == "APPROVE" && current.state == "APPROVED") + || (input.decision == "DENY" && current.state == "DENIED"); + if same && current.human_number.as_deref() == Some(human_number) { + transaction.commit().map_err(db_error("COMMIT"))?; + return Ok(current); + } + return Err("HOLOLAKE_AUTHORIZATION_REQUEST_NOT_PENDING".into()); + } + let (state, ticket_id, ticket_expires) = if input.decision == "APPROVE" { + ( + "APPROVED", + Some(format!("HLP-AUTH-TICKET-{}", Uuid::new_v4().simple())), + Some(now.saturating_add(TICKET_TTL_MS)), + ) + } else { + ("DENIED", None, None) + }; + transaction + .execute( + "UPDATE authorization_requests SET state=?2,human_number=?3,decided_at_unix_ms=?4, + ticket_id=?5,ticket_expires_at_unix_ms=?6 WHERE request_id=?1 AND state='PENDING_HUMAN'", + params![ + input.request_id, + state, + human_number, + now, + ticket_id, + ticket_expires + ], + ) + .map_err(db_error("UPDATE"))?; + let receipt = append_receipt(&transaction, &input.request_id, &input.decision, state, now)?; + transaction + .execute( + "UPDATE authorization_requests SET receipt_hash=?2 WHERE request_id=?1", + params![input.request_id, receipt], + ) + .map_err(db_error("UPDATE"))?; + let view = read_request(&transaction, &input.request_id)?; + transaction.commit().map_err(db_error("COMMIT"))?; + Ok(view) +} + +pub fn consume_at( + root: &Path, + requester: &RequesterContext, + input: ConsumeAuthorizationInput, +) -> Result { + validate_id(&input.request_id, "REQUEST")?; + validate_id(&input.ticket_id, "TICKET")?; + let now = now_ms()?; + let mut connection = open_db(root)?; + let transaction = connection.transaction().map_err(db_error("TRANSACTION"))?; + let request = read_request_with_owner(&transaction, &input.request_id)?; + if request.requester_account_key != requester.account_key + || request.requester_session_id != requester.session_id + || request.requester_client_instance_id != requester.client_instance_id + { + return Err("HOLOLAKE_AUTHORIZATION_TICKET_SESSION_MISMATCH".into()); + } + if request.view.state == "CONSUMED" { + return Err("HOLOLAKE_AUTHORIZATION_TICKET_REPLAYED".into()); + } + if request.view.state != "APPROVED" + || request.view.ticket_id.as_deref() != Some(input.ticket_id.as_str()) + { + return Err("HOLOLAKE_AUTHORIZATION_TICKET_NOT_APPROVED".into()); + } + if request.view.ticket_expires_at_unix_ms.unwrap_or(0) < now { + transaction.execute( + "UPDATE authorization_requests SET state='EXPIRED' WHERE request_id=?1 AND state='APPROVED'", + params![input.request_id], + ).map_err(db_error("UPDATE"))?; + let receipt = append_receipt( + &transaction, + &input.request_id, + "TICKET_EXPIRED", + "EXPIRED", + now, + )?; + transaction + .execute( + "UPDATE authorization_requests SET receipt_hash=?2 WHERE request_id=?1", + params![input.request_id, receipt], + ) + .map_err(db_error("UPDATE"))?; + transaction.commit().map_err(db_error("COMMIT"))?; + return Err("HOLOLAKE_AUTHORIZATION_TICKET_EXPIRED".into()); + } + let scope_sha256 = sha256_hex( + format!( + "{}\n{}\n{}\n{}\n{}", + input.request_id, + input.ticket_id, + request.view.action, + request.view.target_number, + requester.session_id + ) + .as_bytes(), + ); + transaction.execute( + "UPDATE authorization_requests SET state='CONSUMED',consumed_at_unix_ms=?2 WHERE request_id=?1 AND state='APPROVED'", + params![input.request_id, now], + ).map_err(db_error("UPDATE"))?; + let receipt_hash = + append_receipt(&transaction, &input.request_id, "CONSUMED", "CONSUMED", now)?; + transaction + .execute( + "UPDATE authorization_requests SET receipt_hash=?2 WHERE request_id=?1", + params![input.request_id, receipt_hash], + ) + .map_err(db_error("UPDATE"))?; + transaction.commit().map_err(db_error("COMMIT"))?; + Ok(AuthorizationTicketReceipt { + schema: SCHEMA, + state: "CONSUMED_ONCE", + request_id: input.request_id, + ticket_id: input.ticket_id, + action: request.view.action, + target_number: request.view.target_number, + scope_sha256, + consumed_at_unix_ms: now, + receipt_hash, + next_operation: "EXECUTE_ONLY_THE_EXACT_AUTHORIZED_ACTION_AND_APPEND_REALITY_RECEIPT", + }) +} + +fn snapshot_at( + root: &Path, + requester: Option<&RequesterContext>, +) -> Result { + let connection = open_db(root)?; + expire_pending(&connection, now_ms()?)?; + let (sql, args): (&str, Vec<&dyn rusqlite::ToSql>) = match requester { + Some(value) => ( + "SELECT request_id FROM authorization_requests WHERE requester_account_key=?1 AND requester_session_id=?2 AND requester_client_instance_id=?3 ORDER BY created_at_unix_ms DESC LIMIT 100", + vec![&value.account_key, &value.session_id, &value.client_instance_id], + ), + None => ("SELECT request_id FROM authorization_requests ORDER BY created_at_unix_ms DESC LIMIT 100", vec![]), + }; + let mut statement = connection.prepare(sql).map_err(db_error("READ"))?; + let ids = statement + .query_map(args.as_slice(), |row| row.get::<_, String>(0)) + .map_err(db_error("READ"))? + .collect::, _>>() + .map_err(db_error("READ"))?; + drop(statement); + let requests = ids + .iter() + .map(|id| read_request(&connection, id)) + .collect::, _>>()?; + let pending_count = requests + .iter() + .filter(|item| item.state == "PENDING_HUMAN") + .count(); + Ok(AuthorizationCenterSnapshot { + schema: SCHEMA, + state: "ACTIVE_FAIL_CLOSED", + pending_count, + requests, + supported_actions: vec!["OPEN_MAINTENANCE", "UNMOUNT", "PROMOTE_VERSION", "RETIRE"], + purge_enabled: false, + authority: "PERSONA_PROPOSES_HUMAN_DECIDES_SYSTEM_ISSUES_SESSION_BOUND_SINGLE_USE_TICKET", + }) +} + +struct OwnedRequest { + view: AuthorizationRequestView, + requester_account_key: String, + requester_session_id: String, + requester_client_instance_id: String, +} + +fn read_request( + connection: &Connection, + request_id: &str, +) -> Result { + Ok(read_request_with_owner(connection, request_id)?.view) +} + +fn read_request_with_owner( + connection: &Connection, + request_id: &str, +) -> Result { + connection.query_row( + "SELECT request_id,action,target_number,target_kind,target_label,reason,impact,rollback_plan, + requester_client_instance_id,state,created_at_unix_ms,expires_at_unix_ms,human_number, + decided_at_unix_ms,ticket_id,ticket_expires_at_unix_ms,consumed_at_unix_ms,receipt_hash, + requester_account_key,requester_session_id FROM authorization_requests WHERE request_id=?1", + params![request_id], + |row| Ok(OwnedRequest { + view: AuthorizationRequestView { + request_id: row.get(0)?, action: row.get(1)?, target_number: row.get(2)?, + target_kind: row.get(3)?, target_label: row.get(4)?, reason: row.get(5)?, + impact: row.get(6)?, rollback_plan: row.get(7)?, requester_label: row.get(8)?, + state: row.get(9)?, created_at_unix_ms: row.get(10)?, expires_at_unix_ms: row.get(11)?, + human_number: row.get(12)?, decided_at_unix_ms: row.get(13)?, ticket_id: row.get(14)?, + ticket_expires_at_unix_ms: row.get(15)?, consumed_at_unix_ms: row.get(16)?, receipt_hash: row.get(17)?, + }, + requester_account_key: row.get(18)?, requester_session_id: row.get(19)?, + requester_client_instance_id: row.get(8)?, + }), + ).optional().map_err(db_error("READ"))? + .ok_or_else(|| "HOLOLAKE_AUTHORIZATION_REQUEST_UNKNOWN".into()) +} + +fn open_db(root: &Path) -> Result { + fs::create_dir_all(root) + .map_err(|error| format!("HOLOLAKE_AUTHORIZATION_STORAGE_UNAVAILABLE: {error}"))?; + let connection = + Connection::open(root.join("human-authorization.sqlite3")).map_err(db_error("OPEN"))?; + connection + .busy_timeout(std::time::Duration::from_secs(5)) + .map_err(db_error("TIMEOUT"))?; + connection.execute_batch( + "PRAGMA journal_mode=WAL; + CREATE TABLE IF NOT EXISTS authorization_requests( + request_id TEXT PRIMARY KEY,idempotency_key TEXT NOT NULL,requester_account_key TEXT NOT NULL, + requester_session_id TEXT NOT NULL,requester_client_instance_id TEXT NOT NULL,action TEXT NOT NULL, + target_number TEXT NOT NULL,target_kind TEXT NOT NULL,target_label TEXT NOT NULL,reason TEXT NOT NULL, + impact TEXT NOT NULL,rollback_plan TEXT NOT NULL,state TEXT NOT NULL,created_at_unix_ms INTEGER NOT NULL, + expires_at_unix_ms INTEGER NOT NULL,human_number TEXT,decided_at_unix_ms INTEGER,ticket_id TEXT UNIQUE, + ticket_expires_at_unix_ms INTEGER,consumed_at_unix_ms INTEGER,receipt_hash TEXT NOT NULL, + UNIQUE(requester_account_key,idempotency_key) + ); + CREATE TABLE IF NOT EXISTS authorization_receipts( + sequence INTEGER PRIMARY KEY AUTOINCREMENT,request_id TEXT NOT NULL,event TEXT NOT NULL,state TEXT NOT NULL, + observed_at_unix_ms INTEGER NOT NULL,previous_receipt_hash TEXT NOT NULL,receipt_hash TEXT NOT NULL UNIQUE + );" + ).map_err(db_error("SCHEMA"))?; + Ok(connection) +} + +fn expire_pending(connection: &Connection, now: u64) -> Result<(), String> { + let mut statement = connection + .prepare("SELECT request_id FROM authorization_requests WHERE state='PENDING_HUMAN' AND expires_at_unix_ms < ?1") + .map_err(db_error("EXPIRE_READ"))?; + let request_ids = statement + .query_map(params![now], |row| row.get::<_, String>(0)) + .map_err(db_error("EXPIRE_READ"))? + .collect::, _>>() + .map_err(db_error("EXPIRE_READ"))?; + drop(statement); + for request_id in request_ids { + connection + .execute( + "UPDATE authorization_requests SET state='EXPIRED' WHERE request_id=?1 AND state='PENDING_HUMAN'", + params![request_id], + ) + .map_err(db_error("EXPIRE"))?; + let receipt = append_receipt(connection, &request_id, "REQUEST_EXPIRED", "EXPIRED", now)?; + connection + .execute( + "UPDATE authorization_requests SET receipt_hash=?2 WHERE request_id=?1", + params![request_id, receipt], + ) + .map_err(db_error("EXPIRE"))?; + } + Ok(()) +} + +fn append_receipt( + connection: &Connection, + request_id: &str, + event: &str, + state: &str, + now: u64, +) -> Result { + let previous = connection + .query_row( + "SELECT receipt_hash FROM authorization_receipts ORDER BY sequence DESC LIMIT 1", + [], + |row| row.get::<_, String>(0), + ) + .optional() + .map_err(db_error("RECEIPT_READ"))? + .unwrap_or_else(|| "GENESIS".into()); + let receipt_hash = sha256_hex( + format!("HLP-AUTH-RECEIPT-v1\n{previous}\n{request_id}\n{event}\n{state}\n{now}") + .as_bytes(), + ); + connection.execute( + "INSERT INTO authorization_receipts(request_id,event,state,observed_at_unix_ms,previous_receipt_hash,receipt_hash) VALUES(?1,?2,?3,?4,?5,?6)", + params![request_id, event, state, now, previous, receipt_hash], + ).map_err(db_error("RECEIPT_WRITE"))?; + Ok(receipt_hash) +} + +fn validate_proposal(input: &AuthorizationProposalInput) -> Result<(), String> { + validate_id(&input.idempotency_key, "IDEMPOTENCY")?; + if !matches!( + input.action.as_str(), + "OPEN_MAINTENANCE" | "UNMOUNT" | "PROMOTE_VERSION" | "RETIRE" + ) { + return Err("HOLOLAKE_AUTHORIZATION_ACTION_UNSUPPORTED".into()); + } + validate_id(&input.target_number, "TARGET_NUMBER")?; + validate_id(&input.target_kind, "TARGET_KIND")?; + if input.target_kind != "MODULE" || !input.target_number.starts_with("HLP-MOD-") { + return Err("HOLOLAKE_AUTHORIZATION_TARGET_NOT_A_MODULE".into()); + } + validate_text(&input.target_label, 120, "TARGET_LABEL")?; + validate_text(&input.reason, 2_000, "REASON")?; + validate_text(&input.impact, 2_000, "IMPACT")?; + validate_text(&input.rollback_plan, 2_000, "ROLLBACK") +} + +fn validate_id(value: &str, label: &str) -> Result<(), String> { + if value.is_empty() + || value.len() > 160 + || value.chars().any(|c| c.is_control() || c.is_whitespace()) + { + Err(format!("HOLOLAKE_AUTHORIZATION_{label}_INVALID")) + } else { + Ok(()) + } +} + +fn validate_text(value: &str, max: usize, label: &str) -> Result<(), String> { + if value.trim().is_empty() || value.len() > max || value.chars().any(|c| c == '\0') { + Err(format!("HOLOLAKE_AUTHORIZATION_{label}_INVALID")) + } else { + Ok(()) + } +} + +fn now_ms() -> Result { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_millis() as u64) + .map_err(|error| format!("HOLOLAKE_SYSTEM_CLOCK_INVALID: {error}")) +} + +fn sha256_hex(bytes: &[u8]) -> String { + digest(&SHA256, bytes) + .as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn db_error(stage: &'static str) -> impl FnOnce(rusqlite::Error) -> String { + move |error| format!("HOLOLAKE_AUTHORIZATION_DB_{stage}_FAILED: {error}") +} + +#[cfg(test)] +mod tests { + use super::*; + + fn requester() -> RequesterContext { + RequesterContext { + account_key: "account".into(), + session_id: "session".into(), + client_instance_id: "agent-codex".into(), + } + } + + fn proposal() -> AuthorizationProposalInput { + AuthorizationProposalInput { + idempotency_key: "proposal-1".into(), + action: "OPEN_MAINTENANCE".into(), + target_number: "HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001".into(), + target_kind: "MODULE".into(), + target_label: "教育工作台".into(), + reason: "移出问题模块并在干净容器中重建".into(), + impact: "旧模块保持不可写,维护区允许受控修改".into(), + rollback_plan: "关闭维护票据并恢复原挂载".into(), + } + } + + #[test] + fn human_approval_ticket_is_session_bound_and_single_use() { + let temp = tempfile::tempdir().unwrap(); + let created = submit_at(temp.path(), &requester(), proposal()).unwrap(); + let approved = decide_at( + temp.path(), + DecideAuthorizationInput { + request_id: created.request_id.clone(), + decision: "APPROVE".into(), + }, + "ICE-GL∞", + now_ms().unwrap(), + ) + .unwrap(); + let ticket = approved.ticket_id.unwrap(); + let consumed = consume_at( + temp.path(), + &requester(), + ConsumeAuthorizationInput { + request_id: created.request_id.clone(), + ticket_id: ticket.clone(), + }, + ) + .unwrap(); + assert_eq!(consumed.state, "CONSUMED_ONCE"); + let replay = consume_at( + temp.path(), + &requester(), + ConsumeAuthorizationInput { + request_id: created.request_id, + ticket_id: ticket, + }, + ) + .unwrap_err(); + assert_eq!(replay, "HOLOLAKE_AUTHORIZATION_TICKET_REPLAYED"); + } + + #[test] + fn purge_is_not_an_available_action() { + let mut value = proposal(); + value.action = "PURGE".into(); + assert_eq!( + submit_at(tempfile::tempdir().unwrap().path(), &requester(), value).unwrap_err(), + "HOLOLAKE_AUTHORIZATION_ACTION_UNSUPPORTED" + ); + } + + #[test] + fn idempotency_key_cannot_be_reused_for_another_target_or_reason() { + let temp = tempfile::tempdir().unwrap(); + submit_at(temp.path(), &requester(), proposal()).unwrap(); + let mut changed = proposal(); + changed.reason = "另一个原因".into(); + assert_eq!( + submit_at(temp.path(), &requester(), changed).unwrap_err(), + "HOLOLAKE_AUTHORIZATION_IDEMPOTENCY_CONFLICT" + ); + } + + #[test] + fn approved_ticket_cannot_move_to_another_agent_session() { + let temp = tempfile::tempdir().unwrap(); + let created = submit_at(temp.path(), &requester(), proposal()).unwrap(); + let approved = decide_at( + temp.path(), + DecideAuthorizationInput { + request_id: created.request_id.clone(), + decision: "APPROVE".into(), + }, + "ICE-GL∞", + now_ms().unwrap(), + ) + .unwrap(); + let mut another = requester(); + another.session_id = "another-session".into(); + assert_eq!( + consume_at( + temp.path(), + &another, + ConsumeAuthorizationInput { + request_id: created.request_id, + ticket_id: approved.ticket_id.unwrap(), + }, + ) + .unwrap_err(), + "HOLOLAKE_AUTHORIZATION_TICKET_SESSION_MISMATCH" + ); + } +} 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 563af5f67..e34350b4c 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/lib.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/lib.rs @@ -15,6 +15,7 @@ mod gls_bootstrap_compiler; mod gls_protocol_kernel; mod gls_protocol_runtime; mod home_status; +mod human_authorization; mod knowledge_base; mod local_development_bridge; mod metacognitive_zero_layer; @@ -25,6 +26,7 @@ mod number_coordinate_tree; mod numbered_ipc; mod numbered_ipc_dispatch; mod numbered_language_input; +mod online_marketplace; mod persona_carrier_license; mod persona_channel_body; mod persona_time_authority; @@ -33,6 +35,7 @@ mod pncc_receipt_projection; mod pncc_remote_git; mod pncc_repository_binding; mod pncc_server_projection; +mod public_zero_core_distribution; mod release_trust; mod release_update; mod user_pncc_channel; @@ -64,9 +67,11 @@ pub fn run() { metacognitive_zero_layer::start_on_application_open()?; persona_carrier_license::start_on_application_open()?; module_package_runtime::start_on_application_open(app.handle())?; + online_marketplace::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())?; + human_authorization::start_on_application_open()?; // 软件打开即先启动时间主控并发起联网校时;失败只降级,不阻塞人进入 HoloLake。 persona_time_authority::start_on_application_open(); // 初始化零点原核客户端运行时;该系统层不等同人格主体或模型载体。 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 index 7f671fdb4..c93ccc6cd 100644 --- 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 @@ -217,64 +217,64 @@ struct ModuleSelfTest { #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct VerifyModulePackageInput { - package_path: String, - signature_path: String, + pub(crate) package_path: String, + pub(crate) 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, + pub(crate) package_path: String, + pub(crate) signature_path: String, + pub(crate) expected_package_sha256: String, + pub(crate) human_confirmed_permission_expansion: bool, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ModuleNumberInput { - module_number: String, + pub(crate) 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, + pub(crate) state: String, + pub(crate) package_sha256: String, + pub(crate) module_number: String, + pub(crate) display_name: String, + pub(crate) version: String, + pub(crate) adapter: String, + pub(crate) permissions: Vec, + pub(crate) signature_verified: bool, + pub(crate) 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, + pub(crate) module_number: String, + pub(crate) display_name: String, + pub(crate) version: String, + pub(crate) adapter: String, + pub(crate) state: String, + pub(crate) package_sha256: String, + pub(crate) previous_package_sha256: Option, + pub(crate) permissions: Vec, + pub(crate) user_data_preserved: bool, + pub(crate) 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, + pub(crate) schema: &'static str, + pub(crate) state: &'static str, + pub(crate) host_version: &'static str, + pub(crate) package_code_executed: bool, + pub(crate) modules: Vec, + pub(crate) receipt_count: u64, + pub(crate) latest_receipt_hash: String, } #[derive(Clone, Debug, Serialize)] 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 aabd6ade9..0d57cdefa 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 @@ -88,8 +88,8 @@ 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.coordinate_count != 272 - || tree.route_count != 169 + || tree.coordinate_count != 283 + || tree.route_count != 180 || tree.routes.len() != tree.route_count || tree.identity_node_count != 4 || tree.identity_nodes.len() != tree.identity_node_count 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 c35cea065..3b37d9055 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(), 147); + assert_eq!(registry.operations.len(), 155); 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 5bee9d24b..7d385d7e7 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 @@ -43,6 +43,19 @@ pub(crate) async fn dispatch( app.state::(), )?) } + "human_authorization::get_authorization_center" => { + json(crate::human_authorization::get_center(&app)?) + } + "human_authorization::decide_authorization_request" => { + let state = app.state::(); + let (human_number, _) = crate::zero_point::verified_user_route(&state)? + .ok_or_else(|| "HOLOLAKE_AUTHORIZATION_VERIFIED_HUMAN_REQUIRED".to_string())?; + json(crate::human_authorization::decide( + &app, + input(&payload)?, + &human_number, + )?) + } "release_update::check_hololake_update" => { json(crate::release_update::check_hololake_update(app).await?) } @@ -126,6 +139,24 @@ pub(crate) async fn dispatch( "module_package_runtime::activate_bundled_module" => json( crate::module_package_runtime::activate_bundled_module(app, input(&payload)?).await?, ), + "online_marketplace::get_marketplace_snapshot" => { + json(crate::online_marketplace::get_marketplace_snapshot(app).await?) + } + "online_marketplace::sync_marketplace_catalog" => { + json(crate::online_marketplace::sync_marketplace_catalog(app).await?) + } + "online_marketplace::install_marketplace_item" => { + json(crate::online_marketplace::install_marketplace_item(app, input(&payload)?).await?) + } + "online_marketplace::uninstall_marketplace_item" => json( + crate::online_marketplace::uninstall_marketplace_item(app, input(&payload)?).await?, + ), + "online_marketplace::rollback_marketplace_item" => { + json(crate::online_marketplace::rollback_marketplace_item(app, input(&payload)?).await?) + } + "online_marketplace::get_active_cognitive_skills" => { + json(crate::online_marketplace::get_active_cognitive_skills(app).await?) + } "mobile_sync::start_mobile_sync" => json(crate::mobile_sync::start_mobile_sync(app)?), "mobile_sync::get_mobile_sync_status" => { json(crate::mobile_sync::get_mobile_sync_status(app)?) diff --git a/product-source/hololake-native-desktop/src-tauri/src/online_marketplace.rs b/product-source/hololake-native-desktop/src-tauri/src/online_marketplace.rs new file mode 100644 index 000000000..c455f4da5 --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/online_marketplace.rs @@ -0,0 +1,1677 @@ +//! Signed online marketplace for physical modules and read-only cognitive skills. +//! +//! Repositories are provenance evidence, never executable client input. The client +//! synchronizes a small dual-signed catalog, downloads one immutable artifact only +//! after a human chooses it, and then dispatches to two deliberately separate +//! runtimes: registered declarative module adapters or read-only cognitive skills. + +use base64::{engine::general_purpose::STANDARD, Engine as _}; +use ring::{ + digest::{digest, SHA256}, + signature::{UnparsedPublicKey, ED25519}, +}; +use rusqlite::{params, Connection, OptionalExtension, TransactionBehavior}; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use tauri::AppHandle; +use uuid::Uuid; + +const CONTRACT_RAW: &str = include_str!("../../contracts/online-marketplace.json"); +const TRUST_RAW: &str = include_str!("../../contracts/public-zero-core-trust.json"); +const HOST_VERSION: &str = "0.5.0"; +const PLANE_NUMBER: &str = "HLP-DIST-PLANE-0003"; +const CATALOG_URL: &str = "https://guanghu.chat/api/hololake/marketplace/catalog"; +const CATALOG_SIGNATURE_URL: &str = "https://guanghu.chat/api/hololake/marketplace/catalog.sig"; +const CATALOG_SCHEMA: &str = "hololake.marketplace.catalog/v1"; +const CATALOG_SIGNATURE_SCHEMA: &str = "hololake.marketplace.catalog-dual-signature/v1"; +const SKILL_SCHEMA: &str = "hololake.cognitive-skill-package/v1"; +const MAX_CATALOG_BYTES: usize = 1024 * 1024; +const MAX_SIGNATURE_BYTES: usize = 64 * 1024; +const MAX_ARTIFACT_BYTES: usize = 16 * 1024 * 1024; +const MAX_ENTRIES: usize = 256; + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct TrustStore { + schema: String, + record_id: String, + state: String, + allowed_hosts: Vec, + signers: Vec, + required_signer_classes: Vec, + provisioning_rule: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct TrustedSigner { + signer_id: String, + signer_class: String, + algorithm: String, + public_key_base64: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct SignatureBundle { + schema: String, + plane_number: String, + content_sha256: String, + signatures: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct DetachedSignature { + signer_id: String, + signer_class: String, + algorithm: String, + signature_base64: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct MarketplaceCatalog { + schema: String, + plane_number: String, + epoch: u64, + generated_at: String, + minimum_host_version: String, + content_root_sha256: String, + entries: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct MarketplaceCatalogEntry { + item_number: String, + artifact_kind: String, + display_name: String, + summary: String, + version: String, + minimum_host_version: String, + source_repository: String, + source_revision: String, + source_path: String, + artifact_url: String, + artifact_sha256: String, + signature_url: Option, + adapter: Option, + permissions: Vec, + execution_authority: bool, + skill_readonly_guarantee: bool, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct CognitiveSkillPackage { + schema: String, + manifest: CognitiveSkillManifest, + payload: CognitiveSkillPayload, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct CognitiveSkillManifest { + skill_number: String, + display_name: String, + version: String, + minimum_host_version: String, + content_digest: String, + execution_authority: bool, + skill_readonly_guarantee: bool, + permissions: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct CognitiveSkillPayload { + pub purpose: String, + pub triggers: Vec, + pub method: Vec, + pub constraints: Vec, + pub output_contract: Vec, + pub capability_references: Vec, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MarketplaceItemView { + pub item_number: String, + pub artifact_kind: String, + pub display_name: String, + pub summary: String, + pub version: String, + pub source_repository: String, + pub source_revision: String, + pub artifact_sha256: String, + pub adapter: Option, + pub permissions: Vec, + pub execution_authority: bool, + pub installed_state: String, + pub update_available: bool, +} + +#[derive(Clone, Debug, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MarketplaceSnapshot { + pub schema: &'static str, + pub state: String, + pub trust_state: String, + pub catalog_epoch: u64, + pub catalog_sha256: String, + pub item_count: usize, + pub catalog_receipt_count: u64, + pub latest_catalog_receipt_hash: String, + pub last_sync_note: String, + pub items: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct InstallMarketplaceItemInput { + pub item_number: String, + pub expected_catalog_epoch: u64, + pub expected_artifact_sha256: String, + pub human_confirmed_permission_expansion: bool, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct MarketplaceItemInput { + pub item_number: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MarketplaceMutationOutcome { + pub schema: &'static str, + pub state: String, + pub item_number: String, + pub artifact_kind: String, + pub artifact_sha256: String, + pub package_code_executed: bool, + pub reality_authority_granted: bool, + pub snapshot: MarketplaceSnapshot, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ActiveCognitiveSkill { + pub skill_number: String, + pub display_name: String, + pub version: String, + pub package_sha256: String, + pub execution_authority: bool, + pub skill_readonly_guarantee: bool, + pub payload: CognitiveSkillPayload, +} + +#[derive(Clone, Debug)] +struct StoredCatalog { + catalog: MarketplaceCatalog, + catalog_sha256: String, + etag: String, +} + +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(|character| character.is_ascii_digit() || ('a'..='f').contains(&character)) +} + +fn is_git_revision(value: &str) -> bool { + value.len() == 40 && value.chars().all(|character| character.is_ascii_hexdigit()) +} + +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 canonical_value(value: &Value) -> Value { + match value { + Value::Object(map) => { + let mut ordered = Map::new(); + let mut keys = map.keys().collect::>(); + keys.sort(); + for key in keys { + ordered.insert(key.clone(), canonical_value(&map[key])); + } + Value::Object(ordered) + } + Value::Array(items) => Value::Array(items.iter().map(canonical_value).collect()), + other => other.clone(), + } +} + +fn value_digest(value: &T) -> Result { + let value = serde_json::to_value(value) + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_CANONICAL_VALUE_FAILED: {error}"))?; + let bytes = serde_json::to_vec(&canonical_value(&value)) + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_CANONICAL_ENCODE_FAILED: {error}"))?; + Ok(sha256_hex(&bytes)) +} + +fn validate_embedded_contract() -> Result<(), String> { + let value: Value = serde_json::from_str(CONTRACT_RAW) + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_CONTRACT_INVALID: {error}"))?; + if value.get("schema").and_then(Value::as_str) + != Some("hololake.online-marketplace-contract/v1") + || value.get("recordId").and_then(Value::as_str) != Some("HLP-ONLINE-MARKETPLACE-001") + || value.get("planeNumber").and_then(Value::as_str) != Some(PLANE_NUMBER) + || value + .pointer("/artifactKinds/COGNITIVE_SKILL/executionAuthority") + .and_then(Value::as_bool) + != Some(false) + || value + .pointer("/sourceRepositoryBoundary/clientClonesRepository") + .and_then(Value::as_bool) + != Some(false) + || value + .pointer("/sourceRepositoryBoundary/artifactUrlsAreContentAddressed") + .and_then(Value::as_bool) + != Some(true) + { + return Err("HOLOLAKE_MARKETPLACE_CONTRACT_BOUNDARY_INVALID".into()); + } + Ok(()) +} + +fn parse_trust(raw: &str) -> Result { + let trust: TrustStore = serde_json::from_str(raw) + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_TRUST_INVALID: {error}"))?; + let required = BTreeSet::from([ + "ZERO_POINT_ORIGIN_PUBLIC_SCOPE_SIGNER".to_string(), + "ENTERPRISE_ZERO_CORE_DISTRIBUTION_SIGNER".to_string(), + ]); + if trust.schema != "hololake.public-zero-core-trust/v1" + || trust.record_id != "HLP-PUBLIC-ZERO-CORE-TRUST-001" + || trust.state != "PROVISIONED" + || trust.provisioning_rule.trim().is_empty() + || trust + .required_signer_classes + .iter() + .cloned() + .collect::>() + != required + || trust.signers.len() != 2 + || trust.allowed_hosts.is_empty() + { + return Err("HOLOLAKE_MARKETPLACE_TRUST_UNPROVISIONED".into()); + } + let mut ids = BTreeSet::new(); + let mut classes = BTreeSet::new(); + for signer in &trust.signers { + let key = STANDARD + .decode(&signer.public_key_base64) + .map_err(|_| "HOLOLAKE_MARKETPLACE_TRUST_KEY_INVALID".to_string())?; + if signer.signer_id.is_empty() + || signer.algorithm != "Ed25519" + || key.len() != 32 + || !ids.insert(signer.signer_id.clone()) + || !classes.insert(signer.signer_class.clone()) + { + return Err("HOLOLAKE_MARKETPLACE_TRUST_SIGNER_INVALID".into()); + } + } + if classes != required { + return Err("HOLOLAKE_MARKETPLACE_TRUST_CLASSES_INVALID".into()); + } + Ok(trust) +} + +fn require_allowed_https_url(raw: &str, trust: &TrustStore) -> Result { + let url = + reqwest::Url::parse(raw).map_err(|_| "HOLOLAKE_MARKETPLACE_URL_INVALID".to_string())?; + let host = url + .host_str() + .ok_or_else(|| "HOLOLAKE_MARKETPLACE_URL_HOST_REQUIRED".to_string())?; + if url.scheme() != "https" + || !url.username().is_empty() + || url.password().is_some() + || !trust.allowed_hosts.iter().any(|allowed| allowed == host) + { + return Err("HOLOLAKE_MARKETPLACE_URL_NOT_ALLOWED".into()); + } + Ok(url) +} + +fn verify_dual_signatures( + catalog_raw: &[u8], + signature_raw: &[u8], + trust: &TrustStore, +) -> Result<(), String> { + let bundle: SignatureBundle = serde_json::from_slice(signature_raw) + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_SIGNATURE_JSON_INVALID: {error}"))?; + if bundle.schema != CATALOG_SIGNATURE_SCHEMA + || bundle.plane_number != PLANE_NUMBER + || bundle.content_sha256 != sha256_hex(catalog_raw) + || bundle.signatures.len() != 2 + { + return Err("HOLOLAKE_MARKETPLACE_SIGNATURE_BUNDLE_INVALID".into()); + } + let trusted = trust + .signers + .iter() + .map(|signer| (signer.signer_id.as_str(), signer)) + .collect::>(); + let mut ids = BTreeSet::new(); + let mut classes = BTreeSet::new(); + for detached in &bundle.signatures { + let signer = trusted + .get(detached.signer_id.as_str()) + .ok_or_else(|| "HOLOLAKE_MARKETPLACE_SIGNATURE_SIGNER_UNKNOWN".to_string())?; + if detached.algorithm != "Ed25519" + || signer.algorithm != detached.algorithm + || signer.signer_class != detached.signer_class + || !ids.insert(detached.signer_id.clone()) + || !classes.insert(detached.signer_class.clone()) + { + return Err("HOLOLAKE_MARKETPLACE_SIGNATURE_IDENTITY_INVALID".into()); + } + let key = STANDARD + .decode(&signer.public_key_base64) + .map_err(|_| "HOLOLAKE_MARKETPLACE_TRUST_KEY_INVALID".to_string())?; + let signature = STANDARD + .decode(&detached.signature_base64) + .map_err(|_| "HOLOLAKE_MARKETPLACE_SIGNATURE_ENCODING_INVALID".to_string())?; + UnparsedPublicKey::new(&ED25519, key) + .verify(catalog_raw, &signature) + .map_err(|_| "HOLOLAKE_MARKETPLACE_SIGNATURE_REJECTED".to_string())?; + } + if classes + != trust + .required_signer_classes + .iter() + .cloned() + .collect::>() + { + return Err("HOLOLAKE_MARKETPLACE_SIGNATURE_CLASSES_INCOMPLETE".into()); + } + Ok(()) +} + +fn validate_catalog_entry( + entry: &MarketplaceCatalogEntry, + trust: &TrustStore, +) -> Result<(), String> { + let minimum = version_triplet(&entry.minimum_host_version); + if entry.display_name.trim().is_empty() + || entry.display_name.len() > 120 + || entry.summary.trim().is_empty() + || entry.summary.len() > 600 + || version_triplet(&entry.version).is_none() + || minimum.is_none() + || minimum > version_triplet(HOST_VERSION) + || !is_git_revision(&entry.source_revision) + || entry.source_path.is_empty() + || entry.source_path.len() > 320 + || entry.source_path.starts_with('/') + || entry.source_path.split('/').any(|segment| segment == "..") + || !is_sha256(&entry.artifact_sha256) + || entry.permissions.len() > 32 + || entry.permissions.iter().collect::>().len() != entry.permissions.len() + || entry.permissions.iter().any(|permission| { + permission.is_empty() + || permission.len() > 80 + || !permission + .chars() + .all(|item| item.is_ascii_uppercase() || item.is_ascii_digit() || item == '_') + }) + { + return Err("HOLOLAKE_MARKETPLACE_ENTRY_BOUNDARY_INVALID".into()); + } + require_allowed_https_url(&entry.source_repository, trust)?; + let artifact_url = require_allowed_https_url(&entry.artifact_url, trust)?; + let artifact_name = artifact_url + .path_segments() + .and_then(|segments| segments.last()) + .unwrap_or_default(); + if !artifact_name.starts_with(&entry.artifact_sha256) { + return Err("HOLOLAKE_MARKETPLACE_ARTIFACT_URL_NOT_CONTENT_ADDRESSED".into()); + } + match entry.artifact_kind.as_str() { + "PHYSICAL_MODULE" => { + if !entry.item_number.starts_with("HLP-MOD-") + || entry.adapter.as_deref().map(str::is_empty).unwrap_or(true) + || entry + .signature_url + .as_deref() + .map(str::is_empty) + .unwrap_or(true) + || entry.execution_authority + || entry.skill_readonly_guarantee + { + return Err("HOLOLAKE_MARKETPLACE_PHYSICAL_MODULE_BOUNDARY_INVALID".into()); + } + let signature_url = require_allowed_https_url( + entry.signature_url.as_deref().unwrap_or_default(), + trust, + )?; + if artifact_name != format!("{}.ghmod", entry.artifact_sha256) + || signature_url + .path_segments() + .and_then(|segments| segments.last()) + .unwrap_or_default() + != format!("{}.ghmod.sig", entry.artifact_sha256) + { + return Err("HOLOLAKE_MARKETPLACE_MODULE_URL_NOT_CONTENT_ADDRESSED".into()); + } + } + "COGNITIVE_SKILL" => { + if !entry.item_number.starts_with("HLP-SKILL-") + || entry.adapter.is_some() + || entry.signature_url.is_some() + || !entry.permissions.is_empty() + || entry.execution_authority + || !entry.skill_readonly_guarantee + { + return Err("HOLOLAKE_MARKETPLACE_COGNITIVE_SKILL_BOUNDARY_INVALID".into()); + } + if artifact_name != format!("{}.ghskill", entry.artifact_sha256) { + return Err("HOLOLAKE_MARKETPLACE_SKILL_URL_NOT_CONTENT_ADDRESSED".into()); + } + } + _ => return Err("HOLOLAKE_MARKETPLACE_ARTIFACT_KIND_INVALID".into()), + } + Ok(()) +} + +fn validate_catalog( + catalog_raw: &[u8], + signature_raw: &[u8], + trust_raw: &str, +) -> Result { + if catalog_raw.is_empty() + || catalog_raw.len() > MAX_CATALOG_BYTES + || signature_raw.is_empty() + || signature_raw.len() > MAX_SIGNATURE_BYTES + { + return Err("HOLOLAKE_MARKETPLACE_CATALOG_SIZE_INVALID".into()); + } + validate_embedded_contract()?; + let trust = parse_trust(trust_raw)?; + verify_dual_signatures(catalog_raw, signature_raw, &trust)?; + let catalog: MarketplaceCatalog = serde_json::from_slice(catalog_raw) + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_CATALOG_JSON_INVALID: {error}"))?; + if catalog.schema != CATALOG_SCHEMA + || catalog.plane_number != PLANE_NUMBER + || catalog.epoch == 0 + || catalog.generated_at.is_empty() + || catalog.generated_at.len() > 80 + || version_triplet(&catalog.minimum_host_version).is_none() + || version_triplet(&catalog.minimum_host_version) > version_triplet(HOST_VERSION) + || catalog.entries.is_empty() + || catalog.entries.len() > MAX_ENTRIES + || value_digest(&catalog.entries)? != catalog.content_root_sha256 + { + return Err("HOLOLAKE_MARKETPLACE_CATALOG_BOUNDARY_INVALID".into()); + } + let mut item_numbers = BTreeSet::new(); + let mut artifact_urls = BTreeSet::new(); + for entry in &catalog.entries { + validate_catalog_entry(entry, &trust)?; + if !item_numbers.insert(entry.item_number.clone()) + || !artifact_urls.insert(entry.artifact_url.clone()) + { + return Err("HOLOLAKE_MARKETPLACE_CATALOG_DUPLICATE_ENTRY".into()); + } + } + Ok(catalog) +} + +fn validate_skill_package( + raw: &[u8], + expected_entry: &MarketplaceCatalogEntry, +) -> Result { + if raw.is_empty() || raw.len() > MAX_ARTIFACT_BYTES { + return Err("HOLOLAKE_COGNITIVE_SKILL_SIZE_INVALID".into()); + } + let package: CognitiveSkillPackage = serde_json::from_slice(raw) + .map_err(|error| format!("HOLOLAKE_COGNITIVE_SKILL_JSON_INVALID: {error}"))?; + let manifest = &package.manifest; + let payload = &package.payload; + if package.schema != SKILL_SCHEMA + || manifest.skill_number != expected_entry.item_number + || manifest.display_name != expected_entry.display_name + || manifest.version != expected_entry.version + || manifest.minimum_host_version != expected_entry.minimum_host_version + || version_triplet(&manifest.version).is_none() + || version_triplet(&manifest.minimum_host_version) > version_triplet(HOST_VERSION) + || !is_sha256(&manifest.content_digest) + || manifest.content_digest != value_digest(payload)? + || manifest.execution_authority + || !manifest.skill_readonly_guarantee + || !manifest.permissions.is_empty() + || payload.purpose.trim().is_empty() + || payload.purpose.len() > 600 + || payload.triggers.is_empty() + || payload.triggers.len() > 32 + || payload.method.is_empty() + || payload.method.len() > 64 + || payload.constraints.is_empty() + || payload.constraints.len() > 64 + || payload.output_contract.is_empty() + || payload.output_contract.len() > 64 + || payload.capability_references.len() > 32 + || payload + .capability_references + .iter() + .any(|reference| !reference.starts_with("HLP-NIPC-") || reference.len() > 120) + { + return Err("HOLOLAKE_COGNITIVE_SKILL_CONTRACT_INVALID".into()); + } + Ok(package) +} + +fn runtime_root(app: &AppHandle) -> Result { + crate::authenticated_storage::account_storage_root(app, "online-marketplace-v1") +} + +fn open_db(root: &Path) -> Result { + fs::create_dir_all(root) + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_ROOT_FAILED: {error}"))?; + let connection = Connection::open(root.join("marketplace.sqlite3")) + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_DB_OPEN_FAILED: {error}"))?; + connection + .busy_timeout(Duration::from_secs(5)) + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_DB_TIMEOUT_FAILED: {error}"))?; + connection + .execute_batch( + "PRAGMA journal_mode=WAL; + PRAGMA synchronous=FULL; + CREATE TABLE IF NOT EXISTS marketplace_metadata( + key TEXT PRIMARY KEY,value TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS active_catalog( + singleton INTEGER PRIMARY KEY CHECK(singleton=1),epoch INTEGER NOT NULL, + catalog_sha256 TEXT NOT NULL,catalog_raw_base64 TEXT NOT NULL, + signature_raw_base64 TEXT NOT NULL,etag TEXT NOT NULL,updated_at_unix_ms INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS catalog_receipts( + sequence INTEGER PRIMARY KEY AUTOINCREMENT,event TEXT NOT NULL,epoch INTEGER NOT NULL, + catalog_sha256 TEXT NOT NULL,previous_receipt_hash TEXT NOT NULL, + receipt_hash TEXT NOT NULL UNIQUE,observed_at_unix_ms INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS installed_skills( + skill_number TEXT PRIMARY KEY,display_name TEXT NOT NULL,version TEXT NOT NULL, + package_sha256 TEXT NOT NULL,previous_package_sha256 TEXT,state TEXT NOT NULL, + package_raw_base64 TEXT NOT NULL,updated_at_unix_ms INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS skill_receipts( + sequence INTEGER PRIMARY KEY AUTOINCREMENT,event TEXT NOT NULL,skill_number TEXT NOT NULL, + state TEXT NOT NULL,package_sha256 TEXT NOT NULL,previous_receipt_hash TEXT NOT NULL, + receipt_hash TEXT NOT NULL UNIQUE,observed_at_unix_ms INTEGER NOT NULL + ); + INSERT OR IGNORE INTO marketplace_metadata(key,value) + VALUES('schema','hololake.online-marketplace-state/v1');", + ) + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_DB_SCHEMA_FAILED: {error}"))?; + Ok(connection) +} + +fn read_catalog_from_db( + connection: &Connection, + trust_raw: &str, +) -> Result, String> { + let row = connection + .query_row( + "SELECT epoch,catalog_sha256,catalog_raw_base64,signature_raw_base64,etag FROM active_catalog WHERE singleton=1", + [], + |row| Ok((row.get::<_, u64>(0)?,row.get::<_, String>(1)?,row.get::<_, String>(2)?,row.get::<_, String>(3)?,row.get::<_, String>(4)?)), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_CATALOG_READ_FAILED: {error}"))?; + let Some((epoch, stored_sha, catalog_b64, signature_b64, etag)) = row else { + return Ok(None); + }; + let raw = STANDARD + .decode(catalog_b64) + .map_err(|_| "HOLOLAKE_MARKETPLACE_CATALOG_CACHE_ENCODING_INVALID".to_string())?; + let signature_raw = STANDARD + .decode(signature_b64) + .map_err(|_| "HOLOLAKE_MARKETPLACE_SIGNATURE_CACHE_ENCODING_INVALID".to_string())?; + let catalog = validate_catalog(&raw, &signature_raw, trust_raw)?; + if catalog.epoch != epoch || sha256_hex(&raw) != stored_sha { + return Err("HOLOLAKE_MARKETPLACE_CATALOG_CACHE_TAMPERED".into()); + } + Ok(Some(StoredCatalog { + catalog, + catalog_sha256: stored_sha, + etag, + })) +} + +fn latest_catalog_receipt(connection: &Connection) -> Result<(u64, String), String> { + let mut statement = connection + .prepare( + "SELECT event,epoch,catalog_sha256,previous_receipt_hash,receipt_hash,observed_at_unix_ms FROM catalog_receipts ORDER BY sequence", + ) + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_RECEIPT_PREPARE_FAILED: {error}"))?; + let rows = statement + .query_map([], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, u64>(1)?, + row.get::<_, String>(2)?, + row.get::<_, String>(3)?, + row.get::<_, String>(4)?, + row.get::<_, u64>(5)?, + )) + }) + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_RECEIPT_QUERY_FAILED: {error}"))?; + let mut previous = "0".repeat(64); + let mut count = 0_u64; + for row in rows { + let (event, epoch, catalog_sha, linked, receipt, observed) = + row.map_err(|error| format!("HOLOLAKE_MARKETPLACE_RECEIPT_ROW_FAILED: {error}"))?; + let expected = sha256_hex( + format!( + "HLP-MARKETPLACE-CATALOG-RECEIPT-v1\n{event}\n{epoch}\n{catalog_sha}\n{linked}\n{observed}" + ) + .as_bytes(), + ); + if event != "CATALOG_ACTIVATED" + || linked != previous + || receipt != expected + || !is_sha256(&catalog_sha) + { + return Err("HOLOLAKE_MARKETPLACE_RECEIPT_CHAIN_INVALID".into()); + } + previous = receipt; + count += 1; + } + Ok((count, if count == 0 { String::new() } else { previous })) +} + +fn activate_catalog( + root: &Path, + catalog_raw: &[u8], + signature_raw: &[u8], + etag: &str, + trust_raw: &str, +) -> Result { + let catalog = validate_catalog(catalog_raw, signature_raw, trust_raw)?; + let catalog_sha = sha256_hex(catalog_raw); + let mut connection = open_db(root)?; + let transaction = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_CATALOG_TX_FAILED: {error}"))?; + let current = read_catalog_from_db(&transaction, trust_raw)?; + if let Some(current) = current { + if catalog.epoch < current.catalog.epoch { + return Err("HOLOLAKE_MARKETPLACE_CATALOG_ROLLBACK_REJECTED".into()); + } + if catalog.epoch == current.catalog.epoch { + if catalog_sha != current.catalog_sha256 || catalog != current.catalog { + return Err("HOLOLAKE_MARKETPLACE_CATALOG_EQUIVOCATION_REJECTED".into()); + } + return Ok(false); + } + } + let observed = now_unix_ms(); + let (_, previous) = latest_catalog_receipt(&transaction)?; + let linked = if previous.is_empty() { + "0".repeat(64) + } else { + previous + }; + let receipt_hash = sha256_hex( + format!( + "HLP-MARKETPLACE-CATALOG-RECEIPT-v1\nCATALOG_ACTIVATED\n{}\n{}\n{}\n{}", + catalog.epoch, catalog_sha, linked, observed + ) + .as_bytes(), + ); + transaction + .execute( + "INSERT INTO active_catalog(singleton,epoch,catalog_sha256,catalog_raw_base64,signature_raw_base64,etag,updated_at_unix_ms) + VALUES(1,?1,?2,?3,?4,?5,?6) + ON CONFLICT(singleton) DO UPDATE SET epoch=excluded.epoch,catalog_sha256=excluded.catalog_sha256, + catalog_raw_base64=excluded.catalog_raw_base64,signature_raw_base64=excluded.signature_raw_base64, + etag=excluded.etag,updated_at_unix_ms=excluded.updated_at_unix_ms", + params![ + catalog.epoch, + catalog_sha, + STANDARD.encode(catalog_raw), + STANDARD.encode(signature_raw), + etag, + observed + ], + ) + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_CATALOG_WRITE_FAILED: {error}"))?; + transaction + .execute( + "INSERT INTO catalog_receipts(event,epoch,catalog_sha256,previous_receipt_hash,receipt_hash,observed_at_unix_ms) + VALUES('CATALOG_ACTIVATED',?1,?2,?3,?4,?5)", + params![catalog.epoch, catalog_sha, linked, receipt_hash, observed], + ) + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_RECEIPT_WRITE_FAILED: {error}"))?; + transaction + .commit() + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_CATALOG_COMMIT_FAILED: {error}"))?; + Ok(true) +} + +async fn fetch_bounded( + client: &reqwest::Client, + url: reqwest::Url, + maximum: usize, +) -> Result, String> { + let response = client + .get(url) + .send() + .await + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_DOWNLOAD_FAILED: {error}"))?; + if !response.status().is_success() + || response + .content_length() + .is_some_and(|length| length > maximum as u64) + { + return Err("HOLOLAKE_MARKETPLACE_DOWNLOAD_REJECTED".into()); + } + let bytes = response + .bytes() + .await + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_DOWNLOAD_READ_FAILED: {error}"))?; + if bytes.is_empty() || bytes.len() > maximum { + return Err("HOLOLAKE_MARKETPLACE_DOWNLOAD_SIZE_INVALID".into()); + } + Ok(bytes.to_vec()) +} + +fn write_atomic(path: &Path, bytes: &[u8]) -> Result<(), String> { + let parent = path + .parent() + .ok_or("HOLOLAKE_MARKETPLACE_TARGET_PARENT_INVALID")?; + fs::create_dir_all(parent) + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_TARGET_ROOT_FAILED: {error}"))?; + if path.exists() { + let current = fs::read(path) + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_TARGET_READ_FAILED: {error}"))?; + if current != bytes { + return Err("HOLOLAKE_MARKETPLACE_IMMUTABLE_ARTIFACT_CONFLICT".into()); + } + return Ok(()); + } + let temporary = parent.join(format!(".marketplace-{}", Uuid::new_v4())); + fs::write(&temporary, bytes) + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_TARGET_STAGE_FAILED: {error}"))?; + fs::rename(&temporary, path) + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_TARGET_COMMIT_FAILED: {error}")) +} + +fn skill_receipt_tip(connection: &Connection) -> Result<(u64, String), String> { + let mut statement = connection + .prepare( + "SELECT event,skill_number,state,package_sha256,previous_receipt_hash,receipt_hash,observed_at_unix_ms FROM skill_receipts ORDER BY sequence", + ) + .map_err(|error| format!("HOLOLAKE_SKILL_RECEIPT_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::<_, u64>(6)?, + )) + }) + .map_err(|error| format!("HOLOLAKE_SKILL_RECEIPT_QUERY_FAILED: {error}"))?; + let mut previous = "0".repeat(64); + let mut count = 0_u64; + for row in rows { + let (event, number, state, package_sha, linked, receipt, observed) = + row.map_err(|error| format!("HOLOLAKE_SKILL_RECEIPT_ROW_FAILED: {error}"))?; + let expected = sha256_hex( + format!( + "HLP-COGNITIVE-SKILL-RECEIPT-v1\n{event}\n{number}\n{state}\n{package_sha}\n{linked}\n{observed}" + ) + .as_bytes(), + ); + if linked != previous || receipt != expected || !is_sha256(&package_sha) { + return Err("HOLOLAKE_SKILL_RECEIPT_CHAIN_INVALID".into()); + } + previous = receipt; + count += 1; + } + Ok((count, if count == 0 { String::new() } else { previous })) +} + +fn append_skill_receipt( + transaction: &rusqlite::Transaction<'_>, + event: &str, + skill_number: &str, + state: &str, + package_sha: &str, + observed: u64, +) -> Result { + let (_, tip) = skill_receipt_tip(transaction)?; + let previous = if tip.is_empty() { "0".repeat(64) } else { tip }; + let receipt_hash = sha256_hex( + format!( + "HLP-COGNITIVE-SKILL-RECEIPT-v1\n{event}\n{skill_number}\n{state}\n{package_sha}\n{previous}\n{observed}" + ) + .as_bytes(), + ); + transaction + .execute( + "INSERT INTO skill_receipts(event,skill_number,state,package_sha256,previous_receipt_hash,receipt_hash,observed_at_unix_ms) + VALUES(?1,?2,?3,?4,?5,?6,?7)", + params![event, skill_number, state, package_sha, previous, receipt_hash, observed], + ) + .map_err(|error| format!("HOLOLAKE_SKILL_RECEIPT_WRITE_FAILED: {error}"))?; + Ok(receipt_hash) +} + +fn install_skill_at( + root: &Path, + entry: &MarketplaceCatalogEntry, + raw: &[u8], +) -> Result<(), String> { + let package = validate_skill_package(raw, entry)?; + let package_sha = sha256_hex(raw); + if package_sha != entry.artifact_sha256 { + return Err("HOLOLAKE_COGNITIVE_SKILL_ARTIFACT_DIGEST_MISMATCH".into()); + } + write_atomic( + &root.join("skills").join(format!("{package_sha}.ghskill")), + raw, + )?; + let observed = now_unix_ms(); + let mut connection = open_db(root)?; + let transaction = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(|error| format!("HOLOLAKE_SKILL_TX_FAILED: {error}"))?; + let current = transaction + .query_row( + "SELECT package_sha256 FROM installed_skills WHERE skill_number=?1", + params![entry.item_number], + |row| row.get::<_, String>(0), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_SKILL_CURRENT_READ_FAILED: {error}"))?; + if current.as_deref() == Some(package_sha.as_str()) { + transaction + .execute( + "UPDATE installed_skills SET state='ACTIVE_READONLY',updated_at_unix_ms=?2 WHERE skill_number=?1", + params![entry.item_number, observed], + ) + .map_err(|error| format!("HOLOLAKE_SKILL_REACTIVATE_FAILED: {error}"))?; + append_skill_receipt( + &transaction, + "SKILL_REACTIVATED", + &entry.item_number, + "ACTIVE_READONLY", + &package_sha, + observed, + )?; + } else { + transaction + .execute( + "INSERT INTO installed_skills(skill_number,display_name,version,package_sha256,previous_package_sha256,state,package_raw_base64,updated_at_unix_ms) + VALUES(?1,?2,?3,?4,?5,'ACTIVE_READONLY',?6,?7) + ON CONFLICT(skill_number) DO UPDATE SET display_name=excluded.display_name,version=excluded.version, + previous_package_sha256=installed_skills.package_sha256,package_sha256=excluded.package_sha256, + state='ACTIVE_READONLY',package_raw_base64=excluded.package_raw_base64,updated_at_unix_ms=excluded.updated_at_unix_ms", + params![entry.item_number,package.manifest.display_name,package.manifest.version,package_sha,current,STANDARD.encode(raw),observed], + ) + .map_err(|error| format!("HOLOLAKE_SKILL_INSTALL_FAILED: {error}"))?; + append_skill_receipt( + &transaction, + "SKILL_INSTALLED", + &entry.item_number, + "ACTIVE_READONLY", + &package_sha, + observed, + )?; + } + transaction + .commit() + .map_err(|error| format!("HOLOLAKE_SKILL_COMMIT_FAILED: {error}")) +} + +fn installed_skill_states( + connection: &Connection, +) -> Result, String> { + let mut statement = connection + .prepare( + "SELECT skill_number,state,package_sha256 FROM installed_skills ORDER BY skill_number", + ) + .map_err(|error| format!("HOLOLAKE_SKILL_STATE_PREPARE_FAILED: {error}"))?; + let rows = statement + .query_map([], |row| { + Ok(( + row.get::<_, String>(0)?, + (row.get::<_, String>(1)?, row.get::<_, String>(2)?), + )) + }) + .map_err(|error| format!("HOLOLAKE_SKILL_STATE_QUERY_FAILED: {error}"))?; + rows.collect::, _>>() + .map_err(|error| format!("HOLOLAKE_SKILL_STATE_ROW_FAILED: {error}")) +} + +async fn snapshot_with_note(app: AppHandle, note: String) -> Result { + let root = runtime_root(&app)?; + let connection = open_db(&root)?; + let stored = read_catalog_from_db(&connection, TRUST_RAW)?; + let (receipt_count, receipt_tip) = latest_catalog_receipt(&connection)?; + let skills = installed_skill_states(&connection)?; + let module_snapshot = crate::module_package_runtime::get_module_runtime_snapshot(app).await?; + let mut items = Vec::new(); + if let Some(stored) = &stored { + for entry in &stored.catalog.entries { + let (installed_state, installed_digest) = if entry.artifact_kind == "PHYSICAL_MODULE" { + module_snapshot + .modules + .iter() + .find(|module| module.module_number == entry.item_number) + .map(|module| (module.state.clone(), module.package_sha256.clone())) + .unwrap_or_else(|| ("NOT_INSTALLED".into(), String::new())) + } else { + skills + .get(&entry.item_number) + .cloned() + .unwrap_or_else(|| ("NOT_INSTALLED".into(), String::new())) + }; + items.push(MarketplaceItemView { + item_number: entry.item_number.clone(), + artifact_kind: entry.artifact_kind.clone(), + display_name: entry.display_name.clone(), + summary: entry.summary.clone(), + version: entry.version.clone(), + source_repository: entry.source_repository.clone(), + source_revision: entry.source_revision.clone(), + artifact_sha256: entry.artifact_sha256.clone(), + adapter: entry.adapter.clone(), + permissions: entry.permissions.clone(), + execution_authority: entry.execution_authority, + installed_state, + update_available: !installed_digest.is_empty() + && installed_digest != entry.artifact_sha256, + }); + } + } + Ok(MarketplaceSnapshot { + schema: "hololake.online-marketplace-snapshot/v1", + state: if stored.is_some() { + "ACTIVE_VERIFIED_CATALOG".into() + } else { + "WAITING_FIRST_VERIFIED_CATALOG".into() + }, + trust_state: serde_json::from_str::(TRUST_RAW) + .ok() + .and_then(|value| value.get("state")?.as_str().map(str::to_owned)) + .unwrap_or_else(|| "INVALID".into()), + catalog_epoch: stored + .as_ref() + .map(|stored| stored.catalog.epoch) + .unwrap_or(0), + catalog_sha256: stored + .as_ref() + .map(|stored| stored.catalog_sha256.clone()) + .unwrap_or_default(), + item_count: items.len(), + catalog_receipt_count: receipt_count, + latest_catalog_receipt_hash: receipt_tip, + last_sync_note: note, + items, + }) +} + +pub async fn get_marketplace_snapshot(app: AppHandle) -> Result { + snapshot_with_note(app, "已读取本机最后一次验签目录。".into()).await +} + +pub async fn sync_marketplace_catalog(app: AppHandle) -> Result { + let root = runtime_root(&app)?; + validate_embedded_contract()?; + let trust = parse_trust(TRUST_RAW)?; + let catalog_url = require_allowed_https_url(CATALOG_URL, &trust)?; + let signature_url = require_allowed_https_url(CATALOG_SIGNATURE_URL, &trust)?; + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(20)) + .redirect(reqwest::redirect::Policy::none()) + .build() + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_HTTP_FAILED: {error}"))?; + let cached = read_catalog_from_db(&open_db(&root)?, TRUST_RAW)?; + let mut request = client.get(catalog_url); + if let Some(cached) = &cached { + if !cached.etag.is_empty() + && cached.etag.len() <= 256 + && !cached.etag.contains(['\r', '\n']) + { + request = request.header(reqwest::header::IF_NONE_MATCH, &cached.etag); + } + } + let response = request + .send() + .await + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_CATALOG_UNREACHABLE: {error}"))?; + if response.status() == reqwest::StatusCode::NOT_MODIFIED { + return snapshot_with_note(app, "线上目录未变化,本机目录已是当前验签版本。".into()).await; + } + if !response.status().is_success() + || response + .content_length() + .is_some_and(|length| length > MAX_CATALOG_BYTES as u64) + { + return Err("HOLOLAKE_MARKETPLACE_CATALOG_RESPONSE_REJECTED".into()); + } + let etag = response + .headers() + .get(reqwest::header::ETAG) + .and_then(|value| value.to_str().ok()) + .filter(|value| value.len() <= 256 && !value.contains(['\r', '\n'])) + .unwrap_or_default() + .to_owned(); + let catalog_raw = response + .bytes() + .await + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_CATALOG_READ_FAILED: {error}"))? + .to_vec(); + let signature_raw = fetch_bounded(&client, signature_url, MAX_SIGNATURE_BYTES).await?; + let activated = activate_catalog(&root, &catalog_raw, &signature_raw, &etag, TRUST_RAW)?; + snapshot_with_note( + app, + if activated { + "线上双签目录已验真并原子激活。".into() + } else { + "线上目录与本机验签目录一致。".into() + }, + ) + .await +} + +pub async fn install_marketplace_item( + app: AppHandle, + input: InstallMarketplaceItemInput, +) -> Result { + if !is_sha256(&input.expected_artifact_sha256) { + return Err("HOLOLAKE_MARKETPLACE_EXPECTED_DIGEST_INVALID".into()); + } + let root = runtime_root(&app)?; + let trust = parse_trust(TRUST_RAW)?; + let stored = read_catalog_from_db(&open_db(&root)?, TRUST_RAW)? + .ok_or("HOLOLAKE_MARKETPLACE_CATALOG_NOT_SYNCED")?; + if stored.catalog.epoch != input.expected_catalog_epoch { + return Err("HOLOLAKE_MARKETPLACE_CATALOG_CHANGED_RETRY_REQUIRED".into()); + } + let entry = stored + .catalog + .entries + .iter() + .find(|entry| entry.item_number == input.item_number) + .cloned() + .ok_or("HOLOLAKE_MARKETPLACE_ITEM_UNKNOWN")?; + if entry.artifact_sha256 != input.expected_artifact_sha256 { + return Err("HOLOLAKE_MARKETPLACE_ARTIFACT_CHANGED_RETRY_REQUIRED".into()); + } + if entry.artifact_kind == "PHYSICAL_MODULE" + && !entry.permissions.is_empty() + && !input.human_confirmed_permission_expansion + { + return Err("HOLOLAKE_MARKETPLACE_PERMISSION_CONFIRMATION_REQUIRED".into()); + } + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(45)) + .redirect(reqwest::redirect::Policy::none()) + .build() + .map_err(|error| format!("HOLOLAKE_MARKETPLACE_HTTP_FAILED: {error}"))?; + let artifact = fetch_bounded( + &client, + require_allowed_https_url(&entry.artifact_url, &trust)?, + MAX_ARTIFACT_BYTES, + ) + .await?; + if sha256_hex(&artifact) != entry.artifact_sha256 { + return Err("HOLOLAKE_MARKETPLACE_ARTIFACT_DIGEST_MISMATCH".into()); + } + if entry.artifact_kind == "PHYSICAL_MODULE" { + let signature = fetch_bounded( + &client, + require_allowed_https_url(entry.signature_url.as_deref().unwrap_or_default(), &trust)?, + MAX_SIGNATURE_BYTES, + ) + .await?; + let download_root = root.join("download-cache"); + let package_path = download_root.join(format!("{}.ghmod", entry.artifact_sha256)); + let signature_path = download_root.join(format!("{}.ghmod.sig", entry.artifact_sha256)); + write_atomic(&package_path, &artifact)?; + write_atomic(&signature_path, &signature)?; + let verified = crate::module_package_runtime::verify_module_package( + app.clone(), + crate::module_package_runtime::VerifyModulePackageInput { + package_path: package_path.to_string_lossy().into_owned(), + signature_path: signature_path.to_string_lossy().into_owned(), + }, + ) + .await?; + if verified.module_number != entry.item_number + || verified.version != entry.version + || verified.adapter != entry.adapter.clone().unwrap_or_default() + || verified.permissions != entry.permissions + || verified.package_sha256 != entry.artifact_sha256 + || !verified.signature_verified + || verified.package_code_executed + { + return Err("HOLOLAKE_MARKETPLACE_MODULE_CATALOG_IDENTITY_MISMATCH".into()); + } + let before = + crate::module_package_runtime::get_module_runtime_snapshot(app.clone()).await?; + let already_active = before.modules.iter().any(|module| { + module.module_number == entry.item_number + && module.package_sha256 == entry.artifact_sha256 + && module.state == "ACTIVE" + }); + if !already_active { + crate::module_package_runtime::install_module_package( + app.clone(), + crate::module_package_runtime::InstallModulePackageInput { + package_path: package_path.to_string_lossy().into_owned(), + signature_path: signature_path.to_string_lossy().into_owned(), + expected_package_sha256: entry.artifact_sha256.clone(), + human_confirmed_permission_expansion: input + .human_confirmed_permission_expansion, + }, + ) + .await?; + crate::module_package_runtime::mount_module( + app.clone(), + crate::module_package_runtime::ModuleNumberInput { + module_number: entry.item_number.clone(), + }, + ) + .await?; + crate::module_package_runtime::self_test_module( + app.clone(), + crate::module_package_runtime::ModuleNumberInput { + module_number: entry.item_number.clone(), + }, + ) + .await?; + } + let _ = fs::remove_file(&package_path); + let _ = fs::remove_file(&signature_path); + } else { + install_skill_at(&root, &entry, &artifact)?; + } + let snapshot = snapshot_with_note( + app, + format!("{} 已完成下载、验签和安装。", entry.display_name), + ) + .await?; + Ok(MarketplaceMutationOutcome { + schema: "hololake.marketplace-mutation-outcome/v1", + state: "INSTALLED_ACTIVE".into(), + item_number: entry.item_number, + artifact_kind: entry.artifact_kind, + artifact_sha256: entry.artifact_sha256, + package_code_executed: false, + reality_authority_granted: false, + snapshot, + }) +} + +pub async fn uninstall_marketplace_item( + app: AppHandle, + input: MarketplaceItemInput, +) -> Result { + let root = runtime_root(&app)?; + let stored = read_catalog_from_db(&open_db(&root)?, TRUST_RAW)? + .ok_or("HOLOLAKE_MARKETPLACE_CATALOG_NOT_SYNCED")?; + let entry = stored + .catalog + .entries + .iter() + .find(|entry| entry.item_number == input.item_number) + .cloned() + .ok_or("HOLOLAKE_MARKETPLACE_ITEM_UNKNOWN")?; + if entry.artifact_kind == "PHYSICAL_MODULE" { + crate::module_package_runtime::unmount_module( + app.clone(), + crate::module_package_runtime::ModuleNumberInput { + module_number: entry.item_number.clone(), + }, + ) + .await?; + } else { + let observed = now_unix_ms(); + let mut connection = open_db(&root)?; + let transaction = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(|error| format!("HOLOLAKE_SKILL_TX_FAILED: {error}"))?; + let package_sha = transaction + .query_row( + "SELECT package_sha256 FROM installed_skills WHERE skill_number=?1", + params![entry.item_number], + |row| row.get::<_, String>(0), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_SKILL_CURRENT_READ_FAILED: {error}"))? + .ok_or("HOLOLAKE_SKILL_NOT_INSTALLED")?; + transaction + .execute( + "UPDATE installed_skills SET state='DORMANT',updated_at_unix_ms=?2 WHERE skill_number=?1", + params![entry.item_number, observed], + ) + .map_err(|error| format!("HOLOLAKE_SKILL_UNINSTALL_FAILED: {error}"))?; + append_skill_receipt( + &transaction, + "SKILL_UNMOUNTED", + &entry.item_number, + "DORMANT", + &package_sha, + observed, + )?; + transaction + .commit() + .map_err(|error| format!("HOLOLAKE_SKILL_COMMIT_FAILED: {error}"))?; + } + let snapshot = snapshot_with_note( + app, + format!("{} 已停用;用户数据与回执保留。", entry.display_name), + ) + .await?; + Ok(MarketplaceMutationOutcome { + schema: "hololake.marketplace-mutation-outcome/v1", + state: "DORMANT_PRESERVED".into(), + item_number: entry.item_number, + artifact_kind: entry.artifact_kind, + artifact_sha256: entry.artifact_sha256, + package_code_executed: false, + reality_authority_granted: false, + snapshot, + }) +} + +pub async fn rollback_marketplace_item( + app: AppHandle, + input: MarketplaceItemInput, +) -> Result { + let root = runtime_root(&app)?; + let stored = read_catalog_from_db(&open_db(&root)?, TRUST_RAW)? + .ok_or("HOLOLAKE_MARKETPLACE_CATALOG_NOT_SYNCED")?; + let entry = stored + .catalog + .entries + .iter() + .find(|entry| entry.item_number == input.item_number) + .cloned() + .ok_or("HOLOLAKE_MARKETPLACE_ITEM_UNKNOWN")?; + let active_sha = if entry.artifact_kind == "PHYSICAL_MODULE" { + crate::module_package_runtime::rollback_module( + app.clone(), + crate::module_package_runtime::ModuleNumberInput { + module_number: entry.item_number.clone(), + }, + ) + .await?; + crate::module_package_runtime::get_module_runtime_snapshot(app.clone()) + .await? + .modules + .into_iter() + .find(|module| module.module_number == entry.item_number) + .map(|module| module.package_sha256) + .ok_or("HOLOLAKE_MARKETPLACE_ROLLBACK_STATE_MISSING")? + } else { + let observed = now_unix_ms(); + let mut connection = open_db(&root)?; + let transaction = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(|error| format!("HOLOLAKE_SKILL_TX_FAILED: {error}"))?; + let (current_sha, previous_sha) = transaction + .query_row( + "SELECT package_sha256,previous_package_sha256 FROM installed_skills WHERE skill_number=?1", + params![entry.item_number], + |row| Ok((row.get::<_, String>(0)?, row.get::<_, Option>(1)?)), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_SKILL_CURRENT_READ_FAILED: {error}"))? + .ok_or("HOLOLAKE_SKILL_NOT_INSTALLED")?; + let previous_sha = previous_sha.ok_or("HOLOLAKE_SKILL_ROLLBACK_NOT_AVAILABLE")?; + let previous_raw = fs::read(root.join("skills").join(format!("{previous_sha}.ghskill"))) + .map_err(|error| format!("HOLOLAKE_SKILL_ROLLBACK_ARTIFACT_READ_FAILED: {error}"))?; + if sha256_hex(&previous_raw) != previous_sha { + return Err("HOLOLAKE_SKILL_ROLLBACK_ARTIFACT_TAMPERED".into()); + } + let previous_package: CognitiveSkillPackage = serde_json::from_slice(&previous_raw) + .map_err(|error| format!("HOLOLAKE_SKILL_ROLLBACK_JSON_INVALID: {error}"))?; + if previous_package.schema != SKILL_SCHEMA + || previous_package.manifest.skill_number != entry.item_number + || previous_package.manifest.execution_authority + || !previous_package.manifest.skill_readonly_guarantee + || !previous_package.manifest.permissions.is_empty() + || previous_package.manifest.content_digest != value_digest(&previous_package.payload)? + { + return Err("HOLOLAKE_SKILL_ROLLBACK_CONTRACT_INVALID".into()); + } + transaction + .execute( + "UPDATE installed_skills SET display_name=?2,version=?3,package_sha256=?4, + previous_package_sha256=?5,state='ACTIVE_READONLY',package_raw_base64=?6, + updated_at_unix_ms=?7 WHERE skill_number=?1", + params![ + entry.item_number, + previous_package.manifest.display_name, + previous_package.manifest.version, + previous_sha, + current_sha, + STANDARD.encode(&previous_raw), + observed + ], + ) + .map_err(|error| format!("HOLOLAKE_SKILL_ROLLBACK_FAILED: {error}"))?; + append_skill_receipt( + &transaction, + "SKILL_ROLLED_BACK", + &entry.item_number, + "ACTIVE_READONLY", + &previous_sha, + observed, + )?; + transaction + .commit() + .map_err(|error| format!("HOLOLAKE_SKILL_COMMIT_FAILED: {error}"))?; + previous_sha + }; + let snapshot = snapshot_with_note( + app, + format!("{} 已回滚到上一可信版本。", entry.display_name), + ) + .await?; + Ok(MarketplaceMutationOutcome { + schema: "hololake.marketplace-mutation-outcome/v1", + state: "ROLLED_BACK_ACTIVE".into(), + item_number: entry.item_number, + artifact_kind: entry.artifact_kind, + artifact_sha256: active_sha, + package_code_executed: false, + reality_authority_granted: false, + snapshot, + }) +} + +pub async fn get_active_cognitive_skills( + app: AppHandle, +) -> Result, String> { + let root = runtime_root(&app)?; + let connection = open_db(&root)?; + skill_receipt_tip(&connection)?; + let mut statement = connection + .prepare( + "SELECT skill_number,display_name,version,package_sha256,package_raw_base64 FROM installed_skills WHERE state='ACTIVE_READONLY' ORDER BY skill_number", + ) + .map_err(|error| format!("HOLOLAKE_ACTIVE_SKILL_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)?, + )) + }) + .map_err(|error| format!("HOLOLAKE_ACTIVE_SKILL_QUERY_FAILED: {error}"))?; + let mut skills = Vec::new(); + for row in rows { + let (number, display, version, package_sha, raw_b64) = + row.map_err(|error| format!("HOLOLAKE_ACTIVE_SKILL_ROW_FAILED: {error}"))?; + let raw = STANDARD + .decode(raw_b64) + .map_err(|_| "HOLOLAKE_ACTIVE_SKILL_ENCODING_INVALID".to_string())?; + if sha256_hex(&raw) != package_sha { + return Err("HOLOLAKE_ACTIVE_SKILL_ARTIFACT_TAMPERED".into()); + } + let package: CognitiveSkillPackage = serde_json::from_slice(&raw) + .map_err(|error| format!("HOLOLAKE_ACTIVE_SKILL_JSON_INVALID: {error}"))?; + if package.schema != SKILL_SCHEMA + || package.manifest.skill_number != number + || package.manifest.display_name != display + || package.manifest.version != version + || package.manifest.execution_authority + || !package.manifest.skill_readonly_guarantee + || !package.manifest.permissions.is_empty() + || package.manifest.content_digest != value_digest(&package.payload)? + { + return Err("HOLOLAKE_ACTIVE_SKILL_CONTRACT_INVALID".into()); + } + skills.push(ActiveCognitiveSkill { + skill_number: number, + display_name: display, + version, + package_sha256: package_sha, + execution_authority: false, + skill_readonly_guarantee: true, + payload: package.payload, + }); + } + Ok(skills) +} + +pub fn start_on_application_open(app: &AppHandle) -> Result<(), String> { + validate_embedded_contract()?; + let _ = open_db(&runtime_root(app)?)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use ring::signature::{Ed25519KeyPair, KeyPair}; + use serde_json::json; + + fn pair(seed: u8) -> Ed25519KeyPair { + Ed25519KeyPair::from_seed_unchecked(&[seed; 32]).unwrap() + } + + fn trust() -> String { + let origin = pair(21); + let enterprise = pair(22); + serde_json::to_string(&json!({ + "schema":"hololake.public-zero-core-trust/v1", + "recordId":"HLP-PUBLIC-ZERO-CORE-TRUST-001", + "state":"PROVISIONED", + "allowedHosts":["guanghu.chat","guanghulab.com"], + "signers":[ + {"signerId":"origin","signerClass":"ZERO_POINT_ORIGIN_PUBLIC_SCOPE_SIGNER","algorithm":"Ed25519","publicKeyBase64":STANDARD.encode(origin.public_key().as_ref())}, + {"signerId":"enterprise","signerClass":"ENTERPRISE_ZERO_CORE_DISTRIBUTION_SIGNER","algorithm":"Ed25519","publicKeyBase64":STANDARD.encode(enterprise.public_key().as_ref())} + ], + "requiredSignerClasses":["ZERO_POINT_ORIGIN_PUBLIC_SCOPE_SIGNER","ENTERPRISE_ZERO_CORE_DISTRIBUTION_SIGNER"], + "provisioningRule":"test" + })).unwrap() + } + + fn catalog_fixture(epoch: u64, skill_execution: bool) -> (Vec, Vec) { + let origin = pair(21); + let enterprise = pair(22); + let entries = vec![MarketplaceCatalogEntry { + item_number: "HLP-SKILL-OFFICIAL-DELIVERY-0001".into(), + artifact_kind: "COGNITIVE_SKILL".into(), + display_name: "交付核验思维技能".into(), + summary: "按证据区分源码、制品、部署与健康。".into(), + version: "0.1.0".into(), + minimum_host_version: "0.5.0".into(), + source_repository: "https://guanghulab.com/code/bingshuo/hololake-system-architecture" + .into(), + source_revision: "1".repeat(40), + source_path: "marketplace/skills/delivery.ghskill".into(), + artifact_url: format!( + "https://guanghu.chat/api/hololake/marketplace/artifacts/{}.ghskill", + "a".repeat(64) + ), + artifact_sha256: "a".repeat(64), + signature_url: None, + adapter: None, + permissions: vec![], + execution_authority: skill_execution, + skill_readonly_guarantee: true, + }]; + let catalog = MarketplaceCatalog { + schema: CATALOG_SCHEMA.into(), + plane_number: PLANE_NUMBER.into(), + epoch, + generated_at: "2026-08-19T00:00:00Z".into(), + minimum_host_version: HOST_VERSION.into(), + content_root_sha256: value_digest(&entries).unwrap(), + entries, + }; + let raw = serde_json::to_vec(&catalog).unwrap(); + let signature = serde_json::to_vec(&json!({ + "schema":CATALOG_SIGNATURE_SCHEMA, + "planeNumber":PLANE_NUMBER, + "contentSha256":sha256_hex(&raw), + "signatures":[ + {"signerId":"origin","signerClass":"ZERO_POINT_ORIGIN_PUBLIC_SCOPE_SIGNER","algorithm":"Ed25519","signatureBase64":STANDARD.encode(origin.sign(&raw).as_ref())}, + {"signerId":"enterprise","signerClass":"ENTERPRISE_ZERO_CORE_DISTRIBUTION_SIGNER","algorithm":"Ed25519","signatureBase64":STANDARD.encode(enterprise.sign(&raw).as_ref())} + ] + })).unwrap(); + (raw, signature) + } + + #[test] + fn catalog_requires_two_valid_signatures_and_rejects_skill_execution_authority() { + let (raw, signatures) = catalog_fixture(1, false); + assert_eq!( + validate_catalog(&raw, &signatures, &trust()).unwrap().epoch, + 1 + ); + let (unsafe_raw, unsafe_signatures) = catalog_fixture(2, true); + assert_eq!( + validate_catalog(&unsafe_raw, &unsafe_signatures, &trust()).unwrap_err(), + "HOLOLAKE_MARKETPLACE_COGNITIVE_SKILL_BOUNDARY_INVALID" + ); + let mut tampered = raw; + tampered.push(b' '); + assert_eq!( + validate_catalog(&tampered, &signatures, &trust()).unwrap_err(), + "HOLOLAKE_MARKETPLACE_SIGNATURE_BUNDLE_INVALID" + ); + } + + #[test] + fn catalog_epoch_rollback_and_same_epoch_equivocation_fail_closed() { + let temp = tempfile::tempdir().unwrap(); + let (one, one_sig) = catalog_fixture(5, false); + assert!(activate_catalog(temp.path(), &one, &one_sig, "etag-5", &trust()).unwrap()); + let (old, old_sig) = catalog_fixture(4, false); + assert_eq!( + activate_catalog(temp.path(), &old, &old_sig, "etag-4", &trust()).unwrap_err(), + "HOLOLAKE_MARKETPLACE_CATALOG_ROLLBACK_REJECTED" + ); + let mut same: MarketplaceCatalog = serde_json::from_slice(&one).unwrap(); + same.generated_at = "2026-08-19T00:00:01Z".into(); + let same_raw = serde_json::to_vec(&same).unwrap(); + let origin = pair(21); + let enterprise = pair(22); + let same_sig = serde_json::to_vec(&json!({ + "schema":CATALOG_SIGNATURE_SCHEMA,"planeNumber":PLANE_NUMBER,"contentSha256":sha256_hex(&same_raw), + "signatures":[ + {"signerId":"origin","signerClass":"ZERO_POINT_ORIGIN_PUBLIC_SCOPE_SIGNER","algorithm":"Ed25519","signatureBase64":STANDARD.encode(origin.sign(&same_raw).as_ref())}, + {"signerId":"enterprise","signerClass":"ENTERPRISE_ZERO_CORE_DISTRIBUTION_SIGNER","algorithm":"Ed25519","signatureBase64":STANDARD.encode(enterprise.sign(&same_raw).as_ref())} + ] + })).unwrap(); + assert_eq!( + activate_catalog(temp.path(), &same_raw, &same_sig, "etag-other", &trust()) + .unwrap_err(), + "HOLOLAKE_MARKETPLACE_CATALOG_EQUIVOCATION_REJECTED" + ); + assert_eq!( + latest_catalog_receipt(&open_db(temp.path()).unwrap()) + .unwrap() + .0, + 1 + ); + } + + #[test] + fn cognitive_skill_payload_cannot_gain_reality_authority() { + let entry = MarketplaceCatalogEntry { + item_number: "HLP-SKILL-OFFICIAL-DELIVERY-0001".into(), + artifact_kind: "COGNITIVE_SKILL".into(), + display_name: "交付核验思维技能".into(), + summary: "test".into(), + version: "0.1.0".into(), + minimum_host_version: HOST_VERSION.into(), + source_repository: "https://guanghulab.com/code/bingshuo/hololake-system-architecture" + .into(), + source_revision: "1".repeat(40), + source_path: "skill".into(), + artifact_url: "https://guanghu.chat/skill".into(), + artifact_sha256: String::new(), + signature_url: None, + adapter: None, + permissions: vec![], + execution_authority: false, + skill_readonly_guarantee: true, + }; + let payload = CognitiveSkillPayload { + purpose: "verify".into(), + triggers: vec!["交付".into()], + method: vec!["read evidence".into()], + constraints: vec!["no action".into()], + output_contract: vec!["facts".into()], + capability_references: vec![], + }; + let mut package = CognitiveSkillPackage { + schema: SKILL_SCHEMA.into(), + manifest: CognitiveSkillManifest { + skill_number: entry.item_number.clone(), + display_name: entry.display_name.clone(), + version: entry.version.clone(), + minimum_host_version: HOST_VERSION.into(), + content_digest: value_digest(&payload).unwrap(), + execution_authority: true, + skill_readonly_guarantee: true, + permissions: vec![], + }, + payload, + }; + let raw = serde_json::to_vec(&package).unwrap(); + let mut expected = entry.clone(); + expected.artifact_sha256 = sha256_hex(&raw); + assert_eq!( + validate_skill_package(&raw, &expected).unwrap_err(), + "HOLOLAKE_COGNITIVE_SKILL_CONTRACT_INVALID" + ); + package.manifest.execution_authority = false; + package.manifest.permissions = vec!["SHELL".into()]; + let raw = serde_json::to_vec(&package).unwrap(); + expected.artifact_sha256 = sha256_hex(&raw); + assert_eq!( + validate_skill_package(&raw, &expected).unwrap_err(), + "HOLOLAKE_COGNITIVE_SKILL_CONTRACT_INVALID" + ); + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/public_zero_core_distribution.rs b/product-source/hololake-native-desktop/src-tauri/src/public_zero_core_distribution.rs new file mode 100644 index 000000000..44f7d7d5d --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/public_zero_core_distribution.rs @@ -0,0 +1,1210 @@ +//! Public zero-core distribution plane. +//! +//! The enterprise node may host and distribute the public projection, but it cannot +//! become the zero-point origin authority. Every lamp manifest is therefore signed +//! by two independently registered Ed25519 signers before a bounded declarative +//! protocol payload can replace the current local release. + +use base64::{engine::general_purpose::STANDARD, Engine as _}; +use ring::{ + digest::{digest, SHA256}, + signature::{UnparsedPublicKey, ED25519}, +}; +use rusqlite::{params, Connection, OptionalExtension, TransactionBehavior}; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use uuid::Uuid; + +use crate::zero_point::ZeroPointProtocol; + +const TRUST_RAW: &str = include_str!("../../contracts/public-zero-core-trust.json"); +const HOST_VERSION: &str = "0.5.0"; +const TRUST_SCHEMA: &str = "hololake.public-zero-core-trust/v1"; +const LAMP_SCHEMA: &str = "hololake.public-zero-core-lamp/v1"; +const SIGNATURE_SCHEMA: &str = "hololake.public-zero-core-dual-signature/v1"; +const ARTIFACT_SCHEMA: &str = "hololake.public-zero-core-artifact/v1"; +const PLANE_NUMBER: &str = "HLP-DIST-PLANE-0001"; +const MAX_LAMP_BYTES: usize = 64 * 1024; +const MAX_SIGNATURE_BYTES: usize = 64 * 1024; +const MAX_ARTIFACT_BYTES: usize = 512 * 1024; + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct TrustStore { + schema: String, + record_id: String, + state: String, + allowed_hosts: Vec, + signers: Vec, + required_signer_classes: Vec, + provisioning_rule: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct TrustedSigner { + signer_id: String, + signer_class: String, + algorithm: String, + public_key_base64: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct LampManifest { + schema: String, + plane_number: String, + epoch: u64, + version: String, + content_root_sha256: String, + artifact_manifest_url: String, + signature_url: String, + published_at: String, + minimum_host_version: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct SignatureBundle { + schema: String, + plane_number: String, + content_sha256: String, + signatures: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct DetachedSignature { + signer_id: String, + signer_class: String, + algorithm: String, + signature_base64: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct PublicProtocolArtifact { + schema: String, + plane_number: String, + epoch: u64, + version: String, + minimum_host_version: String, + protocol: ZeroPointProtocol, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct ActiveRelease { + schema: String, + lamp: LampManifest, + lamp_sha256: String, + artifact_sha256: String, + protocol: ZeroPointProtocol, + lamp_raw_base64: String, + signature_bundle_raw_base64: String, + artifact_raw_base64: String, + activated_at_unix_ms: u64, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct ActivationReceipt { + schema: String, + event: String, + epoch: u64, + version: String, + lamp_sha256: String, + content_root_sha256: String, + previous_receipt_hash: String, + receipt_hash: String, + observed_at_unix_ms: u64, +} + +#[derive(Clone, Debug)] +struct DistributionState { + active_release: Option, + previous_release: Option, + receipts: Vec, +} + +#[derive(Clone, Debug, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PublicZeroCoreStatus { + pub state: String, + pub epoch: u64, + pub version: String, + pub content_root_sha256: String, + pub rollback_available: bool, + pub receipt_count: u64, + pub latest_receipt_hash: String, + pub trust_state: String, +} + +pub struct PublicZeroCoreSyncOutcome { + pub note: String, + pub activated_protocol: Option, + pub status: PublicZeroCoreStatus, +} + +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(|character| character.is_ascii_digit() || ('a'..='f').contains(&character)) +} + +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 core_root(home: &Path) -> PathBuf { + home.join("core").join("public-zero-core-v1") +} + +fn state_path(root: &Path) -> PathBuf { + root.join("distribution-state.sqlite3") +} + +fn etag_path(root: &Path) -> PathBuf { + root.join("lamp-etag") +} + +fn parse_trust(raw: &str) -> Result { + let trust: TrustStore = serde_json::from_str(raw) + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_TRUST_INVALID: {error}"))?; + let required = trust + .required_signer_classes + .iter() + .cloned() + .collect::>(); + if trust.schema != TRUST_SCHEMA + || trust.record_id != "HLP-PUBLIC-ZERO-CORE-TRUST-001" + || trust.provisioning_rule.trim().is_empty() + || required + != BTreeSet::from([ + "ZERO_POINT_ORIGIN_PUBLIC_SCOPE_SIGNER".to_string(), + "ENTERPRISE_ZERO_CORE_DISTRIBUTION_SIGNER".to_string(), + ]) + || trust.allowed_hosts.is_empty() + || trust.allowed_hosts.iter().any(|host| { + host.is_empty() + || host != &host.to_ascii_lowercase() + || host.contains('/') + || host.contains(':') + }) + { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_TRUST_BOUNDARY_INVALID".into()); + } + if trust.state != "PROVISIONED" { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_TRUST_UNPROVISIONED".into()); + } + if trust.signers.len() != 2 { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_TRUST_SIGNER_COUNT_INVALID".into()); + } + let mut signer_ids = BTreeSet::new(); + let mut signer_classes = BTreeSet::new(); + for signer in &trust.signers { + let key = STANDARD + .decode(&signer.public_key_base64) + .map_err(|_| "HOLOLAKE_PUBLIC_ZERO_CORE_TRUST_KEY_INVALID".to_string())?; + if signer.signer_id.trim().is_empty() + || signer.algorithm != "Ed25519" + || key.len() != 32 + || !signer_ids.insert(signer.signer_id.clone()) + || !signer_classes.insert(signer.signer_class.clone()) + { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_TRUST_SIGNER_INVALID".into()); + } + } + if signer_classes != required { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_TRUST_CLASSES_INVALID".into()); + } + Ok(trust) +} + +fn require_allowed_https_url(raw: &str, trust: &TrustStore) -> Result { + let url = reqwest::Url::parse(raw) + .map_err(|_| "HOLOLAKE_PUBLIC_ZERO_CORE_URL_INVALID".to_string())?; + let host = url + .host_str() + .ok_or_else(|| "HOLOLAKE_PUBLIC_ZERO_CORE_URL_HOST_REQUIRED".to_string())?; + if url.scheme() != "https" + || url.username() != "" + || url.password().is_some() + || !trust.allowed_hosts.iter().any(|allowed| allowed == host) + { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_URL_NOT_ALLOWED".into()); + } + Ok(url) +} + +fn validate_protocol(protocol: &ZeroPointProtocol, trust: &TrustStore) -> Result<(), String> { + if !(1..=30).contains(&protocol.grace_period_days) + || protocol.origin.trim().is_empty() + || protocol.origin.len() > 240 + { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_PROTOCOL_BOUNDARY_INVALID".into()); + } + for raw in [ + &protocol.lighthouse_anchor_url, + &protocol.lighthouse_resolve_url, + &protocol.enterprise_resolve_url, + &protocol.core_channel_source, + ] { + require_allowed_https_url(raw, trust)?; + } + Ok(()) +} + +fn validate_lamp(lamp: &LampManifest, trust: &TrustStore) -> Result<(), String> { + let version = version_triplet(&lamp.version); + let minimum = version_triplet(&lamp.minimum_host_version); + if lamp.schema != LAMP_SCHEMA + || lamp.plane_number != PLANE_NUMBER + || lamp.epoch == 0 + || version.is_none() + || minimum.is_none() + || minimum > version_triplet(HOST_VERSION) + || !is_sha256(&lamp.content_root_sha256) + || lamp.published_at.trim().is_empty() + || lamp.published_at.len() > 80 + { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_LAMP_BOUNDARY_INVALID".into()); + } + let artifact_url = require_allowed_https_url(&lamp.artifact_manifest_url, trust)?; + let signature_url = require_allowed_https_url(&lamp.signature_url, trust)?; + if artifact_url == signature_url { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_RELEASE_URL_COLLISION".into()); + } + Ok(()) +} + +fn verify_dual_signatures( + lamp_raw: &[u8], + signature_raw: &[u8], + trust: &TrustStore, +) -> Result<(), String> { + let bundle: SignatureBundle = serde_json::from_slice(signature_raw) + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_SIGNATURE_JSON_INVALID: {error}"))?; + if bundle.schema != SIGNATURE_SCHEMA + || bundle.plane_number != PLANE_NUMBER + || bundle.content_sha256 != sha256_hex(lamp_raw) + || bundle.signatures.len() != 2 + { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_SIGNATURE_BUNDLE_INVALID".into()); + } + let trusted = trust + .signers + .iter() + .map(|signer| (signer.signer_id.as_str(), signer)) + .collect::>(); + let mut verified_ids = BTreeSet::new(); + let mut verified_classes = BTreeSet::new(); + for detached in &bundle.signatures { + let signer = trusted + .get(detached.signer_id.as_str()) + .ok_or_else(|| "HOLOLAKE_PUBLIC_ZERO_CORE_SIGNATURE_SIGNER_UNKNOWN".to_string())?; + if detached.algorithm != "Ed25519" + || signer.algorithm != detached.algorithm + || signer.signer_class != detached.signer_class + || !verified_ids.insert(detached.signer_id.clone()) + || !verified_classes.insert(detached.signer_class.clone()) + { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_SIGNATURE_IDENTITY_INVALID".into()); + } + let key = STANDARD + .decode(&signer.public_key_base64) + .map_err(|_| "HOLOLAKE_PUBLIC_ZERO_CORE_TRUST_KEY_INVALID".to_string())?; + let signature = STANDARD + .decode(&detached.signature_base64) + .map_err(|_| "HOLOLAKE_PUBLIC_ZERO_CORE_SIGNATURE_ENCODING_INVALID".to_string())?; + UnparsedPublicKey::new(&ED25519, key) + .verify(lamp_raw, &signature) + .map_err(|_| "HOLOLAKE_PUBLIC_ZERO_CORE_SIGNATURE_REJECTED".to_string())?; + } + let required = trust + .required_signer_classes + .iter() + .cloned() + .collect::>(); + if verified_classes != required { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_SIGNATURE_CLASSES_INCOMPLETE".into()); + } + Ok(()) +} + +fn decode_persisted_bytes(value: &str, maximum: usize, label: &str) -> Result, String> { + let bytes = STANDARD + .decode(value) + .map_err(|_| format!("HOLOLAKE_PUBLIC_ZERO_CORE_PERSISTED_{label}_BASE64_INVALID"))?; + if bytes.is_empty() || bytes.len() > maximum { + return Err(format!( + "HOLOLAKE_PUBLIC_ZERO_CORE_PERSISTED_{label}_SIZE_INVALID" + )); + } + Ok(bytes) +} + +fn validate_active(active: &ActiveRelease, trust_raw: &str) -> Result<(), String> { + if active.schema != "hololake.public-zero-core-active-release/v1" + || active.lamp.schema != LAMP_SCHEMA + || active.lamp.plane_number != PLANE_NUMBER + || active.lamp.content_root_sha256 != active.artifact_sha256 + || !is_sha256(&active.lamp_sha256) + || !is_sha256(&active.artifact_sha256) + { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_ACTIVE_BOUNDARY_INVALID".into()); + } + let trust = parse_trust(trust_raw)?; + let lamp_raw = decode_persisted_bytes(&active.lamp_raw_base64, MAX_LAMP_BYTES, "LAMP")?; + let signature_raw = decode_persisted_bytes( + &active.signature_bundle_raw_base64, + MAX_SIGNATURE_BYTES, + "SIGNATURE", + )?; + let artifact_raw = + decode_persisted_bytes(&active.artifact_raw_base64, MAX_ARTIFACT_BYTES, "ARTIFACT")?; + let persisted_lamp: LampManifest = serde_json::from_slice(&lamp_raw) + .map_err(|_| "HOLOLAKE_PUBLIC_ZERO_CORE_PERSISTED_LAMP_JSON_INVALID".to_string())?; + let persisted_artifact: PublicProtocolArtifact = serde_json::from_slice(&artifact_raw) + .map_err(|_| "HOLOLAKE_PUBLIC_ZERO_CORE_PERSISTED_ARTIFACT_JSON_INVALID".to_string())?; + if persisted_lamp != active.lamp + || sha256_hex(&lamp_raw) != active.lamp_sha256 + || sha256_hex(&artifact_raw) != active.artifact_sha256 + || persisted_artifact.schema != ARTIFACT_SCHEMA + || persisted_artifact.plane_number != PLANE_NUMBER + || persisted_artifact.epoch != active.lamp.epoch + || persisted_artifact.version != active.lamp.version + || persisted_artifact.minimum_host_version != active.lamp.minimum_host_version + || persisted_artifact.protocol != active.protocol + { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_PERSISTED_RELEASE_TAMPERED".into()); + } + validate_lamp(&persisted_lamp, &trust)?; + validate_protocol(&persisted_artifact.protocol, &trust)?; + verify_dual_signatures(&lamp_raw, &signature_raw, &trust)?; + Ok(()) +} + +fn receipt_hash(receipt: &ActivationReceipt) -> String { + sha256_hex( + format!( + "HLP-PUBLIC-ZERO-CORE-RECEIPT-v1\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}", + receipt.event, + receipt.epoch, + receipt.version, + receipt.lamp_sha256, + receipt.content_root_sha256, + receipt.previous_receipt_hash, + receipt.observed_at_unix_ms, + receipt.schema + ) + .as_bytes(), + ) +} + +fn validate_receipts(receipts: &[ActivationReceipt]) -> Result<(), String> { + let mut previous = "0".repeat(64); + for receipt in receipts { + if receipt.schema != "hololake.public-zero-core-activation-receipt/v1" + || receipt.event != "PUBLIC_ZERO_CORE_PROTOCOL_ACTIVATED" + || receipt.previous_receipt_hash != previous + || receipt.receipt_hash != receipt_hash(receipt) + || !is_sha256(&receipt.receipt_hash) + { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_RECEIPT_CHAIN_INVALID".into()); + } + previous = receipt.receipt_hash.clone(); + } + Ok(()) +} + +fn open_state_db(root: &Path) -> Result { + fs::create_dir_all(root) + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_ROOT_FAILED: {error}"))?; + let path = state_path(root); + if path.exists() { + let metadata = fs::symlink_metadata(&path) + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_STATE_METADATA_FAILED: {error}"))?; + if metadata.file_type().is_symlink() || !metadata.is_file() { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_STATE_FILE_INVALID".into()); + } + } + let connection = Connection::open(path) + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_STATE_OPEN_FAILED: {error}"))?; + connection + .busy_timeout(Duration::from_secs(5)) + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_STATE_TIMEOUT_FAILED: {error}"))?; + connection + .execute_batch( + "PRAGMA journal_mode=WAL; + PRAGMA synchronous=FULL; + CREATE TABLE IF NOT EXISTS distribution_metadata( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS distribution_releases( + slot TEXT PRIMARY KEY CHECK(slot IN ('active','previous')), + payload_json TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS activation_receipts( + sequence INTEGER PRIMARY KEY AUTOINCREMENT, + payload_json TEXT NOT NULL + ); + INSERT OR IGNORE INTO distribution_metadata(key,value) + VALUES('schema','hololake.public-zero-core-distribution-state/v1');", + ) + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_STATE_SCHEMA_FAILED: {error}"))?; + Ok(connection) +} + +fn release_from_db( + connection: &Connection, + slot: &str, + trust_raw: &str, +) -> Result, String> { + let raw = connection + .query_row( + "SELECT payload_json FROM distribution_releases WHERE slot=?1", + params![slot], + |row| row.get::<_, String>(0), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_RELEASE_READ_FAILED: {error}"))?; + raw.map(|raw| { + let release: ActiveRelease = serde_json::from_str(&raw) + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_RELEASE_JSON_INVALID: {error}"))?; + validate_active(&release, trust_raw)?; + Ok(release) + }) + .transpose() +} + +fn read_state_from_db( + connection: &Connection, + trust_raw: &str, +) -> Result { + let schema = connection + .query_row( + "SELECT value FROM distribution_metadata WHERE key='schema'", + [], + |row| row.get::<_, String>(0), + ) + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_STATE_METADATA_INVALID: {error}"))?; + if schema != "hololake.public-zero-core-distribution-state/v1" { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_STATE_SCHEMA_INVALID".into()); + } + let active_release = release_from_db(connection, "active", trust_raw)?; + let previous_release = release_from_db(connection, "previous", trust_raw)?; + let mut statement = connection + .prepare("SELECT payload_json FROM activation_receipts ORDER BY sequence") + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_RECEIPT_PREPARE_FAILED: {error}"))?; + let rows = statement + .query_map([], |row| row.get::<_, String>(0)) + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_RECEIPT_QUERY_FAILED: {error}"))?; + let mut receipts = Vec::new(); + for raw in rows { + let raw = + raw.map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_RECEIPT_ROW_FAILED: {error}"))?; + receipts.push( + serde_json::from_str(&raw).map_err(|error| { + format!("HOLOLAKE_PUBLIC_ZERO_CORE_RECEIPT_JSON_INVALID: {error}") + })?, + ); + } + validate_receipts(&receipts)?; + match (&active_release, receipts.last()) { + (None, None) => {} + (Some(active), Some(receipt)) + if receipt.epoch == active.lamp.epoch + && receipt.version == active.lamp.version + && receipt.lamp_sha256 == active.lamp_sha256 + && receipt.content_root_sha256 == active.lamp.content_root_sha256 => {} + _ => return Err("HOLOLAKE_PUBLIC_ZERO_CORE_STATE_RECEIPT_MISMATCH".into()), + } + if active_release.is_none() && previous_release.is_some() { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_PREVIOUS_WITHOUT_ACTIVE".into()); + } + if let (Some(active), Some(previous)) = (&active_release, &previous_release) { + if previous.lamp.epoch >= active.lamp.epoch { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_PREVIOUS_RELEASE_INVALID".into()); + } + } + Ok(DistributionState { + active_release, + previous_release, + receipts, + }) +} + +fn read_state(root: &Path, trust_raw: &str) -> Result { + read_state_from_db(&open_state_db(root)?, trust_raw) +} + +fn read_active(root: &Path, trust_raw: &str) -> Result, String> { + Ok(read_state(root, trust_raw)?.active_release) +} + +fn candidate_requires_activation( + current: Option<&ActiveRelease>, + lamp: &LampManifest, + lamp_sha256: &str, +) -> Result { + let Some(current) = current else { + return Ok(true); + }; + if lamp.epoch < current.lamp.epoch { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_ROLLBACK_REJECTED".into()); + } + if lamp.epoch == current.lamp.epoch { + if lamp.version != current.lamp.version + || lamp.content_root_sha256 != current.lamp.content_root_sha256 + || lamp_sha256 != current.lamp_sha256 + { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_EPOCH_EQUIVOCATION_REJECTED".into()); + } + return Ok(false); + } + if version_triplet(&lamp.version) <= version_triplet(¤t.lamp.version) { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_VERSION_NOT_MONOTONIC".into()); + } + Ok(true) +} + +fn write_staged(root: &Path, name: &str, bytes: &[u8]) -> Result { + fs::create_dir_all(root) + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_ROOT_FAILED: {error}"))?; + let path = root.join(format!(".{name}-{}", Uuid::new_v4())); + fs::write(&path, bytes) + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_STAGE_FAILED: {error}"))?; + Ok(path) +} + +fn activate_release( + root: &Path, + lamp: LampManifest, + lamp_raw: &[u8], + signature_raw: &[u8], + artifact_raw: &[u8], + artifact: PublicProtocolArtifact, + trust_raw: &str, +) -> Result<(bool, PublicZeroCoreStatus), String> { + let lamp_sha256 = sha256_hex(lamp_raw); + let observed = now_unix_ms(); + let active = ActiveRelease { + schema: "hololake.public-zero-core-active-release/v1".into(), + lamp: lamp.clone(), + lamp_sha256: lamp_sha256.clone(), + artifact_sha256: lamp.content_root_sha256.clone(), + protocol: artifact.protocol, + lamp_raw_base64: STANDARD.encode(lamp_raw), + signature_bundle_raw_base64: STANDARD.encode(signature_raw), + artifact_raw_base64: STANDARD.encode(artifact_raw), + activated_at_unix_ms: observed, + }; + let mut connection = open_state_db(root)?; + let transaction = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_STATE_TX_FAILED: {error}"))?; + let state = read_state_from_db(&transaction, trust_raw)?; + if !candidate_requires_activation(state.active_release.as_ref(), &lamp, &lamp_sha256)? { + drop(transaction); + return Ok((false, status_at_with_trust(root, trust_raw)?)); + } + let previous_receipt_hash = state + .receipts + .last() + .map(|receipt| receipt.receipt_hash.clone()) + .unwrap_or_else(|| "0".repeat(64)); + let mut receipt = ActivationReceipt { + schema: "hololake.public-zero-core-activation-receipt/v1".into(), + event: "PUBLIC_ZERO_CORE_PROTOCOL_ACTIVATED".into(), + epoch: lamp.epoch, + version: lamp.version.clone(), + lamp_sha256, + content_root_sha256: lamp.content_root_sha256.clone(), + previous_receipt_hash, + receipt_hash: String::new(), + observed_at_unix_ms: observed, + }; + receipt.receipt_hash = receipt_hash(&receipt); + if let Some(previous) = state.active_release { + transaction + .execute( + "INSERT INTO distribution_releases(slot,payload_json) VALUES('previous',?1) + ON CONFLICT(slot) DO UPDATE SET payload_json=excluded.payload_json", + params![serde_json::to_string(&previous).map_err(|error| format!( + "HOLOLAKE_PUBLIC_ZERO_CORE_PREVIOUS_ENCODE_FAILED: {error}" + ))?], + ) + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_PREVIOUS_WRITE_FAILED: {error}"))?; + } else { + transaction + .execute( + "DELETE FROM distribution_releases WHERE slot='previous'", + [], + ) + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_PREVIOUS_CLEAR_FAILED: {error}"))?; + } + transaction + .execute( + "INSERT INTO distribution_releases(slot,payload_json) VALUES('active',?1) + ON CONFLICT(slot) DO UPDATE SET payload_json=excluded.payload_json", + params![serde_json::to_string(&active).map_err(|error| format!( + "HOLOLAKE_PUBLIC_ZERO_CORE_ACTIVE_ENCODE_FAILED: {error}" + ))?], + ) + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_ACTIVE_WRITE_FAILED: {error}"))?; + transaction + .execute( + "INSERT INTO activation_receipts(payload_json) VALUES(?1)", + params![serde_json::to_string(&receipt).map_err(|error| format!( + "HOLOLAKE_PUBLIC_ZERO_CORE_RECEIPT_ENCODE_FAILED: {error}" + ))?], + ) + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_RECEIPT_WRITE_FAILED: {error}"))?; + transaction + .commit() + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_STATE_COMMIT_FAILED: {error}"))?; + Ok((true, status_at_with_trust(root, trust_raw)?)) +} + +fn verify_release_bytes_at( + root: &Path, + lamp_raw: &[u8], + signature_raw: &[u8], + artifact_raw: &[u8], + trust_raw: &str, +) -> Result<(bool, PublicZeroCoreStatus), String> { + if lamp_raw.is_empty() + || lamp_raw.len() > MAX_LAMP_BYTES + || signature_raw.is_empty() + || signature_raw.len() > MAX_SIGNATURE_BYTES + || artifact_raw.is_empty() + || artifact_raw.len() > MAX_ARTIFACT_BYTES + { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_RELEASE_SIZE_INVALID".into()); + } + let trust = parse_trust(trust_raw)?; + let lamp: LampManifest = serde_json::from_slice(lamp_raw) + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_LAMP_JSON_INVALID: {error}"))?; + validate_lamp(&lamp, &trust)?; + verify_dual_signatures(lamp_raw, signature_raw, &trust)?; + if sha256_hex(artifact_raw) != lamp.content_root_sha256 { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_CONTENT_ROOT_MISMATCH".into()); + } + let artifact: PublicProtocolArtifact = serde_json::from_slice(artifact_raw) + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_ARTIFACT_JSON_INVALID: {error}"))?; + if artifact.schema != ARTIFACT_SCHEMA + || artifact.plane_number != PLANE_NUMBER + || artifact.epoch != lamp.epoch + || artifact.version != lamp.version + || artifact.minimum_host_version != lamp.minimum_host_version + { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_ARTIFACT_IDENTITY_INVALID".into()); + } + validate_protocol(&artifact.protocol, &trust)?; + let lamp_sha256 = sha256_hex(lamp_raw); + if !candidate_requires_activation(read_active(root, trust_raw)?.as_ref(), &lamp, &lamp_sha256)? + { + return Ok((false, status_at_with_trust(root, trust_raw)?)); + } + activate_release( + root, + lamp, + lamp_raw, + signature_raw, + artifact_raw, + artifact, + trust_raw, + ) +} + +async fn fetch_bounded( + client: &reqwest::Client, + url: reqwest::Url, + maximum: usize, +) -> Result, String> { + let response = client + .get(url) + .send() + .await + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_DOWNLOAD_FAILED: {error}"))?; + if !response.status().is_success() + || response + .content_length() + .is_some_and(|length| length > maximum as u64) + { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_DOWNLOAD_REJECTED".into()); + } + let bytes = response + .bytes() + .await + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_DOWNLOAD_READ_FAILED: {error}"))?; + if bytes.is_empty() || bytes.len() > maximum { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_DOWNLOAD_SIZE_INVALID".into()); + } + Ok(bytes.to_vec()) +} + +pub async fn sync_public_zero_core( + home: &Path, + anchor_url: &str, +) -> Result { + let root = core_root(home); + fs::create_dir_all(&root) + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_ROOT_FAILED: {error}"))?; + let trust = parse_trust(TRUST_RAW)?; + let anchor = require_allowed_https_url(anchor_url, &trust)?; + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(15)) + .redirect(reqwest::redirect::Policy::none()) + .build() + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_HTTP_FAILED: {error}"))?; + let mut request = client.get(anchor); + if let Ok(etag) = fs::read_to_string(etag_path(&root)) { + let trimmed = etag.trim(); + if !trimmed.is_empty() && trimmed.len() <= 256 && !trimmed.contains(['\r', '\n']) { + request = request.header(reqwest::header::IF_NONE_MATCH, trimmed); + } + } + let response = request + .send() + .await + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_LAMP_UNREACHABLE: {error}"))?; + if response.status() == reqwest::StatusCode::NOT_MODIFIED { + return Ok(PublicZeroCoreSyncOutcome { + note: "公众零点原核灯号未变化,本机协议已是当前已验签版本。".into(), + activated_protocol: None, + status: status_at_with_trust(&root, TRUST_RAW)?, + }); + } + if !response.status().is_success() + || response + .content_length() + .is_some_and(|length| length > MAX_LAMP_BYTES as u64) + { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_LAMP_RESPONSE_REJECTED".into()); + } + let etag = response + .headers() + .get(reqwest::header::ETAG) + .and_then(|value| value.to_str().ok()) + .filter(|value| value.len() <= 256 && !value.contains(['\r', '\n'])) + .map(str::to_owned); + let lamp_raw = response + .bytes() + .await + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_LAMP_READ_FAILED: {error}"))? + .to_vec(); + if lamp_raw.is_empty() || lamp_raw.len() > MAX_LAMP_BYTES { + return Err("HOLOLAKE_PUBLIC_ZERO_CORE_LAMP_SIZE_INVALID".into()); + } + let lamp: LampManifest = serde_json::from_slice(&lamp_raw) + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_LAMP_JSON_INVALID: {error}"))?; + validate_lamp(&lamp, &trust)?; + let signature = fetch_bounded( + &client, + require_allowed_https_url(&lamp.signature_url, &trust)?, + MAX_SIGNATURE_BYTES, + ) + .await?; + let artifact = fetch_bounded( + &client, + require_allowed_https_url(&lamp.artifact_manifest_url, &trust)?, + MAX_ARTIFACT_BYTES, + ) + .await?; + let (activated, status) = + verify_release_bytes_at(&root, &lamp_raw, &signature, &artifact, TRUST_RAW)?; + if let Some(etag) = etag { + let staged = write_staged(&root, "lamp-etag", etag.as_bytes())?; + fs::rename(staged, etag_path(&root)) + .map_err(|error| format!("HOLOLAKE_PUBLIC_ZERO_CORE_ETAG_COMMIT_FAILED: {error}"))?; + } + let activated_protocol = if activated { + read_active(&root, TRUST_RAW)?.map(|active| active.protocol) + } else { + None + }; + Ok(PublicZeroCoreSyncOutcome { + note: if activated { + format!( + "公众零点原核协议已完成双签验真并原子更新至 {}(epoch {})。", + status.version, status.epoch + ) + } else { + "公众零点原核协议已是当前已验签版本。".into() + }, + activated_protocol, + status, + }) +} + +pub fn load_active_protocol(home: &Path) -> Result, String> { + read_active(&core_root(home), TRUST_RAW).map(|active| active.map(|release| release.protocol)) +} + +fn trust_state_from_raw(raw: &str) -> String { + serde_json::from_str::(raw) + .ok() + .and_then(|value| value.get("state")?.as_str().map(str::to_owned)) + .unwrap_or_else(|| "INVALID".into()) +} + +fn status_at_with_trust(root: &Path, trust_raw: &str) -> Result { + let trust_state = trust_state_from_raw(trust_raw); + let state = read_state(root, trust_raw)?; + let active = state.active_release; + let receipts = state.receipts; + Ok(PublicZeroCoreStatus { + state: if active.is_some() { + "ACTIVE_VERIFIED".into() + } else if trust_state == "PROVISIONED" { + "WAITING_FIRST_SIGNED_RELEASE".into() + } else { + "TRUST_NOT_PROVISIONED".into() + }, + epoch: active + .as_ref() + .map(|release| release.lamp.epoch) + .unwrap_or(0), + version: active + .as_ref() + .map(|release| release.lamp.version.clone()) + .unwrap_or_default(), + content_root_sha256: active + .as_ref() + .map(|release| release.lamp.content_root_sha256.clone()) + .unwrap_or_default(), + rollback_available: state.previous_release.is_some(), + receipt_count: receipts.len() as u64, + latest_receipt_hash: receipts + .last() + .map(|receipt| receipt.receipt_hash.clone()) + .unwrap_or_default(), + trust_state, + }) +} + +pub fn status_at(home: &Path) -> Result { + status_at_with_trust(&core_root(home), TRUST_RAW) +} + +#[cfg(test)] +mod tests { + use super::*; + use ring::signature::Ed25519KeyPair; + use serde_json::json; + + fn key_pair(seed: u8) -> Ed25519KeyPair { + Ed25519KeyPair::from_seed_unchecked(&[seed; 32]).unwrap() + } + + fn fixture(epoch: u64, version: &str) -> (Vec, Vec, Vec, String) { + fixture_with_origin(epoch, version, "PUBLIC_ZERO_CORE_TEST") + } + + fn fixture_with_origin( + epoch: u64, + version: &str, + origin_label: &str, + ) -> (Vec, Vec, Vec, String) { + use ring::signature::KeyPair; + let origin = key_pair(11); + let enterprise = key_pair(29); + let artifact = json!({ + "schema": ARTIFACT_SCHEMA, + "planeNumber": PLANE_NUMBER, + "epoch": epoch, + "version": version, + "minimumHostVersion": "0.5.0", + "protocol": { + "gracePeriodDays": 7, + "lighthouseAnchorUrl": "https://guanghu.chat/api/hololake/zero-core/lamp", + "lighthouseResolveUrl": "https://guanghulab.com/api/ai/v1/resolve?id=", + "enterpriseResolveUrl": "https://guanghu.chat/api/hololake/enterprise/resolve", + "coreChannelSource": "https://guanghulab.com/code/bingshuo/guanghu-ice-heart", + "origin": origin_label + } + }); + let artifact_raw = serde_json::to_vec(&artifact).unwrap(); + let lamp = json!({ + "schema": LAMP_SCHEMA, + "planeNumber": PLANE_NUMBER, + "epoch": epoch, + "version": version, + "contentRootSha256": sha256_hex(&artifact_raw), + "artifactManifestUrl": "https://guanghu.chat/api/hololake/zero-core/releases/artifact.json", + "signatureUrl": "https://guanghu.chat/api/hololake/zero-core/releases/lamp.signatures.json", + "publishedAt": "2026-08-19T00:00:00Z", + "minimumHostVersion": "0.5.0" + }); + let lamp_raw = serde_json::to_vec(&lamp).unwrap(); + let signatures = json!({ + "schema": SIGNATURE_SCHEMA, + "planeNumber": PLANE_NUMBER, + "contentSha256": sha256_hex(&lamp_raw), + "signatures": [ + { + "signerId": "HLP-SIGNER-ZERO-ORIGIN-TEST", + "signerClass": "ZERO_POINT_ORIGIN_PUBLIC_SCOPE_SIGNER", + "algorithm": "Ed25519", + "signatureBase64": STANDARD.encode(origin.sign(&lamp_raw).as_ref()) + }, + { + "signerId": "HLP-SIGNER-ENTERPRISE-DIST-TEST", + "signerClass": "ENTERPRISE_ZERO_CORE_DISTRIBUTION_SIGNER", + "algorithm": "Ed25519", + "signatureBase64": STANDARD.encode(enterprise.sign(&lamp_raw).as_ref()) + } + ] + }); + let trust = json!({ + "schema": TRUST_SCHEMA, + "recordId": "HLP-PUBLIC-ZERO-CORE-TRUST-001", + "state": "PROVISIONED", + "allowedHosts": ["guanghu.chat", "guanghulab.com"], + "signers": [ + { + "signerId": "HLP-SIGNER-ZERO-ORIGIN-TEST", + "signerClass": "ZERO_POINT_ORIGIN_PUBLIC_SCOPE_SIGNER", + "algorithm": "Ed25519", + "publicKeyBase64": STANDARD.encode(origin.public_key().as_ref()) + }, + { + "signerId": "HLP-SIGNER-ENTERPRISE-DIST-TEST", + "signerClass": "ENTERPRISE_ZERO_CORE_DISTRIBUTION_SIGNER", + "algorithm": "Ed25519", + "publicKeyBase64": STANDARD.encode(enterprise.public_key().as_ref()) + } + ], + "requiredSignerClasses": [ + "ZERO_POINT_ORIGIN_PUBLIC_SCOPE_SIGNER", + "ENTERPRISE_ZERO_CORE_DISTRIBUTION_SIGNER" + ], + "provisioningRule": "TEST_PUBLIC_KEYS_ONLY" + }); + ( + lamp_raw, + serde_json::to_vec(&signatures).unwrap(), + artifact_raw, + serde_json::to_string(&trust).unwrap(), + ) + } + + #[test] + fn dual_signature_release_activates_and_is_idempotent() { + let temporary = tempfile::tempdir().unwrap(); + let (lamp, signatures, artifact, trust) = fixture(1, "0.5.1"); + let (activated, status) = + verify_release_bytes_at(temporary.path(), &lamp, &signatures, &artifact, &trust) + .unwrap(); + assert!(activated); + assert_eq!(status.state, "ACTIVE_VERIFIED"); + assert_eq!(status.epoch, 1); + assert_eq!(status.receipt_count, 1); + let (activated_again, repeated) = + verify_release_bytes_at(temporary.path(), &lamp, &signatures, &artifact, &trust) + .unwrap(); + assert!(!activated_again); + assert_eq!(repeated.receipt_count, 1); + } + + #[test] + fn active_previous_and_receipts_commit_as_one_sqlite_state() { + let temporary = tempfile::tempdir().unwrap(); + let (lamp1, signatures1, artifact1, trust1) = fixture(1, "0.5.1"); + verify_release_bytes_at(temporary.path(), &lamp1, &signatures1, &artifact1, &trust1) + .unwrap(); + let (lamp2, signatures2, artifact2, trust2) = fixture(2, "0.5.2"); + verify_release_bytes_at(temporary.path(), &lamp2, &signatures2, &artifact2, &trust2) + .unwrap(); + let state = read_state(temporary.path(), &trust2).unwrap(); + assert_eq!(state.active_release.unwrap().lamp.epoch, 2); + assert_eq!(state.previous_release.unwrap().lamp.epoch, 1); + assert_eq!(state.receipts.len(), 2); + assert_eq!(state.receipts.last().unwrap().epoch, 2); + } + + #[test] + fn interrupted_sqlite_transaction_exposes_none_of_its_partial_state() { + let temporary = tempfile::tempdir().unwrap(); + let (lamp, signatures, artifact, trust) = fixture(1, "0.5.1"); + verify_release_bytes_at(temporary.path(), &lamp, &signatures, &artifact, &trust).unwrap(); + let mut connection = open_state_db(temporary.path()).unwrap(); + { + let transaction = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .unwrap(); + transaction + .execute( + "UPDATE distribution_releases SET payload_json='{}' WHERE slot='active'", + [], + ) + .unwrap(); + transaction + .execute( + "INSERT INTO activation_receipts(payload_json) VALUES('{}')", + [], + ) + .unwrap(); + // Simulate a process ending before COMMIT: dropping the transaction must roll back both writes. + } + let state = read_state(temporary.path(), &trust).unwrap(); + assert_eq!(state.active_release.unwrap().lamp.epoch, 1); + assert_eq!(state.receipts.len(), 1); + } + + #[test] + fn persisted_protocol_cannot_be_changed_without_the_original_dual_signatures() { + let temporary = tempfile::tempdir().unwrap(); + let (lamp, signatures, artifact, trust) = fixture(1, "0.5.1"); + verify_release_bytes_at(temporary.path(), &lamp, &signatures, &artifact, &trust).unwrap(); + let connection = open_state_db(temporary.path()).unwrap(); + let raw: String = connection + .query_row( + "SELECT payload_json FROM distribution_releases WHERE slot='active'", + [], + |row| row.get(0), + ) + .unwrap(); + let mut changed: serde_json::Value = serde_json::from_str(&raw).unwrap(); + changed["protocol"]["origin"] = serde_json::Value::String("LOCAL_TAMPER".into()); + connection + .execute( + "UPDATE distribution_releases SET payload_json=?1 WHERE slot='active'", + params![serde_json::to_string(&changed).unwrap()], + ) + .unwrap(); + assert_eq!( + read_state(temporary.path(), &trust).unwrap_err(), + "HOLOLAKE_PUBLIC_ZERO_CORE_PERSISTED_RELEASE_TAMPERED" + ); + } + + #[test] + fn tampered_artifact_and_incomplete_signature_fail_closed() { + let temporary = tempfile::tempdir().unwrap(); + let (lamp, signatures, mut artifact, trust) = fixture(1, "0.5.1"); + artifact.push(b' '); + assert_eq!( + verify_release_bytes_at(temporary.path(), &lamp, &signatures, &artifact, &trust) + .unwrap_err(), + "HOLOLAKE_PUBLIC_ZERO_CORE_CONTENT_ROOT_MISMATCH" + ); + let (lamp, signatures, artifact, trust) = fixture(1, "0.5.1"); + let mut bundle: serde_json::Value = serde_json::from_slice(&signatures).unwrap(); + bundle["signatures"].as_array_mut().unwrap().pop(); + assert_eq!( + verify_release_bytes_at( + temporary.path(), + &lamp, + &serde_json::to_vec(&bundle).unwrap(), + &artifact, + &trust + ) + .unwrap_err(), + "HOLOLAKE_PUBLIC_ZERO_CORE_SIGNATURE_BUNDLE_INVALID" + ); + } + + #[test] + fn rollback_and_same_epoch_equivocation_are_rejected() { + let temporary = tempfile::tempdir().unwrap(); + let (lamp2, signatures2, artifact2, trust2) = fixture(2, "0.5.2"); + verify_release_bytes_at(temporary.path(), &lamp2, &signatures2, &artifact2, &trust2) + .unwrap(); + let (lamp1, signatures1, artifact1, trust1) = fixture(1, "0.5.1"); + assert_eq!( + verify_release_bytes_at(temporary.path(), &lamp1, &signatures1, &artifact1, &trust1) + .unwrap_err(), + "HOLOLAKE_PUBLIC_ZERO_CORE_ROLLBACK_REJECTED" + ); + let (lamp_other, signatures_other, artifact_other, trust_other) = fixture(2, "0.5.3"); + assert_eq!( + verify_release_bytes_at( + temporary.path(), + &lamp_other, + &signatures_other, + &artifact_other, + &trust_other + ) + .unwrap_err(), + "HOLOLAKE_PUBLIC_ZERO_CORE_EPOCH_EQUIVOCATION_REJECTED" + ); + } + + #[test] + fn same_epoch_and_version_cannot_silently_replace_the_content_root() { + let temporary = tempfile::tempdir().unwrap(); + let (lamp, signatures, artifact, trust) = fixture(1, "0.5.1"); + verify_release_bytes_at(temporary.path(), &lamp, &signatures, &artifact, &trust).unwrap(); + let before = read_state(temporary.path(), &trust).unwrap(); + let original_root = before + .active_release + .as_ref() + .unwrap() + .lamp + .content_root_sha256 + .clone(); + let (other_lamp, other_signatures, other_artifact, other_trust) = + fixture_with_origin(1, "0.5.1", "PUBLIC_ZERO_CORE_OTHER_CONTENT"); + assert_eq!( + verify_release_bytes_at( + temporary.path(), + &other_lamp, + &other_signatures, + &other_artifact, + &other_trust + ) + .unwrap_err(), + "HOLOLAKE_PUBLIC_ZERO_CORE_EPOCH_EQUIVOCATION_REJECTED" + ); + let after = read_state(temporary.path(), &trust).unwrap(); + assert_eq!(after.receipts.len(), 1); + assert_eq!( + after.active_release.unwrap().lamp.content_root_sha256, + original_root + ); + } + + #[test] + fn production_trust_contains_only_the_two_provisioned_public_keys() { + assert_eq!(trust_state_from_raw(TRUST_RAW), "PROVISIONED"); + let trust = parse_trust(TRUST_RAW).unwrap(); + assert_eq!(trust.signers.len(), 2); + assert!(trust.signers.iter().all(|signer| { + signer.algorithm == "Ed25519" + && STANDARD + .decode(&signer.public_key_base64) + .is_ok_and(|key| key.len() == 32) + })); + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/zero_point.rs b/product-source/hololake-native-desktop/src-tauri/src/zero_point.rs index d40386c98..7f371ade1 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/zero_point.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/zero_point.rs @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; use tauri::{Manager, State}; /// 零点原核运行协议参数。PROTOCOL.json 存在时优先读取;否则使用明确标注的出厂默认值。 -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ZeroPointProtocol { #[serde(default = "default_grace_days")] @@ -34,7 +34,7 @@ fn default_grace_days() -> u64 { 7 } fn default_anchor_url() -> String { - "https://guanghulab.com/api/ai/v1/anchor".into() + "https://guanghu.chat/api/hololake/zero-core/lamp".into() } fn default_resolve_url() -> String { "https://guanghulab.com/api/ai/v1/resolve?id=".into() @@ -79,6 +79,8 @@ pub struct ZeroPointSnapshot { pub protocol: ZeroPointProtocol, /// 底层协议静默比对的最近结论。 pub sync_note: String, + /// 企业服务器公众零点原核投影的签名分发状态;不包含私人第五域数据。 + pub public_distribution: crate::public_zero_core_distribution::PublicZeroCoreStatus, } pub struct ZeroPointState { @@ -151,9 +153,12 @@ pub fn boot_zero_point(app: &tauri::AppHandle, state: &ZeroPointState) -> Result fs::write(&charter, text).map_err(|e| format!("HOLOLAKE_ZP_CHARTER_FAILED: {e}"))?; // 协议参数:PROTOCOL.json 在则读(第五域就位即自动生效),缺则出厂兜底。 - let protocol = fs::read_to_string(home.join("PROTOCOL.json")) - .ok() - .and_then(|raw| serde_json::from_str::(&raw).ok()) + let protocol = crate::public_zero_core_distribution::load_active_protocol(&home)? + .or_else(|| { + fs::read_to_string(home.join("PROTOCOL.json")) + .ok() + .and_then(|raw| serde_json::from_str::(&raw).ok()) + }) .unwrap_or_default(); let (binding, user_number, resolved_name, resolved_domain, last_valid_check) = read_binding(&home); @@ -472,45 +477,34 @@ fn lighthouse_resolution(body: &str, expected_number: &str) -> Option Result<(), String> { let home = home_of_inner(state)?; let anchor_url = { lock(state)?.protocol.lighthouse_anchor_url.clone() }; - let local_version = fs::read_to_string(home.join("core").join("VERSION")).unwrap_or_default(); - let pubkey_ready = home.join("core").join("pubkey.pem").exists(); - - let client = reqwest::Client::builder() - .timeout(Duration::from_secs(15)) - .build() - .map_err(|e| format!("HOLOLAKE_ZP_HTTP_FAILED: {e}"))?; - let note = match client.get(&anchor_url).send().await { - Ok(resp) if resp.status().is_success() => { - let body: serde_json::Value = resp.json().await.unwrap_or_default(); - let remote_version = body - .get("version") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); - if remote_version.is_empty() { - "协议版本信息不可用,本次未执行更新。".into() - } else if remote_version == local_version.trim() { - append_heartbeat(&home, "sync verdict=ALREADY_CURRENT"); - "协议版本已是最新。".into() - } else if !pubkey_ready { - append_heartbeat(&home, "sync verdict=UPDATE_PENDING_SIGNATURE_KEY_ABSENT"); - "发现新版本,但验签公钥尚未配置;更新未执行。".into() - } else { - // 验签三闸(来源/签名/版本单调)完整施工待第五域发布管道就位。 - append_heartbeat(&home, "sync verdict=UPDATE_FOUND_GATE_PENDING"); - "发现新版本,但发布验证流程尚未就绪;更新未执行。".into() + match crate::public_zero_core_distribution::sync_public_zero_core(&home, &anchor_url).await { + Ok(outcome) => { + if let Some(protocol) = outcome.activated_protocol { + lock(state)?.protocol = protocol; } + append_heartbeat( + &home, + &format!( + "sync verdict={} epoch={} version={}", + outcome.status.state, outcome.status.epoch, outcome.status.version + ), + ); + lock(state)?.sync_note = outcome.note; } - _ => { - append_heartbeat(&home, "sync verdict=ANCHOR_UNREACHABLE"); - "协议服务当前不可达,本次未完成版本检查。".into() + Err(error) => { + append_heartbeat(&home, &format!("sync verdict=FAIL_CLOSED reason={error}")); + lock(state)?.sync_note = match error.as_str() { + "HOLOLAKE_PUBLIC_ZERO_CORE_TRUST_UNPROVISIONED" => { + "公众零点原核双签公钥尚未配置,本次更新保持失败关闭。".into() + } + _ => "公众零点原核更新未通过完整验真,本机继续使用上一个可信版本。".into(), + }; } - }; - lock(state)?.sync_note = note; + } Ok(()) } @@ -526,6 +520,7 @@ pub async fn zero_point_status( state: State<'_, ZeroPointState>, ) -> Result { let inner = lock(&state)?; + let public_distribution = crate::public_zero_core_distribution::status_at(&inner.home)?; let grace = inner.protocol.grace_period_days.saturating_mul(86_400); Ok(ZeroPointSnapshot { route: inner.route.clone(), @@ -541,6 +536,7 @@ pub async fn zero_point_status( }, protocol: inner.protocol.clone(), sync_note: inner.sync_note.clone(), + public_distribution, }) } @@ -571,6 +567,10 @@ mod tests { assert!(protocol .enterprise_resolve_url .starts_with("https://guanghu.chat")); + assert_eq!( + protocol.lighthouse_anchor_url, + "https://guanghu.chat/api/hololake/zero-core/lamp" + ); } #[test] diff --git a/product-source/hololake-native-desktop/src/main.tsx b/product-source/hololake-native-desktop/src/main.tsx index 4d4fae088..3127b2863 100644 --- a/product-source/hololake-native-desktop/src/main.tsx +++ b/product-source/hololake-native-desktop/src/main.tsx @@ -8,6 +8,7 @@ import { StarlakeSurface, type DomainId } from './modules/qoder-surface/Starlake import { FINISHES, TraditionalSurface, type FinishId, type TraditionalBroadcast, type TraditionalChannel, type TraditionalSystem } from './modules/qoder-surface/TraditionalSurface' import { resolveVisualBalance } from './modules/qoder-surface/visual-balance' import { PrivateChannelSurface, type InstalledChannelModule, type PrivateChannelAction } from './modules/private-channel/PrivateChannelSurface' +import { HumanAuthorizationCenter, type DirectSessionProjection } from './modules/human-authorization-center' const ChannelWorkbenchStudio = lazy(() => import('./modules/channel-workbench').then((module) => ({ default: module.ChannelWorkbenchStudio }))) const PersonaChannelBody = lazy(() => import('./modules/persona-channel-body').then((module) => ({ default: module.PersonaChannelBody }))) @@ -60,7 +61,7 @@ import './design-tokens.css' import './styles.css' type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear' -type ViewId = 'overview' | 'knowledge' | 'composition' | 'workbench' | 'education' | 'webNovel' | 'mobileSync' | 'persona' | 'code' | 'receipts' | 'system' +type ViewId = 'overview' | 'knowledge' | 'composition' | 'workbench' | 'education' | 'webNovel' | 'mobileSync' | 'persona' | 'code' | 'marketplace' | 'receipts' | 'system' type WorldStage = 'domain' | 'heart' | 'heartbeat' | 'lightLake' | 'love' | 'tomorrow' | 'bottle' | 'channel' | 'enterpriseWork' | 'personalNodeGuide' | 'tool' type KnowledgeSource = 'native' | 'legacy' @@ -74,6 +75,43 @@ interface BundledModuleDescriptor { installedState: string signatureVerified: boolean } +type MarketplaceKind = 'PHYSICAL_MODULE' | 'COGNITIVE_SKILL' +interface MarketplaceItemView { + itemNumber: string + artifactKind: MarketplaceKind + displayName: string + summary: string + version: string + sourceRepository: string + sourceRevision: string + artifactSha256: string + adapter?: string | null + permissions: string[] + executionAuthority: boolean + installedState: string + updateAvailable: boolean +} +interface MarketplaceSnapshot { + schema: 'hololake.online-marketplace-snapshot/v1' + state: string + trustState: string + catalogEpoch: number + catalogSha256: string + itemCount: number + catalogReceiptCount: number + latestCatalogReceiptHash: string + lastSyncNote: string + items: MarketplaceItemView[] +} +interface MarketplaceMutationOutcome { + state: string + itemNumber: string + artifactKind: MarketplaceKind + artifactSha256: string + packageCodeExecuted: false + realityAuthorityGranted: false + snapshot: MarketplaceSnapshot +} interface WorldClimateSnapshot { schema: 'hololake.world-climate/v1' state: 'VERIFIED_LIVE' | 'TIME_ONLY_WEATHER_UNAVAILABLE' @@ -93,6 +131,7 @@ interface HomeStatus { directLocalBrokerState: string directConnectionCount: number resumableSessionCount: number + directSessions: DirectSessionProjection[] codeRepositoryMountCount: number pnccReceiptCount: number updateState: string @@ -284,8 +323,18 @@ interface ZeroPointSnapshot { resolvedDomain: string lastValidCheck: number graceDeadline: number - protocol: { gracePeriodDays: number; lighthouseAnchorUrl: string; lighthouseResolveUrl: string; coreChannelSource: string; origin: string } + protocol: { gracePeriodDays: number; lighthouseAnchorUrl: string; lighthouseResolveUrl: string; enterpriseResolveUrl: string; coreChannelSource: string; origin: string } syncNote: string + publicDistribution: { + state: string + epoch: number + version: string + contentRootSha256: string + rollbackAvailable: boolean + receiptCount: number + latestReceiptHash: string + trustState: string + } } function domainDisplayName(domain: string): string { @@ -385,14 +434,14 @@ interface EnterpriseReceiptEnvelope { repository_projection?: EnterpriseReceiptProjection } -const previewStatus: HomeStatus = { directLocalBrokerState: 'UNVERIFIED', directConnectionCount: 0, resumableSessionCount: 0, codeRepositoryMountCount: 0, pnccReceiptCount: 0, updateState: 'READY_HUMAN_CONFIRMATION_REQUIRED', releaseRecoveryState: 'NONE', mcpRole: 'DISCOVERY_RECOVERY_COMPATIBILITY_ONLY', terminalLinkProtocol: 'HOLOLAKE_TERMINAL_LINK/3', terminalLinkTransport: 'UNVERIFIED', environmentFramePolicy: 'REQUIRED_AFTER_CONNECT_RESUME_AND_BEFORE_MUTATION' } +const previewStatus: HomeStatus = { directLocalBrokerState: 'UNVERIFIED', directConnectionCount: 0, resumableSessionCount: 0, directSessions: [], codeRepositoryMountCount: 0, pnccReceiptCount: 0, updateState: 'READY_HUMAN_CONFIRMATION_REQUIRED', releaseRecoveryState: 'NONE', mcpRole: 'DISCOVERY_RECOVERY_COMPATIBILITY_ONLY', terminalLinkProtocol: 'HOLOLAKE_TERMINAL_LINK/3', terminalLinkTransport: 'UNVERIFIED', environmentFramePolicy: 'REQUIRED_AFTER_CONNECT_RESUME_AND_BEFORE_MUTATION' } const previewPersonal: PersonalChannelSnapshot = { state: 'UNAVAILABLE', recentEvents: [], modules: [], integrity: { state: 'UNKNOWN', eventCount: 0, receiptCount: 0 } } const previewKnowledge: KnowledgeSnapshot = { state: 'UNAVAILABLE', nativeRoot: '', legacyAvailable: false, documents: [], rawDocumentCount: 0, uniqueDocumentCount: 0, duplicateDocumentCount: 0, truncated: false } const previewCode: CodeChannelSnapshot = { state: 'UNAVAILABLE', channels: [], authority: 'LOCAL_SOURCE_ACCESS_ONLY_NO_PUSH_OR_DEPLOY_AUTHORITY' } const themes: Array<{ id: ThemeId; name: string }> = [ { id: 'night', name: '夜湖星光' }, { id: 'dawn', name: '晨湖曦光' }, { id: 'nebula', name: '星云紫夜' }, { id: 'candle', name: '烛畔暖湖' }, { id: 'clear', name: '清浅澄湖' }, ] -const viewLabels: Record = { overview: '个人频道', knowledge: '知识空间', composition: '结构组合', workbench: '频道资料工作台', education: '教育工作台', webNovel: '网文作者工作台', mobileSync: '移动同步桥', persona: '人格频道本体', code: '人格代码频道', receipts: '运行回执', system: '系统详情' } +const viewLabels: Record = { overview: '个人频道', knowledge: '知识空间', composition: '结构组合', workbench: '频道资料工作台', education: '教育工作台', webNovel: '网文作者工作台', mobileSync: '移动同步桥', persona: '人格频道本体', code: '人格代码频道', marketplace: '分域模块商城', receipts: '运行回执', system: '授权与连接' } const domainGates = [ { domain: 'BRANCH_DOMAIN', className: 'd-sub', title: '光湖分域', gate: 'GATE 02 · ONLINE', facts: [ ['公共作用', '行业成品模块商城 · 人格体大脑技能商城'], ['域标识', 'BRANCH_DOMAIN'], ['责任主体', '花尔 · TCS-GL-0005∞'], ['人格体主体', '爆米花 · PER-BMH001 · AGE'], ['关系支持', '糖星云 · PER-TXY001 · AGE'], ['工作仓库', 'PRIVATE · 1 · LIVE'], @@ -498,6 +547,11 @@ function humanError(error: unknown, context: 'knowledge' | 'code' | 'identity' | if (value.includes('LOGIN_ACCOUNT_BINDING_UNREGISTERED')) return '这个编号尚未登记对应的仓库账号,系统已停止进入。' if (value.includes('DOMAIN_LOGIN_NOT_PROVISIONED')) return '该域的企业服务器尚未接入,当前不能继续登录。' if (value.includes('PERSISTENT_CREDENTIAL_UNAVAILABLE')) return '当前系统尚未接通安全凭证存储,不能完成签署。' + if (value.includes('MARKETPLACE_PERMISSION_CONFIRMATION_REQUIRED')) return '新增现实权限尚未得到本人确认,安装已停止。' + if (value.includes('MARKETPLACE_CATALOG_NOT_SYNCED')) return '模块商城目录尚未完成双签同步。' + if (value.includes('MARKETPLACE_CATALOG_CHANGED')) return '商城目录已更新,请重新确认后安装。' + if (value.includes('MARKETPLACE') && value.includes('SIGNATURE')) return '商城签名验证未通过,目录或安装包没有被采用。' + if (value.includes('MARKETPLACE') || value.includes('SKILL_')) return '模块商城操作未完成;本机现有安装保持不变。' if (value.includes('ENTERPRISE_RECEIPT_FAILED')) return '企业回执没有写入,请稍后重新提交。' if (value.includes('ENTERPRISE_RECEIPT_ROUTE_REQUIRED')) return '此签署入口仅适用于企业四域。' if (value.includes('DOMAIN_ROUTE_REQUIRED') || value.includes('ZP_DOMAIN_ROUTE_REQUIRED')) return '编号尚未解析到已登记域,不能开始登录。' @@ -648,6 +702,11 @@ function HoloLakeApp() { const [worldClimate, setWorldClimate] = useState(null) const [dynamicSurfaceBusy, setDynamicSurfaceBusy] = useState(false) const [dynamicSurfaceMessage, setDynamicSurfaceMessage] = useState('') + const [marketplace, setMarketplace] = useState(null) + const [marketplaceKind, setMarketplaceKind] = useState('PHYSICAL_MODULE') + const [marketplaceBusyItem, setMarketplaceBusyItem] = useState('') + const [marketplaceSyncing, setMarketplaceSyncing] = useState(false) + const [marketplaceMessage, setMarketplaceMessage] = useState('') const [expanded, setExpanded] = useState>(() => new Set(['导入'])) const [editing, setEditing] = useState(false) const [draft, setDraft] = useState('') @@ -1295,6 +1354,86 @@ function HoloLakeApp() { else { setDynamicSurfaceModule(null); setWorldClimate(null); setDynamicSurfaceMessage('') } }, [repoLogin?.username, repoLogin?.domain]) + const refreshMarketplace = async () => { + try { + const snapshot = await invoke('get_marketplace_snapshot') + setMarketplace(snapshot) + setMarketplaceMessage(snapshot.lastSyncNote) + return snapshot + } catch (error) { + setMarketplaceMessage(humanError(error, 'system')) + return null + } + } + const syncMarketplace = async () => { + setMarketplaceSyncing(true) + setMarketplaceMessage('正在读取线上目录并核验零点原核与企业发布双签……') + try { + const snapshot = await invoke('sync_marketplace_catalog') + setMarketplace(snapshot) + setMarketplaceMessage(snapshot.lastSyncNote) + } catch (error) { + setMarketplaceMessage(humanError(error, 'system')) + } finally { + setMarketplaceSyncing(false) + } + } + const openMarketplace = () => { + openWorldTool('marketplace') + void refreshMarketplace().then((snapshot) => { + if (!snapshot || snapshot.state !== 'ACTIVE_VERIFIED_CATALOG') void syncMarketplace() + }) + } + const installMarketplaceItem = async (item: MarketplaceItemView) => { + if (!marketplace) return + const permissionText = item.permissions.length + ? `该模块请求以下现实能力:\n\n${item.permissions.map((permission) => `• ${permission}`).join('\n')}\n\n是否确认安装?` + : `确认安装“${item.displayName}”?` + if (!window.confirm(permissionText)) return + setMarketplaceBusyItem(item.itemNumber) + setMarketplaceMessage(`正在下载、验签并安装 ${item.displayName}……`) + try { + const result = await invoke('install_marketplace_item', { input: { + itemNumber: item.itemNumber, + expectedCatalogEpoch: marketplace.catalogEpoch, + expectedArtifactSha256: item.artifactSha256, + humanConfirmedPermissionExpansion: item.artifactKind === 'PHYSICAL_MODULE' && item.permissions.length > 0, + } }) + setMarketplace(result.snapshot) + setMarketplaceMessage(`${item.displayName} 已完成下载、验签、自检和安装。`) + } catch (error) { + setMarketplaceMessage(humanError(error, 'system')) + } finally { + setMarketplaceBusyItem('') + } + } + const uninstallMarketplaceItem = async (item: MarketplaceItemView) => { + if (!window.confirm(`确认停用“${item.displayName}”?安装证据与用户数据会保留。`)) return + setMarketplaceBusyItem(item.itemNumber) + try { + const result = await invoke('uninstall_marketplace_item', { input: { itemNumber: item.itemNumber } }) + setMarketplace(result.snapshot) + setMarketplaceMessage(`${item.displayName} 已停用;数据与回执保留。`) + } catch (error) { + setMarketplaceMessage(humanError(error, 'system')) + } finally { + setMarketplaceBusyItem('') + } + } + const rollbackMarketplaceItem = async (item: MarketplaceItemView) => { + if (!window.confirm(`确认把“${item.displayName}”回退到上一份已验签版本?`)) return + setMarketplaceBusyItem(item.itemNumber) + try { + const result = await invoke('rollback_marketplace_item', { input: { itemNumber: item.itemNumber } }) + setMarketplace(result.snapshot) + setMarketplaceMessage(`${item.displayName} 已回退到上一份已验签版本。`) + } catch (error) { + setMarketplaceMessage(humanError(error, 'system')) + } finally { + setMarketplaceBusyItem('') + } + } + const cloneCodeChannel = async (event: React.FormEvent) => { event.preventDefault() if (!cloneUrl.trim()) return @@ -1558,7 +1697,11 @@ function HoloLakeApp() { } const syncZeroPoint = async () => { setZpBusy(true); setZpMessage('') - try { setZeroPoint(await invoke('zero_point_sync')) } catch (error) { setZpMessage(String(error)) } finally { setZpBusy(false) } + try { + const snapshot = await invoke('zero_point_sync') + setZeroPoint(snapshot) + setZpMessage(snapshot.syncNote) + } catch (error) { setZpMessage(String(error)) } finally { setZpBusy(false) } } const renderOverview = () => (
@@ -1779,9 +1922,54 @@ function HoloLakeApp() {
) + const renderMarketplace = () => { + const items = marketplace?.items.filter((item) => item.artifactKind === marketplaceKind) || [] + const active = marketplace?.state === 'ACTIVE_VERIFIED_CATALOG' + return
+
BRANCH DOMAIN · VERIFIED DISTRIBUTION

分域模块商城

同一个入口,两条互不混用的安装链:成品模块进入现实功能容器;思维技能只进入只读认知层。

+
+
目录状态{active ? '线上双签已核验' : '等待首次同步'}
+
目录纪元{marketplace?.catalogEpoch || '—'}
+
已登记资源{marketplace?.itemCount ?? '—'}
+
验真回执{marketplace?.catalogReceiptCount || 0}
+
+
+ + +
+

{marketplaceKind === 'PHYSICAL_MODULE' ? '成品模块可以声明现实权限;每次新增权限都必须由当前人类看见并确认。代码仓库只作为来源证据,客户端不会克隆并执行仓库。' : '思维技能执行权固定为 false、权限列表固定为空;系统只在需要时读取,不会自动塞进每轮提示词。'}

+
+ {items.length ? items.map((item) => { + const busy = marketplaceBusyItem === item.itemNumber + const installed = item.installedState === 'ACTIVE' || item.installedState === 'ACTIVE_READONLY' + return
+
{item.artifactKind === 'PHYSICAL_MODULE' ? '成品模块' : '只读思维技能'}

{item.displayName}

{item.summary}

{item.updateAvailable ? '有已验签更新' : installed ? '已安装' : '可安装'}
+
+
版本 / 编号
{item.version} · {item.itemNumber}
+
来源证据
{item.sourceRepository} · {item.sourceRevision.slice(0, 12)}
+
现实执行边界
{item.artifactKind === 'PHYSICAL_MODULE' ? '仅经已登记适配器与所列权限' : '无现实执行权'}
+
权限
{item.permissions.length ? item.permissions.join(' · ') : '无现实权限'}
+
+
+ {installed && !item.updateAvailable + ? + : } + {installed && } +
+
+ }) :
{active ? '当前分类尚无已登记资源' : '尚未取得可验证目录'}

{active ? '资源发布后会在下一次目录同步中出现。' : '点击“同步线上目录”;只有零点原核与企业发布双签同时通过,目录才会显示。'}

} +
+ {marketplaceMessage &&

{marketplaceMessage}

} + {marketplace?.latestCatalogReceiptHash &&
最新目录回执 · {marketplace.latestCatalogReceiptHash.slice(0, 20)} · 目录摘要 {marketplace.catalogSha256.slice(0, 20)}
} +
+ } + const renderSystem = () => (
-
SYSTEM EVIDENCE

系统详情

显示可验证的当前状态;无法读取的项目将明确标记为不可用。

+
HUMAN CONTROL

授权与连接

先把需要你决定的事和当前协作对象说清楚;机器诊断放在后面。

+ +
+ 系统诊断与协议证据

光湖近场连接

外部 AI 可像发现附近网络一样发现本机 HoloLake;MCP 仅用于兼容与恢复。

{nearbyDiscovery?.automaticSameDeviceDiscovery ? '本机自动发现已开启' : '自动发现不可用'}
@@ -1812,6 +2000,10 @@ function HoloLakeApp() {
绑定状态
{zeroPoint ? (zeroPoint.binding === 'bound' ? `已绑定(${zeroPoint.userNumber})` : '等待绑定(空白态)') : '—'}
协议来源
{zeroPoint ? protocolOriginDisplayName(zeroPoint.protocol.origin) : '—'}
+
公众运行面
企业服务器 · 不包含私人第五域
+
双签信任
{zeroPoint?.publicDistribution.trustState === 'PROVISIONED' ? '零点原点与企业分发公钥已配置' : '等待配置两把独立发布公钥'}
+
已激活协议
{zeroPoint?.publicDistribution.version ? `${zeroPoint.publicDistribution.version} · epoch ${zeroPoint.publicDistribution.epoch}` : '尚无线上双签版本'}
+
激活回执
{zeroPoint ? `${zeroPoint.publicDistribution.receiptCount} 条${zeroPoint.publicDistribution.rollbackAvailable ? ' · 保留上一可信版本' : ''}` : '—'}
离线有效期
{zeroPoint ? `${zeroPoint.protocol.gracePeriodDays} 天` : '—'}
同步状态
{zeroPoint?.syncNote ?? '—'}
@@ -1819,7 +2011,7 @@ function HoloLakeApp() { setZpNumber(event.target.value)}/> - +
{zpMessage &&

{zpMessage}

}
@@ -1831,6 +2023,7 @@ function HoloLakeApp() {

软件更新

仅接受已登记信任根验证通过的 HoloLake 发布包。

{releaseCandidate ?
HoloLake {releaseCandidate.version}

{releaseCandidate.notes}

: }
+ {systemMessage &&

{systemMessage}

} ) @@ -2032,6 +2225,7 @@ function HoloLakeApp() {

{repoLogin.domain === 'FIFTH_DOMAIN' ? domainDisplayName(repoLogin.domain) : '光湖零感域'}

{repoLogin.domain === 'FIFTH_DOMAIN' ? '世界正在发生什么' : '公共工作入口 · 世界正在发生什么'}

光湖公共灯塔 · 在线

协议版本 · {enterpriseEntry?.registry_version || 'HLDP v1.0'}

{repoLogin.domain !== 'FIFTH_DOMAIN' &&

本人责任域 · {domainDisplayName(enterpriseEntry?.subject.domain || repoLogin.domain)}

}

HoloLake · V0.5.0

setWorldStage(repoLogin.domain === 'FIFTH_DOMAIN' ? (isZhizhi ? 'heart' : 'channel') : 'channel')}/> + openWorldTool('system')}/> } @@ -2071,12 +2265,14 @@ function HoloLakeApp() { setWorldStage('heartbeat')}/> setWorldStage('lightLake')}/> setWorldStage('love')}/> + openWorldTool('system')}/> : <> setWorldStage('enterpriseWork')}/> void openEnterpriseRepository()}/> openWorldTool('receipts')}/> setWorldStage('personalNodeGuide')}/> + openWorldTool('system')}/> } } @@ -2102,7 +2298,7 @@ function HoloLakeApp() {
当前状态 · 待本人接入可由本人人格体协助执行,但不能替代人类节点所有权确认。
} - {worldStage === 'heartbeat' && repoLogin.domain === 'FIFTH_DOMAIN' && setWorldStage('channel')} onMarketplace={() => { setWorldStage('domain'); setActiveDomainInfo('BRANCH_DOMAIN') }} nativeActions={privateNativeActions} installedModules={privateInstalledModules}/>} + {worldStage === 'heartbeat' && repoLogin.domain === 'FIFTH_DOMAIN' && setWorldStage('channel')} onMarketplace={openMarketplace} nativeActions={privateNativeActions} installedModules={privateInstalledModules}/>} {worldStage === 'lightLake' && repoLogin.domain === 'FIFTH_DOMAIN' &&

光之湖

人格体居所 · AGE 人格体语言生活空间

@@ -2113,7 +2309,7 @@ function HoloLakeApp() {
} {worldStage === 'tool' &&
{viewLabels[view]}{domainDisplayName(repoLogin.domain)}
-
{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'composition' ? renderComposition() : view === 'workbench' ? renderWorkbench() : view === 'education' ? renderEducation() : view === 'webNovel' ? renderWebNovel() : view === 'mobileSync' ? renderMobileSync() : view === 'persona' ? renderPersonaBody() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : renderSystem()}
+
{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'composition' ? renderComposition() : view === 'workbench' ? renderWorkbench() : view === 'education' ? renderEducation() : view === 'webNovel' ? renderWebNovel() : view === 'mobileSync' ? renderMobileSync() : view === 'persona' ? renderPersonaBody() : view === 'code' ? renderCode() : view === 'marketplace' ? renderMarketplace() : view === 'receipts' ? renderReceipts() : renderSystem()}
} {worldStage === 'domain' && surface === 'world' && signedActiveGate && <>{signedActiveGate.title}{signedActiveGate.gate}{signedActiveGate.facts.map(([label, value]) => {label}{value})}} diff --git a/product-source/hololake-native-desktop/src/modules/human-authorization-center.tsx b/product-source/hololake-native-desktop/src/modules/human-authorization-center.tsx new file mode 100644 index 000000000..2b6d81a65 --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/human-authorization-center.tsx @@ -0,0 +1,125 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' +import { numberedInvoke } from './numbered-ipc' + +export interface DirectSessionProjection { + sessionId: string + laneId: string + clientInstanceId: string + state: 'LIVE' | 'RESUMABLE' + openedAtUnixMs: number + observedAtUnixMs: number + lastEventSequence: number +} + +interface AuthorizationRequest { + requestId: string + action: 'OPEN_MAINTENANCE' | 'UNMOUNT' | 'PROMOTE_VERSION' | 'RETIRE' + targetNumber: string + targetKind: string + targetLabel: string + reason: string + impact: string + rollbackPlan: string + requesterLabel: string + state: 'PENDING_HUMAN' | 'APPROVED' | 'DENIED' | 'EXPIRED' | 'CONSUMED' + createdAtUnixMs: number + expiresAtUnixMs: number + humanNumber?: string + decidedAtUnixMs?: number + ticketId?: string + ticketExpiresAtUnixMs?: number + consumedAtUnixMs?: number + receiptHash: string +} + +interface AuthorizationCenterSnapshot { + state: string + pendingCount: number + requests: AuthorizationRequest[] + purgeEnabled: boolean +} + +interface Props { + brokerState: string + activeConnectionCount: number + sessions: DirectSessionProjection[] +} + +const actionLabels: Record = { + OPEN_MAINTENANCE: '打开受控维护区', + UNMOUNT: '从当前频道卸载', + PROMOTE_VERSION: '登记并启用新版本', + RETIRE: '停用并保留历史坐标', +} + +const stateLabels: Record = { + PENDING_HUMAN: '等待你决定', APPROVED: '已同意 · 等待执行体领取', DENIED: '已拒绝', + EXPIRED: '已过期', CONSUMED: '票据已领取一次', +} + +function time(value?: number) { + return value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '—' +} + +export function HumanAuthorizationCenter({ brokerState, activeConnectionCount, sessions }: Props) { + const [snapshot, setSnapshot] = useState(null) + const [busy, setBusy] = useState('') + const [message, setMessage] = useState('') + + const refresh = useCallback(async () => { + try { + setSnapshot(await numberedInvoke('get_authorization_center')) + setMessage('') + } catch (error) { + setSnapshot(null) + setMessage(String(error).includes('AUTHENTICATED_ACCOUNT_REQUIRED') ? '登录并完成编号验证后,授权中心才会打开。' : '授权中心当前不可读取;系统不会把未知状态当成已授权。') + } + }, []) + + useEffect(() => { + void refresh() + const timer = window.setInterval(() => { if (!document.hidden) void refresh() }, 4_000) + return () => window.clearInterval(timer) + }, [refresh]) + + const decide = async (requestId: string, decision: 'APPROVE' | 'DENY') => { + setBusy(requestId) + setMessage('') + try { + await numberedInvoke('decide_authorization_request', { input: { requestId, decision } }) + setMessage(decision === 'APPROVE' ? '已签发一次性票据;它只对原执行会话和本次目标有效。' : '已拒绝;执行体不会获得继续通道。') + await refresh() + } catch { + setMessage('这次决定没有写入。授权状态保持不变,请刷新后再看。') + } finally { + setBusy('') + } + } + + const pending = snapshot?.requests.filter((item) => item.state === 'PENDING_HUMAN') || [] + const history = snapshot?.requests.filter((item) => item.state !== 'PENDING_HUMAN').slice(0, 6) || [] + const connectionState = activeConnectionCount > 0 ? '正在直连' : sessions.some((item) => item.state === 'LIVE') ? '最近仍有心跳' : '当前没有在线执行体' + const sessionList = useMemo(() => sessions.slice(0, 6), [sessions]) + + return
+
+
HUMAN DECISION

需要你决定的事

人格体提出申请,系统把对象、原因、影响和退路交给你;只有你点同意,原执行会话才会收到一次性票据。

{pending.length ? `${pending.length} 项待确认` : '没有待确认事项'}
+ {pending.length === 0 ?
现在不需要你操作删除、停用、维护和正式更新不会在后台静默通过。
:
+ {pending.map((request) =>
+
{actionLabels[request.action]}

{request.targetLabel}

{request.targetNumber}
{stateLabels[request.state]}
+
为什么要做
{request.reason}
会影响什么
{request.impact}
出问题怎么退
{request.rollbackPlan}
谁在申请
{request.requesterLabel}
申请到期
{time(request.expiresAtUnixMs)}
+
+
)} +
} + {message &&

{message}

} +
+ +
+
LIVE COLLABORATION

当前谁连着 HoloLake

这里显示真实本机会话和最近心跳,不把“装过 Agent”冒充成“正在连接”。

0 ? 'status-chip online' : 'status-chip'}>{connectionState}
+
{activeConnectionCount}当前直连
{sessions.length}可续接会话
{brokerState === 'READY' ? '就绪' : '等待'}本机入口
+ {sessionList.length ?
{sessionList.map((session) =>
{session.clientInstanceId}{session.laneId} · 最近心跳 {time(session.observedAtUnixMs)}
{session.state === 'LIVE' ? '在线' : '可续接'}
)}
:
还没有执行体会话外部编程 AI 通过 HoloLake 本机连接后,会在这里出现。
} +
+ + {history.length > 0 &&
最近的授权回执 · {history.length}
{history.map((request) =>
{request.targetLabel}{actionLabels[request.action]}
{stateLabels[request.state]}{request.humanNumber ? `由 ${request.humanNumber} 决定 · ` : ''}{time(request.decidedAtUnixMs || request.createdAtUnixMs)}
)}
} +
+} 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 94fd224d5..fe3e82de5 100644 --- a/product-source/hololake-native-desktop/src/modules/numbered-ipc.ts +++ b/product-source/hololake-native-desktop/src/modules/numbered-ipc.ts @@ -1176,6 +1176,70 @@ const ROUTES = { "moduleNumber": "HLP-NIPC-MOD-0031", "operationNumber": "HLP-NIPC-OP-0147", "targetNumber": "HLP-NIPC-TGT-0031" + }, + "get_authorization_center": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0032", + "operationNumber": "HLP-NIPC-OP-0148", + "targetNumber": "HLP-NIPC-TGT-0032" + }, + "decide_authorization_request": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0032", + "operationNumber": "HLP-NIPC-OP-0149", + "targetNumber": "HLP-NIPC-TGT-0032" + }, + "get_marketplace_snapshot": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0033", + "operationNumber": "HLP-NIPC-OP-0150", + "targetNumber": "HLP-NIPC-TGT-0033" + }, + "sync_marketplace_catalog": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0033", + "operationNumber": "HLP-NIPC-OP-0151", + "targetNumber": "HLP-NIPC-TGT-0033" + }, + "install_marketplace_item": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0033", + "operationNumber": "HLP-NIPC-OP-0152", + "targetNumber": "HLP-NIPC-TGT-0033" + }, + "uninstall_marketplace_item": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0033", + "operationNumber": "HLP-NIPC-OP-0153", + "targetNumber": "HLP-NIPC-TGT-0033" + }, + "rollback_marketplace_item": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0033", + "operationNumber": "HLP-NIPC-OP-0154", + "targetNumber": "HLP-NIPC-TGT-0033" + }, + "get_active_cognitive_skills": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0033", + "operationNumber": "HLP-NIPC-OP-0155", + "targetNumber": "HLP-NIPC-TGT-0033" } } as const diff --git a/product-source/hololake-native-desktop/src/styles.css b/product-source/hololake-native-desktop/src/styles.css index 8f8319208..70a436183 100644 --- a/product-source/hololake-native-desktop/src/styles.css +++ b/product-source/hololake-native-desktop/src/styles.css @@ -331,6 +331,49 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca .receipt-row b { color: var(--content-secondary); font-size: 14px; font-weight: 590; } .receipt-row small { margin-top: 6px; color: var(--content-muted); font-size: 12px; } .system-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; } +.human-authorization-layout { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(320px, .65fr); gap: 18px; margin-bottom: 18px; } +.authorization-primary { grid-row: span 2; } +.plain-panel header .kicker { display: block; margin-bottom: 8px; color: var(--accent-light); font-size: 10px; font-weight: 750; letter-spacing: .14em; } +.status-chip.attention { border-color: color-mix(in srgb, var(--warn) 48%, transparent); color: var(--warn); background: color-mix(in srgb, var(--warn) 10%, transparent); } +.authorization-empty { display: grid; gap: 7px; min-height: 128px; place-content: center; margin-top: 18px; padding: 24px; border: 1px dashed var(--panel-edge); border-radius: 14px; text-align: center; } +.authorization-empty b { color: var(--content-primary); font-size: 16px; } +.authorization-empty span { color: var(--content-muted); font-size: 13px; } +.authorization-empty.compact { min-height: 94px; } +.authorization-list { display: grid; gap: 14px; margin-top: 20px; } +.authorization-card { padding: 18px; border: 1px solid color-mix(in srgb, var(--accent-light) 24%, var(--panel-edge)); border-radius: 14px; background: color-mix(in srgb, var(--primitive-glass) 80%, transparent); } +.authorization-card-title { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; } +.authorization-card-title small { color: var(--accent-light); font-weight: 700; } +.authorization-card-title h3 { margin: 5px 0 4px; color: var(--content-primary); font-size: 18px; } +.authorization-card-title code { color: var(--content-muted); font-size: 11px; overflow-wrap: anywhere; } +.authorization-card-title > span { flex: none; padding: 5px 9px; border-radius: 999px; color: var(--warn); background: color-mix(in srgb, var(--warn) 10%, transparent); font-size: 11px; } +.authorization-card dl { display: grid; gap: 10px; margin: 18px 0; } +.authorization-card dl div { display: grid; grid-template-columns: 88px minmax(0, 1fr); gap: 12px; } +.authorization-card dt { color: var(--content-muted); font-size: 12px; } +.authorization-card dd { margin: 0; color: var(--content-secondary); font-size: 13px; line-height: 1.55; } +.authorization-actions { display: flex; justify-content: flex-end; gap: 10px; } +.authorization-actions button { min-height: 38px; padding: 0 15px; } +.collaboration-summary { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin: 20px 0 14px; } +.collaboration-summary div { display: grid; gap: 3px; padding: 13px 10px; border-radius: 11px; background: var(--primitive-glass); text-align: center; } +.collaboration-summary b { color: var(--content-primary); font-size: 18px; } +.collaboration-summary span { color: var(--content-muted); font-size: 11px; } +.session-projection-list { display: grid; gap: 7px; } +.session-projection-list article { display: grid; grid-template-columns: 9px minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: 10px 2px; border-bottom: 1px solid var(--panel-edge); } +.session-projection-list i { width: 8px; height: 8px; border-radius: 50%; background: var(--content-muted); } +.session-projection-list i.live { background: var(--good); box-shadow: 0 0 10px color-mix(in srgb, var(--good) 70%, transparent); } +.session-projection-list div { display: grid; gap: 3px; min-width: 0; } +.session-projection-list b { overflow: hidden; color: var(--content-primary); font-size: 13px; text-overflow: ellipsis; white-space: nowrap; } +.session-projection-list span, .session-projection-list em { color: var(--content-muted); font-size: 11px; font-style: normal; } +.authorization-history, .system-diagnostics { grid-column: 1 / -1; border: 1px solid var(--panel-edge); border-radius: 13px; background: var(--primitive-glass); } +.authorization-history > summary, .system-diagnostics > summary { padding: 14px 17px; color: var(--content-secondary); font-size: 13px; font-weight: 650; cursor: pointer; } +.authorization-history > div { display: grid; gap: 1px; padding: 0 14px 14px; } +.authorization-history article { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 14px; padding: 11px; border-radius: 9px; background: color-mix(in srgb, var(--primitive-glass-hover) 55%, transparent); } +.authorization-history article div { display: flex; gap: 9px; } +.authorization-history article b { color: var(--content-primary); font-size: 13px; } +.authorization-history article span, .authorization-history article small, .authorization-history article em { color: var(--content-muted); font-size: 11px; font-style: normal; } +.authorization-history article small { grid-column: 1 / -1; } +.system-diagnostics { margin-top: 4px; } +.system-diagnostics[open] > summary { border-bottom: 1px solid var(--panel-edge); } +.system-diagnostics .system-grid { padding: 18px; } .connection-panel { grid-row: span 2; } .status-chip { padding: 6px 9px; border: 1px solid var(--panel-edge); border-radius: 999px; color: var(--content-muted); background: var(--primitive-glass); font-size: 12px; } .status-chip.online { color: var(--state-ready); } @@ -366,7 +409,7 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca .knowledge-page { grid-template-columns: 250px minmax(390px, 1fr); } .document-inspector { display: none; } .code-workbench { grid-template-columns: 220px 230px minmax(0, 1fr); } - .overview-grid, .system-grid { grid-template-columns: 1fr; } + .overview-grid, .system-grid, .human-authorization-layout { grid-template-columns: 1fr; } .overview-columns { grid-template-columns: 1fr; } } @media (prefers-reduced-motion: reduce) { @@ -783,6 +826,45 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca } .receipt-message { color: var(--accent-light) !important; font-weight: 650; text-align: right; } +/* 分域商城是一张可核验清单,不伪装成促销卡片。 */ +.marketplace-page { max-width: 1180px; } +.marketplace-title { align-items: flex-end; } +.marketplace-title > button { flex: 0 0 auto; margin-bottom: 7px; } +.marketplace-proof { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 0 0 22px; border: 1px solid var(--panel-edge); border-radius: 14px; overflow: hidden; background: color-mix(in srgb, var(--panel-bg) 78%, transparent); } +.marketplace-proof > div { min-width: 0; padding: 14px 17px; border-right: 1px solid var(--panel-edge); } +.marketplace-proof > div:last-child { border-right: 0; } +.marketplace-proof span, .marketplace-proof b { display: block; } +.marketplace-proof span { margin-bottom: 6px; color: var(--content-faint); font-size: 10px; font-weight: 700; letter-spacing: .11em; text-transform: uppercase; } +.marketplace-proof b { overflow: hidden; color: var(--content-primary); font-size: 13px; text-overflow: ellipsis; white-space: nowrap; } +.marketplace-tabs { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; } +.marketplace-tabs button { display: grid; gap: 6px; padding: 16px 18px; border: 1px solid var(--panel-edge); border-radius: 13px; color: var(--content-secondary); background: var(--primitive-glass); text-align: left; cursor: pointer; } +.marketplace-tabs button.active { border-color: color-mix(in srgb, var(--primitive-cool-glow) 58%, var(--panel-edge)); background: color-mix(in srgb, var(--primitive-cool-glow) 11%, var(--panel-bg)); box-shadow: inset 0 0 22px color-mix(in srgb, var(--primitive-cool-glow) 5%, transparent); } +.marketplace-tabs b { color: var(--content-primary); font-size: 15px; } +.marketplace-tabs span { color: var(--content-muted); font-size: 11.5px; line-height: 1.55; } +.marketplace-boundary { margin: 12px 0 20px; padding: 11px 14px; border-left: 2px solid var(--accent-light); color: var(--content-muted); background: color-mix(in srgb, var(--primitive-glass) 58%, transparent); font-size: 11.5px; line-height: 1.7; } +.marketplace-list { display: grid; border-top: 1px solid var(--panel-edge); } +.marketplace-item { padding: 22px 2px; border-bottom: 1px solid var(--panel-edge); } +.marketplace-item > header { display: flex; justify-content: space-between; gap: 28px; } +.marketplace-item > header > div { min-width: 0; } +.marketplace-item > header span { color: var(--accent-light); font-size: 9.5px; font-weight: 750; letter-spacing: .15em; } +.marketplace-item h2 { margin: 6px 0; color: var(--content-primary); font-size: 19px; } +.marketplace-item header p { max-width: 760px; margin: 0; color: var(--content-muted); font-size: 12px; line-height: 1.65; } +.marketplace-item header em { align-self: start; flex: 0 0 auto; padding: 5px 9px; border: 1px solid var(--panel-edge); border-radius: 999px; color: var(--content-faint); font-size: 10px; font-style: normal; } +.marketplace-item header em.installed { border-color: color-mix(in srgb, var(--primitive-cool-glow) 45%, var(--panel-edge)); color: var(--accent-light); } +.marketplace-item dl { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px 26px; margin: 16px 0 0; } +.marketplace-item dl div { display: grid; grid-template-columns: 96px minmax(0, 1fr); gap: 10px; padding: 8px 0; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 62%, transparent); } +.marketplace-item dt { color: var(--content-faint); font-size: 10.5px; } +.marketplace-item dd { overflow-wrap: anywhere; margin: 0; color: var(--content-secondary); font-size: 10.5px; } +.marketplace-actions { display: flex; justify-content: flex-end; gap: 9px; margin-top: 16px; } +.marketplace-actions button { min-height: 36px; padding: 0 13px; border: 1px solid var(--panel-edge); border-radius: 9px; color: var(--content-secondary); background: var(--primitive-glass); font-size: 11px; font-weight: 680; cursor: pointer; } +.marketplace-actions button:disabled { opacity: .52; cursor: default; } +.marketplace-actions .primary-button { color: var(--button-primary-text); background: var(--button-primary-bg); } +.marketplace-empty { padding: 52px 18px; color: var(--content-muted); text-align: center; } +.marketplace-empty b { color: var(--content-primary); } +.marketplace-empty p { font-size: 12px; } +.marketplace-message { margin-top: 18px; } +.marketplace-receipt { padding: 15px 2px 0; color: var(--content-faint); font-size: 10px; overflow-wrap: anywhere; } + @media (max-width: 900px) { .d-main { left: 2%; } .d-sub { left: 22%; } .d-zero { right: 22%; } .d-zs { right: 2%; } .domain-info-card.info-main { left: 18px; } .domain-info-card.info-sub { left: calc(22% - 75px); } @@ -791,6 +873,10 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca .channel-knowledge { left: 4%; } .channel-code { left: 25%; } .channel-light { right: 25%; } .channel-status { right: 4%; } .broadcast-stream { left: 5%; } .personal-node-guide ol { grid-template-columns: 1fr; } + .marketplace-proof { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .marketplace-proof > div:nth-child(2) { border-right: 0; } + .marketplace-proof > div:nth-child(-n+2) { border-bottom: 1px solid var(--panel-edge); } + .marketplace-item dl { grid-template-columns: 1fr; } } @media (max-height: 720px) { .official-hero { top: 14px; } .official-hero h1 { font-size: 24px; }