release: formalize HoloLake 0.5.0 numbered root
This commit is contained in:
parent
5d01607459
commit
40b8c49324
10 changed files with 401 additions and 6 deletions
|
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"schema": "hololake.installed-numbered-root-acceptance/v1",
|
||||
"record_id": "HLP-HOLOLAKE-0.5.0-NUMBERED-ROOT-INSTALLED-20260818",
|
||||
"state": "LOCAL_DEVELOPER_ID_SIGNED_INSTALLED_ACCEPTANCE_PASS_PUBLIC_NOTARIZATION_PENDING",
|
||||
"observed_at": "2026-08-18T15:18:00Z",
|
||||
"source": {
|
||||
"branch": "codex/hololake-clean-reassembly-20260818",
|
||||
"source_commit": "5d01607459043d0713bc562ef6d04dec198930a9",
|
||||
"numbered_root_commit": "64abf969bfbc1c576d4fa84ae3282d32efbfcc38"
|
||||
},
|
||||
"installed_application": {
|
||||
"path": "/Users/bingshuolingdianyuanhe/Desktop/HoloLake.app",
|
||||
"version": "0.5.0",
|
||||
"bundle_identifier": "world.guanghu.hololake",
|
||||
"architecture": "arm64",
|
||||
"binary_sha256": "2e162ff077187310f816f58df2250e3ee939eb489fe3c7b00e94557ff8f3370d",
|
||||
"process_path_verified": true
|
||||
},
|
||||
"developer_id": {
|
||||
"identity": "Developer ID Application: bei sun (825A9L3G7Q)",
|
||||
"team_identifier": "825A9L3G7Q",
|
||||
"cdhash": "7a2372ed85fc3b775e9a352217b123a4bdee0d64",
|
||||
"strict_signature_verification": "PASS",
|
||||
"designated_requirement": "PASS",
|
||||
"gatekeeper": "REJECTED_UNNOTARIZED_DEVELOPER_ID",
|
||||
"apple_notarization_and_stapling": "PENDING"
|
||||
},
|
||||
"runtime": {
|
||||
"real_webview_loaded": true,
|
||||
"visible_version": "V0.5.0",
|
||||
"visible_domain": "第五域 · 光湖本源域",
|
||||
"entered_surface": "永恒湖心系统",
|
||||
"numbered_ipc_receipt_rows": 50,
|
||||
"grant_and_execution_rows_present": true,
|
||||
"empty_authority_binding_digest_rows": 0,
|
||||
"recalculated_receipt_chain_failures": 0,
|
||||
"last_sequence": 50,
|
||||
"last_receipt_hash": "a294463ad9e6959d0f12ce5c2b59e11d97581e067b9480df9ae598dfafc067df"
|
||||
},
|
||||
"old_application": {
|
||||
"version": "0.4.1",
|
||||
"role": "READ_ONLY_PRE_NUMBERED_ROOT_MODULE_DONOR",
|
||||
"archive_receipt": "/Volumes/JZAO/HoloLake/artifacts/hololake-release/0.4.1/macos-arm64/pre-numbered-root-donor/archive-receipt.json",
|
||||
"repair_in_place": false
|
||||
},
|
||||
"release_boundary": {
|
||||
"local_signed_install_complete": true,
|
||||
"public_signed_notarized_release_complete": false,
|
||||
"updater_public_key_continuity": "PASS_EXISTING_0.4.1_TRUST_ROOT_REUSED",
|
||||
"updater_private_key_available_to_current_pipeline": true,
|
||||
"updater_signature_generated_and_verified": true,
|
||||
"pre_notarization_updater_artifact": {
|
||||
"path": "src-tauri/target/release/bundle/macos/HoloLake.app.tar.gz",
|
||||
"sha256": "4176bb7ea83c820744239c228671840289f87675329182bc055938e03bba9693",
|
||||
"signature_path": "src-tauri/target/release/bundle/macos/HoloLake.app.tar.gz.sig",
|
||||
"signature_sha256": "ae02c2918c26b6e3cc2389fec8d9f98884ae639d448edc78a4ec80ee30a8ae92",
|
||||
"publication_allowed": false
|
||||
},
|
||||
"apple_notarization_credentials_available_to_current_pipeline": false,
|
||||
"public_broadcast_activated": false
|
||||
},
|
||||
"persona_boundary": {
|
||||
"current_codex_carrier_binding_claimed": false,
|
||||
"runtime_persona_binding_created_by_numbered_ipc": false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
{
|
||||
"schema": "hololake.module-donor-admission-registry/v1",
|
||||
"record_id": "HLP-MODULE-DONOR-ADMISSION-001",
|
||||
"state": "DONORS_QUARANTINED_CANDIDATES_NUMBERED_NOT_ADMITTED",
|
||||
"root_rule": {
|
||||
"official_base": "HOLOLAKE_0.5.0_NUMBERED_IPC_ROOT",
|
||||
"repair_old_application_in_place": false,
|
||||
"bulk_merge_or_wholesale_copy_allowed": false,
|
||||
"one_candidate_per_admission_cycle": true,
|
||||
"candidate_number_is_runtime_module_number": false,
|
||||
"permanent_module_number_assignment_before_acceptance": false,
|
||||
"all_frontend_backend_calls_must_cross_numbered_ipc": true,
|
||||
"shared_file_merge_is_admission_evidence": false
|
||||
},
|
||||
"donors": [
|
||||
{
|
||||
"donor_id": "HLP-DONOR-COMPILED-DESKTOP-0.4.1",
|
||||
"kind": "COMPILED_MACOS_APPLICATION",
|
||||
"path": "/Volumes/JZAO/HoloLake/artifacts/hololake-release/0.4.1/macos-arm64/pre-numbered-root-donor/HoloLake.app",
|
||||
"state": "READ_ONLY",
|
||||
"source_commit": null,
|
||||
"source_commit_state": "UNKNOWN_NOT_INFERRED_FROM_COMPILED_BUNDLE",
|
||||
"binary_sha256": "adc0d8b028a8b874c39909265ed7c41e7c24e4fe5b38adba04e8f72b64900c15",
|
||||
"use": "BEHAVIOR_AND_VISIBLE_PRODUCT_REFERENCE_ONLY"
|
||||
},
|
||||
{
|
||||
"donor_id": "HLP-DONOR-CHAOTIC-WORKTREE-20260818",
|
||||
"kind": "DIRTY_SOURCE_WORKTREE",
|
||||
"path": "/Users/bingshuolingdianyuanhe/Documents/Codex/2026-08-15/new-chat-2/work/jd-guanghu-supervisor/product-source/hololake-native-desktop",
|
||||
"observed_head": "a8fe571b5d5c0a45207b64ac538d729b3d719d21",
|
||||
"observed_dirty_path_count": 30,
|
||||
"state": "READ_ONLY_UNTRUSTED_AS_A_WHOLE",
|
||||
"use": "INDIVIDUAL_MODULE_SOURCE_CANDIDATES_ONLY"
|
||||
}
|
||||
],
|
||||
"rejected_inputs": [
|
||||
{
|
||||
"candidate_number": "HLP-DONOR-CAND-0000",
|
||||
"name": "legacy_numbered_operation_runtime",
|
||||
"state": "REJECTED_SUPERSEDED",
|
||||
"why": "It predates the single numbered IPC root and must not become a second numbering authority.",
|
||||
"paths": [
|
||||
"contracts/numbered-operation-runtime.json",
|
||||
"src/modules/numbered-runtime.ts",
|
||||
"src-tauri/src/numbered_operation_runtime.rs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"candidate_number": "HLP-DONOR-CAND-SHARED-MUTATIONS",
|
||||
"name": "shared_file_mutation_set",
|
||||
"state": "REJECTED_AS_MERGE_UNIT",
|
||||
"why": "These files mix unrelated modules and divergent frontend/backend routes; each needed behavior must be reconstructed behind its owning numbered module.",
|
||||
"examples": [
|
||||
"src/main.tsx",
|
||||
"src/styles.css",
|
||||
"src-tauri/src/lib.rs",
|
||||
"src-tauri/src/knowledge_base.rs",
|
||||
"src-tauri/src/gls_protocol_runtime.rs"
|
||||
]
|
||||
}
|
||||
],
|
||||
"candidates": [
|
||||
{
|
||||
"admission_order": 1,
|
||||
"candidate_number": "HLP-DONOR-CAND-0001",
|
||||
"name": "native_composition_module_runtime",
|
||||
"state": "QUARANTINED_ASSESS_FIRST",
|
||||
"paths": [
|
||||
"contracts/native-composition-runtime.json",
|
||||
"src/modules/native-composition",
|
||||
"src-tauri/src/native_composition.rs"
|
||||
],
|
||||
"why_first": "A module needs an isolated mount, self-test, unmount and rollback boundary before content modules can be admitted safely."
|
||||
},
|
||||
{
|
||||
"admission_order": 2,
|
||||
"candidate_number": "HLP-DONOR-CAND-0002",
|
||||
"name": "channel_document_and_spreadsheet_workbench",
|
||||
"state": "QUARANTINED_PENDING_ADMISSION",
|
||||
"paths": [
|
||||
"src/modules/channel-workbench"
|
||||
]
|
||||
},
|
||||
{
|
||||
"admission_order": 3,
|
||||
"candidate_number": "HLP-DONOR-CAND-0003",
|
||||
"name": "persona_channel_body",
|
||||
"state": "QUARANTINED_PENDING_ADMISSION",
|
||||
"paths": [
|
||||
"contracts/persona-channel-body.json",
|
||||
"src/PersonaChannelBody.tsx",
|
||||
"src-tauri/src/persona_channel_body.rs",
|
||||
"src-tauri/src/channel_growth.rs"
|
||||
],
|
||||
"boundary": "UI_AND_PERSISTENCE_BODY_ONLY; DOES_NOT_CREATE_PERSONA_BINDING"
|
||||
},
|
||||
{
|
||||
"admission_order": 4,
|
||||
"candidate_number": "HLP-DONOR-CAND-0004",
|
||||
"name": "education_workbench",
|
||||
"state": "QUARANTINED_PENDING_ADMISSION",
|
||||
"paths": [
|
||||
"contracts/education-workspace.json",
|
||||
"src/modules/education-adaptive-rendering.ts",
|
||||
"src/modules/education-data.ts",
|
||||
"src/modules/education-document-engine.tsx",
|
||||
"src-tauri/src/education_translation.rs",
|
||||
"src-tauri/src/education_workspace.rs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"admission_order": 5,
|
||||
"candidate_number": "HLP-DONOR-CAND-0005",
|
||||
"name": "web_novel_workbench_and_author_modules",
|
||||
"state": "QUARANTINED_PENDING_ADMISSION",
|
||||
"paths": [
|
||||
"contracts/web-novel-workspace.json",
|
||||
"contracts/web-novel-module-marketplace-plan.json",
|
||||
"src/WebNovelWorkspace.tsx",
|
||||
"src/AuthorModuleCenter.tsx",
|
||||
"src/AuthorWritingSidecar.tsx",
|
||||
"src-tauri/src/web_novel_workspace.rs",
|
||||
"src-tauri/src/web_novel_import.rs",
|
||||
"src-tauri/src/web_novel_author.rs",
|
||||
"src-tauri/src/web_novel_modules.rs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"admission_order": 6,
|
||||
"candidate_number": "HLP-DONOR-CAND-0006",
|
||||
"name": "mobile_sync",
|
||||
"state": "QUARANTINED_PENDING_ADMISSION",
|
||||
"paths": [
|
||||
"contracts/mobile-sync-v1.json",
|
||||
"src/MobileSyncPanel.tsx",
|
||||
"src-tauri/src/mobile_sync.rs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"admission_order": 7,
|
||||
"candidate_number": "HLP-DONOR-CAND-0007",
|
||||
"name": "dynamic_language_world_visual_surface",
|
||||
"state": "QUARANTINED_PENDING_ADMISSION",
|
||||
"paths": [
|
||||
"contracts/dynamic-language-world-visual-system.json",
|
||||
"src/StarlakeSurface.tsx",
|
||||
"src/starlake-surface.css",
|
||||
"src/traditional-surface.tsx",
|
||||
"src/traditional-surface.css",
|
||||
"src-tauri/src/world_climate.rs"
|
||||
]
|
||||
}
|
||||
],
|
||||
"admission_gate": [
|
||||
"EXTRACT_ONLY_DECLARED_CANDIDATE_PATHS",
|
||||
"REVIEW_SOURCE_PROVENANCE_AND_LICENSE",
|
||||
"DEFINE_DATA_PERMISSION_AND_RESOURCE_BOUNDARY",
|
||||
"ALLOCATE_NUMBERED_IPC_MODULE_TARGET_AND_OPERATION_COORDINATES",
|
||||
"IMPLEMENT_ADAPTER_WITHOUT_RAW_TAURI_INVOKE",
|
||||
"ADD_UNIT_INTEGRATION_AND_NEGATIVE_ROUTE_TESTS",
|
||||
"BUILD_FROM_CLEAN_OFFICIAL_BASE",
|
||||
"INSTALL_IN_ISOLATED_RUNTIME",
|
||||
"VERIFY_MOUNT_SELF_TEST_RESTART_UNMOUNT_AND_ROLLBACK",
|
||||
"WRITE_HASH_CHAINED_RUNTIME_RECEIPT",
|
||||
"ONLY_THEN_ASSIGN_PERMANENT_MODULE_NUMBER"
|
||||
],
|
||||
"hot_install_boundary": {
|
||||
"source_repository_is_directly_executable": false,
|
||||
"immutable_signed_artifact_required": true,
|
||||
"compatibility_manifest_required": true,
|
||||
"permissions_declared_before_mount": true,
|
||||
"human_confirmation_required_when_boundary_expands": true,
|
||||
"rollback_on_self_test_failure": true,
|
||||
"user_data_survives_unmount": true
|
||||
}
|
||||
}
|
||||
|
|
@ -90,10 +90,16 @@ HoloLake 0.3.0 includes a live, read-only projection of the PNCC resident runtim
|
|||
|
||||
## Release pipeline
|
||||
|
||||
`npm run release:macos -- release/inputs/<version>.json` is the only product-owned macOS release entry. It fails before building unless the embedded trust contains the exact registered HoloLake HTTPS endpoint and updater public key, the immutable `v<version>` tag equals the clean `main` head, and the Developer ID, Tauri updater-signing and Apple notarization credential sets are supplied at runtime. The pipeline runs all product and Rust gates, creates updater artifacts through a temporary Tauri override, then requires strict code-signature verification, Gatekeeper acceptance and stapled Apple notarization before writing the HoloLake broadcast and receipts.
|
||||
`npm run release:macos -- release/inputs/<version>.json` is the only product-owned macOS release entry. It fails before building unless the embedded trust contains the exact registered HoloLake HTTPS endpoint and updater public key, the immutable `v<version>` tag equals the clean `main` head, and the Developer ID, Tauri updater-signing and Apple notarization credential sets are supplied at runtime. A protected updater-key path is materialized only into the child build environment; the key is never printed or copied into source. The pipeline runs all product and Rust gates, creates updater artifacts through a temporary Tauri override, verifies the updater signature against the embedded product trust, then requires strict code-signature verification, Gatekeeper acceptance and stapled Apple notarization before writing the HoloLake broadcast and receipts.
|
||||
|
||||
Generated packages, private release inputs and credentials are not committed. The pipeline never uploads or activates a release; its terminal artifact is a bounded folder ready for a separately authorized JD-controller upload and server-owned readback receipt.
|
||||
|
||||
## Numbered-root module admission
|
||||
|
||||
HoloLake 0.5.0 is the clean numbered-root base. The compiled 0.4.1 desktop application and the divergent dirty source worktree are read-only donors, not merge bases. Their old numbered-operation runtime is explicitly superseded, and mutations to shared files such as `src/main.tsx` or `src-tauri/src/lib.rs` are never accepted as a unit.
|
||||
|
||||
Each donor capability receives a candidate coordinate, but no permanent runtime module number, until one isolated admission cycle has reviewed provenance and permissions, allocated numbered IPC module/target/operation coordinates, implemented an adapter without raw Tauri invoke, passed negative-route and data tests, and produced installed mount, restart, unmount and rollback receipts. The admission order and candidate inventory are recorded in `contracts/module-donor-admission-registry.json`.
|
||||
|
||||
## Stage-one convergence verdict
|
||||
|
||||
The Tauri source in this directory is the only future HoloLake desktop mainline. An installed build of it is an acceptance candidate, not a separate product line and not proof that stage one exists. The Electron 0.8.0 product and the legacy Tauri/platform sources remain read-only UX, behavior, engineering and protected-data donors until inventory, backup, readback, reversible migration rehearsal and signed installed-runtime acceptance all pass.
|
||||
|
|
|
|||
|
|
@ -56,9 +56,11 @@
|
|||
"local_runtime_acceptance_receipt": "audit/local-runtime-acceptance-20260815.json",
|
||||
"domain_membrane_pncc_installed_acceptance_receipt": "audit/hololake-0.4.0-domain-membrane-pncc-installed-acceptance-20260816.json",
|
||||
"updater_bootstrap_installed_acceptance_receipt": "audit/hololake-0.4.1-updater-bootstrap-installed-acceptance-20260817.json",
|
||||
"installed_desktop_path": "/Applications/HoloLake.app",
|
||||
"installed_desktop_path": "/Users/bingshuolingdianyuanhe/Desktop/HoloLake.app",
|
||||
"old_desktop_apps_recoverably_archived": true,
|
||||
"public_developer_id_and_notarized_acceptance": false,
|
||||
"local_developer_id_signed_0_5_0_installed_acceptance": true,
|
||||
"numbered_root_installed_acceptance_receipt": "audit/hololake-0.5.0-numbered-root-installed-acceptance-20260818.json",
|
||||
"stage_one_implementation_order": [
|
||||
"PERSONAL_CHANNEL_IDENTITY_TASK_EVENT_RECEIPT_KERNEL",
|
||||
"KNOWLEDGE_TREE_PAGE_SEARCH_AND_LOCAL_PERSISTENCE",
|
||||
|
|
@ -99,8 +101,11 @@
|
|||
"automatic_restart_allowed": false,
|
||||
"private_signing_material_allowed_in_source": false,
|
||||
"donor_audit_record": "audit/donor-audit.json",
|
||||
"donor_audit_complete": false,
|
||||
"donor_audit_complete": true,
|
||||
"donor_source_copying_allowed": false,
|
||||
"module_donor_admission_registry": "contracts/module-donor-admission-registry.json",
|
||||
"module_donor_bulk_merge_allowed": false,
|
||||
"module_donor_one_candidate_per_admission_cycle": true,
|
||||
"stage_one_product_contract": "contracts/stage-one-platform.json",
|
||||
"local_development_bridge_contract": "contracts/local-development-bridge.json",
|
||||
"account_single_writer_kernel_implemented": true,
|
||||
|
|
@ -128,7 +133,7 @@
|
|||
"pncc_jd_live_server_projection_repository_content_exposed": false,
|
||||
"pncc_jd_live_server_projection_write_authority": false,
|
||||
"pncc_jd_live_server_projection_carrier_state": "UNBOUND_EVIDENCE_REQUIRED",
|
||||
"installed_local_product_version": "0.4.1",
|
||||
"installed_local_product_version": "0.5.0",
|
||||
"pncc_authenticated_direct_broker_integration_implemented": true,
|
||||
"pncc_human_mount_registration_implemented": true,
|
||||
"pncc_human_mount_registration_gate": "SATISFIED_NATIVE_FILE_PICKER_EXACT_CONFIRMATION",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const catalog = JSON.parse(readFileSync(new URL('../contracts/module-donor-admission-registry.json', import.meta.url), 'utf8'))
|
||||
const numbered = JSON.parse(readFileSync(new URL('../contracts/numbered-ipc-registry.json', import.meta.url), 'utf8'))
|
||||
|
||||
test('chaotic donors are read-only candidates and never a bulk merge source', () => {
|
||||
assert.equal(catalog.state, 'DONORS_QUARANTINED_CANDIDATES_NUMBERED_NOT_ADMITTED')
|
||||
assert.equal(catalog.root_rule.repair_old_application_in_place, false)
|
||||
assert.equal(catalog.root_rule.bulk_merge_or_wholesale_copy_allowed, false)
|
||||
assert.equal(catalog.root_rule.one_candidate_per_admission_cycle, true)
|
||||
assert.ok(catalog.donors.every((donor) => donor.state.startsWith('READ_ONLY')))
|
||||
})
|
||||
|
||||
test('candidate coordinates are unique but are not permanent runtime module numbers', () => {
|
||||
const coordinates = catalog.candidates.map((candidate) => candidate.candidate_number)
|
||||
assert.equal(new Set(coordinates).size, coordinates.length)
|
||||
assert.ok(catalog.candidates.every((candidate) => candidate.state.startsWith('QUARANTINED')))
|
||||
assert.equal(catalog.root_rule.candidate_number_is_runtime_module_number, false)
|
||||
assert.equal(catalog.root_rule.permanent_module_number_assignment_before_acceptance, false)
|
||||
})
|
||||
|
||||
test('legacy numbering donor is rejected and the numbered IPC root remains singular', () => {
|
||||
const legacy = catalog.rejected_inputs.find((candidate) => candidate.name === 'legacy_numbered_operation_runtime')
|
||||
assert.equal(legacy.state, 'REJECTED_SUPERSEDED')
|
||||
assert.equal(numbered.runtime.public_tauri_command, 'numbered_ipc')
|
||||
assert.equal(numbered.runtime.legacy_direct_commands_allowed, false)
|
||||
assert.ok(catalog.admission_gate.includes('ALLOCATE_NUMBERED_IPC_MODULE_TARGET_AND_OPERATION_COORDINATES'))
|
||||
assert.ok(catalog.admission_gate.includes('IMPLEMENT_ADAPTER_WITHOUT_RAW_TAURI_INVOKE'))
|
||||
})
|
||||
|
||||
test('hot installation is artifact, permission, self-test and rollback backed', () => {
|
||||
assert.equal(catalog.hot_install_boundary.source_repository_is_directly_executable, false)
|
||||
assert.equal(catalog.hot_install_boundary.immutable_signed_artifact_required, true)
|
||||
assert.equal(catalog.hot_install_boundary.compatibility_manifest_required, true)
|
||||
assert.equal(catalog.hot_install_boundary.rollback_on_self_test_failure, true)
|
||||
assert.equal(catalog.hot_install_boundary.user_data_survives_unmount, true)
|
||||
})
|
||||
|
|
@ -106,12 +106,30 @@ export function validateCredentialEnvironment(env) {
|
|||
if (missing.length) fail(`HOLOLAKE_RELEASE_PIPELINE_CREDENTIALS_MISSING:${[...new Set(missing)].sort().join(',')}`)
|
||||
}
|
||||
|
||||
export function materializeUpdaterPrivateKey(env, readText = (file) => fs.readFileSync(file, 'utf8')) {
|
||||
if (typeof env.TAURI_SIGNING_PRIVATE_KEY === 'string' && env.TAURI_SIGNING_PRIVATE_KEY.trim()) {
|
||||
return { ...env }
|
||||
}
|
||||
const privateKeyPath = requireText(
|
||||
env.TAURI_SIGNING_PRIVATE_KEY_PATH,
|
||||
'HOLOLAKE_RELEASE_PIPELINE_UPDATER_PRIVATE_KEY_PATH_REQUIRED',
|
||||
)
|
||||
const privateKey = requireText(
|
||||
readText(privateKeyPath),
|
||||
'HOLOLAKE_RELEASE_PIPELINE_UPDATER_PRIVATE_KEY_EMPTY',
|
||||
)
|
||||
return {
|
||||
...env,
|
||||
TAURI_SIGNING_PRIVATE_KEY: privateKey,
|
||||
}
|
||||
}
|
||||
|
||||
function run(program, args, options = {}) {
|
||||
return execFileSync(program, args, {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
stdio: options.capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
||||
env: process.env,
|
||||
env: options.env || process.env,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -194,6 +212,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|||
const trustFacts = validateReleaseTrust(trust)
|
||||
const input = validateReleaseInput(readJson(inputPath), trustFacts)
|
||||
validateCredentialEnvironment(process.env)
|
||||
const credentialEnvironment = materializeUpdaterPrivateKey(process.env)
|
||||
requireCleanImmutableSource(input)
|
||||
if (process.platform !== 'darwin' || process.arch !== 'arm64') fail('HOLOLAKE_RELEASE_PIPELINE_BUILD_HOST_MISMATCH')
|
||||
if (!process.env.APPLE_SIGNING_IDENTITY.includes(input.appleTeamIdentifier)) fail('HOLOLAKE_RELEASE_PIPELINE_SIGNING_TEAM_MISMATCH')
|
||||
|
|
@ -205,7 +224,9 @@ export async function main(argv = process.argv.slice(2)) {
|
|||
const buildConfig = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'hololake-release-config-')), 'tauri.release.json')
|
||||
fs.writeFileSync(buildConfig, `${JSON.stringify({ bundle: { createUpdaterArtifacts: true } })}\n`, { mode: 0o600 })
|
||||
try {
|
||||
run('npm', ['run', 'tauri', '--', 'build', '--ci', '--config', buildConfig])
|
||||
run('npm', ['run', 'tauri', '--', 'build', '--ci', '--config', buildConfig], {
|
||||
env: credentialEnvironment,
|
||||
})
|
||||
} finally {
|
||||
fs.rmSync(path.dirname(buildConfig), { recursive: true, force: true })
|
||||
}
|
||||
|
|
@ -217,6 +238,18 @@ export async function main(argv = process.argv.slice(2)) {
|
|||
const updater = findOne(bundleRoot, (file) => file.endsWith('.app.tar.gz'), 'HOLOLAKE_RELEASE_PIPELINE_UPDATER_NOT_UNIQUE')
|
||||
const updaterSignature = `${updater}.sig`
|
||||
if (!fs.statSync(updaterSignature, { throwIfNoEntry: false })?.isFile()) fail('HOLOLAKE_RELEASE_PIPELINE_UPDATER_SIGNATURE_MISSING')
|
||||
run('cargo', [
|
||||
'run',
|
||||
'--quiet',
|
||||
'--manifest-path',
|
||||
'src-tauri/Cargo.toml',
|
||||
'--example',
|
||||
'verify_updater_signature',
|
||||
'--',
|
||||
'src-tauri/release-trust.json',
|
||||
updater,
|
||||
updaterSignature,
|
||||
])
|
||||
if (decodeURIComponent(input.packageUrl.pathname.split('/').pop()) !== path.basename(updater)) {
|
||||
fail('HOLOLAKE_RELEASE_PIPELINE_PACKAGE_URL_FILENAME_MISMATCH')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs'
|
|||
import test from 'node:test'
|
||||
|
||||
import {
|
||||
materializeUpdaterPrivateKey,
|
||||
validateCredentialEnvironment,
|
||||
validateReleaseInput,
|
||||
validateReleaseTrust,
|
||||
|
|
@ -76,6 +77,25 @@ test('release pipeline requires updater signing, Developer ID and Apple notariza
|
|||
}))
|
||||
})
|
||||
|
||||
test('release pipeline materializes a protected updater key path only inside the build environment', () => {
|
||||
const source = {
|
||||
TAURI_SIGNING_PRIVATE_KEY_PATH: '/protected/hololake-updater.key',
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: 'provided-at-runtime',
|
||||
}
|
||||
const hydrated = materializeUpdaterPrivateKey(source, (file) => {
|
||||
assert.equal(file, source.TAURI_SIGNING_PRIVATE_KEY_PATH)
|
||||
return 'encrypted-private-key-material'
|
||||
})
|
||||
assert.equal(hydrated.TAURI_SIGNING_PRIVATE_KEY, 'encrypted-private-key-material')
|
||||
assert.equal(source.TAURI_SIGNING_PRIVATE_KEY, undefined)
|
||||
})
|
||||
|
||||
test('release pipeline verifies the updater signature against embedded product trust before broadcast', () => {
|
||||
const source = readFileSync(new URL('./release-pipeline.mjs', import.meta.url), 'utf8')
|
||||
assert.match(source, /--example',\s*'verify_updater_signature'/)
|
||||
assert.match(source, /src-tauri\/release-trust\.json/)
|
||||
})
|
||||
|
||||
test('Windows updater keeps signed installation but never claims the macOS rollback boundary', () => {
|
||||
const source = readFileSync(new URL('../src-tauri/src/release_update.rs', import.meta.url), 'utf8')
|
||||
assert.match(source, /SIGNED_PACKAGE_VERIFIED_INSTALLING_NO_LOCAL_ROLLBACK/)
|
||||
|
|
|
|||
|
|
@ -1499,6 +1499,7 @@ dependencies = [
|
|||
"fs2",
|
||||
"futures-util",
|
||||
"interprocess",
|
||||
"minisign-verify",
|
||||
"reqwest",
|
||||
"ring",
|
||||
"rusqlite",
|
||||
|
|
|
|||
|
|
@ -38,4 +38,5 @@ futures-util = "0.3"
|
|||
widestring = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
minisign-verify = "0.2.5"
|
||||
tempfile = "3"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
use minisign_verify::{PublicKey, Signature};
|
||||
use serde_json::Value;
|
||||
use std::{env, fs, path::Path};
|
||||
|
||||
fn decode_outer_base64(value: &str, label: &str) -> Result<String, String> {
|
||||
let bytes = STANDARD
|
||||
.decode(value.trim())
|
||||
.map_err(|error| format!("{label}_BASE64_INVALID: {error}"))?;
|
||||
String::from_utf8(bytes).map_err(|error| format!("{label}_UTF8_INVALID: {error}"))
|
||||
}
|
||||
|
||||
fn main() -> Result<(), String> {
|
||||
let arguments: Vec<String> = env::args().collect();
|
||||
let [_, trust_path, updater_path, signature_path] = arguments.as_slice() else {
|
||||
return Err(
|
||||
"USAGE: verify_updater_signature <release-trust.json> <updater> <signature>"
|
||||
.to_string(),
|
||||
);
|
||||
};
|
||||
let trust: Value = serde_json::from_str(
|
||||
&fs::read_to_string(trust_path)
|
||||
.map_err(|error| format!("RELEASE_TRUST_READ_FAILED: {error}"))?,
|
||||
)
|
||||
.map_err(|error| format!("RELEASE_TRUST_JSON_INVALID: {error}"))?;
|
||||
let encoded_public_key = trust
|
||||
.get("publicKey")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| "RELEASE_TRUST_PUBLIC_KEY_MISSING".to_string())?;
|
||||
let public_key = PublicKey::decode(&decode_outer_base64(
|
||||
encoded_public_key,
|
||||
"RELEASE_TRUST_PUBLIC_KEY",
|
||||
)?)
|
||||
.map_err(|error| format!("RELEASE_TRUST_PUBLIC_KEY_INVALID: {error}"))?;
|
||||
let signature = Signature::decode(&decode_outer_base64(
|
||||
&fs::read_to_string(signature_path)
|
||||
.map_err(|error| format!("UPDATER_SIGNATURE_READ_FAILED: {error}"))?,
|
||||
"UPDATER_SIGNATURE",
|
||||
)?)
|
||||
.map_err(|error| format!("UPDATER_SIGNATURE_INVALID: {error}"))?;
|
||||
let updater = fs::read(Path::new(updater_path))
|
||||
.map_err(|error| format!("UPDATER_ARTIFACT_READ_FAILED: {error}"))?;
|
||||
public_key
|
||||
.verify(&updater, &signature, true)
|
||||
.map_err(|error| format!("UPDATER_SIGNATURE_VERIFICATION_FAILED: {error}"))?;
|
||||
println!("HOLOLAKE_UPDATER_SIGNATURE_VERIFIED");
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Reference in a new issue