blob: 3da1a4998723350f93ef934d8d82d4b1573ddfc2 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { randomUUID } from 'node:crypto';
import { homedir } from 'node:os';
import { join, resolve } from 'node:path';
import { isDeepStrictEqual } from 'node:util';
import { truncateUtf8 } from '@maka/core/diagnostic-log';
import { generalizedErrorMessage } from '@maka/core/redaction';
import {
activateRuntimeHostManagedDeployment,
connectRemoteRuntimeHost,
ensureRuntimeHostPeerIdentity,
RuntimeHostOperationError,
} from '@maka/runtime-host/client';
import {
RuntimeHostManagedDeploymentError as RuntimeHostDeploymentAuthorityError,
encodeRuntimeHostSetupFrame,
isSha512PackageIntegrity,
resolveRuntimeHostManagedDeployment,
resolveRuntimeHostManagedDeploymentAuthority,
runtimeHostManagedOperatorCommand,
RUNTIME_HOST_SETUP_ERROR_CODE_MAX_BYTES,
RUNTIME_HOST_SETUP_ERROR_MESSAGE_MAX_BYTES,
type RuntimeHostManagedDeploymentConfig,
type RuntimeHostNodeOperatorCommand,
type RuntimeHostSetupFrame,
type RuntimeHostSetupPhase,
} from '@maka/runtime-host/operator';
import {
INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID,
RUNTIME_HOST_PROTOCOL_VERSION,
} from '@maka/runtime-host/protocol';
import {
prepareRuntimeHostAccessCredential,
replaceRuntimeHostAccessCredential,
revokeRuntimeHostAccessCredential,
RuntimeHostAccessUnavailableError,
type RuntimeHostAccessPreset,
} from './runtime-host-access-command.js';
import {
convergeRuntimeHostManagedOperator,
verifyRuntimeHostManagedOperator,
isRuntimeHostDevelopmentPackageVersion,
openRuntimeHostManagedPackageDeployment,
prepareRuntimeHostManagedPackageDeployment,
pruneRuntimeHostManagedPackages,
removeRuntimeHostManagedDeployment,
resolveRuntimeHostManagedPackageCliPath,
resolveRuntimeHostManagedControlRoot,
resolveRuntimeHostManagedDeploymentRoot,
restoreRuntimeHostLegacyManagedOperator,
RuntimeHostManagedDeploymentError,
} from './runtime-host-managed-deployment.js';
import {
RuntimeHostUpdatePackageError,
withRuntimeHostRegistryUpdatePackage,
} from './runtime-host-update-package.js';
import {
readRuntimeHostManagedUpdatePolicy,
writeRuntimeHostManagedUpdatePolicy,
} from './runtime-host-update-policy-store.js';
import {
resolveRuntimeHostRegistryUpdateCandidate,
RuntimeHostUpdateDiscoveryError,
type RuntimeHostUpdateCandidate,
} from './runtime-host-update-discovery.js';
import { repairStorageRootAfterRemount, resolveStorageRoot } from '@maka/storage/root-authority';
import {
createPlatformRuntimeHostServiceBackend,
discoverRuntimeHostLifecycleProvider,
resolveRuntimeHostLifecycleProvider,
} from './runtime-host-service-management-command.js';
import {
allocateRuntimeHostLoopbackPort,
allocateRuntimeHostPeerPort,
effectiveRuntimeHostProjectDirectoryRoots,
manageRuntimeHostService,
readRuntimeHostManagedServiceConfig,
removeRuntimeHostServiceFile,
resolveRuntimeHostManagedServiceConfigPath,
resolveRuntimeHostManagedServiceId,
resolveRuntimeHostManagedProjectDirectoryRoots,
RuntimeHostServiceManagerError,
withRuntimeHostManagedServiceDeploymentLock,
withRuntimeHostManagedServiceLifecycleLock,
type RuntimeHostManagedServiceResult,
type RuntimeHostManagedServiceConfig,
type RuntimeHostManagedServiceTarget,
type RuntimeHostServiceBackend,
} from './runtime-host-service-manager.js';
import { expandWildcardListenAddresses } from './runtime-host-peer-management-command.js';
import {
canDiscardRuntimeHostLifecycleDesiredArtifacts,
replaceRuntimeHostLifecycle,
resolveRecoverableRuntimeHostManagedDeployment,
RUNTIME_HOST_READY_TIMEOUT_MS,
RuntimeHostLifecycleTransactionError,
type RuntimeHostLifecycleTransactionDeps,
} from './runtime-host-lifecycle-transaction.js';
import type {
RuntimeHostLifecycleProvider,
RuntimeHostLifecycleProviderOffer,
} from './runtime-host-lifecycle-provider.js';
import {
resolveRuntimeHostManagedPeerKeyPath,
resolveRuntimeHostNativePath,
} from './runtime-host-peer-artifact.js';
import { activateRuntimeHostManagedDeploymentWithReconciliation } from './runtime-host-activation-command.js';
const SETUP_LOCK_TIMEOUT_MS = 5 * 60_000;
const PAIRING_AVAILABILITY_POLL_MS = 100;
export interface RuntimeHostSetupCliOptions {
readonly json: boolean;
readonly clientDataRoot: string;
readonly defaultRootPath: string;
readonly sourcePackageRoot: string;
readonly version: string;
readonly sourcePackageIntegrity?: string;
readonly principalId: string;
readonly preset: RuntimeHostAccessPreset;
readonly lifecycle?: 'supervised' | 'on_demand';
readonly deferPairingCommit?: boolean;
readonly bindPairingToClient?: boolean;
readonly repairRootAfterRemount?: true;
readonly updateExisting?: boolean;
readonly reuseExistingEnvironment?: boolean;
readonly allowInterruptActiveTasks?: boolean;
readonly rootPath?: string;
readonly projectDirectoryRoots?: readonly {
readonly label: string;
readonly path: string;
}[];
readonly websocketPort?: number;
readonly websocketPath?: string;
readonly directPeer?: {
readonly coordinationRelays: readonly string[];
};
readonly expectedTarget?: RuntimeHostManagedServiceTarget;
}
interface RuntimeHostSetupDeps {
readonly manageService: typeof manageRuntimeHostService;
readonly createBackend: (serviceId: string, clientDataRoot: string) => RuntimeHostServiceBackend;
readonly discoverLifecycleProvider: (
rootId: string,
) => Promise<RuntimeHostLifecycleProviderOffer>;
readonly resolveLifecycleProvider: (
config: RuntimeHostManagedDeploymentConfig,
) => RuntimeHostLifecycleProvider;
readonly replaceLifecycle: typeof replaceRuntimeHostLifecycle;
readonly openDeployment: typeof openRuntimeHostManagedPackageDeployment;
readonly prepareDeployment: typeof prepareRuntimeHostManagedPackageDeployment;
readonly prunePackages: typeof pruneRuntimeHostManagedPackages;
readonly prepareCredential: typeof prepareRuntimeHostAccessCredential;
readonly replaceCredential: typeof replaceRuntimeHostAccessCredential;
readonly revokeCredential: typeof revokeRuntimeHostAccessCredential;
readonly verifyCredential: typeof verifyRuntimeHostSetupCredential;
readonly activateDesired: typeof activateRuntimeHostManagedDeployment;
readonly activateManaged: typeof activateRuntimeHostManagedDeployment;
readonly convergeOperator: typeof convergeRuntimeHostManagedOperator;
readonly verifyOperator: typeof verifyRuntimeHostManagedOperator;
readonly resolveRegistryCandidate: typeof resolveRuntimeHostRegistryUpdateCandidate;
readonly withRegistryPackage: typeof withRuntimeHostRegistryUpdatePackage;
readonly ensurePeerIdentity: typeof ensureRuntimeHostPeerIdentity;
readonly resolvePeerNativePath: typeof resolveRuntimeHostNativePath;
readonly allocateLoopbackPort: typeof allocateRuntimeHostLoopbackPort;
readonly allocatePeerPort: typeof allocateRuntimeHostPeerPort;
readonly writeOutput: (value: string) => unknown;
readonly writeError: (value: string) => unknown;
}
class RuntimeHostSetupError extends Error {
constructor(
readonly code: string,
message: string,
options?: ErrorOptions,
) {
super(message, options);
this.name = 'RuntimeHostSetupError';
}
}
interface ResolvedRuntimeHostSetupPackage {
readonly candidate: RuntimeHostUpdateCandidate;
use<T>(operation: (packageRoot: string) => Promise<T>): Promise<T>;
}
async function resolveRuntimeHostSetupPackage(
options: RuntimeHostSetupCliOptions,
deps: Pick<RuntimeHostSetupDeps, 'resolveRegistryCandidate' | 'withRegistryPackage'>,
): Promise<ResolvedRuntimeHostSetupPackage> {
if (isRuntimeHostDevelopmentPackageVersion(options.version)) {
const integrity = options.sourcePackageIntegrity;
if (typeof integrity !== 'string' || !isSha512PackageIntegrity(integrity)) {
throw new RuntimeHostSetupError(
'development_package_unverified',
'The Runtime Host development package is missing exact artifact evidence',
);
}
return {
candidate: {
kind: 'npm_registry',
version: options.version,
integrity,
},
use: (operation) => operation(options.sourcePackageRoot),
};
}
const candidate = await deps.resolveRegistryCandidate({
kind: 'exact',
version: options.version,
});
return {
candidate,
use: (operation) => deps.withRegistryPackage(candidate, operation),
};
}
export async function runRuntimeHostSetupCli(
options: RuntimeHostSetupCliOptions,
overrides: Partial<RuntimeHostSetupDeps> = {},
): Promise<number> {
const deps: RuntimeHostSetupDeps = {
manageService: manageRuntimeHostService,
createBackend: createPlatformRuntimeHostServiceBackend,
discoverLifecycleProvider: discoverRuntimeHostLifecycleProvider,
resolveLifecycleProvider: resolveRuntimeHostLifecycleProvider,
replaceLifecycle: replaceRuntimeHostLifecycle,
openDeployment: openRuntimeHostManagedPackageDeployment,
prepareDeployment: prepareRuntimeHostManagedPackageDeployment,
prunePackages: pruneRuntimeHostManagedPackages,
prepareCredential: prepareRuntimeHostAccessCredential,
replaceCredential: replaceRuntimeHostAccessCredential,
revokeCredential: revokeRuntimeHostAccessCredential,
verifyCredential: verifyRuntimeHostSetupCredential,
activateDesired: (input) =>
activateRuntimeHostManagedDeployment(input, {
reconcileActivation: async () => undefined,
}),
activateManaged: (input) =>
activateRuntimeHostManagedDeploymentWithReconciliation(input, {
deploymentLockHeld: true,
}),
convergeOperator: convergeRuntimeHostManagedOperator,
verifyOperator: verifyRuntimeHostManagedOperator,
resolveRegistryCandidate: resolveRuntimeHostRegistryUpdateCandidate,
withRegistryPackage: withRuntimeHostRegistryUpdatePackage,
ensurePeerIdentity: ensureRuntimeHostPeerIdentity,
resolvePeerNativePath: resolveRuntimeHostNativePath,
allocateLoopbackPort: allocateRuntimeHostLoopbackPort,
allocatePeerPort: allocateRuntimeHostPeerPort,
writeOutput: (value) => process.stdout.write(value),
writeError: (value) => process.stderr.write(value),
...overrides,
};
const emit = createEmitter(options.json, deps);
try {
const rootId = await resolveRuntimeHostSetupRootId(options);
const controlRoot = resolveRuntimeHostManagedControlRoot(rootId);
await withRuntimeHostManagedServiceDeploymentLock(
options.clientDataRoot,
() =>
withRuntimeHostManagedServiceLifecycleLock(
options.clientDataRoot,
() =>
withRuntimeHostManagedServiceDeploymentLock(
controlRoot,
() =>
withRuntimeHostManagedServiceLifecycleLock(
controlRoot,
() => runRuntimeHostSetupLocked(options, deps, emit, rootId),
SETUP_LOCK_TIMEOUT_MS,
),
SETUP_LOCK_TIMEOUT_MS,
),
SETUP_LOCK_TIMEOUT_MS,
),
SETUP_LOCK_TIMEOUT_MS,
);
return 0;
} catch (error) {
const failure = setupFailure(error);
emit({ kind: 'error', error: failure });
return 1;
}
}
async function resolveRuntimeHostSetupRootId(options: RuntimeHostSetupCliOptions): Promise<string> {
const legacyRootPath = (await readOptionalLegacyServiceConfig(options.clientDataRoot))?.rootPath;
const path = resolve(
options.rootPath ??
legacyRootPath ??
options.expectedTarget?.rootPath ??
options.defaultRootPath,
);
if (options.repairRootAfterRemount) {
await repairStorageRootAfterRemount({ path, kind: 'interactive' });
}
return (await resolveStorageRoot({ path, kind: 'interactive' })).rootId;
}
async function readOptionalLegacyServiceConfig(
clientDataRoot: string,
): Promise<RuntimeHostManagedServiceConfig | null> {
try {
return await readRuntimeHostManagedServiceConfig(
resolveRuntimeHostManagedServiceConfigPath(clientDataRoot),
);
} catch (error) {
if (error instanceof RuntimeHostServiceManagerError && error.code === 'not_installed') {
return null;
}
throw error;
}
}
async function runRuntimeHostSetupLocked(
options: RuntimeHostSetupCliOptions,
deps: RuntimeHostSetupDeps,
emit: SetupEmitter,
rootId: string,
): Promise<void> {
if (options.reuseExistingEnvironment) {
if (options.lifecycle !== 'on_demand' || options.updateExisting) {
throw new RuntimeHostSetupError(
'invalid_setup',
'Environment discovery cannot replace a deployment',
);
}
const existing = await resolveRuntimeHostManagedDeploymentAuthority(rootId);
if (existing) {
const { config, capability } = await resolveRuntimeHostManagedDeployment(rootId);
assertCanonicalSetupTarget(options.expectedTarget, rootId, capability.canonicalPath);
assertExpectedDeploymentGeneration(options.expectedTarget, config);
// The binding comes from canonical authority. The installed operator validates
// its own projection when connected; discovery must not rewrite an older launcher.
emit({
kind: 'existing_environment',
version: config.launch.package.version,
serviceId: rootId,
deploymentId: config.deploymentId,
rootId,
rootPath: capability.canonicalPath,
operator: runtimeHostManagedOperatorCommand(
config,
process.platform === 'win32' ? 'win32' : 'posix',
),
});
return;
}
}
if (options.lifecycle === 'on_demand') {
await runRuntimeHostOnDemandSetupLocked(options, deps, emit);
return;
}
const target = await runRuntimeHostSupervisedSetupLocked(options, deps, emit);
await pairAndVerifyRuntimeHostSetup(options, target, deps, emit);
}
async function runRuntimeHostSupervisedSetupLocked(
options: RuntimeHostSetupCliOptions,
deps: RuntimeHostSetupDeps,
emit: SetupEmitter,
): Promise<{
readonly serviceId: string;
readonly deploymentId: string;
readonly operator: RuntimeHostNodeOperatorCommand;
readonly rootPath: string;
readonly endpoint: string;
readonly directPeer?: {
readonly peerId: string;
readonly routeHints: readonly string[];
readonly coordinationRelays: readonly string[];
};
}> {
emit({ kind: 'progress', phase: 'checking_environment' });
const legacyServiceId = resolveRuntimeHostManagedServiceId(options.clientDataRoot);
const legacyConfig = await readOptionalLegacyServiceConfig(options.clientDataRoot);
const legacyBackend = legacyConfig
? deps.createBackend(legacyServiceId, options.clientDataRoot)
: undefined;
const legacyCommon = {
clientDataRoot: options.clientDataRoot,
defaultRootPath: options.defaultRootPath,
nodePath: process.execPath,
cliPath: join(options.sourcePackageRoot, 'dist', 'cli.js'),
...(options.expectedTarget
? { expectedTarget: legacyManagedTarget(options.expectedTarget, legacyServiceId) }
: {}),
} as const;
const legacyStatus = legacyBackend
? await deps.manageService({ ...legacyCommon, action: 'status' }, legacyBackend)
: undefined;
const capability = await resolveStorageRoot({
path: resolve(
options.rootPath ??
legacyConfig?.rootPath ??
options.expectedTarget?.rootPath ??
options.defaultRootPath,
),
kind: 'interactive',
});
assertCanonicalSetupTarget(options.expectedTarget, capability.rootId, capability.canonicalPath);
const lifecycleDeps: RuntimeHostLifecycleTransactionDeps = {
convergeOperator: (currentConfig, desiredConfig) =>
deps.convergeOperator(currentConfig, desiredConfig),
verifyOperator: deps.verifyOperator,
resolveProvider: deps.resolveLifecycleProvider,
...(legacyConfig && legacyBackend
? legacyMigrationDeps(legacyConfig, legacyBackend, legacyServiceId, options.clientDataRoot)
: {}),
};
const recovered = await resolveRecoverableRuntimeHostManagedDeployment(
capability.rootId,
lifecycleDeps,
{
...(legacyBackend ? { retirementSupervisor: legacyBackend } : {}),
...(legacyBackend
? {
activatePrevious: () =>
deps
.manageService({ ...legacyCommon, action: 'start' }, legacyBackend)
.then(() => undefined),
}
: {}),
...(options.expectedTarget ? { expectedTarget: options.expectedTarget } : {}),
},
);
const current = recovered.kind === 'active' ? recovered.config : undefined;
assertExpectedDeploymentGeneration(options.expectedTarget, current);
const legacyToMigrate = current ? null : legacyConfig;
if (current && legacyBackend) await assertLegacyArtifactsAbsent(legacyBackend);
if (legacyToMigrate && legacyStatus) {
await assertCompatibleExistingVersion(legacyStatus, options.version);
}
if (current && current.launch.package.version !== options.version && !options.updateExisting) {
throw new RuntimeHostSetupError(
'version_change_requires_update',
`Runtime Host ${current.launch.package.version} is already installed; changing to ${options.version} requires the update workflow`,
);
}
const resolvedPackage = await resolveRuntimeHostSetupPackage(options, deps);
const { candidate } = resolvedPackage;
const packageChanged = current !== undefined && !sameExactPackage(current, candidate);
if (current && packageChanged && !options.updateExisting) {
throw new RuntimeHostSetupError(
'version_change_requires_update',
`Runtime Host ${current.launch.package.version} is already installed; changing its exact package requires the update workflow`,
);
}
const lifecycleOffer: RuntimeHostLifecycleProviderOffer =
current?.lifecycle.mode === 'supervised'
? {
provider: deps.resolveLifecycleProvider(current),
availability: current.lifecycle.availability,
}
: await deps.discoverLifecycleProvider(capability.rootId);
return resolvedPackage.use(async (packageRoot) => {
emit({ kind: 'progress', phase: 'installing_package' });
const deployment = await deps.prepareDeployment({
serviceId: capability.rootId,
clientDataRoot: options.clientDataRoot,
sourcePackageRoot: packageRoot,
version: candidate.version,
packageIntegrity: candidate.integrity,
...(current ? { deploymentRoot: current.deploymentRoot } : {}),
});
let committed = false;
try {
const desired = await prepareSupervisedDeploymentConfig(
options,
deps,
capability,
deployment.cliPath,
deployment.root,
candidate,
current,
legacyToMigrate,
lifecycleOffer,
);
if (
current &&
!sameDesiredManagedDeployment(current, desired) &&
!options.updateExisting &&
current.lifecycle.mode === 'supervised'
) {
throw new RuntimeHostSetupError(
'configuration_changed',
'Change an existing supervised Runtime Host through its explicit configure or update workflow',
);
}
emit({ kind: 'progress', phase: 'installing_service' });
if (legacyToMigrate && legacyBackend) {
await legacyBackend.verifyDeployment(legacyToMigrate, {
acceptLegacyConfigLaunch: true,
});
}
const replacement = await deps.replaceLifecycle({
operation: legacyToMigrate
? 'legacy_migration'
: current
? packageChanged
? 'update'
: isDeepStrictEqual(current.lifecycle, desired.lifecycle)
? 'configure'
: 'lifecycle_change'
: 'install',
...(current ? { current } : {}),
desired,
...(legacyToMigrate && legacyBackend ? { retirementSupervisor: legacyBackend } : {}),
...(legacyToMigrate && legacyBackend
? {
activatePrevious: () =>
deps
.manageService({ ...legacyCommon, action: 'start' }, legacyBackend)
.then(() => undefined),
}
: {}),
allowInterruptActiveTasks: options.allowInterruptActiveTasks === true,
deps: lifecycleDeps,
});
if (replacement.kind === 'active_tasks') {
throw new RuntimeHostSetupError(
'active_tasks',
'Runtime Host setup is waiting for active work to finish',
);
}
committed = true;
if (legacyConfig) {
await removeRuntimeHostServiceFile(
resolveRuntimeHostManagedServiceConfigPath(options.clientDataRoot),
'legacy service config',
);
if (
legacyConfig.managedDeploymentRoot &&
resolve(legacyConfig.managedDeploymentRoot) !== resolve(deployment.root)
) {
await removeRuntimeHostManagedDeployment(
legacyConfig.managedDeploymentRoot,
legacyServiceId,
);
}
}
await deps.prunePackages(desired);
const websocket = desired.listeners.websocket;
if (!websocket) {
throw new RuntimeHostSetupError(
'service_not_ready',
'Supervised Runtime Host setup requires a WebSocket listener',
);
}
const directPeer = desired.listeners.directPeer?.enabled
? desired.listeners.directPeer
: undefined;
return {
serviceId: capability.rootId,
deploymentId: desired.deploymentId,
operator: runtimeHostManagedOperatorCommand(
desired,
process.platform === 'win32' ? 'win32' : 'posix',
),
rootPath: capability.canonicalPath,
endpoint: websocketUrl(websocket),
...(directPeer
? {
directPeer: {
peerId: directPeer.peerId,
routeHints: expandWildcardListenAddresses(directPeer.listenAddresses),
coordinationRelays: [...directPeer.coordinationRelays],
},
}
: {}),
};
} catch (error) {
if (!committed && canDiscardRuntimeHostLifecycleDesiredArtifacts(error)) {
if (current && packageChanged) {
await deployment.rollback().catch(() => undefined);
} else if (!current) {
await removeRuntimeHostManagedDeployment(deployment.root, capability.rootId).catch(
() => undefined,
);
}
}
throw error;
}
});
}
async function prepareSupervisedDeploymentConfig(
options: RuntimeHostSetupCliOptions,
deps: RuntimeHostSetupDeps,
capability: Awaited<ReturnType<typeof resolveStorageRoot>>,
cliPath: string,
deploymentRoot: string,
candidate: { readonly version: string; readonly integrity: string },
current: RuntimeHostManagedDeploymentConfig | undefined,
legacy: RuntimeHostManagedServiceConfig | null,
offer: RuntimeHostLifecycleProviderOffer,
): Promise<RuntimeHostManagedDeploymentConfig> {
const projectDirectoryRoots = await resolveRuntimeHostManagedProjectDirectoryRoots(
options.projectDirectoryRoots ??
current?.projectDirectoryRoots ??
(legacy
? effectiveRuntimeHostProjectDirectoryRoots(legacy)
: [{ label: '~', path: resolve(homedir()) }]),
);
const currentWebSocket = current?.listeners.websocket;
const websocketPort =
options.websocketPort ??
(currentWebSocket && currentWebSocket.port > 0
? currentWebSocket.port
: legacy?.websocket.port) ??
(await deps.allocateLoopbackPort());
const directPeer = await prepareSupervisedDirectPeer(
options,
deps,
cliPath,
deploymentRoot,
current?.listeners.directPeer,
);
const draft: RuntimeHostManagedDeploymentConfig = {
schemaVersion: 1,
state: 'active',
deploymentId: current?.deploymentId ?? randomUUID(),
configRevision: current ? current.configRevision + 1 : 1,
deploymentRoot,
root: { path: capability.canonicalPath, id: capability.rootId },
projectDirectoryRoots: [...projectDirectoryRoots],
launch: {
kind: 'exact_package',
nodePath: current?.launch.nodePath ?? process.execPath,
package: {
kind: 'npm_registry',
version: candidate.version,
integrity: candidate.integrity,
},
},
listeners: {
localIpc: true,
websocket: {
host: '127.0.0.1',
port: websocketPort,
path:
options.websocketPath ??
currentWebSocket?.path ??
legacy?.websocket.path ??
'/runtime-host',
},
...(directPeer ? { directPeer } : {}),
},
lifecycle: {
mode: 'supervised',
provider: offer.provider.supervisor.provider,
availability: offer.availability,
},
reconciliation: {
trigger: 'scheduled',
provider: offer.provider.reconciliationTrigger.provider,
},
};
return draft;
}
async function prepareSupervisedDirectPeer(
options: RuntimeHostSetupCliOptions,
deps: RuntimeHostSetupDeps,
cliPath: string,
deploymentRoot: string,
current: RuntimeHostManagedDeploymentConfig['listeners']['directPeer'],
): Promise<RuntimeHostManagedDeploymentConfig['listeners']['directPeer']> {
if (!options.directPeer && !current) return undefined;
const keyPath = current?.keyPath ?? resolveRuntimeHostManagedPeerKeyPath(deploymentRoot);
const peerId = await deps.ensurePeerIdentity({
nativePath: await deps.resolvePeerNativePath(cliPath),
keyPath,
});
const expectedPeerId = current?.peerId;
if (expectedPeerId && expectedPeerId !== peerId) {
throw new RuntimeHostSetupError(
'invalid_config',
'The Runtime Host peer identity does not match its persisted deployment',
);
}
return {
enabled: options.directPeer ? true : (current?.enabled ?? true),
keyPath,
peerId,
listenAddresses: [
...(current?.listenAddresses ?? [
`/ip4/0.0.0.0/udp/${String(await deps.allocatePeerPort())}/quic-v1`,
]),
],
coordinationRelays: [
...(options.directPeer?.coordinationRelays ?? current?.coordinationRelays ?? []),
],
automaticRelayDiscovery: current?.automaticRelayDiscovery ?? true,
webRtcStunPolicy: current?.webRtcStunPolicy ?? { kind: 'default' },
};
}
function sameDesiredManagedDeployment(
current: RuntimeHostManagedDeploymentConfig,
desired: RuntimeHostManagedDeploymentConfig,
): boolean {
const { configRevision: _currentRevision, ...currentState } = current;
const { configRevision: _desiredRevision, ...desiredState } = desired;
return isDeepStrictEqual(currentState, desiredState);
}
async function runRuntimeHostOnDemandSetupLocked(
options: RuntimeHostSetupCliOptions,
deps: RuntimeHostSetupDeps,
emit: SetupEmitter,
): Promise<void> {
if (options.directPeer) {
throw new RuntimeHostSetupError(
'unsupported_lifecycle_configuration',
'On-demand setup does not support a Direct peer listener',
);
}
emit({ kind: 'progress', phase: 'checking_environment' });
const legacyServiceId = resolveRuntimeHostManagedServiceId(options.clientDataRoot);
const legacyConfig = await readOptionalLegacyServiceConfig(options.clientDataRoot);
const legacyBackend = legacyConfig
? deps.createBackend(legacyServiceId, options.clientDataRoot)
: undefined;
let legacyStatus: RuntimeHostManagedServiceResult | undefined;
if (legacyBackend) {
legacyStatus = await deps.manageService(
{
action: 'status',
clientDataRoot: options.clientDataRoot,
defaultRootPath: options.defaultRootPath,
nodePath: process.execPath,
cliPath: join(options.sourcePackageRoot, 'dist', 'cli.js'),
...(options.expectedTarget
? { expectedTarget: legacyManagedTarget(options.expectedTarget, legacyServiceId) }
: {}),
},
legacyBackend,
);
}
const capability = await resolveStorageRoot({
path: resolve(
options.rootPath ??
legacyConfig?.rootPath ??
options.expectedTarget?.rootPath ??
options.defaultRootPath,
),
kind: 'interactive',
});
assertCanonicalSetupTarget(options.expectedTarget, capability.rootId, capability.canonicalPath);
const recoveryDeps: RuntimeHostLifecycleTransactionDeps = {
convergeOperator: (currentConfig, desiredConfig) =>
deps.convergeOperator(currentConfig, desiredConfig),
verifyOperator: deps.verifyOperator,
resolveProvider: deps.resolveLifecycleProvider,
...(legacyConfig && legacyBackend
? legacyMigrationDeps(legacyConfig, legacyBackend, legacyServiceId, options.clientDataRoot)
: {}),
};
const recovered = await resolveRecoverableRuntimeHostManagedDeployment(
capability.rootId,
recoveryDeps,
{
...(legacyBackend ? { retirementSupervisor: legacyBackend } : {}),
...(legacyBackend ? { activatePrevious: () => legacyBackend.start() } : {}),
...(options.expectedTarget ? { expectedTarget: options.expectedTarget } : {}),
},
);
const current = recovered.kind === 'active' ? recovered.config : undefined;
const legacyToMigrate = current ? null : legacyConfig;
if (current && legacyBackend) await assertLegacyArtifactsAbsent(legacyBackend);
if (legacyToMigrate && legacyStatus) {
await assertCompatibleExistingVersion(legacyStatus, options.version);
}
assertExpectedDeploymentGeneration(options.expectedTarget, current);
const resolvedPackage = await resolveRuntimeHostSetupPackage(options, deps);
const { candidate } = resolvedPackage;
const serviceId = capability.rootId;
const deploymentRoot =
current?.deploymentRoot ?? resolveRuntimeHostManagedDeploymentRoot(serviceId);
const packageChanged = current !== undefined && !sameExactPackage(current, candidate);
if (current && packageChanged && !options.updateExisting) {
throw new RuntimeHostSetupError(
'version_change_requires_update',
`Runtime Host ${current.launch.package.version} is already installed; changing its exact package requires the update workflow`,
);
}
const draft: RuntimeHostManagedDeploymentConfig = {
schemaVersion: 1,
state: 'active',
deploymentId: current?.deploymentId ?? randomUUID(),
configRevision: current ? current.configRevision + 1 : 1,
deploymentRoot,
root: { path: capability.canonicalPath, id: capability.rootId },
projectDirectoryRoots: options.projectDirectoryRoots?.map(({ label, path }) => ({
label,
path: resolve(path),
})) ??
current?.projectDirectoryRoots ?? [{ label: '~', path: resolve(homedir()) }],
launch: {
kind: 'exact_package',
nodePath: current?.launch.nodePath ?? process.execPath,
package: {
kind: 'npm_registry',
version: candidate.version,
integrity: candidate.integrity,
},
},
listeners: {
localIpc: true,
websocket: {
host: '127.0.0.1',
port: options.websocketPort ?? 0,
path: options.websocketPath ?? current?.listeners.websocket?.path ?? '/runtime-host',
},
...(current?.listeners.directPeer
? { directPeer: { ...current.listeners.directPeer, enabled: false } }
: {}),
},
lifecycle: { mode: 'on_demand', availability: 'activation' },
reconciliation: { trigger: 'activation' },
};
const reuseCurrent =
current?.lifecycle.mode === 'on_demand' && sameDesiredManagedDeployment(current, draft);
const config = reuseCurrent ? current : draft;
let activation: Awaited<ReturnType<typeof activateRuntimeHostManagedDeployment>> | undefined;
const lifecycleDeps: RuntimeHostLifecycleTransactionDeps = {
convergeOperator: (currentConfig, desiredConfig) =>
deps.convergeOperator(currentConfig, desiredConfig),
verifyOperator: deps.verifyOperator,
resolveProvider: deps.resolveLifecycleProvider,
...(legacyToMigrate && legacyBackend
? legacyMigrationDeps(legacyToMigrate, legacyBackend, legacyServiceId, options.clientDataRoot)
: {}),
};
const deployedConfig = await resolvedPackage.use(async (packageRoot) => {
let committed = false;
const created = !current;
let deployment: Awaited<ReturnType<typeof deps.prepareDeployment>> | undefined;
try {
emit({ kind: 'progress', phase: 'installing_package' });
deployment =
current && !packageChanged
? await deps.openDeployment({
serviceId,
clientDataRoot: options.clientDataRoot,
deploymentRoot,
cliPath: resolveRuntimeHostManagedPackageCliPath(
deploymentRoot,
candidate.version,
candidate.integrity,
),
version: candidate.version,
})
: await deps.prepareDeployment({
serviceId,
clientDataRoot: options.clientDataRoot,
sourcePackageRoot: packageRoot,
version: candidate.version,
packageIntegrity: candidate.integrity,
...(current ? { deploymentRoot } : {}),
});
const desiredConfig: RuntimeHostManagedDeploymentConfig = current
? config
: { ...config, deploymentRoot: deployment.root };
emit({ kind: 'progress', phase: 'installing_service' });
if (legacyToMigrate && legacyBackend) {
await legacyBackend.verifyDeployment(legacyToMigrate, {
acceptLegacyConfigLaunch: true,
});
}
if (reuseCurrent) {
await deps.convergeOperator(current, current);
await deps.verifyOperator(current);
} else {
const replacement = await deps.replaceLifecycle({
operation: legacyToMigrate
? 'legacy_migration'
: packageChanged
? 'update'
: current
? isDeepStrictEqual(current.lifecycle, config.lifecycle)
? 'configure'
: 'lifecycle_change'
: 'install',
...(current ? { current } : {}),
desired: desiredConfig,
...(legacyToMigrate && legacyBackend ? { retirementSupervisor: legacyBackend } : {}),
...(legacyToMigrate && legacyBackend
? { activatePrevious: () => legacyBackend.start() }
: {}),
activateDesired: async () => {
await deps.activateDesired({ rootId: capability.rootId });
},
allowInterruptActiveTasks: options.allowInterruptActiveTasks === true,
deps: lifecycleDeps,
});
if (replacement.kind === 'active_tasks') {
throw new RuntimeHostSetupError(
'active_tasks',
'Runtime Host setup is waiting for active work to finish',
);
}
}
committed = true;
await deps.prunePackages(
(await resolveRuntimeHostManagedDeployment(capability.rootId)).config,
);
return desiredConfig;
} catch (error) {
if (!committed && canDiscardRuntimeHostLifecycleDesiredArtifacts(error)) {
if (packageChanged && deployment) await deployment.rollback().catch(() => undefined);
else if (created) {
await removeRuntimeHostManagedDeployment(deploymentRoot, serviceId).catch(
() => undefined,
);
}
}
throw error;
}
});
activation = await deps.activateManaged({ rootId: capability.rootId });
if (legacyConfig) {
await removeRuntimeHostServiceFile(
resolveRuntimeHostManagedServiceConfigPath(options.clientDataRoot),
'legacy service config',
);
if (
legacyConfig.managedDeploymentRoot &&
resolve(legacyConfig.managedDeploymentRoot) !== resolve(config.deploymentRoot)
) {
await removeRuntimeHostManagedDeployment(legacyConfig.managedDeploymentRoot, legacyServiceId);
}
}
await pairAndVerifyRuntimeHostSetup(
options,
{
serviceId,
deploymentId: deployedConfig.deploymentId,
operator: runtimeHostManagedOperatorCommand(
deployedConfig,
process.platform === 'win32' ? 'win32' : 'posix',
),
rootPath: capability.canonicalPath,
endpoint: websocketUrl({
host: activation.endpoint.host,
port: activation.endpoint.port,
path: activation.endpoint.websocketPath,
}),
},
deps,
emit,
);
}
function legacyMigrationDeps(
config: RuntimeHostManagedServiceConfig,
backend: RuntimeHostServiceBackend,
legacyServiceId: string,
clientDataRoot: string,
): Pick<RuntimeHostLifecycleTransactionDeps, 'uninstallLegacy' | 'restoreLegacy'> {
return {
uninstallLegacy: async (transition) => {
await projectLegacyUpdatePolicy(config, legacyServiceId, transition.to ?? transition.from);
await backend.uninstall();
},
restoreLegacy: async (transition) => {
await removeProjectedLegacyUpdatePolicy(config, transition.to ?? transition.from);
if (config.managedDeploymentRoot) {
await restoreRuntimeHostLegacyManagedOperator({
deploymentRoot: config.managedDeploymentRoot,
nodePath: config.launch.nodePath,
cliPath: config.launch.cliPath,
clientDataRoot,
serviceId: legacyServiceId,
});
}
const restoration = await backend.stageDeployment();
await restoration.apply(config, false);
},
};
}
async function projectLegacyUpdatePolicy(
config: RuntimeHostManagedServiceConfig,
legacyServiceId: string,
desired: RuntimeHostManagedDeploymentConfig | null,
): Promise<void> {
if (!config.managedDeploymentRoot || !desired) return;
const record = await readRuntimeHostManagedUpdatePolicy(config.managedDeploymentRoot);
if (record) {
if (
record.target.serviceId !== legacyServiceId ||
record.target.rootId !== desired.root.id ||
resolve(record.target.rootPath) !== resolve(desired.root.path)
) {
throw new RuntimeHostSetupError(
'target_mismatch',
'The legacy automatic update policy does not match the Runtime Host migration target',
);
}
await writeRuntimeHostManagedUpdatePolicy(desired.deploymentRoot, {
...record,
target: {
serviceId: desired.root.id,
rootId: desired.root.id,
rootPath: desired.root.path,
deploymentId: desired.deploymentId,
},
});
} else {
await writeRuntimeHostManagedUpdatePolicy(desired.deploymentRoot, null);
}
}
async function removeProjectedLegacyUpdatePolicy(
config: RuntimeHostManagedServiceConfig,
desired: RuntimeHostManagedDeploymentConfig | null,
): Promise<void> {
if (
config.managedDeploymentRoot &&
desired &&
resolve(config.managedDeploymentRoot) !== resolve(desired.deploymentRoot)
) {
await writeRuntimeHostManagedUpdatePolicy(desired.deploymentRoot, null);
}
}
function legacyManagedTarget(
target: RuntimeHostManagedServiceTarget,
legacyServiceId: string,
): RuntimeHostManagedServiceTarget {
return {
serviceId: legacyServiceId,
rootPath: target.rootPath,
rootId: target.rootId,
};
}
function assertCanonicalSetupTarget(
target: RuntimeHostManagedServiceTarget | undefined,
rootId: string,
rootPath: string,
): void {
if (
target &&
(target.serviceId !== rootId || target.rootId !== rootId || target.rootPath !== rootPath)
) {
throw new RuntimeHostSetupError(
'target_mismatch',
'The managed Runtime Host does not match the expected deployment identity',
);
}
}
function assertExpectedDeploymentGeneration(
target: RuntimeHostManagedServiceTarget | undefined,
current: RuntimeHostManagedDeploymentConfig | undefined,
): void {
if (
target?.deploymentId !== undefined &&
(!current || target.deploymentId !== current.deploymentId)
) {
throw new RuntimeHostSetupError(
'target_mismatch',
'The managed Runtime Host deployment generation changed before setup',
);
}
}
async function assertLegacyArtifactsAbsent(backend: RuntimeHostServiceBackend): Promise<void> {
const status = await backend.status();
if (status.installed || status.enabled || status.active) {
throw new RuntimeHostSetupError(
'lifecycle_owner_exists',
'The canonical deployment is active but its legacy lifecycle artifact can still start',
);
}
}
function sameExactPackage(
config: RuntimeHostManagedDeploymentConfig,
candidate: { readonly version: string; readonly integrity: string },
): boolean {
return (
config.launch.package.version === candidate.version &&
config.launch.package.integrity === candidate.integrity
);
}
async function pairAndVerifyRuntimeHostSetup(
options: RuntimeHostSetupCliOptions,
target: {
readonly serviceId: string;
readonly deploymentId: string;
readonly operator: RuntimeHostNodeOperatorCommand;
readonly rootPath: string;
readonly endpoint: string;
readonly directPeer?: {
readonly peerId: string;
readonly routeHints: readonly string[];
readonly coordinationRelays: readonly string[];
};
},
deps: RuntimeHostSetupDeps,
emit: SetupEmitter,
): Promise<void> {
emit({ kind: 'progress', phase: 'pairing_client' });
let paired: Awaited<ReturnType<typeof prepareRuntimeHostAccessCredential>>;
try {
const pairCredential = options.deferPairingCommit
? deps.prepareCredential
: deps.replaceCredential;
const credentialInput = {
rootPath: target.rootPath,
principalKind: 'remote_owner' as const,
principalId: options.principalId,
operationGrants: [],
canPublishClientCapabilities: false,
canUseHostPaths: false,
preset: options.preset,
...(options.bindPairingToClient ? { bindClientInstance: true } : {}),
};
const deadline = Date.now() + RUNTIME_HOST_READY_TIMEOUT_MS;
while (true) {
try {
paired = await pairCredential(credentialInput);
break;
} catch (error) {
if (!isTransientPairingAvailabilityError(error) || Date.now() >= deadline) {
throw error;
}
await new Promise<void>((resolveWait) =>
setTimeout(resolveWait, Math.min(PAIRING_AVAILABILITY_POLL_MS, deadline - Date.now())),
);
}
}
} catch (error) {
const reason =
error instanceof RuntimeHostAccessUnavailableError
? error.message
: generalizedErrorMessage(error, 'Runtime Host access service is unavailable');
throw new RuntimeHostSetupError(
'pairing_failed',
`Runtime Host could not pair the requested Client identity: ${reason}`,
{ cause: error },
);
}
emit({ kind: 'progress', phase: 'verifying_connection' });
try {
await deps.verifyCredential({
endpoint: target.endpoint,
rootId: paired.rootId,
credential: paired.credential,
});
emit({
kind: 'complete',
version: options.version,
serviceId: target.serviceId,
deploymentId: target.deploymentId,
operator: target.operator,
rootPath: target.rootPath,
rootId: paired.rootId,
endpoint: target.endpoint,
credentialId: paired.credentialId,
credential: paired.credential,
...(target.directPeer
? {
directPeer: {
peerId: target.directPeer.peerId,
routeHints: [...target.directPeer.routeHints],
coordinationRelays: [...target.directPeer.coordinationRelays],
},
}
: {}),
});
} catch (error) {
if (options.deferPairingCommit) {
try {
await deps.revokeCredential({
rootPath: target.rootPath,
credentialId: paired.credentialId,
});
} catch (rollbackError) {
throw new AggregateError(
[error, rollbackError],
'Runtime Host pairing failed and its candidate credential could not be revoked',
);
}
}
throw error;
}
}
function isTransientPairingAvailabilityError(error: unknown): boolean {
return (
error instanceof RuntimeHostAccessUnavailableError ||
(error instanceof RuntimeHostOperationError &&
(error.code === 'host_not_ready' || error.code === 'host_draining'))
);
}
async function assertCompatibleExistingVersion(
status: RuntimeHostManagedServiceResult,
version: string,
): Promise<void> {
if (!status.service.config) {
if (!status.service.installed) return;
throw new RuntimeHostSetupError(
'existing_installation_unknown',
'The installed Runtime Host configuration is unavailable; repair it before setup',
);
}
const existingVersion = status.service.installedVersion;
if (!existingVersion) {
throw new RuntimeHostSetupError(
'existing_installation_unknown',
'The installed Runtime Host version could not be identified; repair it before setup',
);
}
if (
existingVersion !== version &&
!(
isRuntimeHostDevelopmentPackageVersion(existingVersion) &&
isRuntimeHostDevelopmentPackageVersion(version)
)
) {
throw new RuntimeHostSetupError(
'version_change_requires_update',
`Runtime Host ${String(existingVersion)} is already installed; changing to ${version} requires the update workflow`,
);
}
}
async function verifyRuntimeHostSetupCredential(input: {
readonly endpoint: string;
readonly rootId: string;
readonly credential: string;
}): Promise<void> {
const result = await connectRemoteRuntimeHost({
url: input.endpoint,
credential: input.credential,
expectedRootId: input.rootId,
compositionId: INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID,
protocol: {
min: RUNTIME_HOST_PROTOCOL_VERSION,
max: RUNTIME_HOST_PROTOCOL_VERSION,
},
});
if (result.kind !== 'connected') {
throw new RuntimeHostSetupError(
'verification_failed',
`The paired Runtime Host connection could not be verified (${result.kind})`,
);
}
try {
const status = await result.connection.status();
if (status.state !== 'ready') {
throw new RuntimeHostSetupError(
'verification_failed',
`The paired Runtime Host is ${status.state}`,
);
}
} finally {
await result.connection.close();
}
}
type SetupEmitter = (
frame:
| Omit<Extract<RuntimeHostSetupFrame, { kind: 'progress' }>, 'schemaVersion' | 'sequence'>
| Omit<Extract<RuntimeHostSetupFrame, { kind: 'complete' }>, 'schemaVersion' | 'sequence'>
| Omit<
Extract<RuntimeHostSetupFrame, { kind: 'existing_environment' }>,
'schemaVersion' | 'sequence'
>
| Omit<Extract<RuntimeHostSetupFrame, { kind: 'error' }>, 'schemaVersion' | 'sequence'>,
) => void;
function createEmitter(json: boolean, deps: RuntimeHostSetupDeps): SetupEmitter {
let sequence = 0;
return (input) => {
const frame = {
schemaVersion: 1,
sequence: sequence++,
...input,
} as RuntimeHostSetupFrame;
if (json) {
deps.writeOutput(encodeRuntimeHostSetupFrame(frame));
return;
}
if (frame.kind === 'progress') {
deps.writeOutput(`${humanPhase(frame.phase)}\n`);
} else if (frame.kind === 'complete' || frame.kind === 'existing_environment') {
deps.writeOutput(`${JSON.stringify(frame, null, 2)}\n`);
} else {
deps.writeError(`${frame.error.message}\n`);
}
};
}
function setupFailure(error: unknown): { code: string; message: string } {
let code = 'internal_setup_failure';
let message = 'Runtime Host setup failed';
if (
error instanceof RuntimeHostSetupError ||
error instanceof RuntimeHostServiceManagerError ||
error instanceof RuntimeHostManagedDeploymentError ||
error instanceof RuntimeHostDeploymentAuthorityError ||
error instanceof RuntimeHostUpdateDiscoveryError ||
error instanceof RuntimeHostUpdatePackageError ||
error instanceof RuntimeHostLifecycleTransactionError
) {
code = error.code;
message = error.message;
}
return {
code: truncateUtf8(code, RUNTIME_HOST_SETUP_ERROR_CODE_MAX_BYTES) || 'internal_setup_failure',
message:
truncateUtf8(message, RUNTIME_HOST_SETUP_ERROR_MESSAGE_MAX_BYTES) ||
'Runtime Host setup failed',
};
}
function websocketUrl(input: {
readonly host: string;
readonly port: number;
readonly path: string;
}) {
return `ws://${input.host}:${input.port}${input.path}`;
}
function humanPhase(phase: RuntimeHostSetupPhase): string {
switch (phase) {
case 'checking_environment':
return 'Checking the remote environment...';
case 'installing_package':
return 'Installing the managed Maka package...';
case 'installing_service':
return 'Installing the Runtime Host deployment...';
case 'pairing_client':
return 'Pairing the Client...';
case 'verifying_connection':
return 'Verifying the Runtime Host connection...';
}
}