From 4357d25ea8d4c8daa58790ad216a38f1272c07f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Thu, 13 Aug 2026 18:54:04 +0800 Subject: [PATCH] feat(hololake): add explicit JD release activation operator --- .../hololake-native-desktop/foundation.json | 4 + .../scripts/foundation.test.mjs | 4 + .../release-broadcast-operator.test.mjs | 167 +++++++ .../server/release-broadcast/README.md | 27 ++ .../server/release-broadcast/operator.mjs | 406 ++++++++++++++++++ 5 files changed, 608 insertions(+) create mode 100644 product-source/hololake-native-desktop/scripts/release-broadcast-operator.test.mjs create mode 100755 product-source/hololake-native-desktop/server/release-broadcast/operator.mjs diff --git a/product-source/hololake-native-desktop/foundation.json b/product-source/hololake-native-desktop/foundation.json index 5d1d26b..3f6fb62 100644 --- a/product-source/hololake-native-desktop/foundation.json +++ b/product-source/hololake-native-desktop/foundation.json @@ -23,6 +23,10 @@ "release_broadcast_candidate_service_implemented": true, "release_broadcast_candidate_service_default_state": "EMPTY_FAIL_CLOSED_LOOPBACK_ONLY", "release_broadcast_candidate_service_source": "server/release-broadcast/server.mjs", + "release_broadcast_explicit_operator_activation_implemented": true, + "release_broadcast_activation_requires_exact_human_approval": true, + "release_broadcast_activation_requires_repeated_expected_facts": true, + "release_broadcast_operator_automatic_restart_allowed": false, "release_pipeline_automatic_upload_allowed": false, "release_production_activation_state": "BLOCKED_PENDING_PUBLIC_HTTPS_TRUST_UPDATER_KEY_PIPELINE_EXECUTION_AND_APPLE_NOTARIZATION", "tauri_update_artifacts_enabled": false, diff --git a/product-source/hololake-native-desktop/scripts/foundation.test.mjs b/product-source/hololake-native-desktop/scripts/foundation.test.mjs index db13cd2..beacff7 100644 --- a/product-source/hololake-native-desktop/scripts/foundation.test.mjs +++ b/product-source/hololake-native-desktop/scripts/foundation.test.mjs @@ -47,6 +47,10 @@ test('release activation remains explicitly human controlled', () => { assert.equal(foundation.release_signed_notarized_pipeline_implemented, true) assert.equal(foundation.release_broadcast_candidate_service_implemented, true) assert.equal(foundation.release_broadcast_candidate_service_default_state, 'EMPTY_FAIL_CLOSED_LOOPBACK_ONLY') + assert.equal(foundation.release_broadcast_explicit_operator_activation_implemented, true) + assert.equal(foundation.release_broadcast_activation_requires_exact_human_approval, true) + assert.equal(foundation.release_broadcast_activation_requires_repeated_expected_facts, true) + assert.equal(foundation.release_broadcast_operator_automatic_restart_allowed, false) assert.equal(foundation.release_pipeline_automatic_upload_allowed, false) assert.equal( foundation.release_production_activation_state, diff --git a/product-source/hololake-native-desktop/scripts/release-broadcast-operator.test.mjs b/product-source/hololake-native-desktop/scripts/release-broadcast-operator.test.mjs new file mode 100644 index 0000000..2bc443d --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/release-broadcast-operator.test.mjs @@ -0,0 +1,167 @@ +import assert from 'node:assert/strict' +import crypto from 'node:crypto' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import test from 'node:test' + +import { activateRelease, verifyReleaseBundle } from '../server/release-broadcast/operator.mjs' +import { loadRuntimeState } from '../server/release-broadcast/server.mjs' + +const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex') +const writeJson = (file, value) => fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 }) + +function buildOperatorFixture() { + const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'hololake-release-operator-fixture-')) + const source = path.join(fixtureRoot, 'source') + const approvals = path.join(fixtureRoot, 'approvals') + const stateRoot = path.join(fixtureRoot, 'state') + for (const directory of [source, approvals, stateRoot]) fs.mkdirSync(directory, { mode: 0o700 }) + const packageName = 'HoloLake.app.tar.gz' + const packageBytes = Buffer.from('signed-notarized-updater-package') + const sourceCommit = '2'.repeat(40) + fs.writeFileSync(path.join(source, packageName), packageBytes, { mode: 0o600 }) + writeJson(path.join(source, 'HOLOLAKE-CODESIGN.json'), { + schema: 'hololake.platform-code-signature-receipt/v1', + state: 'DEVELOPER_ID_SIGNATURE_STRICT_AND_GATEKEEPER_ACCEPTED', + sourceCommit, + }) + writeJson(path.join(source, 'HOLOLAKE-NOTARIZATION.json'), { + schema: 'hololake.apple-notarization-receipt/v1', + state: 'APPLE_NOTARIZATION_ACCEPTED_AND_STAPLED', + sourceCommit, + }) + const broadcast = { + schema: 'hololake.release-broadcast/v1', + releaseId: 'GH-HOLOLAKE-RELEASE-0.2.0', + version: '0.2.0', + pub_date: '2026-08-13T10:00:00.000Z', + notes: 'Signed release', + platforms: { + 'darwin-aarch64': { + url: `https://guanghulab.com/hololake/releases/0.2.0/${packageName}`, + signature: 'trusted-updater-signature', + size: packageBytes.length, + sha256: sha256(packageBytes), + platformCodeSignatureReceipt: 'HOLOLAKE-CODESIGN', + notarizationReceipt: 'HOLOLAKE-NOTARIZATION', + }, + }, + hololake: { + features: ['Stage one'], + fixes: [], + compatibility: { minimumVersion: '0.1.0', dataMigrationRequired: false }, + restart: { required: true, automaticAllowed: false }, + rollback: { supported: true, healthReceiptRequired: true, previousVersion: '0.1.0' }, + }, + } + const broadcastPath = path.join(source, 'latest.json') + writeJson(broadcastPath, broadcast) + const broadcastSha256 = sha256(fs.readFileSync(broadcastPath)) + writeJson(path.join(source, 'pipeline-receipt.json'), { + schema: 'hololake.signed-release-pipeline-receipt/v1', + state: 'SIGNED_NOTARIZED_RELEASE_BROADCAST_READY_FOR_JD_CONTROLLER_UPLOAD', + sourceCommit, + broadcastSha256, + automaticUpload: false, + automaticActivation: false, + }) + const approval = path.join(approvals, 'approval.json') + writeJson(approval, { + schema: 'hololake.release-broadcast-human-approval/v1', + state: 'HUMAN_APPROVED_EXACT_SIGNED_NOTARIZED_RELEASE', + approvalId: 'GH-HUMAN-RELEASE-APPROVAL-002', + releaseId: broadcast.releaseId, + version: broadcast.version, + broadcastSha256, + }) + return { + fixtureRoot, + source, + approval, + stateRoot, + expected: { releaseId: broadcast.releaseId, version: broadcast.version, sourceCommit, broadcastSha256 }, + } +} + +test('verify reconstructs and validates an exact private candidate without mutating state', () => { + const fixture = buildOperatorFixture() + try { + const result = verifyReleaseBundle(fixture.source, fixture.approval) + assert.equal(result.state, 'EXACT_SIGNED_NOTARIZED_HUMAN_APPROVED_BUNDLE_VERIFIED') + assert.equal(result.broadcastSha256, fixture.expected.broadcastSha256) + assert.equal(result.artifacts.some((artifact) => artifact.name === 'HoloLake.app.tar.gz'), true) + assert.deepEqual(fs.readdirSync(fixture.stateRoot), []) + } finally { + fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true }) + } +}) + +test('activation repeats expected facts, commits atomically, and still requires explicit restart', () => { + const fixture = buildOperatorFixture() + try { + const receipt = activateRelease({ + sourceDirectory: fixture.source, + humanApprovalPath: fixture.approval, + stateRoot: fixture.stateRoot, + expected: fixture.expected, + enforceRootOwner: false, + }) + assert.equal(receipt.state, 'ACTIVATED_EXPLICIT_SERVICE_RESTART_REQUIRED') + assert.equal(receipt.automaticUpload, false) + assert.equal(receipt.automaticActivation, false) + assert.equal(receipt.automaticRestart, false) + assert.equal(loadRuntimeState(fixture.stateRoot).state, 'READY_SIGNED_NOTARIZED_BROADCAST') + assert.equal(fs.existsSync(path.join(fixture.stateRoot, '.operator-lock')), false) + } finally { + fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true }) + } +}) + +test('expected-fact mismatch performs no activation or release staging', () => { + const fixture = buildOperatorFixture() + try { + assert.throws(() => activateRelease({ + sourceDirectory: fixture.source, + humanApprovalPath: fixture.approval, + stateRoot: fixture.stateRoot, + expected: { ...fixture.expected, broadcastSha256: '3'.repeat(64) }, + enforceRootOwner: false, + }), /HOLOLAKE_RELEASE_OPERATOR_EXPECTED_BROADCAST_SHA256_MISMATCH/) + assert.deepEqual(fs.readdirSync(fixture.stateRoot), []) + } finally { + fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true }) + } +}) + +test('human approval cannot be reused after broadcast bytes change', () => { + const fixture = buildOperatorFixture() + try { + const latestPath = path.join(fixture.source, 'latest.json') + const latest = JSON.parse(fs.readFileSync(latestPath, 'utf8')) + latest.notes = 'Changed after approval' + writeJson(latestPath, latest) + assert.throws( + () => verifyReleaseBundle(fixture.source, fixture.approval), + /HOLOLAKE_RELEASE_OPERATOR_PIPELINE_RECEIPT_INVALID/, + ) + } finally { + fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true }) + } +}) + +test('symlinked package inputs are rejected instead of followed', () => { + const fixture = buildOperatorFixture() + try { + const packagePath = path.join(fixture.source, 'HoloLake.app.tar.gz') + const moved = path.join(fixture.fixtureRoot, 'moved-package') + fs.renameSync(packagePath, moved) + fs.symlinkSync(moved, packagePath) + assert.throws( + () => verifyReleaseBundle(fixture.source, fixture.approval), + /HOLOLAKE_RELEASE_OPERATOR_FILE_INVALID/, + ) + } finally { + fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true }) + } +}) diff --git a/product-source/hololake-native-desktop/server/release-broadcast/README.md b/product-source/hololake-native-desktop/server/release-broadcast/README.md index d8af9a5..f15d8eb 100644 --- a/product-source/hololake-native-desktop/server/release-broadcast/README.md +++ b/product-source/hololake-native-desktop/server/release-broadcast/README.md @@ -14,3 +14,30 @@ service. The service listens only on `127.0.0.1`. Public HTTPS routing, updater trust-key provisioning, artifact upload, activation, and desktop rollout are independent deployment gates. + +`operator.mjs` supplies the separate, root-operated verification and activation +boundary. `verify` reconstructs a private candidate tree and accepts it only when +the broadcast, pipeline receipt, package bytes, Developer ID receipt, Apple +notarization receipt, and exact human approval all agree. `activate` additionally +requires the operator to repeat the expected release id, version, source commit, +and broadcast SHA-256. It copies only referenced immutable artifacts, commits +`ACTIVE.json` atomically, and reports that an explicit service restart is still +required. It never uploads, activates, or restarts on its own. + +```text +node operator.mjs verify \ + --source /secure/release/out/0.2.0 \ + --human-approval /secure/approvals/0.2.0.json + +sudo node operator.mjs activate \ + --source /secure/release/out/0.2.0 \ + --human-approval /secure/approvals/0.2.0.json \ + --state-root /var/lib/guanghu/hololake-release-broadcast \ + --expect-release-id GH-HOLOLAKE-RELEASE-0.2.0 \ + --expect-version 0.2.0 \ + --expect-source-commit 0000000000000000000000000000000000000000 \ + --expect-broadcast-sha256 0000000000000000000000000000000000000000000000000000000000000000 +``` + +The zeros above are placeholders, not deployable values. A real activation must +use the exact facts printed by `verify` and a separately issued human approval. diff --git a/product-source/hololake-native-desktop/server/release-broadcast/operator.mjs b/product-source/hololake-native-desktop/server/release-broadcast/operator.mjs new file mode 100755 index 0000000..a05d307 --- /dev/null +++ b/product-source/hololake-native-desktop/server/release-broadcast/operator.mjs @@ -0,0 +1,406 @@ +#!/usr/bin/env node + +import crypto from 'node:crypto' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' + +import { isMainModule, loadRuntimeState } from './server.mjs' + +const MAX_CONTROL_BYTES = 1024 * 1024 +const SHA256_PATTERN = /^[a-f0-9]{64}$/ +const SOURCE_COMMIT_PATTERN = /^[a-f0-9]{40}$/ +const VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/ + +const fail = (code) => { + throw new Error(code) +} + +const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex') + +const requireText = (value, code) => { + if (typeof value !== 'string' || value.trim() === '') fail(code) + return value.trim() +} + +function requireTrustedDirectory(directory, { enforceRootOwner = false } = {}) { + const info = fs.lstatSync(directory) + if (!info.isDirectory() || info.isSymbolicLink()) fail('HOLOLAKE_RELEASE_OPERATOR_DIRECTORY_INVALID') + if ((info.mode & 0o022) !== 0) fail('HOLOLAKE_RELEASE_OPERATOR_DIRECTORY_WRITABLE_BY_NON_OWNER') + if (enforceRootOwner && info.uid !== 0) fail('HOLOLAKE_RELEASE_OPERATOR_DIRECTORY_NOT_ROOT_OWNED') + return info +} + +function requireTrustedFile(file, { enforceRootOwner = false, maxBytes = null } = {}) { + const info = fs.lstatSync(file) + if (!info.isFile() || info.isSymbolicLink()) fail('HOLOLAKE_RELEASE_OPERATOR_FILE_INVALID') + if ((info.mode & 0o022) !== 0) fail('HOLOLAKE_RELEASE_OPERATOR_FILE_WRITABLE_BY_NON_OWNER') + if (enforceRootOwner && info.uid !== 0) fail('HOLOLAKE_RELEASE_OPERATOR_FILE_NOT_ROOT_OWNED') + if (maxBytes !== null && info.size > maxBytes) fail('HOLOLAKE_RELEASE_OPERATOR_CONTROL_FILE_TOO_LARGE') + return info +} + +function readJson(file, { enforceRootOwner = false } = {}) { + requireTrustedFile(file, { enforceRootOwner, maxBytes: MAX_CONTROL_BYTES }) + const bytes = fs.readFileSync(file) + return { bytes, value: JSON.parse(bytes.toString('utf8')) } +} + +function directFile(directory, name, code) { + const value = requireText(name, code) + if (value !== path.basename(value) || value === '.' || value === '..' || value.includes('\0')) fail(code) + const resolved = path.resolve(directory, value) + if (path.dirname(resolved) !== directory) fail(code) + return resolved +} + +function packageNameFromUrl(value) { + const url = new URL(requireText(value, 'HOLOLAKE_RELEASE_OPERATOR_PACKAGE_URL_REQUIRED')) + if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) { + fail('HOLOLAKE_RELEASE_OPERATOR_PACKAGE_URL_INVALID') + } + let packageName + try { + packageName = decodeURIComponent(path.posix.basename(url.pathname)) + } catch { + fail('HOLOLAKE_RELEASE_OPERATOR_PACKAGE_URL_INVALID') + } + if (!packageName || packageName === '.' || packageName === '..') fail('HOLOLAKE_RELEASE_OPERATOR_PACKAGE_NAME_INVALID') + return packageName +} + +function addArtifact(artifacts, source, name, type) { + const previous = artifacts.get(name) + if (previous && previous.source !== source) fail('HOLOLAKE_RELEASE_OPERATOR_ARTIFACT_NAME_COLLISION') + artifacts.set(name, { source, name, type }) +} + +export function inspectReleaseBundle(sourceDirectory, humanApprovalPath, { enforceRootOwner = false } = {}) { + const source = path.resolve(sourceDirectory) + requireTrustedDirectory(source, { enforceRootOwner }) + const latestPath = directFile(source, 'latest.json', 'HOLOLAKE_RELEASE_OPERATOR_BROADCAST_PATH_INVALID') + const pipelinePath = directFile(source, 'pipeline-receipt.json', 'HOLOLAKE_RELEASE_OPERATOR_PIPELINE_PATH_INVALID') + const approvalPath = path.resolve(humanApprovalPath) + const latestDocument = readJson(latestPath, { enforceRootOwner }) + const pipelineReceipt = readJson(pipelinePath, { enforceRootOwner }).value + const approvalReceipt = readJson(approvalPath, { enforceRootOwner }).value + const broadcast = latestDocument.value + const broadcastSha256 = sha256(latestDocument.bytes) + + if (broadcast?.schema !== 'hololake.release-broadcast/v1') fail('HOLOLAKE_RELEASE_OPERATOR_BROADCAST_INVALID') + const releaseId = requireText(broadcast.releaseId, 'HOLOLAKE_RELEASE_OPERATOR_RELEASE_ID_REQUIRED') + const version = requireText(broadcast.version, 'HOLOLAKE_RELEASE_OPERATOR_VERSION_REQUIRED') + if (!VERSION_PATTERN.test(version)) fail('HOLOLAKE_RELEASE_OPERATOR_VERSION_INVALID') + if ( + broadcast?.hololake?.restart?.required !== true || + broadcast?.hololake?.restart?.automaticAllowed !== false || + broadcast?.hololake?.rollback?.supported !== true || + broadcast?.hololake?.rollback?.healthReceiptRequired !== true || + !broadcast.platforms || + typeof broadcast.platforms !== 'object' || + Array.isArray(broadcast.platforms) || + Object.keys(broadcast.platforms).length === 0 + ) fail('HOLOLAKE_RELEASE_OPERATOR_BROADCAST_POLICY_INVALID') + + if ( + pipelineReceipt?.schema !== 'hololake.signed-release-pipeline-receipt/v1' || + pipelineReceipt?.state !== 'SIGNED_NOTARIZED_RELEASE_BROADCAST_READY_FOR_JD_CONTROLLER_UPLOAD' || + pipelineReceipt?.broadcastSha256 !== broadcastSha256 || + !SOURCE_COMMIT_PATTERN.test(pipelineReceipt?.sourceCommit || '') || + pipelineReceipt?.automaticUpload !== false || + pipelineReceipt?.automaticActivation !== false + ) fail('HOLOLAKE_RELEASE_OPERATOR_PIPELINE_RECEIPT_INVALID') + + const approvalId = requireText(approvalReceipt?.approvalId, 'HOLOLAKE_RELEASE_OPERATOR_APPROVAL_ID_REQUIRED') + if ( + approvalReceipt?.schema !== 'hololake.release-broadcast-human-approval/v1' || + approvalReceipt?.state !== 'HUMAN_APPROVED_EXACT_SIGNED_NOTARIZED_RELEASE' || + approvalReceipt?.releaseId !== releaseId || + approvalReceipt?.version !== version || + approvalReceipt?.broadcastSha256 !== broadcastSha256 + ) fail('HOLOLAKE_RELEASE_OPERATOR_HUMAN_APPROVAL_INVALID') + + const artifacts = new Map() + addArtifact(artifacts, latestPath, 'latest.json', 'control') + addArtifact(artifacts, pipelinePath, 'pipeline-receipt.json', 'control') + addArtifact(artifacts, approvalPath, 'human-approval.json', 'control') + for (const platform of Object.values(broadcast.platforms)) { + const packageName = packageNameFromUrl(platform?.url) + const packagePath = directFile(source, packageName, 'HOLOLAKE_RELEASE_OPERATOR_PACKAGE_PATH_INVALID') + const packageInfo = requireTrustedFile(packagePath, { enforceRootOwner }) + if (!Number.isSafeInteger(platform.size) || platform.size <= 0 || platform.size !== packageInfo.size) { + fail('HOLOLAKE_RELEASE_OPERATOR_PACKAGE_SIZE_MISMATCH') + } + if (!SHA256_PATTERN.test(platform.sha256 || '') || sha256(fs.readFileSync(packagePath)) !== platform.sha256) { + fail('HOLOLAKE_RELEASE_OPERATOR_PACKAGE_SHA256_MISMATCH') + } + requireText(platform.signature, 'HOLOLAKE_RELEASE_OPERATOR_PACKAGE_SIGNATURE_REQUIRED') + const codeReceiptId = requireText(platform.platformCodeSignatureReceipt, 'HOLOLAKE_RELEASE_OPERATOR_CODESIGN_RECEIPT_REQUIRED') + const notarizationReceiptId = requireText(platform.notarizationReceipt, 'HOLOLAKE_RELEASE_OPERATOR_NOTARIZATION_RECEIPT_REQUIRED') + const codeReceiptName = `${codeReceiptId}.json` + const notarizationReceiptName = `${notarizationReceiptId}.json` + const codeReceiptPath = directFile(source, codeReceiptName, 'HOLOLAKE_RELEASE_OPERATOR_CODESIGN_RECEIPT_PATH_INVALID') + const notarizationReceiptPath = directFile(source, notarizationReceiptName, 'HOLOLAKE_RELEASE_OPERATOR_NOTARIZATION_RECEIPT_PATH_INVALID') + const codeReceipt = readJson(codeReceiptPath, { enforceRootOwner }).value + const notarizationReceipt = readJson(notarizationReceiptPath, { enforceRootOwner }).value + if ( + codeReceipt?.schema !== 'hololake.platform-code-signature-receipt/v1' || + codeReceipt?.state !== 'DEVELOPER_ID_SIGNATURE_STRICT_AND_GATEKEEPER_ACCEPTED' || + codeReceipt?.sourceCommit !== pipelineReceipt.sourceCommit + ) fail('HOLOLAKE_RELEASE_OPERATOR_CODESIGN_RECEIPT_INVALID') + if ( + notarizationReceipt?.schema !== 'hololake.apple-notarization-receipt/v1' || + notarizationReceipt?.state !== 'APPLE_NOTARIZATION_ACCEPTED_AND_STAPLED' || + notarizationReceipt?.sourceCommit !== pipelineReceipt.sourceCommit + ) fail('HOLOLAKE_RELEASE_OPERATOR_NOTARIZATION_RECEIPT_INVALID') + addArtifact(artifacts, packagePath, packageName, 'package') + addArtifact(artifacts, codeReceiptPath, codeReceiptName, 'control') + addArtifact(artifacts, notarizationReceiptPath, notarizationReceiptName, 'control') + } + + return { + schema: 'hololake.release-broadcast-operator-plan/v1', + state: 'EXACT_SIGNED_NOTARIZED_HUMAN_APPROVED_BUNDLE_VERIFIED', + releaseId, + version, + sourceCommit: pipelineReceipt.sourceCommit, + broadcastSha256, + approvalId, + artifacts: [...artifacts.values()], + } +} + +function copyFileNoFollow(source, destination, mode) { + const sourceDescriptor = fs.openSync(source, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW) + let destinationDescriptor = null + try { + const sourceInfo = fs.fstatSync(sourceDescriptor) + if (!sourceInfo.isFile() || (sourceInfo.mode & 0o022) !== 0) fail('HOLOLAKE_RELEASE_OPERATOR_SOURCE_CHANGED') + destinationDescriptor = fs.openSync(destination, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, mode) + const buffer = Buffer.allocUnsafe(1024 * 1024) + let position = 0 + while (position < sourceInfo.size) { + const read = fs.readSync(sourceDescriptor, buffer, 0, Math.min(buffer.length, sourceInfo.size - position), position) + if (read <= 0) fail('HOLOLAKE_RELEASE_OPERATOR_SOURCE_TRUNCATED') + let written = 0 + while (written < read) written += fs.writeSync(destinationDescriptor, buffer, written, read - written) + position += read + } + fs.fsyncSync(destinationDescriptor) + } finally { + if (destinationDescriptor !== null) fs.closeSync(destinationDescriptor) + fs.closeSync(sourceDescriptor) + } +} + +function writeJsonExclusive(file, value, mode) { + const descriptor = fs.openSync(file, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, mode) + try { + fs.writeFileSync(descriptor, `${JSON.stringify(value, null, 2)}\n`) + fs.fsyncSync(descriptor) + } finally { + fs.closeSync(descriptor) + } +} + +function activationFor(plan) { + return { + schema: 'hololake.release-broadcast-activation/v1', + state: 'HUMAN_APPROVED_SIGNED_NOTARIZED_RELEASE_ACTIVE', + releaseId: plan.releaseId, + version: plan.version, + broadcastRelativePath: `releases/${plan.version}/latest.json`, + broadcastSha256: plan.broadcastSha256, + pipelineReceiptRelativePath: `releases/${plan.version}/pipeline-receipt.json`, + humanApprovalReceipt: plan.approvalId, + humanApprovalReceiptRelativePath: `releases/${plan.version}/human-approval.json`, + } +} + +function prepareCandidate(candidateRoot, plan, { enforceRootOwner, trustedGroupId }) { + const releases = path.join(candidateRoot, 'releases') + const releaseDirectory = path.join(releases, plan.version) + fs.mkdirSync(releaseDirectory, { recursive: true, mode: enforceRootOwner ? 0o750 : 0o700 }) + for (const artifact of plan.artifacts) { + const destination = path.join(releaseDirectory, artifact.name) + copyFileNoFollow(artifact.source, destination, enforceRootOwner ? 0o640 : 0o600) + if (enforceRootOwner) fs.chownSync(destination, 0, trustedGroupId) + } + const activationPath = path.join(candidateRoot, 'ACTIVE.json') + writeJsonExclusive(activationPath, activationFor(plan), enforceRootOwner ? 0o640 : 0o600) + if (enforceRootOwner) { + for (const directory of [candidateRoot, releases, releaseDirectory]) { + fs.chownSync(directory, 0, trustedGroupId) + fs.chmodSync(directory, 0o750) + } + fs.chownSync(activationPath, 0, trustedGroupId) + } + const state = loadRuntimeState(candidateRoot, { enforceRootOwner }) + if (state.state !== 'READY_SIGNED_NOTARIZED_BROADCAST') { + fail(`HOLOLAKE_RELEASE_OPERATOR_CANDIDATE_INVALID:${state.reasonCode || state.state}`) + } + return { releaseDirectory, activationPath } +} + +function requireExpected(plan, expected) { + for (const [field, code] of [ + ['releaseId', 'HOLOLAKE_RELEASE_OPERATOR_EXPECTED_RELEASE_ID_MISMATCH'], + ['version', 'HOLOLAKE_RELEASE_OPERATOR_EXPECTED_VERSION_MISMATCH'], + ['sourceCommit', 'HOLOLAKE_RELEASE_OPERATOR_EXPECTED_SOURCE_COMMIT_MISMATCH'], + ['broadcastSha256', 'HOLOLAKE_RELEASE_OPERATOR_EXPECTED_BROADCAST_SHA256_MISMATCH'], + ]) { + if (requireText(expected?.[field], `${code}_REQUIRED`) !== plan[field]) fail(code) + } +} + +function replaceActivation(stateRoot, candidateActivation, previousBytes, enforceRootOwner, trustedGroupId) { + const tempActivation = path.join(stateRoot, `.ACTIVE.${crypto.randomUUID()}.tmp`) + copyFileNoFollow(candidateActivation, tempActivation, enforceRootOwner ? 0o640 : 0o600) + if (enforceRootOwner) fs.chownSync(tempActivation, 0, trustedGroupId) + fs.renameSync(tempActivation, path.join(stateRoot, 'ACTIVE.json')) + const committed = loadRuntimeState(stateRoot, { enforceRootOwner }) + if (committed.state === 'READY_SIGNED_NOTARIZED_BROADCAST') return committed + const failedActive = path.join(stateRoot, 'ACTIVE.json') + if (previousBytes === null) { + fs.unlinkSync(failedActive) + } else { + const rollback = path.join(stateRoot, `.ACTIVE.rollback.${crypto.randomUUID()}.tmp`) + const descriptor = fs.openSync(rollback, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, enforceRootOwner ? 0o640 : 0o600) + try { + fs.writeFileSync(descriptor, previousBytes) + fs.fsyncSync(descriptor) + } finally { + fs.closeSync(descriptor) + } + if (enforceRootOwner) fs.chownSync(rollback, 0, trustedGroupId) + fs.renameSync(rollback, failedActive) + } + fail(`HOLOLAKE_RELEASE_OPERATOR_POST_COMMIT_INVALID:${committed.reasonCode || committed.state}`) +} + +function acquireOperatorLock(stateRoot, enforceRootOwner, trustedGroupId) { + const lockDirectory = path.join(stateRoot, '.operator-lock') + try { + fs.mkdirSync(lockDirectory, { mode: enforceRootOwner ? 0o750 : 0o700 }) + } catch (error) { + if (error?.code === 'EEXIST') fail('HOLOLAKE_RELEASE_OPERATOR_LOCKED') + throw error + } + try { + if (enforceRootOwner) fs.chownSync(lockDirectory, 0, trustedGroupId) + writeJsonExclusive(path.join(lockDirectory, 'owner.json'), { + schema: 'hololake.release-broadcast-operator-lock/v1', + pid: process.pid, + acquiredAt: new Date().toISOString(), + }, enforceRootOwner ? 0o640 : 0o600) + if (enforceRootOwner) fs.chownSync(path.join(lockDirectory, 'owner.json'), 0, trustedGroupId) + return lockDirectory + } catch (error) { + fs.rmSync(lockDirectory, { recursive: true, force: true }) + throw error + } +} + +export function verifyReleaseBundle(sourceDirectory, humanApprovalPath) { + const plan = inspectReleaseBundle(sourceDirectory, humanApprovalPath) + const candidateRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'hololake-release-operator-verify-')) + try { + prepareCandidate(candidateRoot, plan, { enforceRootOwner: false, trustedGroupId: process.getgid?.() ?? 0 }) + return { ...plan, artifacts: plan.artifacts.map(({ name, type }) => ({ name, type })) } + } finally { + fs.rmSync(candidateRoot, { recursive: true, force: true }) + } +} + +export function activateRelease({ sourceDirectory, humanApprovalPath, stateRoot, expected, enforceRootOwner = true }) { + if (enforceRootOwner && process.getuid?.() !== 0) fail('HOLOLAKE_RELEASE_OPERATOR_ROOT_REQUIRED') + const root = path.resolve(stateRoot) + const rootInfo = requireTrustedDirectory(root, { enforceRootOwner }) + const trustedGroupId = rootInfo.gid + const lockDirectory = acquireOperatorLock(root, enforceRootOwner, trustedGroupId) + let candidateRoot = null + try { + const plan = inspectReleaseBundle(sourceDirectory, humanApprovalPath, { enforceRootOwner }) + requireExpected(plan, expected) + candidateRoot = fs.mkdtempSync(path.join(root, '.candidate-')) + const candidate = prepareCandidate(candidateRoot, plan, { enforceRootOwner, trustedGroupId }) + const releasesRoot = path.join(root, 'releases') + if (!fs.existsSync(releasesRoot)) fs.mkdirSync(releasesRoot, { mode: enforceRootOwner ? 0o750 : 0o700 }) + requireTrustedDirectory(releasesRoot, { enforceRootOwner }) + if (enforceRootOwner) { + fs.chownSync(releasesRoot, 0, trustedGroupId) + fs.chmodSync(releasesRoot, 0o750) + } + const finalRelease = path.join(releasesRoot, plan.version) + if (fs.existsSync(finalRelease)) fail('HOLOLAKE_RELEASE_OPERATOR_VERSION_ALREADY_STAGED') + const activePath = path.join(root, 'ACTIVE.json') + const previousBytes = fs.existsSync(activePath) ? readJson(activePath, { enforceRootOwner }).bytes : null + fs.renameSync(candidate.releaseDirectory, finalRelease) + let committed + try { + committed = replaceActivation(root, candidate.activationPath, previousBytes, enforceRootOwner, trustedGroupId) + } catch (error) { + fs.rmSync(finalRelease, { recursive: true, force: true }) + throw error + } + return { + schema: 'hololake.release-broadcast-operator-receipt/v1', + state: 'ACTIVATED_EXPLICIT_SERVICE_RESTART_REQUIRED', + releaseId: plan.releaseId, + version: plan.version, + sourceCommit: plan.sourceCommit, + broadcastSha256: plan.broadcastSha256, + approvalId: plan.approvalId, + automaticUpload: false, + automaticActivation: false, + automaticRestart: false, + runtimeStateAfterDiskCommit: committed.state, + } + } finally { + if (candidateRoot) fs.rmSync(candidateRoot, { recursive: true, force: true }) + fs.rmSync(lockDirectory, { recursive: true, force: true }) + } +} + +function parseArguments(argv) { + const command = argv[0] + const values = {} + for (let index = 1; index < argv.length; index += 1) { + const key = argv[index] + if (!key.startsWith('--') || index + 1 >= argv.length) fail('HOLOLAKE_RELEASE_OPERATOR_ARGUMENT_INVALID') + values[key.slice(2)] = argv[++index] + } + return { command, values } +} + +export function main(argv = process.argv.slice(2)) { + const { command, values } = parseArguments(argv) + const sourceDirectory = requireText(values.source, 'HOLOLAKE_RELEASE_OPERATOR_SOURCE_REQUIRED') + const humanApprovalPath = requireText(values['human-approval'], 'HOLOLAKE_RELEASE_OPERATOR_HUMAN_APPROVAL_PATH_REQUIRED') + if (command === 'verify') { + process.stdout.write(`${JSON.stringify(verifyReleaseBundle(sourceDirectory, humanApprovalPath), null, 2)}\n`) + return + } + if (command !== 'activate') fail('HOLOLAKE_RELEASE_OPERATOR_COMMAND_INVALID:verify|activate') + const receipt = activateRelease({ + sourceDirectory, + humanApprovalPath, + stateRoot: requireText(values['state-root'], 'HOLOLAKE_RELEASE_OPERATOR_STATE_ROOT_REQUIRED'), + expected: { + releaseId: values['expect-release-id'], + version: values['expect-version'], + sourceCommit: values['expect-source-commit'], + broadcastSha256: values['expect-broadcast-sha256'], + }, + }) + process.stdout.write(`${JSON.stringify(receipt, null, 2)}\n`) +} + +if (isMainModule(process.argv[1], import.meta.url)) { + try { + main() + } catch (error) { + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`) + process.exitCode = 1 + } +}