@runuai/host 0.9.13 → 0.9.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +22 -5
  2. package/db/migrations/0014_host_inventory_event_index.sql +1 -0
  3. package/db/migrations/0015_host_settings.sql +9 -0
  4. package/db/migrations/0016_task_environment.sql +2 -0
  5. package/db/migrations/meta/_journal.json +21 -0
  6. package/db/schema.ts +80 -30
  7. package/images/standard/Dockerfile +36 -10
  8. package/images/standard/README.md +63 -18
  9. package/images/standard/container/corepack-version +1 -0
  10. package/images/standard/container/uai-init +308 -38
  11. package/images/standard/container/uai-materialize-runtimes +1527 -0
  12. package/lib/agent-cli.ts +69 -4
  13. package/lib/agent.ts +46 -7
  14. package/lib/agents/claude.ts +13 -8
  15. package/lib/agents/codex.ts +11 -6
  16. package/lib/agents/cursor.ts +39 -29
  17. package/lib/agents/durable-proc.ts +20 -27
  18. package/lib/agents/factory.ts +9 -25
  19. package/lib/agents/grok.ts +43 -30
  20. package/lib/agents/kimi.ts +44 -29
  21. package/lib/agents/opencode.ts +43 -31
  22. package/lib/agents/proc.ts +149 -114
  23. package/lib/agents/transport.ts +62 -50
  24. package/lib/agents/types.ts +6 -4
  25. package/lib/apple-runtime-recycle.ts +236 -0
  26. package/lib/apple-uninstall-teardown.ts +224 -0
  27. package/lib/browser-testing.ts +233 -93
  28. package/lib/codex-auth.ts +40 -6
  29. package/lib/command-db.ts +20 -0
  30. package/lib/container-runtime.ts +1338 -0
  31. package/lib/db.ts +1 -0
  32. package/lib/docker-exec.ts +87 -5
  33. package/lib/engine-accounts.ts +68 -5
  34. package/lib/engine-login.ts +1952 -0
  35. package/lib/enrollment-state.ts +251 -0
  36. package/lib/env-file.ts +155 -0
  37. package/lib/env.ts +4 -0
  38. package/lib/git-diff.ts +98 -32
  39. package/lib/git-identity.ts +199 -87
  40. package/lib/github-tokens.ts +202 -91
  41. package/lib/host-cloud-url.ts +62 -0
  42. package/lib/host-config.ts +279 -0
  43. package/lib/host-logs.ts +962 -0
  44. package/lib/keyed-promise-tail.ts +23 -0
  45. package/lib/legacy-runtime-v1.fixture.ts +627 -0
  46. package/lib/managed-activation-watcher.ts +72 -0
  47. package/lib/managed-install-owner-watcher.ts +55 -0
  48. package/lib/managed-operation-drain.ts +49 -0
  49. package/lib/managed-runtime.ts +3644 -0
  50. package/lib/managed-update-scheduler.ts +125 -0
  51. package/lib/mcp-gateway.ts +450 -23
  52. package/lib/orchestrator.ts +3070 -223
  53. package/lib/preview-sidecar.ts +57 -13
  54. package/lib/release-manifest.ts +708 -0
  55. package/lib/release-trust.ts +28 -0
  56. package/lib/runtime-activation-tail.ts +232 -0
  57. package/lib/runtime-archive.ts +1086 -0
  58. package/lib/runtime-authority.ts +79 -0
  59. package/lib/runtime-guard.ts +36 -0
  60. package/lib/runtime-provider-state.ts +169 -0
  61. package/lib/runtime-state.ts +232 -12
  62. package/lib/skills.ts +24 -3
  63. package/lib/ssh.ts +18 -0
  64. package/lib/standard-image.ts +1104 -141
  65. package/lib/stopped-task-status-queue.ts +44 -0
  66. package/lib/task-container-cli.ts +269 -0
  67. package/lib/task-diff.ts +66 -46
  68. package/lib/task-environment/apple-container.ts +757 -0
  69. package/lib/task-environment/docker.ts +945 -0
  70. package/lib/task-environment/index.ts +364 -0
  71. package/lib/task-environment/legacy-adoption.ts +443 -0
  72. package/lib/task-environment/registry.ts +58 -0
  73. package/lib/task-environment/types.ts +408 -0
  74. package/lib/task-identity.ts +19 -0
  75. package/lib/task-inventory.ts +585 -0
  76. package/lib/tunnel-registry.ts +135 -19
  77. package/lib/tunnel-runtime.ts +235 -0
  78. package/package.json +1 -1
  79. package/scripts/agent/_common.sh +123 -3
  80. package/scripts/agent/task-down.sh +146 -38
  81. package/scripts/agent/task-status.sh +19 -3
  82. package/scripts/agent/task-up.sh +1463 -109
  83. package/scripts/install/darwin.ts +848 -50
  84. package/scripts/install/linux.ts +838 -35
  85. package/scripts/install/types.ts +43 -0
  86. package/scripts/install/util.ts +215 -8
  87. package/scripts/install/win.ts +12 -0
  88. package/src/apple-tunnel-route.ts +104 -0
  89. package/src/cli.ts +1464 -72
  90. package/src/event-outbox.ts +83 -4
  91. package/src/index.ts +871 -50
  92. package/src/main.ts +1398 -255
  93. package/src/paths.ts +17 -1
  94. package/src/protocol.ts +695 -1
  95. package/src/runtime-bootstrap.ts +165 -0
  96. package/src/ui/server.ts +46 -10
  97. package/src/ui/types.ts +37 -0
@@ -0,0 +1,3644 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { constants as fsConstants, lstatSync } from "node:fs";
3
+ import {
4
+ access,
5
+ chmod,
6
+ lstat,
7
+ mkdir,
8
+ mkdtemp,
9
+ open,
10
+ readFile,
11
+ readlink,
12
+ readdir,
13
+ realpath,
14
+ rename,
15
+ rm,
16
+ symlink,
17
+ unlink,
18
+ } from "node:fs/promises";
19
+ import { homedir, release as osRelease } from "node:os";
20
+ import path from "node:path";
21
+ import { spawnSync } from "node:child_process";
22
+
23
+ import {
24
+ DEFAULT_RELEASE_DOWNLOAD_BASE,
25
+ MAX_RELEASE_ARTIFACT_BYTES,
26
+ MAX_RELEASE_MANIFEST_BYTES,
27
+ isReleasePlatform,
28
+ releaseArtifactFilename,
29
+ releaseArtifactForPlatform,
30
+ type PinnedReleaseManifestKey,
31
+ type ReleaseArtifact,
32
+ type ReleasePlatform,
33
+ verifyReleaseManifest,
34
+ } from "./release-manifest";
35
+ import {
36
+ BUNDLED_RUNTIME_CAPABILITY,
37
+ BUNDLED_RUNTIME_ROOT,
38
+ extractVerifiedRuntimeArchive,
39
+ readVerifiedArtifactFile,
40
+ type ResolvedRuntimeDescriptor,
41
+ type RuntimeCapabilityDescriptorV3,
42
+ type RuntimeAssetManifest,
43
+ validateRuntimeAssetManifest,
44
+ validateRuntimeCapabilityDescriptor,
45
+ validateRuntimeDescriptor,
46
+ } from "./runtime-archive";
47
+ import {
48
+ PINNED_RELEASE_MANIFEST_KEYS,
49
+ STABLE_RELEASE_MANIFEST_URL,
50
+ } from "./release-trust";
51
+
52
+ const VERSION = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/;
53
+ const INSTALL_INTENT_NONCE = /^[a-f0-9]{64}$/;
54
+ const OPERATION_LOCK_HELD = Symbol("managed-runtime-operation-lock-held");
55
+ const OPERATION_LOCK_WAIT_MS = 30_000;
56
+ const OPERATION_LOCK_POLL_MS = 100;
57
+ export const MANAGED_MANIFEST_TIMEOUT_MS = 10_000;
58
+ export const MANAGED_ARTIFACT_TIMEOUT_MS = 10 * 60_000;
59
+ const MAX_OPERATION_LOCK_BYTES = 4 * 1024;
60
+ const MAX_PACKAGE_JSON_BYTES = 64 * 1024;
61
+ const INCOMPLETE_LOCK_STALE_MS = 5 * 60_000;
62
+ export const MANAGED_UPDATE_RESTART_EXIT_CODE = 75;
63
+ const REQUIRED_BUNDLED_RUNTIME_LICENSES = Object.freeze([
64
+ "Container-LICENSE",
65
+ "Kernel-GPL-2.0",
66
+ ]);
67
+ /** Plugin directory == plugin binary name in Apple's signed pkg payload. */
68
+ const APPLE_CONTAINER_PLUGINS = Object.freeze([
69
+ "container-core-images",
70
+ "container-network-vmnet",
71
+ "container-runtime-linux",
72
+ "k8s",
73
+ "machine-apiserver",
74
+ ]);
75
+
76
+ export interface ManagedRuntimePaths {
77
+ readonly home: string;
78
+ readonly root: string;
79
+ readonly runtime: string;
80
+ readonly current: string;
81
+ readonly previous: string;
82
+ readonly shim: string;
83
+ readonly recoveryEntrypoint: string;
84
+ readonly recoveryShim: string;
85
+ readonly data: string;
86
+ readonly rollbackJournal: string;
87
+ readonly rollbackUpdateHold: string;
88
+ /** Monotonic compatibility floor claimed before the bundled runtime may own
89
+ * task state. It lives outside version generations so pruning/rollback
90
+ * cannot erase the evidence an older runtime must honor. */
91
+ readonly minimumRuntimeCapability: string;
92
+ /** Older Phase-B builds may have claimed provider state before publishing
93
+ * the durable capability marker. This preserved marker implies floor 2. */
94
+ readonly runtimeProviderState: string;
95
+ readonly operationLock: string;
96
+ readonly activationPending: string;
97
+ /**
98
+ * Durable task-admission gate for destructive maintenance. This deliberately
99
+ * lives beside `root`, not inside it: uninstall detaches/removes the whole
100
+ * managed tree before it may reopen admission.
101
+ */
102
+ readonly maintenancePending: string;
103
+ /** Deterministic holding path for a runtime whose service is already down. */
104
+ readonly uninstallPendingRoot: string;
105
+ /** End-to-end fence held by the public installer across enrollment/attach. */
106
+ readonly installIntent: string;
107
+ /** Durable ownership record for a validated legacy npm command collision. */
108
+ readonly legacyShimTakeover: string;
109
+ /** Same-directory parking preserves a relative npm symlink's resolution. */
110
+ readonly legacyShimBackup: string;
111
+ }
112
+
113
+ export type ManagedRuntimeRemovalStep =
114
+ | "service-teardown-complete"
115
+ | "runtime-detached"
116
+ | "commands-removed"
117
+ | "runtime-removed"
118
+ | "data-removed"
119
+ | "cleanup-complete";
120
+
121
+ export interface RemoveManagedRuntimeOptions {
122
+ readonly home?: string;
123
+ readonly purgeData?: boolean;
124
+ /** Rechecked only after the durable admission marker has been written. */
125
+ readonly canRemove: () => boolean | Promise<boolean>;
126
+ /** Service teardown runs under the same cross-process runtime lock. */
127
+ readonly beforeRemove?: () => Promise<void>;
128
+ /** Runtime-provider teardown runs only after the service is down and before
129
+ * the signed generation or host data can be detached. */
130
+ readonly afterServiceTeardown?: () => Promise<void>;
131
+ /** Durable-boundary observer; an error leaves cleanup safely resumable. */
132
+ readonly onRemovalStep?: (
133
+ step: ManagedRuntimeRemovalStep,
134
+ ) => void | Promise<void>;
135
+ }
136
+
137
+ export interface BeginManagedRuntimeInstallOptions {
138
+ readonly home?: string;
139
+ readonly ownerPid: number;
140
+ /** Prove definition ownership and snapshot manager state before quiescence. */
141
+ readonly definitionState: (
142
+ managedCommand: string,
143
+ ) => Promise<ManagedServiceDefinitionState>;
144
+ /** Settle a crashed standalone definition update before classifying it. */
145
+ readonly reconcileStandaloneDefinition?: () => Promise<void>;
146
+ /** Stop the replacement and restore the prior definition without starting it. */
147
+ readonly prepareRecovery?: (installIntent: string) => Promise<void>;
148
+ /** Resume the prior service only after runtime links and commands are restored. */
149
+ readonly resumeRecovery?: (installIntent: string) => Promise<void>;
150
+ /** Restart a legacy service durably stopped before any definition journal. */
151
+ readonly resumeStoppedService?: (
152
+ installIntent: string,
153
+ priorManager: ManagedServiceManagerState,
154
+ ) => Promise<void>;
155
+ /** Finish an attested prior install; that commit decision cannot roll back. */
156
+ readonly beforeFinalize?: (installIntent?: string) => Promise<void>;
157
+ }
158
+
159
+ export interface ManagedRuntimeInstallIntent {
160
+ readonly nonce: string;
161
+ readonly legacyShim: boolean;
162
+ readonly serviceOwnership: "missing" | "managed" | "legacy";
163
+ readonly serviceQuiescence: "quiesce" | "idle";
164
+ readonly serviceManager: ManagedServiceManagerState;
165
+ }
166
+
167
+ /** Durable, platform-neutral service-manager state captured before conversion. */
168
+ export interface ManagedServiceManagerState {
169
+ readonly loaded: boolean;
170
+ readonly active: boolean;
171
+ readonly enabled: boolean;
172
+ }
173
+
174
+ export interface ManagedServiceDefinitionState {
175
+ readonly ownership: "missing" | "managed" | "legacy";
176
+ readonly manager: ManagedServiceManagerState;
177
+ }
178
+
179
+ export interface ManagedRuntimeInstallIdentity {
180
+ readonly nonce: string;
181
+ readonly ownerPid: number;
182
+ }
183
+
184
+ export interface ManagedRuntimeOwnerIdentity {
185
+ readonly uid: number;
186
+ readonly startedAt: string;
187
+ }
188
+
189
+ export type ManagedRuntimeInstallStartupState =
190
+ | "none"
191
+ | "live"
192
+ | "stale"
193
+ | "attested"
194
+ | "unknown";
195
+
196
+ export interface CompatibilityProbe {
197
+ readonly platform?: NodeJS.Platform;
198
+ readonly arch?: string;
199
+ readonly kernelVersion?: string;
200
+ readonly macosVersion?: string;
201
+ readonly glibcVersion?: string | null;
202
+ readonly uid?: number;
203
+ }
204
+
205
+ /**
206
+ * Why the activation gate is being consulted, so the caller can hold two
207
+ * policies. Legacy conversion takes over ANOTHER service's live workload —
208
+ * its tasks do not survive the takeover, so it needs every task idle. A
209
+ * managed version update only restarts our own service, which running tasks
210
+ * survive by design (durable sessions); it needs only in-flight lifecycle
211
+ * mutations drained. Live 2026-08-17: one strict policy for both made every
212
+ * update fail while any task merely EXISTED.
213
+ */
214
+ export interface ActivationGateContext {
215
+ readonly legacyConversion: boolean;
216
+ }
217
+
218
+ export interface InstallManagedRuntimeOptions {
219
+ readonly archivePath: string;
220
+ readonly artifact: ReleaseArtifact;
221
+ readonly version: string;
222
+ readonly platform: ReleasePlatform;
223
+ readonly home?: string;
224
+ readonly allowDowngrade?: boolean;
225
+ readonly compatibility?: CompatibilityProbe;
226
+ /** Rechecked immediately before an automatic activation. */
227
+ readonly canActivate?: (
228
+ context: ActivationGateContext,
229
+ ) => boolean | Promise<boolean>;
230
+ /** Explicit operator action may release a prior rollback's auto-update hold. */
231
+ readonly releaseRollbackUpdateHold?: boolean;
232
+ /** Capability returned by `beginManagedRuntimeInstall` for public bootstrap. */
233
+ readonly installIntent?: string;
234
+ }
235
+
236
+ export interface UpdateManagedRuntimeOptions {
237
+ readonly home?: string;
238
+ readonly manifestUrl?: string;
239
+ readonly releaseBaseUrl?: string;
240
+ readonly trustedKeys?: ReadonlyMap<string, PinnedReleaseManifestKey>;
241
+ readonly fetchImpl?: typeof fetch;
242
+ readonly compatibility?: CompatibilityProbe;
243
+ readonly canActivate?: (
244
+ context: ActivationGateContext,
245
+ ) => boolean | Promise<boolean>;
246
+ readonly manifestTimeoutMs?: number;
247
+ readonly artifactTimeoutMs?: number;
248
+ /** Manual `uai-host update` opts in; automatic checks deliberately do not. */
249
+ readonly releaseRollbackUpdateHold?: boolean;
250
+ }
251
+
252
+ export type ManagedUpdateResult =
253
+ | { readonly status: "not-managed" }
254
+ | { readonly status: "deferred"; readonly version: string }
255
+ | { readonly status: "current"; readonly version: string }
256
+ | {
257
+ readonly status: "updated";
258
+ readonly fromVersion: string;
259
+ readonly version: string;
260
+ };
261
+
262
+ export function managedRuntimePaths(home?: string): ManagedRuntimePaths {
263
+ const explicitHome = home !== undefined;
264
+ const resolvedHome = path.resolve(home ?? homedir());
265
+ if (resolvedHome === path.parse(resolvedHome).root) {
266
+ throw new Error("refusing to use a filesystem root as the managed host home");
267
+ }
268
+ const root = path.join(resolvedHome, ".uai-host");
269
+ const runtime = path.join(root, "runtime");
270
+ const configuredUaiHome = !explicitHome && process.env.UAI_HOME
271
+ ? path.resolve(expandManagedHome(process.env.UAI_HOME, resolvedHome))
272
+ : !explicitHome && process.env.UAI_DATA_DIR
273
+ ? path.dirname(path.resolve(expandManagedHome(process.env.UAI_DATA_DIR, resolvedHome)))
274
+ : path.join(resolvedHome, ".uai");
275
+ const configuredDataDir = !explicitHome && process.env.UAI_DATA_DIR
276
+ ? path.resolve(expandManagedHome(process.env.UAI_DATA_DIR, resolvedHome))
277
+ : path.join(configuredUaiHome, "data");
278
+ return {
279
+ home: resolvedHome,
280
+ root,
281
+ runtime,
282
+ current: path.join(runtime, "current"),
283
+ previous: path.join(runtime, "previous"),
284
+ shim: path.join(resolvedHome, ".local", "bin", "uai-host"),
285
+ recoveryEntrypoint: path.join(root, "bin", "uai-host-recover"),
286
+ recoveryShim: path.join(resolvedHome, ".local", "bin", "uai-host-recover"),
287
+ data: configuredUaiHome,
288
+ rollbackJournal: path.join(runtime, ".rollback.json"),
289
+ rollbackUpdateHold: path.join(runtime, ".rollback-update-hold.json"),
290
+ minimumRuntimeCapability: path.join(
291
+ configuredUaiHome,
292
+ ".minimum-runtime-capability.json",
293
+ ),
294
+ runtimeProviderState: path.join(configuredDataDir, "container-runtime-provider.json"),
295
+ // The lock must not live inside `root`: uninstall atomically renames that
296
+ // whole tree, and moving the lock would let another process create a new
297
+ // root and enter before deletion finishes.
298
+ operationLock: path.join(resolvedHome, ".uai-host.operation.lock"),
299
+ activationPending: path.join(runtime, ".activation-pending.json"),
300
+ maintenancePending: path.join(
301
+ resolvedHome,
302
+ ".uai-host.maintenance-pending.json",
303
+ ),
304
+ uninstallPendingRoot: path.join(
305
+ resolvedHome,
306
+ ".uai-host.uninstall-pending",
307
+ ),
308
+ installIntent: path.join(resolvedHome, ".uai-host.install-intent.json"),
309
+ legacyShimTakeover: path.join(
310
+ resolvedHome,
311
+ ".uai-host.legacy-shim-takeover.json",
312
+ ),
313
+ legacyShimBackup: path.join(
314
+ resolvedHome,
315
+ ".local",
316
+ "bin",
317
+ ".uai-host.legacy-backup",
318
+ ),
319
+ };
320
+ }
321
+
322
+ function expandManagedHome(value: string, home: string): string {
323
+ if (value === "~") return home;
324
+ if (value.startsWith("~/")) return path.join(home, value.slice(2));
325
+ return value;
326
+ }
327
+
328
+ export function nativeReleasePlatform(
329
+ platform: NodeJS.Platform = process.platform,
330
+ arch: string = process.arch,
331
+ ): ReleasePlatform {
332
+ const os = platform === "darwin" ? "darwin" : platform === "linux" ? "linux" : null;
333
+ const cpu = arch === "arm64" ? "arm64" : arch === "x64" ? "x64" : null;
334
+ const value = os && cpu ? `${os}-${cpu}` : `${platform}-${arch}`;
335
+ if (!isReleasePlatform(value)) {
336
+ throw new Error(`unsupported managed host platform: ${value}`);
337
+ }
338
+ return value;
339
+ }
340
+
341
+ export function assertRuntimeCompatibility(probe: CompatibilityProbe = {}): ReleasePlatform {
342
+ const platform = probe.platform ?? process.platform;
343
+ const arch = probe.arch ?? process.arch;
344
+ const releasePlatform = nativeReleasePlatform(platform, arch);
345
+ const uid = probe.uid ?? process.getuid?.() ?? -1;
346
+ if (uid === 0) {
347
+ throw new Error("the managed host installer refuses to run with sudo or as root");
348
+ }
349
+ if (platform === "darwin") {
350
+ const version = probe.macosVersion ?? readMacosVersion();
351
+ assertMinimumVersion(version, "13.5", "macOS");
352
+ return releasePlatform;
353
+ }
354
+
355
+ assertMinimumVersion(probe.kernelVersion ?? osRelease(), "4.18", "Linux kernel");
356
+ const glibc = probe.glibcVersion === undefined ? readGlibcVersion() : probe.glibcVersion;
357
+ if (!glibc) {
358
+ throw new Error("managed Linux hosts require glibc 2.29 or newer; musl/Alpine is unsupported");
359
+ }
360
+ assertMinimumVersion(glibc, "2.29", "glibc");
361
+ return releasePlatform;
362
+ }
363
+
364
+ export function compareStableVersions(left: string, right: string): number {
365
+ const a = parseStableVersion(left);
366
+ const b = parseStableVersion(right);
367
+ for (let index = 0; index < a.length; index += 1) {
368
+ const delta = a[index]! - b[index]!;
369
+ if (delta !== 0) return delta < 0 ? -1 : 1;
370
+ }
371
+ return 0;
372
+ }
373
+
374
+ export async function currentManagedVersion(home?: string): Promise<string | null> {
375
+ const paths = managedRuntimePaths(home);
376
+ return readVersionLink(paths.current, paths.runtime);
377
+ }
378
+
379
+ export async function installManagedRuntimeArchive(
380
+ options: InstallManagedRuntimeOptions,
381
+ ): Promise<{
382
+ version: string;
383
+ previousVersion: string | null;
384
+ changed: boolean;
385
+ deferred: boolean;
386
+ }> {
387
+ assertStableVersion(options.version);
388
+ const nativePlatform = assertRuntimeCompatibility(options.compatibility);
389
+ if (nativePlatform !== options.platform) {
390
+ throw new Error(
391
+ `runtime archive targets ${options.platform}, but this host is ${nativePlatform}`,
392
+ );
393
+ }
394
+ const paths = managedRuntimePaths(options.home);
395
+ const operation = async () => {
396
+ await reconcileInterruptedRemovalForInstall(paths);
397
+ const storedInstallIntent = await readInstallIntent(paths);
398
+ let installIntent: InstallIntentMarker | null = null;
399
+ if (options.installIntent) {
400
+ installIntent = await requireInstallIntent(
401
+ paths,
402
+ options.installIntent,
403
+ true,
404
+ );
405
+ if (installIntent.phase === "attested") {
406
+ throw new Error("managed host install is already attested and must complete");
407
+ }
408
+ if (
409
+ installIntent.serviceOwnership === "legacy" &&
410
+ installIntent.priorServiceStop !== "stopped"
411
+ ) {
412
+ throw new Error(
413
+ "managed host legacy service must be durably quiesced before runtime activation",
414
+ );
415
+ }
416
+ if (
417
+ installIntent.serviceOwnership === "legacy" &&
418
+ !options.canActivate
419
+ ) {
420
+ throw new Error(
421
+ "managed host legacy conversion requires a final workload-idle gate",
422
+ );
423
+ }
424
+ } else if (storedInstallIntent) {
425
+ throw new Error(
426
+ "managed host runtime activation requires the active install intent",
427
+ );
428
+ }
429
+ await ensureManagedDirectories(paths);
430
+ await recoverRollback(paths);
431
+ const currentVersion = await readVersionLink(paths.current, paths.runtime);
432
+ const rollbackUpdateHold = options.releaseRollbackUpdateHold
433
+ ? await readRollbackUpdateHold(paths)
434
+ : null;
435
+ if (
436
+ currentVersion &&
437
+ compareStableVersions(options.version, currentVersion) < 0 &&
438
+ !options.allowDowngrade
439
+ ) {
440
+ throw new Error(
441
+ `refusing to downgrade managed host ${currentVersion} to ${options.version}; use rollback or an explicit downgrade`,
442
+ );
443
+ }
444
+ const activationContext: ActivationGateContext = {
445
+ legacyConversion: installIntent?.serviceOwnership === "legacy",
446
+ };
447
+
448
+ // A true managed same-version rerun has no activation edge to gate. A
449
+ // legacy conversion is different: taking over its command/service is the
450
+ // activation edge, so it still needs the final workload-idle proof below.
451
+ // Otherwise validate the selected runtime, repair the stable shim, and
452
+ // keep a harmless managed rerun idempotent even while tasks are active.
453
+ if (currentVersion === options.version) {
454
+ // The rerun keeps this exact generation, so it must fully validate.
455
+ // Every other path below replaces current — validating the generation
456
+ // being abandoned would let its own corruption block the escape route.
457
+ const currentDescriptor = await validateInstalledRuntime(
458
+ path.join(paths.runtime, currentVersion),
459
+ currentVersion,
460
+ nativePlatform,
461
+ );
462
+ await assertRuntimeMeetsDurableCapabilityFloor(paths, currentDescriptor);
463
+ if (
464
+ installIntent?.serviceOwnership === "legacy" &&
465
+ !(await options.canActivate!({ legacyConversion: true }))
466
+ ) {
467
+ return {
468
+ version: options.version,
469
+ previousVersion: await readVersionLink(paths.previous, paths.runtime),
470
+ changed: false,
471
+ deferred: true,
472
+ };
473
+ }
474
+ if (installIntent) await parkLegacyShimTakeover(paths);
475
+ await ensureStableCommands(paths);
476
+ if (rollbackUpdateHold) {
477
+ await removeRollbackUpdateHold(paths, rollbackUpdateHold);
478
+ }
479
+ if (installIntent) {
480
+ await advanceInstallIntentToActivated(paths, installIntent);
481
+ }
482
+ return {
483
+ version: options.version,
484
+ previousVersion: await readVersionLink(paths.previous, paths.runtime),
485
+ changed: false,
486
+ deferred: false,
487
+ };
488
+ }
489
+
490
+ const archiveBytes = await readVerifiedArtifactFile(options.archivePath, options.artifact);
491
+ const work = await mkdtemp(path.join(paths.runtime, ".install-"));
492
+ await chmod(work, 0o700);
493
+ const staged = path.join(work, "payload");
494
+ const target = path.join(paths.runtime, options.version);
495
+ try {
496
+ const inspected = await extractVerifiedRuntimeArchive(archiveBytes, staged, {
497
+ version: options.version,
498
+ platform: options.platform,
499
+ });
500
+ await assertRuntimeMeetsDurableCapabilityFloor(paths, {
501
+ ...inspected.descriptor,
502
+ ...inspected.capabilities,
503
+ });
504
+ if (
505
+ options.canActivate &&
506
+ !(await options.canActivate(activationContext))
507
+ ) {
508
+ return {
509
+ version: options.version,
510
+ previousVersion: currentVersion,
511
+ changed: false,
512
+ deferred: true,
513
+ };
514
+ }
515
+ if (installIntent) await parkLegacyShimTakeover(paths);
516
+ await assertStableCommandsReplaceable(paths);
517
+ await writeAtomicJson(paths.activationPending, {
518
+ schemaVersion: 1,
519
+ fromVersion: currentVersion,
520
+ version: options.version,
521
+ });
522
+ let activated = false;
523
+ try {
524
+ // The durable gate makes the running service reject new taskUp calls;
525
+ // this second check catches work that began immediately before it.
526
+ if (
527
+ options.canActivate &&
528
+ !(await options.canActivate(activationContext))
529
+ ) {
530
+ return {
531
+ version: options.version,
532
+ previousVersion: currentVersion,
533
+ changed: false,
534
+ deferred: true,
535
+ };
536
+ }
537
+ if (await pathExists(target)) {
538
+ const targetDescriptor = await validateInstalledRuntime(
539
+ target,
540
+ options.version,
541
+ options.platform,
542
+ );
543
+ await assertRuntimeMeetsDurableCapabilityFloor(paths, targetDescriptor);
544
+ } else {
545
+ await rename(staged, target);
546
+ await syncDirectory(paths.runtime);
547
+ }
548
+ if (
549
+ options.canActivate &&
550
+ !(await options.canActivate(activationContext))
551
+ ) {
552
+ return {
553
+ version: options.version,
554
+ previousVersion: currentVersion,
555
+ changed: false,
556
+ deferred: true,
557
+ };
558
+ }
559
+ await atomicVersionLink(paths.current, options.version);
560
+ activated = true;
561
+ if (currentVersion) await atomicVersionLink(paths.previous, currentVersion);
562
+ await ensureStableCommands(paths);
563
+ await pruneRuntimeGenerations(paths);
564
+ if (rollbackUpdateHold) {
565
+ await removeRollbackUpdateHold(paths, rollbackUpdateHold);
566
+ }
567
+ if (installIntent) {
568
+ await advanceInstallIntentToActivated(paths, installIntent);
569
+ }
570
+ return {
571
+ version: options.version,
572
+ previousVersion: currentVersion,
573
+ changed: true,
574
+ deferred: false,
575
+ };
576
+ } finally {
577
+ if (
578
+ !activated &&
579
+ (await readVersionLink(paths.current, paths.runtime)) !== options.version
580
+ ) {
581
+ await removeDurably(paths.activationPending);
582
+ }
583
+ }
584
+ } finally {
585
+ await rm(work, { recursive: true, force: true });
586
+ await syncDirectory(paths.runtime);
587
+ }
588
+ };
589
+ const internal = options as InstallManagedRuntimeOptions & {
590
+ [OPERATION_LOCK_HELD]?: true;
591
+ };
592
+ return internal[OPERATION_LOCK_HELD]
593
+ ? operation()
594
+ : withManagedOperationLock(paths, operation);
595
+ }
596
+
597
+ export async function updateManagedRuntime(
598
+ options: UpdateManagedRuntimeOptions = {},
599
+ ): Promise<ManagedUpdateResult> {
600
+ const paths = managedRuntimePaths(options.home);
601
+ if (pathExistsSync(paths.installIntent)) {
602
+ throw new Error("managed host update is fenced by an installation intent");
603
+ }
604
+ if (!(await currentManagedVersion(paths.home))) return { status: "not-managed" };
605
+ const platform = assertRuntimeCompatibility(options.compatibility);
606
+ const trustedKeys = options.trustedKeys ?? PINNED_RELEASE_MANIFEST_KEYS;
607
+ if (trustedKeys.size === 0) {
608
+ throw new Error("managed host updates are unavailable: no release signing key is pinned");
609
+ }
610
+ return withManagedOperationLock(paths, async () => {
611
+ await assertNoManagedInstallIntent(paths, "update");
612
+ if (!(await pathExists(paths.root))) return { status: "not-managed" };
613
+ await ensureManagedDirectories(paths);
614
+ await recoverRollback(paths);
615
+ const current = await readVersionLink(paths.current, paths.runtime);
616
+ if (!current) return { status: "not-managed" };
617
+ const rollbackUpdateHold = await readRollbackUpdateHold(paths);
618
+ if (rollbackUpdateHold && !options.releaseRollbackUpdateHold) {
619
+ // Rollback is an explicit verdict that the newer release is unsafe on
620
+ // this host. Automatic checks stay completely offline until an operator
621
+ // explicitly runs `uai-host update` and a signed check succeeds.
622
+ return { status: "current", version: current };
623
+ }
624
+ const fetchImpl = options.fetchImpl ?? fetch;
625
+ const manifestBytes = await fetchBounded(
626
+ fetchImpl,
627
+ options.manifestUrl ?? STABLE_RELEASE_MANIFEST_URL,
628
+ MAX_RELEASE_MANIFEST_BYTES,
629
+ checkedTimeout(options.manifestTimeoutMs, MANAGED_MANIFEST_TIMEOUT_MS, 60_000),
630
+ );
631
+ const manifest = verifyReleaseManifest(manifestBytes, {
632
+ trustedKeys,
633
+ releaseBaseUrl: options.releaseBaseUrl ?? DEFAULT_RELEASE_DOWNLOAD_BASE,
634
+ });
635
+ if (compareStableVersions(manifest.version, current) <= 0) {
636
+ if (rollbackUpdateHold && options.releaseRollbackUpdateHold) {
637
+ await removeRollbackUpdateHold(paths, rollbackUpdateHold);
638
+ }
639
+ return { status: "current", version: current };
640
+ }
641
+ const artifact = releaseArtifactForPlatform(manifest, platform);
642
+ const downloadDir = await mkdtemp(path.join(paths.runtime, ".download-"));
643
+ await chmod(downloadDir, 0o700);
644
+ const archivePath = path.join(
645
+ downloadDir,
646
+ releaseArtifactFilename(manifest.version, platform),
647
+ );
648
+ try {
649
+ await downloadArtifact(
650
+ fetchImpl,
651
+ artifact,
652
+ archivePath,
653
+ checkedTimeout(
654
+ options.artifactTimeoutMs,
655
+ MANAGED_ARTIFACT_TIMEOUT_MS,
656
+ 30 * 60_000,
657
+ ),
658
+ );
659
+ const installed = await installManagedRuntimeArchive({
660
+ archivePath,
661
+ artifact,
662
+ version: manifest.version,
663
+ platform,
664
+ // Forward the caller's (possibly absent) home verbatim: an explicit
665
+ // home pins data/floor paths to `<home>/.uai` and DISABLES the
666
+ // UAI_HOME/UAI_DATA_DIR lookup, so substituting the resolved
667
+ // `paths.home` here would make the nested install read the durable
668
+ // capability floor from the wrong directory on env-configured hosts.
669
+ home: options.home,
670
+ compatibility: options.compatibility,
671
+ canActivate: options.canActivate,
672
+ releaseRollbackUpdateHold: options.releaseRollbackUpdateHold,
673
+ [OPERATION_LOCK_HELD]: true,
674
+ } as InstallManagedRuntimeOptions & { [OPERATION_LOCK_HELD]: true });
675
+ if (installed.deferred) {
676
+ return { status: "deferred", version: manifest.version };
677
+ }
678
+ return { status: "updated", fromVersion: current, version: manifest.version };
679
+ } finally {
680
+ await rm(downloadDir, { recursive: true, force: true });
681
+ await syncDirectory(paths.runtime);
682
+ }
683
+ });
684
+ }
685
+
686
+ export async function rollbackManagedRuntime(
687
+ home?: string,
688
+ compatibility?: CompatibilityProbe,
689
+ canActivate?: () => boolean | Promise<boolean>,
690
+ ): Promise<{ fromVersion: string; version: string }> {
691
+ const paths = managedRuntimePaths(home);
692
+ const platform = assertRuntimeCompatibility(compatibility);
693
+ return withManagedOperationLock(paths, async () => {
694
+ await assertNoManagedInstallIntent(paths, "rollback");
695
+ await ensureManagedDirectories(paths);
696
+ const recovered = await recoverRollback(paths);
697
+ if (recovered) {
698
+ await ensureStableCommands(paths);
699
+ return recovered;
700
+ }
701
+ const fromVersion = await readVersionLink(paths.current, paths.runtime);
702
+ const version = await readVersionLink(paths.previous, paths.runtime);
703
+ if (!fromVersion || !version || fromVersion === version) {
704
+ throw new Error("no previous managed host runtime is available to roll back to");
705
+ }
706
+ // Only the generation being switched TO validates (plus the capability
707
+ // floor below). Rollback exists precisely for a damaged current; requiring
708
+ // the abandoned generation to validate would block every escape route.
709
+ const targetDescriptor = await validateInstalledRuntime(
710
+ path.join(paths.runtime, version),
711
+ version,
712
+ platform,
713
+ );
714
+ await assertRuntimeMeetsDurableCapabilityFloor(paths, targetDescriptor);
715
+ if (canActivate && !(await canActivate())) {
716
+ throw new Error("refusing to roll back while host tasks are active");
717
+ }
718
+ await assertStableCommandsReplaceable(paths);
719
+ await writeAtomicJson(paths.activationPending, {
720
+ schemaVersion: 1,
721
+ fromVersion,
722
+ version,
723
+ });
724
+ let activated = false;
725
+ try {
726
+ if (canActivate && !(await canActivate())) {
727
+ throw new Error("refusing to roll back while host tasks are active");
728
+ }
729
+ await writeAtomicJson(paths.rollbackJournal, { schemaVersion: 1, fromVersion, version });
730
+ await atomicVersionLink(paths.current, version);
731
+ activated = true;
732
+ await atomicVersionLink(paths.previous, fromVersion);
733
+ await writeRollbackUpdateHold(paths, fromVersion, version);
734
+ await removeDurably(paths.rollbackJournal);
735
+ await ensureStableCommands(paths);
736
+ return { fromVersion, version };
737
+ } finally {
738
+ if (
739
+ !activated &&
740
+ (await readVersionLink(paths.current, paths.runtime)) !== version
741
+ ) {
742
+ // The journal precedes the current-link commit. If activation never
743
+ // committed, remove both pieces of intent; otherwise a later recovery
744
+ // could flip versions after admission reopened.
745
+ await removeDurably(paths.rollbackJournal);
746
+ await removeDurably(paths.activationPending);
747
+ }
748
+ }
749
+ });
750
+ }
751
+
752
+ /**
753
+ * Claim the minimum managed-runtime capability before the bundled runtime's
754
+ * first durable provider claim. The managed-operation lock makes this atomic
755
+ * with rollback:
756
+ * either rollback sees the floor and refuses capability 1, or the task sees the
757
+ * staged activation and cannot create provider state.
758
+ */
759
+ export async function claimManagedRuntimeCapabilityFloor(
760
+ minimumCapabilityVersion = 2,
761
+ home?: string,
762
+ ): Promise<void> {
763
+ if (!Number.isSafeInteger(minimumCapabilityVersion) || minimumCapabilityVersion < 2) {
764
+ throw new Error("managed runtime capability floor is invalid");
765
+ }
766
+ const paths = managedRuntimePaths(home);
767
+ // Waiting acquisition: this runs during runtime detection, concurrently
768
+ // with the startup settlement's own locked read. First-collision-dies took
769
+ // the whole service down at boot (live 2026-08-16, launchd boots exiting 1
770
+ // with "another managed host install/update/rollback is already running"
771
+ // while no external holder existed).
772
+ await withManagedOperationLockWaiting(paths, async () => {
773
+ await assertNoManagedInstallIntent(paths, "runtime capability claim");
774
+ await ensureManagedDirectories(paths);
775
+ if (
776
+ pathExistsSync(paths.activationPending) ||
777
+ pathExistsSync(paths.rollbackJournal) ||
778
+ pathExistsSync(paths.maintenancePending)
779
+ ) {
780
+ throw new Error("managed runtime activation is already in progress");
781
+ }
782
+ await mkdir(path.dirname(paths.minimumRuntimeCapability), {
783
+ recursive: true,
784
+ mode: 0o700,
785
+ });
786
+ const existing = await readMinimumRuntimeCapability(paths);
787
+ if (existing !== null && existing >= minimumCapabilityVersion) return;
788
+ await writeAtomicJson(paths.minimumRuntimeCapability, {
789
+ schemaVersion: 1,
790
+ minimumRuntimeCapability: minimumCapabilityVersion,
791
+ });
792
+ });
793
+ }
794
+
795
+ /** True while `current` has changed but the supervisor has not exec'd it yet. */
796
+ export function managedRuntimeActivationPending(home?: string): boolean {
797
+ return pathExistsSync(managedRuntimePaths(home).activationPending);
798
+ }
799
+
800
+ /**
801
+ * True while installation, activation, or destructive maintenance has closed
802
+ * new
803
+ * task admission. The maintenance marker is outside the runtime tree so
804
+ * uninstall cannot accidentally remove its own gate before teardown finishes.
805
+ * Existence is intentionally fail-closed: a malformed marker still blocks
806
+ * taskUp and must be repaired by a safe uninstall retry.
807
+ */
808
+ export function managedRuntimeAdmissionPending(home?: string): boolean {
809
+ const paths = managedRuntimePaths(home);
810
+ return (
811
+ pathExistsSync(paths.activationPending) ||
812
+ pathExistsSync(paths.maintenancePending) ||
813
+ pathExistsSync(paths.installIntent)
814
+ );
815
+ }
816
+
817
+ /**
818
+ * True only after an activation marker's target has become `current` and the
819
+ * process asking is still running a different version. The marker is written
820
+ * before the link flip so task admission can close first; checking both
821
+ * records prevents the old service from restarting during that preparation
822
+ * window.
823
+ */
824
+ export async function managedRuntimeRestartRequired(
825
+ runningVersion: string,
826
+ home?: string,
827
+ ): Promise<boolean> {
828
+ assertStableVersion(runningVersion);
829
+ const paths = managedRuntimePaths(home);
830
+ const current = await readVersionLink(paths.current, paths.runtime);
831
+ if (current === null || current === runningVersion) return false;
832
+ let marker: ActivationMarker;
833
+ try {
834
+ marker = parseActivationMarker(
835
+ await readBoundedRegularFile(paths.activationPending),
836
+ );
837
+ } catch (error) {
838
+ // The selected process may already have acknowledged and removed the
839
+ // marker while this older process was still alive. A `current` mismatch is
840
+ // independently sufficient to require a supervisor restart.
841
+ if (isMissing(error)) return true;
842
+ throw error;
843
+ }
844
+ if (current !== marker.version) {
845
+ throw new Error("managed host activation marker does not match the current runtime");
846
+ }
847
+ return true;
848
+ }
849
+
850
+ /** Reconcile an external activation attempt from the already-running daemon. */
851
+ export async function reconcileManagedRuntimeActivation(
852
+ runningVersion: string,
853
+ home?: string,
854
+ ): Promise<boolean> {
855
+ // A provisional service runs while its public installer owns the operation
856
+ // lock. The install intent itself gates taskUp; leave the activation marker
857
+ // intact until attestation removes that intent, then acknowledge normally.
858
+ if (pathExistsSync(managedRuntimePaths(home).installIntent)) return false;
859
+ if (managedRuntimeActivationPending(home)) {
860
+ // A live activating process holds the operation lock, so this throws and
861
+ // the watcher retries. Once the owner exits, startup settlement either
862
+ // aborts its pre-link intent or reports that a restart is required.
863
+ return !(await acknowledgeManagedRuntimeActivation(runningVersion, home));
864
+ }
865
+ return managedRuntimeRestartRequired(runningVersion, home);
866
+ }
867
+
868
+ /**
869
+ * Prove that this process is the runtime selected by `current` without opening
870
+ * task admission. `cmdRun` performs this before importing the service module;
871
+ * the activation marker deliberately remains until that import succeeds.
872
+ */
873
+ export async function validateManagedRuntimeSelection(
874
+ runningVersion: string,
875
+ home?: string,
876
+ ): Promise<boolean> {
877
+ return reconcileManagedRuntimeSelection(runningVersion, home, false);
878
+ }
879
+
880
+ /**
881
+ * Prove the generation selected for a provisional daemon without taking the
882
+ * operation lock held by its own service-definition installer. This never
883
+ * acknowledges activation: the live install intent remains the task-admission
884
+ * fence until shell attestation commits, after which the activation watcher
885
+ * performs the ordinary locked acknowledgement.
886
+ */
887
+ export async function validateProvisionalManagedRuntimeSelection(
888
+ runningVersion: string,
889
+ home?: string,
890
+ ): Promise<boolean> {
891
+ assertStableVersion(runningVersion);
892
+ const paths = managedRuntimePaths(home);
893
+ const intent = await readInstallIntent(paths);
894
+ if (!intent || (intent.phase !== "activated" && intent.phase !== "attested")) {
895
+ throw new Error("managed host provisional service has no activated install intent");
896
+ }
897
+ const current = await readVersionLink(paths.current, paths.runtime);
898
+ if (current === null) {
899
+ throw new Error("managed host provisional service has no selected runtime");
900
+ }
901
+ let marker: ActivationMarker | null = null;
902
+ try {
903
+ marker = parseActivationMarker(
904
+ await readBoundedRegularFile(paths.activationPending),
905
+ );
906
+ } catch (error) {
907
+ if (!isMissing(error)) throw error;
908
+ }
909
+ if (marker && marker.version !== current) {
910
+ throw new Error("managed host activation marker does not match the current runtime");
911
+ }
912
+ return current === runningVersion;
913
+ }
914
+
915
+ /**
916
+ * Settle a pending activation before `run` accepts work. `true` means this
917
+ * process is safe to start: it either acknowledged the selected version or
918
+ * recovered an abandoned pre-link intent and kept the prior version current.
919
+ * `false` means `current` selected another version, so this stale process must
920
+ * exit and let the supervisor launch the stable shim again.
921
+ */
922
+ export async function acknowledgeManagedRuntimeActivation(
923
+ runningVersion: string,
924
+ home?: string,
925
+ ): Promise<boolean> {
926
+ return reconcileManagedRuntimeSelection(runningVersion, home, true);
927
+ }
928
+
929
+ async function reconcileManagedRuntimeSelection(
930
+ runningVersion: string,
931
+ home: string | undefined,
932
+ acknowledgeHealthyImport: boolean,
933
+ ): Promise<boolean> {
934
+ assertStableVersion(runningVersion);
935
+ const paths = managedRuntimePaths(home);
936
+ if (!(await pathExists(paths.root))) return true;
937
+ // Busy handling is holder-dependent. An INSTALLER-owned lock (install
938
+ // intent on disk) must fail fast: a provisional boot defers to the
939
+ // installer, and the intent-hold test pins that contract. A lock held by a
940
+ // SIBLING boot step (detection's capability-floor claim) is milliseconds —
941
+ // first-collision-dies there killed launchd boots with "already running"
942
+ // while no external holder existed (live 2026-08-16).
943
+ const deadline = Date.now() + OPERATION_LOCK_WAIT_MS;
944
+ for (;;) {
945
+ try {
946
+ return await reconcileManagedRuntimeSelectionLocked(
947
+ runningVersion,
948
+ paths,
949
+ acknowledgeHealthyImport,
950
+ );
951
+ } catch (error) {
952
+ if (
953
+ !(error instanceof ManagedOperationBusyError) ||
954
+ pathExistsSync(paths.installIntent) ||
955
+ Date.now() >= deadline
956
+ ) {
957
+ throw error;
958
+ }
959
+ await new Promise<void>((resolve) =>
960
+ setTimeout(resolve, OPERATION_LOCK_POLL_MS),
961
+ );
962
+ }
963
+ }
964
+ }
965
+
966
+ async function reconcileManagedRuntimeSelectionLocked(
967
+ runningVersion: string,
968
+ paths: ManagedRuntimePaths,
969
+ acknowledgeHealthyImport: boolean,
970
+ ): Promise<boolean> {
971
+ return withManagedOperationLock(paths, async () => {
972
+ await ensureManagedDirectories(paths);
973
+ await recoverRollback(paths);
974
+ const selectedVersion = await readVersionLink(paths.current, paths.runtime);
975
+ if (selectedVersion !== null) {
976
+ const selectedDescriptor = await validateInstalledRuntime(
977
+ path.join(paths.runtime, selectedVersion),
978
+ selectedVersion,
979
+ );
980
+ await assertRuntimeMeetsDurableCapabilityFloor(paths, selectedDescriptor);
981
+ }
982
+ let marker: ActivationMarker;
983
+ try {
984
+ marker = parseActivationMarker(
985
+ await readBoundedRegularFile(paths.activationPending),
986
+ );
987
+ } catch (error) {
988
+ // Another process may have deferred and removed the marker between the
989
+ // caller's synchronous admission check and lock acquisition. It may also
990
+ // have acknowledged a newly selected runtime, so compare `current`
991
+ // before allowing this process to continue.
992
+ if (isMissing(error)) {
993
+ const current = await readVersionLink(paths.current, paths.runtime);
994
+ return current === null || current === runningVersion;
995
+ }
996
+ throw error;
997
+ }
998
+ const current = await readVersionLink(paths.current, paths.runtime);
999
+ if (current === marker.version) {
1000
+ if (marker.fromVersion) {
1001
+ // `current` is the commit point. Repair a crash between that link and
1002
+ // `previous`, then keep admission closed until the selected binary is
1003
+ // the process performing the acknowledgement.
1004
+ await atomicVersionLink(paths.previous, marker.fromVersion);
1005
+ }
1006
+ if (marker.version !== runningVersion) return false;
1007
+ if (acknowledgeHealthyImport) {
1008
+ await removeDurably(paths.activationPending);
1009
+ }
1010
+ return true;
1011
+ }
1012
+ if (current === marker.fromVersion) {
1013
+ // The activating process died before committing `current`. Its durable
1014
+ // intent closed admission, but there is nothing to restart into. Abort
1015
+ // the intent and continue only if this process is the still-current
1016
+ // runtime.
1017
+ const selected = current === null || current === runningVersion;
1018
+ if (selected && acknowledgeHealthyImport) {
1019
+ await removeDurably(paths.activationPending);
1020
+ }
1021
+ return selected;
1022
+ }
1023
+ throw new Error(
1024
+ "managed host activation marker does not match the current runtime",
1025
+ );
1026
+ });
1027
+ }
1028
+
1029
+ /**
1030
+ * Bundled-installer preflight. This must run before enrollment is inspected or
1031
+ * spent: a committed `--purge` uninstall owns the old identity directory too.
1032
+ * `installManagedRuntimeArchive` repeats the same check under its own lock to
1033
+ * close the preflight-to-activation race.
1034
+ */
1035
+ export async function reconcileManagedRuntimeUninstall(
1036
+ home?: string,
1037
+ ): Promise<boolean> {
1038
+ const paths = managedRuntimePaths(home);
1039
+ return withManagedOperationLock(paths, () =>
1040
+ reconcileInterruptedRemovalForInstall(paths),
1041
+ );
1042
+ }
1043
+
1044
+ /**
1045
+ * Start the public install transaction before enrollment is read or attached.
1046
+ * The external marker fences `uninstall --purge` after this function releases
1047
+ * the short-lived filesystem lock. The legacy command is only recorded here;
1048
+ * it remains callable until install-archive runs after service stop.
1049
+ */
1050
+ export async function beginManagedRuntimeInstall(
1051
+ options: BeginManagedRuntimeInstallOptions,
1052
+ ): Promise<ManagedRuntimeInstallIntent> {
1053
+ assertOwnerPid(options.ownerPid);
1054
+ const owner = processIdentity(options.ownerPid);
1055
+ if (!owner) {
1056
+ throw new Error("managed host installer owner process is not running");
1057
+ }
1058
+ const currentUid = process.getuid?.();
1059
+ if (currentUid !== undefined && owner.uid !== currentUid) {
1060
+ throw new Error("managed host installer owner process belongs to another user");
1061
+ }
1062
+ const paths = managedRuntimePaths(options.home);
1063
+ return withManagedOperationLock(paths, async () => {
1064
+ await reconcileInterruptedRemovalForInstall(paths);
1065
+ const existing = await readInstallIntent(paths);
1066
+ if (existing) {
1067
+ const live = installIntentOwnerIsLive(existing);
1068
+ if (live) {
1069
+ if (
1070
+ existing.ownerPid !== options.ownerPid ||
1071
+ existing.ownerStartedAt !== owner.startedAt ||
1072
+ existing.ownerUid !== owner.uid
1073
+ ) {
1074
+ throw new Error("another managed host installation is already running");
1075
+ }
1076
+ return {
1077
+ nonce: existing.nonce,
1078
+ legacyShim: (await readLegacyShimTakeover(paths)) !== null,
1079
+ serviceOwnership: existing.serviceOwnership,
1080
+ serviceQuiescence:
1081
+ existing.serviceOwnership === "legacy" ? "quiesce" : "idle",
1082
+ serviceManager: existing.serviceManager,
1083
+ };
1084
+ }
1085
+ if (existing.phase === "attested") {
1086
+ // Attestation is the durable commit decision. A killed shell after it
1087
+ // may finish, but must never restore the old definition or command.
1088
+ if (!options.beforeFinalize) {
1089
+ throw new Error("managed host install recovery cannot commit its service definition");
1090
+ }
1091
+ await options.beforeFinalize(existing.nonce);
1092
+ await finalizeLegacyShimTakeover(paths);
1093
+ await removeDurably(paths.installIntent);
1094
+ } else {
1095
+ // A prior definition may execute the legacy command. Restore it without
1096
+ // starting first, then restore runtime selection/command ownership, and
1097
+ // only then resume the recorded prior service. Every step is idempotent;
1098
+ // the old intent remains fail-closed until all three have succeeded.
1099
+ if (!options.prepareRecovery || !options.resumeRecovery) {
1100
+ throw new Error("managed host install recovery cannot restore its prior service");
1101
+ }
1102
+ await options.prepareRecovery(existing.nonce);
1103
+ await restorePreInstallRuntimeSelection(paths, existing);
1104
+ await restoreLegacyShimTakeover(paths);
1105
+ await options.resumeRecovery(existing.nonce);
1106
+ if (existing.priorServiceStop !== "none") {
1107
+ if (!options.resumeStoppedService) {
1108
+ throw new Error(
1109
+ "managed host install recovery cannot resume its prior service",
1110
+ );
1111
+ }
1112
+ await options.resumeStoppedService(
1113
+ existing.nonce,
1114
+ existing.serviceManager,
1115
+ );
1116
+ }
1117
+ await removeDurably(paths.installIntent);
1118
+ }
1119
+ }
1120
+ // A standalone `uai-host install` can leave an owner-null definition
1121
+ // journal. Settle it after any dead public transaction and immediately
1122
+ // before ownership classification. A protected journal refuses this
1123
+ // ownerless callback and therefore cannot be adopted by a new nonce.
1124
+ await options.reconcileStandaloneDefinition?.();
1125
+ if (await readLegacyShimTakeover(paths)) {
1126
+ throw new Error(
1127
+ "managed host legacy command takeover has no install intent; repair the interrupted install",
1128
+ );
1129
+ }
1130
+
1131
+ const preInstall = await capturePreInstallState(paths);
1132
+ const service = await options.definitionState(paths.shim);
1133
+ assertServiceDefinitionState(service);
1134
+ if (
1135
+ service.ownership === "missing" &&
1136
+ (service.manager.loaded ||
1137
+ service.manager.active ||
1138
+ service.manager.enabled)
1139
+ ) {
1140
+ throw new Error(
1141
+ "managed host service manager reports state for a missing definition",
1142
+ );
1143
+ }
1144
+ const serviceOwnership = service.ownership;
1145
+ const marker: InstallIntentMarker = {
1146
+ schemaVersion: 1,
1147
+ operation: "install",
1148
+ phase: "prepared",
1149
+ nonce: randomBytes(32).toString("hex"),
1150
+ ownerPid: options.ownerPid,
1151
+ ownerUid: owner.uid,
1152
+ ownerStartedAt: owner.startedAt,
1153
+ createdAt: Date.now(),
1154
+ rootExisted: preInstall.rootExisted,
1155
+ currentVersion: preInstall.currentVersion,
1156
+ previousVersion: preInstall.previousVersion,
1157
+ activationPending: preInstall.activationPending,
1158
+ shimOwnership: preInstall.shimOwnership,
1159
+ recoveryShimExisted: preInstall.recoveryShimExisted,
1160
+ recoveryEntrypointExisted: preInstall.recoveryEntrypointExisted,
1161
+ serviceOwnership,
1162
+ serviceManager: service.manager,
1163
+ priorServiceStop: "none",
1164
+ };
1165
+ await writeAtomicJson(paths.installIntent, marker);
1166
+ if (marker.shimOwnership === "legacy") {
1167
+ const legacyShim = await recordLegacyShimTakeover(paths);
1168
+ if (!legacyShim) {
1169
+ throw new Error("recorded legacy host command disappeared during install begin");
1170
+ }
1171
+ }
1172
+ return {
1173
+ nonce: marker.nonce,
1174
+ legacyShim: marker.shimOwnership === "legacy",
1175
+ serviceOwnership,
1176
+ serviceQuiescence:
1177
+ serviceOwnership === "legacy" ? "quiesce" : "idle",
1178
+ serviceManager: service.manager,
1179
+ };
1180
+ });
1181
+ }
1182
+
1183
+ /** A provisional daemon may connect for shell attestation only while its
1184
+ * installer shell is still the exact live process that owns the intent. */
1185
+ export async function managedRuntimeInstallStartupState(
1186
+ home?: string,
1187
+ identityProbe: (pid: number) => ManagedRuntimeOwnerIdentity | null = processIdentity,
1188
+ ): Promise<ManagedRuntimeInstallStartupState> {
1189
+ const paths = managedRuntimePaths(home);
1190
+ if (!pathExistsSync(paths.installIntent)) return "none";
1191
+ const intent = await readInstallIntent(paths);
1192
+ if (!intent) return "none";
1193
+ try {
1194
+ if (installIntentOwnerIsLive(intent, identityProbe)) return "live";
1195
+ } catch (error) {
1196
+ if (error instanceof ProcessIdentityUnavailableError) return "unknown";
1197
+ throw error;
1198
+ }
1199
+ return intent.phase === "attested" ? "attested" : "stale";
1200
+ }
1201
+
1202
+ export async function validateManagedRuntimeInstallIntent(
1203
+ installIntent: string,
1204
+ home?: string,
1205
+ ): Promise<ManagedRuntimeInstallIdentity> {
1206
+ const paths = managedRuntimePaths(home);
1207
+ return withManagedOperationLock(paths, async () => {
1208
+ const intent = await requireInstallIntent(paths, installIntent, true);
1209
+ if (intent.phase !== "activated") {
1210
+ throw new Error(
1211
+ "managed host install intent is not ready for service definition installation",
1212
+ );
1213
+ }
1214
+ return { nonce: intent.nonce, ownerPid: intent.ownerPid };
1215
+ });
1216
+ }
1217
+
1218
+ /** Hold the runtime fence through an intent-owned service-definition write. */
1219
+ export async function runManagedRuntimeInstallIntentMutation<T>(options: {
1220
+ readonly home?: string;
1221
+ readonly installIntent: string;
1222
+ readonly mutation: (identity: ManagedRuntimeInstallIdentity) => Promise<T>;
1223
+ }): Promise<T> {
1224
+ const paths = managedRuntimePaths(options.home);
1225
+ return withManagedOperationLock(paths, async () => {
1226
+ const intent = await requireInstallIntent(paths, options.installIntent, true);
1227
+ if (intent.phase !== "activated") {
1228
+ throw new Error(
1229
+ "managed host install intent is not ready for service definition installation",
1230
+ );
1231
+ }
1232
+ return options.mutation({ nonce: intent.nonce, ownerPid: intent.ownerPid });
1233
+ });
1234
+ }
1235
+
1236
+ /**
1237
+ * Durably stop the legacy service during first conversion. The intent write is
1238
+ * ordered before the manager call so a killed shell is recoverable even before
1239
+ * a service-definition journal exists.
1240
+ */
1241
+ export async function stopManagedRuntimeServiceForInstall(options: {
1242
+ readonly home?: string;
1243
+ readonly installIntent: string;
1244
+ readonly stopService: (
1245
+ installIntent: string,
1246
+ priorManager: ManagedServiceManagerState,
1247
+ ) => Promise<void>;
1248
+ }): Promise<void> {
1249
+ const paths = managedRuntimePaths(options.home);
1250
+ await withManagedOperationLock(paths, async () => {
1251
+ let intent = await requireInstallIntent(paths, options.installIntent, true);
1252
+ if (intent.phase !== "prepared") {
1253
+ throw new Error("managed host prior service may only stop before activation");
1254
+ }
1255
+ if (intent.serviceOwnership !== "legacy") {
1256
+ throw new Error("managed host prior service is not a proven legacy definition");
1257
+ }
1258
+ if (intent.priorServiceStop === "stopped") return;
1259
+ if (intent.priorServiceStop === "none") {
1260
+ intent = { ...intent, priorServiceStop: "requested" };
1261
+ await writeAtomicJson(paths.installIntent, intent);
1262
+ }
1263
+ await options.stopService(intent.nonce, intent.serviceManager);
1264
+ const current = await requireInstallIntent(paths, intent.nonce, true);
1265
+ if (current.phase !== "prepared" || current.priorServiceStop !== "requested") {
1266
+ throw new Error("managed host install intent changed while stopping its service");
1267
+ }
1268
+ await writeAtomicJson(paths.installIntent, {
1269
+ ...current,
1270
+ priorServiceStop: "stopped",
1271
+ } satisfies InstallIntentMarker);
1272
+ });
1273
+ }
1274
+
1275
+ export interface ReconcileManagedRuntimeInstallLifecycleOptions {
1276
+ readonly home?: string;
1277
+ readonly reconcileDefinition: (installIntent?: string) => Promise<void>;
1278
+ readonly completeDefinition: (installIntent?: string) => Promise<void>;
1279
+ readonly prepareDefinitionRollback: (installIntent: string) => Promise<void>;
1280
+ readonly resumeDefinitionRollback: (installIntent: string) => Promise<void>;
1281
+ readonly resumeStoppedService: (
1282
+ installIntent: string,
1283
+ priorManager: ManagedServiceManagerState,
1284
+ ) => Promise<void>;
1285
+ }
1286
+
1287
+ /** Reconcile service/command journals before any generic lifecycle mutation. */
1288
+ export async function reconcileManagedRuntimeInstallLifecycle(
1289
+ options: ReconcileManagedRuntimeInstallLifecycleOptions,
1290
+ ): Promise<void> {
1291
+ const paths = managedRuntimePaths(options.home);
1292
+ await withManagedOperationLock(paths, () =>
1293
+ reconcileManagedRuntimeInstallLifecycleLocked(paths, options),
1294
+ );
1295
+ }
1296
+
1297
+ /** Keep journal reconciliation and the manager mutation in one begin-proof lock. */
1298
+ export async function runManagedRuntimeInstallLifecycleMutation<T>(
1299
+ options: ReconcileManagedRuntimeInstallLifecycleOptions & {
1300
+ readonly mutation: () => Promise<T>;
1301
+ },
1302
+ ): Promise<T> {
1303
+ const paths = managedRuntimePaths(options.home);
1304
+ return withManagedOperationLock(paths, async () => {
1305
+ await reconcileManagedRuntimeInstallLifecycleLocked(paths, options);
1306
+ return options.mutation();
1307
+ });
1308
+ }
1309
+
1310
+ async function reconcileManagedRuntimeInstallLifecycleLocked(
1311
+ paths: ManagedRuntimePaths,
1312
+ options: ReconcileManagedRuntimeInstallLifecycleOptions,
1313
+ ): Promise<void> {
1314
+ const intent = await readInstallIntent(paths);
1315
+ if (!intent) {
1316
+ await options.reconcileDefinition();
1317
+ if (await readLegacyShimTakeover(paths)) {
1318
+ await restoreLegacyShimTakeover(paths);
1319
+ }
1320
+ return;
1321
+ }
1322
+ if (installIntentOwnerIsLive(intent)) {
1323
+ throw new Error(
1324
+ "managed host lifecycle mutation is fenced by a running installation",
1325
+ );
1326
+ }
1327
+ if (intent.phase === "attested") {
1328
+ await options.completeDefinition(intent.nonce);
1329
+ await finalizeLegacyShimTakeover(paths);
1330
+ } else {
1331
+ await options.prepareDefinitionRollback(intent.nonce);
1332
+ await restorePreInstallRuntimeSelection(paths, intent);
1333
+ await restoreLegacyShimTakeover(paths);
1334
+ await options.resumeDefinitionRollback(intent.nonce);
1335
+ if (intent.priorServiceStop !== "none") {
1336
+ await options.resumeStoppedService(intent.nonce, intent.serviceManager);
1337
+ }
1338
+ }
1339
+ await removeDurably(paths.installIntent);
1340
+ }
1341
+
1342
+ export async function completeManagedRuntimeInstall(options: {
1343
+ readonly home?: string;
1344
+ readonly installIntent: string;
1345
+ readonly beforeFinalize: (installIntent: string) => Promise<void>;
1346
+ }): Promise<void> {
1347
+ const paths = managedRuntimePaths(options.home);
1348
+ await withManagedOperationLockWaiting(paths, async () => {
1349
+ let intent = await requireInstallIntent(paths, options.installIntent, true);
1350
+ if (intent.phase === "prepared") {
1351
+ throw new Error("managed host install has not activated its runtime");
1352
+ }
1353
+ if (intent.phase !== "attested") {
1354
+ intent = { ...intent, phase: "attested" };
1355
+ await writeAtomicJson(paths.installIntent, intent);
1356
+ }
1357
+ await options.beforeFinalize(intent.nonce);
1358
+ await finalizeLegacyShimTakeover(paths);
1359
+ await removeDurably(paths.installIntent);
1360
+ });
1361
+ }
1362
+
1363
+ export async function abortManagedRuntimeInstall(options: {
1364
+ readonly home?: string;
1365
+ readonly installIntent: string;
1366
+ /** Stop the replacement and restore the prior definition without starting it. */
1367
+ readonly prepareRestore?: (installIntent: string) => Promise<void>;
1368
+ /** Resume the prior service after its exact runtime links/command are restored. */
1369
+ readonly resumeRestore?: (installIntent: string) => Promise<void>;
1370
+ /** Restart a service stopped before a definition transaction existed. */
1371
+ readonly resumeStoppedService?: (
1372
+ installIntent: string,
1373
+ priorManager: ManagedServiceManagerState,
1374
+ ) => Promise<void>;
1375
+ /** Resume an already-attested commit instead of rolling it back. */
1376
+ readonly beforeFinalize?: (installIntent: string) => Promise<void>;
1377
+ }): Promise<void> {
1378
+ const paths = managedRuntimePaths(options.home);
1379
+ await withManagedOperationLockWaiting(paths, async () => {
1380
+ const intent = await requireInstallIntent(
1381
+ paths,
1382
+ options.installIntent,
1383
+ false,
1384
+ );
1385
+ if (intent.phase === "attested") {
1386
+ if (!options.beforeFinalize) {
1387
+ throw new Error("managed host install abort cannot resume its committed service");
1388
+ }
1389
+ await options.beforeFinalize(intent.nonce);
1390
+ await finalizeLegacyShimTakeover(paths);
1391
+ await removeDurably(paths.installIntent);
1392
+ return;
1393
+ }
1394
+ // If definition rollback fails, retain the install intent and legacy
1395
+ // backup so the next abort/begin can retry without starting the wrong unit.
1396
+ if (!options.prepareRestore || !options.resumeRestore) {
1397
+ throw new Error("managed host install abort cannot restore its prior service");
1398
+ }
1399
+ await options.prepareRestore(intent.nonce);
1400
+ await restorePreInstallRuntimeSelection(paths, intent);
1401
+ await restoreLegacyShimTakeover(paths);
1402
+ await options.resumeRestore(intent.nonce);
1403
+ if (intent.priorServiceStop !== "none") {
1404
+ if (!options.resumeStoppedService) {
1405
+ throw new Error("managed host install abort cannot resume its prior service");
1406
+ }
1407
+ await options.resumeStoppedService(intent.nonce, intent.serviceManager);
1408
+ }
1409
+ await removeDurably(paths.installIntent);
1410
+ });
1411
+ }
1412
+
1413
+ export async function removeManagedRuntime(
1414
+ options: RemoveManagedRuntimeOptions,
1415
+ ): Promise<void> {
1416
+ const paths = managedRuntimePaths(options.home);
1417
+ await withManagedOperationLock(paths, async () => {
1418
+ await reconcileInstallIntentForUninstall(paths);
1419
+ let marker = await readMaintenanceMarker(paths);
1420
+ if (!marker) {
1421
+ if (await pathEntryExists(paths.uninstallPendingRoot)) {
1422
+ throw new Error(
1423
+ "managed host uninstall staging exists without its durable marker",
1424
+ );
1425
+ }
1426
+ marker = {
1427
+ schemaVersion: 1,
1428
+ operation: "uninstall",
1429
+ phase: "checking",
1430
+ purgeData: options.purgeData === true,
1431
+ startedAt: Date.now(),
1432
+ };
1433
+ await writeAtomicJson(paths.maintenancePending, marker);
1434
+ } else if (marker.purgeData !== (options.purgeData === true)) {
1435
+ throw new Error(
1436
+ `unfinished managed host uninstall must be retried ${
1437
+ marker.purgeData ? "with" : "without"
1438
+ } --purge`,
1439
+ );
1440
+ }
1441
+
1442
+ if (marker.phase === "cleanup") {
1443
+ await completeManagedRuntimeRemoval(paths, marker, options.onRemovalStep);
1444
+ return;
1445
+ }
1446
+
1447
+ // Two-sided taskUp/uninstall handshake: maintenance writes its durable gate
1448
+ // before inspecting SQLite; taskUp writes `starting` before its final gate
1449
+ // check. Whichever arrives second observes the first, so teardown cannot
1450
+ // begin while an unrecorded task proceeds into Docker side effects.
1451
+ if (!(await options.canRemove())) {
1452
+ if (marker.phase === "checking") {
1453
+ // No teardown began, so this ordinary busy refusal may safely reopen
1454
+ // admission. A retry of a previously-started teardown stays fail-closed.
1455
+ await removeDurably(paths.maintenancePending);
1456
+ }
1457
+ throw new Error(
1458
+ "refusing to uninstall while local task workloads are not terminal",
1459
+ );
1460
+ }
1461
+
1462
+ if (marker.phase === "checking") {
1463
+ marker = { ...marker, phase: "teardown" };
1464
+ await writeAtomicJson(paths.maintenancePending, marker);
1465
+ }
1466
+
1467
+ // From this point onward every failure deliberately leaves the teardown
1468
+ // marker in place. The stopped (or half-stopped) service may be retried, but
1469
+ // no new taskUp can enter between attempts.
1470
+ await options.beforeRemove?.();
1471
+ await options.afterServiceTeardown?.();
1472
+ marker = { ...marker, phase: "cleanup" };
1473
+ await writeAtomicJson(paths.maintenancePending, marker);
1474
+ await options.onRemovalStep?.("service-teardown-complete");
1475
+ await completeManagedRuntimeRemoval(paths, marker, options.onRemovalStep);
1476
+ });
1477
+ }
1478
+
1479
+ /**
1480
+ * The public bootstrap executes the freshly downloaded agent directly, so it
1481
+ * remains available even if an interrupted uninstall already removed the
1482
+ * stable commands. Only the post-service `cleanup` phase is safe to continue
1483
+ * without calling the OS service manager again; earlier phases leave the old
1484
+ * command intact and require an explicit uninstall retry.
1485
+ */
1486
+ async function reconcileInterruptedRemovalForInstall(
1487
+ paths: ManagedRuntimePaths,
1488
+ ): Promise<boolean> {
1489
+ const marker = await readMaintenanceMarker(paths);
1490
+ if (!marker) {
1491
+ if (await pathEntryExists(paths.uninstallPendingRoot)) {
1492
+ throw new Error(
1493
+ "managed host uninstall staging exists without its durable marker",
1494
+ );
1495
+ }
1496
+ return false;
1497
+ }
1498
+ if (marker.phase !== "cleanup") {
1499
+ throw new Error(
1500
+ "managed host uninstall has not completed service teardown; retry `uai-host uninstall`",
1501
+ );
1502
+ }
1503
+ await completeManagedRuntimeRemoval(paths, marker);
1504
+ return true;
1505
+ }
1506
+
1507
+ async function completeManagedRuntimeRemoval(
1508
+ paths: ManagedRuntimePaths,
1509
+ marker: MaintenanceMarker,
1510
+ onRemovalStep?: RemoveManagedRuntimeOptions["onRemovalStep"],
1511
+ ): Promise<void> {
1512
+ const rootExists = await pathEntryExists(paths.root);
1513
+ const pendingExists = await pathEntryExists(paths.uninstallPendingRoot);
1514
+ if (rootExists && pendingExists) {
1515
+ throw new Error(
1516
+ "managed host runtime and uninstall staging both exist; refusing ambiguous cleanup",
1517
+ );
1518
+ }
1519
+ if (rootExists) {
1520
+ // Validate the managed tree before moving it into the only path cleanup is
1521
+ // allowed to remove. The rename is atomic; a crash leaves the complete or
1522
+ // partially-removed runtime at a deterministic location for bootstrap to
1523
+ // reconcile, never in an unrecorded random directory.
1524
+ await ensureManagedDirectories(paths);
1525
+ await rename(paths.root, paths.uninstallPendingRoot);
1526
+ await syncDirectory(path.dirname(paths.root));
1527
+ }
1528
+ await onRemovalStep?.("runtime-detached");
1529
+
1530
+ await removeStableCommands(paths);
1531
+ await onRemovalStep?.("commands-removed");
1532
+
1533
+ await rm(paths.uninstallPendingRoot, { recursive: true, force: true });
1534
+ await syncDirectory(path.dirname(paths.uninstallPendingRoot));
1535
+ await onRemovalStep?.("runtime-removed");
1536
+
1537
+ if (marker.purgeData) {
1538
+ await purgeManagedData(paths);
1539
+ await syncDirectory(path.dirname(paths.data));
1540
+ await onRemovalStep?.("data-removed");
1541
+ }
1542
+
1543
+ await onRemovalStep?.("cleanup-complete");
1544
+ await removeDurably(paths.maintenancePending);
1545
+ }
1546
+
1547
+ /**
1548
+ * Purge host data without deleting the operation lock that guards this
1549
+ * transaction. In the supported installed layout `paths.data` is a child of
1550
+ * the home directory, but an explicit UAI_HOME may intentionally make the
1551
+ * data root equal to that home. Removing the containing directory while the
1552
+ * lock is held changes its owner metadata and makes the unlock step fail
1553
+ * closed. Delete children in that topology, preserving only the exact lock
1554
+ * directory until the outer transaction releases it.
1555
+ */
1556
+ async function purgeManagedData(paths: ManagedRuntimePaths): Promise<void> {
1557
+ const dataRoot = path.resolve(paths.data);
1558
+ const lockRoot = path.resolve(paths.operationLock);
1559
+ const lockParent = path.dirname(lockRoot);
1560
+ if (dataRoot !== lockParent && !isInside(dataRoot, lockRoot)) {
1561
+ await rm(paths.data, { recursive: true, force: true });
1562
+ return;
1563
+ }
1564
+ let entries: string[];
1565
+ try {
1566
+ entries = await readdir(dataRoot);
1567
+ } catch (error) {
1568
+ if (isMissing(error)) return;
1569
+ throw error;
1570
+ }
1571
+ for (const entry of entries) {
1572
+ const candidate = path.join(dataRoot, entry);
1573
+ if (path.resolve(candidate) === lockRoot) continue;
1574
+ await rm(candidate, { recursive: true, force: true });
1575
+ }
1576
+ }
1577
+
1578
+ async function requireInstallIntent(
1579
+ paths: ManagedRuntimePaths,
1580
+ nonce: string,
1581
+ requireLiveOwner: boolean,
1582
+ ): Promise<InstallIntentMarker> {
1583
+ if (!INSTALL_INTENT_NONCE.test(nonce)) {
1584
+ throw new Error("managed host install intent capability is invalid");
1585
+ }
1586
+ const intent = await readInstallIntent(paths);
1587
+ if (!intent || intent.nonce !== nonce) {
1588
+ throw new Error("managed host install intent does not match");
1589
+ }
1590
+ if (requireLiveOwner && !installIntentOwnerIsLive(intent)) {
1591
+ throw new Error(
1592
+ "managed host installer owner exited; begin the installation again",
1593
+ );
1594
+ }
1595
+ return intent;
1596
+ }
1597
+
1598
+ async function assertNoManagedInstallIntent(
1599
+ paths: ManagedRuntimePaths,
1600
+ operation: string,
1601
+ ): Promise<void> {
1602
+ if (await readInstallIntent(paths)) {
1603
+ throw new Error(
1604
+ `managed host ${operation} is fenced by an installation intent`,
1605
+ );
1606
+ }
1607
+ }
1608
+
1609
+ async function advanceInstallIntentToActivated(
1610
+ paths: ManagedRuntimePaths,
1611
+ expected: InstallIntentMarker,
1612
+ ): Promise<void> {
1613
+ const current = await readInstallIntent(paths);
1614
+ if (!current || current.nonce !== expected.nonce) {
1615
+ throw new Error("managed host install intent changed during activation");
1616
+ }
1617
+ if (!installIntentOwnerIsLive(current)) {
1618
+ throw new Error("managed host installer owner exited during activation");
1619
+ }
1620
+ if (current.phase !== "activated") {
1621
+ await writeAtomicJson(paths.installIntent, {
1622
+ ...current,
1623
+ phase: "activated",
1624
+ } satisfies InstallIntentMarker);
1625
+ }
1626
+ }
1627
+
1628
+ async function restorePreInstallRuntimeSelection(
1629
+ paths: ManagedRuntimePaths,
1630
+ intent: InstallIntentMarker,
1631
+ ): Promise<void> {
1632
+ if (!intent.rootExisted) {
1633
+ if (await pathEntryExists(paths.root)) {
1634
+ await ensureManagedDirectories(paths);
1635
+ }
1636
+ await removeStableCommands(paths);
1637
+ await rm(paths.root, { recursive: true, force: true });
1638
+ await syncDirectory(path.dirname(paths.root));
1639
+ return;
1640
+ }
1641
+
1642
+ if (!(await pathEntryExists(paths.root))) {
1643
+ throw new Error("pre-install managed runtime disappeared during rollback");
1644
+ }
1645
+ await ensureManagedDirectories(paths);
1646
+ if (intent.currentVersion) {
1647
+ await atomicVersionLink(paths.current, intent.currentVersion);
1648
+ } else {
1649
+ await removeDurably(paths.current);
1650
+ }
1651
+ if (intent.previousVersion) {
1652
+ await atomicVersionLink(paths.previous, intent.previousVersion);
1653
+ } else {
1654
+ await removeDurably(paths.previous);
1655
+ }
1656
+ if (intent.activationPending) {
1657
+ await writeAtomicJson(paths.activationPending, intent.activationPending);
1658
+ } else {
1659
+ await removeDurably(paths.activationPending);
1660
+ }
1661
+
1662
+ if (intent.shimOwnership === "missing") {
1663
+ await removeOwnedSymlink(
1664
+ paths.shim,
1665
+ path.join(paths.runtime, "current", "bin", "uai-host"),
1666
+ );
1667
+ }
1668
+ if (!intent.recoveryShimExisted) {
1669
+ await removeOwnedSymlink(paths.recoveryShim, paths.recoveryEntrypoint);
1670
+ }
1671
+ if (!intent.recoveryEntrypointExisted) {
1672
+ const exists = await assertRecoveryEntrypointReplaceable(paths);
1673
+ if (exists) await removeDurably(paths.recoveryEntrypoint);
1674
+ }
1675
+ }
1676
+
1677
+ async function reconcileInstallIntentForUninstall(
1678
+ paths: ManagedRuntimePaths,
1679
+ ): Promise<void> {
1680
+ const intent = await readInstallIntent(paths);
1681
+ if (!intent) {
1682
+ if (await readLegacyShimTakeover(paths)) {
1683
+ throw new Error(
1684
+ "managed host legacy command takeover has no install intent; rerun the installer",
1685
+ );
1686
+ }
1687
+ return;
1688
+ }
1689
+ if (installIntentOwnerIsLive(intent)) {
1690
+ throw new Error("refusing to uninstall while managed host installation is running");
1691
+ }
1692
+ // Service-definition recovery must precede legacy command restoration. Only
1693
+ // the bundled begin/abort commands have that platform hook, so a dead owner
1694
+ // remains fenced here until the public installer safely adopts the intent.
1695
+ throw new Error(
1696
+ "managed host installation was interrupted; rerun the installer before uninstalling",
1697
+ );
1698
+ }
1699
+
1700
+ export async function validateInstalledRuntime(
1701
+ directory: string,
1702
+ expectedVersion: string,
1703
+ expectedPlatform?: string,
1704
+ ): Promise<ResolvedRuntimeDescriptor> {
1705
+ const root = await realpath(directory);
1706
+ const raw = JSON.parse(await readFile(path.join(root, "runtime.json"), "utf8")) as {
1707
+ platform?: unknown;
1708
+ };
1709
+ const platform = expectedPlatform ??
1710
+ (typeof raw.platform === "string" && isReleasePlatform(raw.platform)
1711
+ ? raw.platform
1712
+ : null);
1713
+ if (!platform) throw new Error("managed runtime descriptor platform is invalid");
1714
+ const descriptor = validateRuntimeDescriptor(raw, {
1715
+ version: expectedVersion,
1716
+ platform,
1717
+ });
1718
+ const topLevel = (await readdir(root)).sort();
1719
+ if (topLevel.join("\0") !== ["agent", "bin", "node", "runtime.json"].join("\0")) {
1720
+ throw new Error(`managed runtime ${expectedVersion} has an invalid top-level layout`);
1721
+ }
1722
+ const requiredFiles: Array<readonly [string, boolean]> = [
1723
+ ["agent/bin/uai-host.mjs", false],
1724
+ // Host-spawned bash entrypoints: packaging once shipped these 0644
1725
+ // (pnpm-store mode normalization) and the first installed task died with
1726
+ // spawn EACCES. Executability is part of the validated contract now.
1727
+ ["agent/scripts/agent/task-up.sh", true],
1728
+ ["agent/scripts/agent/task-down.sh", true],
1729
+ ["agent/scripts/agent/task-status.sh", true],
1730
+ ["bin/uai-host", true],
1731
+ ["node/bin/node", true],
1732
+ ["runtime.json", false],
1733
+ ];
1734
+ const capabilities = await resolveInstalledRuntimeCapabilities(root, descriptor.platform);
1735
+ if (capabilities.capabilitySchemaVersion === 3) {
1736
+ const bundled = capabilities.bundledRuntime;
1737
+ requiredFiles.push(
1738
+ [BUNDLED_RUNTIME_CAPABILITY, false],
1739
+ [bundled.layout.assetManifest, false],
1740
+ [bundled.layout.containerCli, true],
1741
+ [bundled.layout.apiserver, true],
1742
+ ...APPLE_CONTAINER_PLUGINS.map(
1743
+ (plugin) =>
1744
+ [`${bundled.layout.plugins}/${plugin}/bin/${plugin}`, true] as const,
1745
+ ),
1746
+ [bundled.layout.kernel, false],
1747
+ ...REQUIRED_BUNDLED_RUNTIME_LICENSES.map(
1748
+ (license) => [`${bundled.layout.licenses}/${license}`, false] as const,
1749
+ ),
1750
+ );
1751
+ }
1752
+ for (const [relative, executable] of requiredFiles) {
1753
+ const absolute = path.join(root, ...relative.split("/"));
1754
+ const stat = await lstat(absolute);
1755
+ if (!stat.isFile()) throw new Error(`managed runtime is missing regular file ${relative}`);
1756
+ if (executable && (stat.mode & 0o111) === 0) {
1757
+ throw new Error(`managed runtime file is not executable: ${relative}`);
1758
+ }
1759
+ const resolved = await realpath(absolute);
1760
+ if (!isInside(root, resolved)) {
1761
+ throw new Error(`managed runtime file escapes its version directory: ${relative}`);
1762
+ }
1763
+ }
1764
+ if (capabilities.capabilitySchemaVersion === 3) {
1765
+ await validateInstalledRuntimeAssets(root, capabilities);
1766
+ }
1767
+ return { ...descriptor, ...capabilities };
1768
+ }
1769
+
1770
+ async function resolveInstalledRuntimeCapabilities(
1771
+ root: string,
1772
+ platform: string,
1773
+ ): Promise<{ readonly capabilitySchemaVersion: 1 } | RuntimeCapabilityDescriptorV3> {
1774
+ const bundledRoot = path.join(root, ...BUNDLED_RUNTIME_ROOT.split("/"));
1775
+ let bundledMetadata;
1776
+ try {
1777
+ bundledMetadata = await lstat(bundledRoot);
1778
+ } catch (error) {
1779
+ if (isMissing(error)) return { capabilitySchemaVersion: 1 };
1780
+ throw error;
1781
+ }
1782
+ if (!platform.startsWith("darwin-")) {
1783
+ throw new Error("managed Linux runtime contains bundled Darwin assets");
1784
+ }
1785
+ if (!bundledMetadata.isDirectory() || bundledMetadata.isSymbolicLink()) {
1786
+ throw new Error("managed runtime bundled capability root is invalid");
1787
+ }
1788
+ const capabilityPath = path.join(root, ...BUNDLED_RUNTIME_CAPABILITY.split("/"));
1789
+ const metadata = await lstat(capabilityPath);
1790
+ if (
1791
+ !metadata.isFile() ||
1792
+ metadata.isSymbolicLink() ||
1793
+ metadata.size > MAX_OPERATION_LOCK_BYTES ||
1794
+ !isInside(root, await realpath(capabilityPath))
1795
+ ) {
1796
+ throw new Error("managed runtime capability descriptor is invalid");
1797
+ }
1798
+ let raw: unknown;
1799
+ try {
1800
+ raw = JSON.parse(await readFile(capabilityPath, "utf8"));
1801
+ } catch {
1802
+ throw new Error("managed runtime capability descriptor is invalid JSON");
1803
+ }
1804
+ return validateRuntimeCapabilityDescriptor(raw, platform);
1805
+ }
1806
+
1807
+ async function validateInstalledRuntimeAssets(
1808
+ root: string,
1809
+ capabilities: RuntimeCapabilityDescriptorV3,
1810
+ ): Promise<RuntimeAssetManifest> {
1811
+ const descriptor = capabilities.bundledRuntime;
1812
+ const manifestPath = path.join(
1813
+ root,
1814
+ ...descriptor.layout.assetManifest.split("/"),
1815
+ );
1816
+ const manifestStat = await lstat(manifestPath);
1817
+ if (!manifestStat.isFile() || manifestStat.isSymbolicLink() || manifestStat.size > 2 * 1024 * 1024) {
1818
+ throw new Error("managed runtime asset manifest is invalid");
1819
+ }
1820
+ const manifest = validateRuntimeAssetManifest(
1821
+ JSON.parse(await readFile(manifestPath, "utf8")),
1822
+ descriptor.assetFingerprint,
1823
+ );
1824
+ const sourceRoot = path.join(
1825
+ root,
1826
+ ...descriptor.layout.assetSourceRoot.split("/"),
1827
+ );
1828
+ const seen = new Set<string>();
1829
+ for (const entry of manifest.entries) {
1830
+ const absolute = path.join(sourceRoot, ...entry.path.split("/"));
1831
+ const metadata = await lstat(absolute);
1832
+ if (
1833
+ !metadata.isFile() ||
1834
+ metadata.isSymbolicLink() ||
1835
+ metadata.size !== entry.size ||
1836
+ (metadata.mode & 0o777) !== entry.mode ||
1837
+ !isInside(root, await realpath(absolute))
1838
+ ) {
1839
+ throw new Error(`managed runtime asset does not match its manifest: ${entry.path}`);
1840
+ }
1841
+ const hash = createHash("sha256").update(await readFile(absolute)).digest("hex");
1842
+ if (hash !== entry.sha256) {
1843
+ throw new Error(`managed runtime asset digest does not match its manifest: ${entry.path}`);
1844
+ }
1845
+ seen.add(entry.path);
1846
+ }
1847
+ for (const selectedRoot of [
1848
+ "apple-container",
1849
+ "host-assets",
1850
+ "kernel",
1851
+ "licenses",
1852
+ ]) {
1853
+ async function visit(relativeDirectory: string): Promise<void> {
1854
+ const directory = path.join(sourceRoot, ...relativeDirectory.split("/"));
1855
+ for (const child of await readdir(directory, { withFileTypes: true })) {
1856
+ const relative = `${relativeDirectory}/${child.name}`;
1857
+ const absolute = path.join(sourceRoot, ...relative.split("/"));
1858
+ const metadata = await lstat(absolute);
1859
+ if (metadata.isDirectory() && !metadata.isSymbolicLink()) {
1860
+ await visit(relative);
1861
+ } else if (!metadata.isFile() || metadata.isSymbolicLink() || !seen.has(relative)) {
1862
+ throw new Error(`managed runtime asset tree is not closed by its manifest: ${relative}`);
1863
+ }
1864
+ }
1865
+ }
1866
+ await visit(selectedRoot);
1867
+ }
1868
+ await validateInstalledBundledSubtreeClosure(root, capabilities, manifest);
1869
+ return manifest;
1870
+ }
1871
+
1872
+ async function validateInstalledBundledSubtreeClosure(
1873
+ root: string,
1874
+ capabilities: RuntimeCapabilityDescriptorV3,
1875
+ manifest: RuntimeAssetManifest,
1876
+ ): Promise<void> {
1877
+ const bundled = capabilities.bundledRuntime;
1878
+ // Every staged payload file is a manifest entry; only the two metadata
1879
+ // documents live outside the digest closure.
1880
+ const expected = new Set([
1881
+ BUNDLED_RUNTIME_CAPABILITY,
1882
+ bundled.layout.assetManifest,
1883
+ ...manifest.entries.map(
1884
+ (entry) => `${bundled.layout.assetSourceRoot}/${entry.path}`,
1885
+ ),
1886
+ ]);
1887
+ const actual = new Set<string>();
1888
+ const bundledRoot = path.join(root, ...BUNDLED_RUNTIME_ROOT.split("/"));
1889
+ async function visit(relativeDirectory: string): Promise<void> {
1890
+ for (const child of await readdir(path.join(bundledRoot, relativeDirectory), {
1891
+ withFileTypes: true,
1892
+ })) {
1893
+ const relative = relativeDirectory ? `${relativeDirectory}/${child.name}` : child.name;
1894
+ const absolute = path.join(bundledRoot, ...relative.split("/"));
1895
+ const member = `${BUNDLED_RUNTIME_ROOT}/${relative}`;
1896
+ const metadata = await lstat(absolute);
1897
+ if (metadata.isDirectory() && !metadata.isSymbolicLink()) {
1898
+ if (![...expected].some((file) => file.startsWith(`${member}/`))) {
1899
+ throw new Error(`managed runtime bundled subtree has an unexpected directory: ${member}`);
1900
+ }
1901
+ await visit(relative);
1902
+ } else if (
1903
+ metadata.isFile() &&
1904
+ !metadata.isSymbolicLink() &&
1905
+ isInside(root, await realpath(absolute))
1906
+ ) {
1907
+ actual.add(member);
1908
+ } else {
1909
+ throw new Error(`managed runtime bundled subtree has an unsafe entry: ${member}`);
1910
+ }
1911
+ }
1912
+ }
1913
+ await visit("");
1914
+ const compare = (left: string, right: string): number =>
1915
+ Buffer.compare(Buffer.from(left), Buffer.from(right));
1916
+ if (
1917
+ [...actual].sort(compare).join("\0") !==
1918
+ [...expected].sort(compare).join("\0")
1919
+ ) {
1920
+ throw new Error("managed runtime bundled subtree is not closed by its descriptor and manifest");
1921
+ }
1922
+ }
1923
+
1924
+ export interface ResolvedBundledRuntime {
1925
+ readonly root: string;
1926
+ readonly descriptor: ResolvedRuntimeDescriptor & RuntimeCapabilityDescriptorV3;
1927
+ readonly containerCliPath: string;
1928
+ readonly apiserverPath: string;
1929
+ readonly pluginsPath: string;
1930
+ readonly kernelPath: string;
1931
+ readonly runtimeAssetSourceRoot: string;
1932
+ readonly runtimeAssetManifestPath: string;
1933
+ readonly runtimeAssetFingerprint: string;
1934
+ }
1935
+
1936
+ export async function resolveBundledRuntimeFromEnvironment(
1937
+ env: NodeJS.ProcessEnv = process.env,
1938
+ ): Promise<ResolvedBundledRuntime | null> {
1939
+ const configured = env.UAI_HOST_RUNTIME_ROOT;
1940
+ if (!configured) return null;
1941
+ if (!path.isAbsolute(configured)) {
1942
+ throw new Error("UAI_HOST_RUNTIME_ROOT must be absolute");
1943
+ }
1944
+ const root = await realpath(configured);
1945
+ const raw = JSON.parse(await readFile(path.join(root, "runtime.json"), "utf8")) as {
1946
+ hostVersion?: unknown;
1947
+ platform?: unknown;
1948
+ };
1949
+ if (typeof raw.hostVersion !== "string" || !VERSION.test(raw.hostVersion)) {
1950
+ throw new Error("managed runtime descriptor version is invalid");
1951
+ }
1952
+ if (typeof raw.platform !== "string" || !isReleasePlatform(raw.platform)) {
1953
+ throw new Error("managed runtime descriptor platform is invalid");
1954
+ }
1955
+ const descriptor = await validateInstalledRuntime(
1956
+ root,
1957
+ raw.hostVersion,
1958
+ raw.platform,
1959
+ );
1960
+ if (descriptor.capabilitySchemaVersion === 1) return null;
1961
+ const resolveInside = async (relative: string): Promise<string> => {
1962
+ const resolved = await realpath(path.join(root, ...relative.split("/")));
1963
+ if (!isInside(root, resolved)) {
1964
+ throw new Error(`managed bundled runtime path escapes its generation: ${relative}`);
1965
+ }
1966
+ return resolved;
1967
+ };
1968
+ return {
1969
+ root,
1970
+ descriptor,
1971
+ containerCliPath: await resolveInside(
1972
+ descriptor.bundledRuntime.layout.containerCli,
1973
+ ),
1974
+ apiserverPath: await resolveInside(descriptor.bundledRuntime.layout.apiserver),
1975
+ pluginsPath: await resolveInside(descriptor.bundledRuntime.layout.plugins),
1976
+ kernelPath: await resolveInside(descriptor.bundledRuntime.layout.kernel),
1977
+ runtimeAssetSourceRoot: await resolveInside(
1978
+ descriptor.bundledRuntime.layout.assetSourceRoot,
1979
+ ),
1980
+ runtimeAssetManifestPath: await resolveInside(
1981
+ descriptor.bundledRuntime.layout.assetManifest,
1982
+ ),
1983
+ runtimeAssetFingerprint: descriptor.bundledRuntime.assetFingerprint,
1984
+ };
1985
+ }
1986
+
1987
+ async function ensureManagedDirectories(paths: ManagedRuntimePaths): Promise<void> {
1988
+ await mkdir(paths.root, { recursive: true, mode: 0o700 });
1989
+ const rootStat = await lstat(paths.root);
1990
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
1991
+ throw new Error(`${paths.root} is not a private managed-host directory`);
1992
+ }
1993
+ await chmod(paths.root, 0o700);
1994
+ await mkdir(paths.runtime, { recursive: false, mode: 0o700 }).catch((error) => {
1995
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
1996
+ });
1997
+ const runtimeStat = await lstat(paths.runtime);
1998
+ if (!runtimeStat.isDirectory() || runtimeStat.isSymbolicLink()) {
1999
+ throw new Error(`${paths.runtime} is not a managed runtime directory`);
2000
+ }
2001
+ await chmod(paths.runtime, 0o700);
2002
+ }
2003
+
2004
+ async function capturePreInstallState(paths: ManagedRuntimePaths): Promise<
2005
+ Pick<
2006
+ InstallIntentMarker,
2007
+ | "rootExisted"
2008
+ | "currentVersion"
2009
+ | "previousVersion"
2010
+ | "activationPending"
2011
+ | "shimOwnership"
2012
+ | "recoveryShimExisted"
2013
+ | "recoveryEntrypointExisted"
2014
+ >
2015
+ > {
2016
+ const rootExisted = await pathEntryExists(paths.root);
2017
+ if (rootExisted) await ensureManagedDirectories(paths);
2018
+ const currentVersion = await readVersionLink(paths.current, paths.runtime);
2019
+ const previousVersion = await readVersionLink(paths.previous, paths.runtime);
2020
+ let activationPending: ActivationMarker | null = null;
2021
+ try {
2022
+ activationPending = parseActivationMarker(
2023
+ await readBoundedRegularFile(paths.activationPending),
2024
+ );
2025
+ } catch (error) {
2026
+ if (!isMissing(error)) throw error;
2027
+ }
2028
+
2029
+ let shimOwnership: InstallIntentMarker["shimOwnership"] = "missing";
2030
+ if (await pathEntryExists(paths.shim)) {
2031
+ if (await isExactManagedShim(paths)) {
2032
+ shimOwnership = "managed";
2033
+ } else {
2034
+ await proveLegacyNpmShim(paths);
2035
+ shimOwnership = "legacy";
2036
+ }
2037
+ }
2038
+
2039
+ const recoveryShimExisted = await pathEntryExists(paths.recoveryShim);
2040
+ if (recoveryShimExisted && !(await isExactRecoveryShim(paths))) {
2041
+ throw new Error(
2042
+ `refusing to replace unrelated host recovery command at ${paths.recoveryShim}`,
2043
+ );
2044
+ }
2045
+ const recoveryEntrypointExisted = await assertRecoveryEntrypointReplaceable(
2046
+ paths,
2047
+ );
2048
+ return {
2049
+ rootExisted,
2050
+ currentVersion,
2051
+ previousVersion,
2052
+ activationPending,
2053
+ shimOwnership,
2054
+ recoveryShimExisted,
2055
+ recoveryEntrypointExisted,
2056
+ };
2057
+ }
2058
+
2059
+ /** Record a proven npm-owned collision without moving the still-needed CLI. */
2060
+ async function recordLegacyShimTakeover(
2061
+ paths: ManagedRuntimePaths,
2062
+ ): Promise<boolean> {
2063
+ const existing = await readLegacyShimTakeover(paths);
2064
+ if (existing) {
2065
+ await validateLegacyTakeoverState(paths, existing);
2066
+ return true;
2067
+ }
2068
+ if (await pathEntryExists(paths.legacyShimBackup)) {
2069
+ throw new Error("legacy host command backup exists without its durable journal");
2070
+ }
2071
+ let stat;
2072
+ try {
2073
+ stat = await lstat(paths.shim);
2074
+ } catch (error) {
2075
+ if (isMissing(error)) return false;
2076
+ throw error;
2077
+ }
2078
+ if (!stat.isSymbolicLink()) {
2079
+ throw new Error(`refusing to replace non-symlink host command at ${paths.shim}`);
2080
+ }
2081
+ if (await isExactManagedShim(paths)) return false;
2082
+ const proof = await proveLegacyNpmShim(paths);
2083
+ await writeAtomicJson(paths.legacyShimTakeover, {
2084
+ schemaVersion: 1,
2085
+ operation: "legacy-shim-takeover",
2086
+ phase: "recorded",
2087
+ ...proof,
2088
+ startedAt: Date.now(),
2089
+ } satisfies LegacyShimTakeoverMarker);
2090
+ return true;
2091
+ }
2092
+
2093
+ async function parkLegacyShimTakeover(paths: ManagedRuntimePaths): Promise<void> {
2094
+ let marker = await readLegacyShimTakeover(paths);
2095
+ if (!marker) return;
2096
+ if (marker.phase === "restoring" || marker.phase === "finalizing") {
2097
+ throw new Error("legacy host command takeover is settling another outcome");
2098
+ }
2099
+ const backupExists = await pathEntryExists(paths.legacyShimBackup);
2100
+ if (!backupExists) {
2101
+ await assertExactLegacySymlink(paths.shim, marker);
2102
+ await rename(paths.shim, paths.legacyShimBackup);
2103
+ await syncDirectory(path.dirname(paths.shim));
2104
+ } else {
2105
+ await assertExactLegacySymlink(paths.legacyShimBackup, marker);
2106
+ await assertMissingOrManagedShim(paths);
2107
+ }
2108
+ if (marker.phase !== "parked") {
2109
+ marker = { ...marker, phase: "parked" };
2110
+ await writeAtomicJson(paths.legacyShimTakeover, marker);
2111
+ }
2112
+ }
2113
+
2114
+ async function restoreLegacyShimTakeover(paths: ManagedRuntimePaths): Promise<void> {
2115
+ let marker = await readLegacyShimTakeover(paths);
2116
+ if (!marker) {
2117
+ if (await pathEntryExists(paths.legacyShimBackup)) {
2118
+ throw new Error("legacy host command backup exists without its durable journal");
2119
+ }
2120
+ return;
2121
+ }
2122
+ if (marker.phase === "finalizing") {
2123
+ await finalizeLegacyShimTakeover(paths);
2124
+ return;
2125
+ }
2126
+
2127
+ const backupExists = await pathEntryExists(paths.legacyShimBackup);
2128
+ if (!backupExists && marker.phase === "recorded") {
2129
+ await assertExactLegacySymlink(paths.shim, marker);
2130
+ await removeDurably(paths.legacyShimTakeover);
2131
+ return;
2132
+ }
2133
+ if (marker.phase !== "restoring") {
2134
+ marker = { ...marker, phase: "restoring" };
2135
+ await writeAtomicJson(paths.legacyShimTakeover, marker);
2136
+ }
2137
+
2138
+ if (await pathEntryExists(paths.legacyShimBackup)) {
2139
+ await assertExactLegacySymlink(paths.legacyShimBackup, marker);
2140
+ await assertMissingOrManagedShim(paths);
2141
+ // Atomic replacement avoids a window where the prior service has no CLI.
2142
+ await rename(paths.legacyShimBackup, paths.shim);
2143
+ await syncDirectory(path.dirname(paths.shim));
2144
+ } else {
2145
+ // Crash after the rename but before journal deletion.
2146
+ await assertExactLegacySymlink(paths.shim, marker);
2147
+ }
2148
+ await removeDurably(paths.legacyShimTakeover);
2149
+ }
2150
+
2151
+ async function finalizeLegacyShimTakeover(paths: ManagedRuntimePaths): Promise<void> {
2152
+ let marker = await readLegacyShimTakeover(paths);
2153
+ if (!marker) {
2154
+ if (await pathEntryExists(paths.legacyShimBackup)) {
2155
+ throw new Error("legacy host command backup exists without its durable journal");
2156
+ }
2157
+ return;
2158
+ }
2159
+ if (marker.phase === "restoring") {
2160
+ throw new Error("legacy host command restoration is incomplete");
2161
+ }
2162
+ if (marker.phase === "recorded") {
2163
+ throw new Error("legacy host command was not parked by runtime activation");
2164
+ }
2165
+ await assertExactManagedShim(paths);
2166
+ if (marker.phase !== "finalizing") {
2167
+ marker = { ...marker, phase: "finalizing" };
2168
+ await writeAtomicJson(paths.legacyShimTakeover, marker);
2169
+ }
2170
+ if (await pathEntryExists(paths.legacyShimBackup)) {
2171
+ await assertExactLegacySymlink(paths.legacyShimBackup, marker);
2172
+ await unlink(paths.legacyShimBackup);
2173
+ await syncDirectory(path.dirname(paths.legacyShimBackup));
2174
+ }
2175
+ await removeDurably(paths.legacyShimTakeover);
2176
+ }
2177
+
2178
+ async function validateLegacyTakeoverState(
2179
+ paths: ManagedRuntimePaths,
2180
+ marker: LegacyShimTakeoverMarker,
2181
+ ): Promise<void> {
2182
+ if (marker.phase === "recorded") {
2183
+ if (await pathEntryExists(paths.legacyShimBackup)) {
2184
+ await assertExactLegacySymlink(paths.legacyShimBackup, marker);
2185
+ await assertMissingOrManagedShim(paths);
2186
+ } else {
2187
+ await assertExactLegacySymlink(paths.shim, marker);
2188
+ }
2189
+ return;
2190
+ }
2191
+ if (marker.phase === "parked") {
2192
+ await assertExactLegacySymlink(paths.legacyShimBackup, marker);
2193
+ await assertMissingOrManagedShim(paths);
2194
+ return;
2195
+ }
2196
+ throw new Error("legacy host command takeover has an incomplete terminal phase");
2197
+ }
2198
+
2199
+ async function proveLegacyNpmShim(paths: ManagedRuntimePaths): Promise<{
2200
+ originalTarget: string;
2201
+ resolvedTarget: string;
2202
+ packageRoot: string;
2203
+ }> {
2204
+ const originalTarget = await readlink(paths.shim);
2205
+ const resolvedTarget = path.resolve(path.dirname(paths.shim), originalTarget);
2206
+ const targetStat = await lstat(resolvedTarget);
2207
+ if (!targetStat.isFile() || targetStat.isSymbolicLink()) {
2208
+ throw new Error("legacy host command does not target a regular package bin");
2209
+ }
2210
+ const canonicalTarget = await realpath(resolvedTarget);
2211
+ let directory = path.dirname(canonicalTarget);
2212
+ for (;;) {
2213
+ const packageFile = path.join(directory, "package.json");
2214
+ let bytes: Buffer | null = null;
2215
+ try {
2216
+ bytes = await readBoundedRegularFile(packageFile, MAX_PACKAGE_JSON_BYTES);
2217
+ } catch (error) {
2218
+ if (!isMissing(error)) throw error;
2219
+ }
2220
+ if (bytes) {
2221
+ let raw: unknown;
2222
+ try {
2223
+ raw = JSON.parse(bytes.toString("utf8"));
2224
+ } catch {
2225
+ throw new Error("legacy host command package.json is invalid");
2226
+ }
2227
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
2228
+ throw new Error("legacy host command package.json is invalid");
2229
+ }
2230
+ const manifest = raw as Record<string, unknown>;
2231
+ const bin = manifest.bin;
2232
+ if (
2233
+ manifest.name !== "@runuai/host" ||
2234
+ !bin ||
2235
+ typeof bin !== "object" ||
2236
+ Array.isArray(bin) ||
2237
+ typeof (bin as Record<string, unknown>)["uai-host"] !== "string"
2238
+ ) {
2239
+ throw new Error(
2240
+ "host command collision is not the @runuai/host package bin",
2241
+ );
2242
+ }
2243
+ const mapped = path.resolve(
2244
+ directory,
2245
+ (bin as Record<string, string>)["uai-host"]!,
2246
+ );
2247
+ if (!isInside(directory, mapped)) {
2248
+ throw new Error("legacy host command package bin escapes its package");
2249
+ }
2250
+ const mappedStat = await lstat(mapped);
2251
+ if (!mappedStat.isFile() || mappedStat.isSymbolicLink()) {
2252
+ throw new Error("legacy host command package bin is not a regular file");
2253
+ }
2254
+ if ((await realpath(mapped)) !== canonicalTarget) {
2255
+ throw new Error("legacy host command does not match its package bin mapping");
2256
+ }
2257
+ return { originalTarget, resolvedTarget, packageRoot: directory };
2258
+ }
2259
+ const parent = path.dirname(directory);
2260
+ if (parent === directory) break;
2261
+ directory = parent;
2262
+ }
2263
+ throw new Error("legacy host command has no owning package.json");
2264
+ }
2265
+
2266
+ async function assertExactLegacySymlink(
2267
+ link: string,
2268
+ marker: LegacyShimTakeoverMarker,
2269
+ ): Promise<void> {
2270
+ const stat = await lstat(link);
2271
+ if (!stat.isSymbolicLink()) {
2272
+ throw new Error("legacy host command backup is not a symlink");
2273
+ }
2274
+ const target = await readlink(link);
2275
+ if (
2276
+ target !== marker.originalTarget ||
2277
+ path.resolve(path.dirname(link), target) !== marker.resolvedTarget
2278
+ ) {
2279
+ throw new Error("legacy host command changed during managed takeover");
2280
+ }
2281
+ }
2282
+
2283
+ async function isExactManagedShim(paths: ManagedRuntimePaths): Promise<boolean> {
2284
+ try {
2285
+ const stat = await lstat(paths.shim);
2286
+ if (!stat.isSymbolicLink()) return false;
2287
+ const target = await readlink(paths.shim);
2288
+ return (
2289
+ path.resolve(path.dirname(paths.shim), target) ===
2290
+ path.join(paths.runtime, "current", "bin", "uai-host")
2291
+ );
2292
+ } catch (error) {
2293
+ if (isMissing(error)) return false;
2294
+ throw error;
2295
+ }
2296
+ }
2297
+
2298
+ async function isExactRecoveryShim(paths: ManagedRuntimePaths): Promise<boolean> {
2299
+ try {
2300
+ const stat = await lstat(paths.recoveryShim);
2301
+ if (!stat.isSymbolicLink()) return false;
2302
+ const target = await readlink(paths.recoveryShim);
2303
+ return (
2304
+ path.resolve(path.dirname(paths.recoveryShim), target) ===
2305
+ paths.recoveryEntrypoint
2306
+ );
2307
+ } catch (error) {
2308
+ if (isMissing(error)) return false;
2309
+ throw error;
2310
+ }
2311
+ }
2312
+
2313
+ async function assertExactManagedShim(paths: ManagedRuntimePaths): Promise<void> {
2314
+ if (!(await isExactManagedShim(paths))) {
2315
+ throw new Error("managed host command is missing during takeover settlement");
2316
+ }
2317
+ }
2318
+
2319
+ async function assertMissingOrManagedShim(paths: ManagedRuntimePaths): Promise<void> {
2320
+ if (!(await pathEntryExists(paths.shim))) return;
2321
+ if (await isExactManagedShim(paths)) return;
2322
+ throw new Error("refusing to replace unrelated host command during takeover");
2323
+ }
2324
+
2325
+ async function ensureStableCommands(paths: ManagedRuntimePaths): Promise<void> {
2326
+ await assertStableCommandsReplaceable(paths);
2327
+ await ensureRecoveryEntrypoint(paths);
2328
+ await ensureStableShim(paths);
2329
+ await ensureRecoveryShim(paths);
2330
+ }
2331
+
2332
+ async function assertStableCommandsReplaceable(
2333
+ paths: ManagedRuntimePaths,
2334
+ ): Promise<void> {
2335
+ await assertOwnedSymlinkReplaceable(
2336
+ paths.shim,
2337
+ path.join(paths.runtime, "current", "bin", "uai-host"),
2338
+ "host command",
2339
+ );
2340
+ await assertRecoveryEntrypointReplaceable(paths);
2341
+ await assertOwnedSymlinkReplaceable(
2342
+ paths.recoveryShim,
2343
+ paths.recoveryEntrypoint,
2344
+ "host recovery command",
2345
+ );
2346
+ }
2347
+
2348
+ async function ensureStableShim(paths: ManagedRuntimePaths): Promise<void> {
2349
+ await mkdir(path.dirname(paths.shim), { recursive: true, mode: 0o755 });
2350
+ const expectedTarget = path.join(paths.runtime, "current", "bin", "uai-host");
2351
+ await assertOwnedSymlinkReplaceable(paths.shim, expectedTarget, "host command");
2352
+ await atomicSymlink(paths.shim, expectedTarget);
2353
+ }
2354
+
2355
+ async function ensureRecoveryShim(paths: ManagedRuntimePaths): Promise<void> {
2356
+ await mkdir(path.dirname(paths.recoveryShim), { recursive: true, mode: 0o755 });
2357
+ await assertOwnedSymlinkReplaceable(
2358
+ paths.recoveryShim,
2359
+ paths.recoveryEntrypoint,
2360
+ "host recovery command",
2361
+ );
2362
+ await atomicSymlink(paths.recoveryShim, paths.recoveryEntrypoint);
2363
+ }
2364
+
2365
+ async function assertOwnedSymlinkReplaceable(
2366
+ link: string,
2367
+ expectedTarget: string,
2368
+ description: string,
2369
+ ): Promise<void> {
2370
+ try {
2371
+ const stat = await lstat(link);
2372
+ if (!stat.isSymbolicLink()) {
2373
+ throw new Error(`refusing to replace non-symlink ${description} at ${link}`);
2374
+ }
2375
+ const existing = await readlink(link);
2376
+ if (path.resolve(path.dirname(link), existing) !== expectedTarget) {
2377
+ throw new Error(`refusing to replace unrelated ${description} at ${link}`);
2378
+ }
2379
+ } catch (error) {
2380
+ if (!isMissing(error)) throw error;
2381
+ }
2382
+ }
2383
+
2384
+ async function ensureRecoveryEntrypoint(paths: ManagedRuntimePaths): Promise<void> {
2385
+ const directory = path.dirname(paths.recoveryEntrypoint);
2386
+ await mkdir(directory, { recursive: false, mode: 0o700 }).catch((error) => {
2387
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
2388
+ });
2389
+ const directoryStat = await lstat(directory);
2390
+ if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) {
2391
+ throw new Error(`${directory} is not a managed recovery directory`);
2392
+ }
2393
+ await chmod(directory, 0o700);
2394
+ const expected = recoveryEntrypointContents(paths);
2395
+ const exists = await assertRecoveryEntrypointReplaceable(paths);
2396
+ if (!exists) {
2397
+ await writeAtomicExecutable(paths.recoveryEntrypoint, expected);
2398
+ } else {
2399
+ await chmod(paths.recoveryEntrypoint, 0o700);
2400
+ }
2401
+ }
2402
+
2403
+ /** Return true when the exact managed entrypoint already exists. */
2404
+ async function assertRecoveryEntrypointReplaceable(
2405
+ paths: ManagedRuntimePaths,
2406
+ ): Promise<boolean> {
2407
+ try {
2408
+ const stat = await lstat(paths.recoveryEntrypoint);
2409
+ if (!stat.isFile() || stat.isSymbolicLink()) {
2410
+ throw new Error(
2411
+ `refusing to replace non-regular host recovery entrypoint at ${paths.recoveryEntrypoint}`,
2412
+ );
2413
+ }
2414
+ const existing = await readBoundedRegularFile(paths.recoveryEntrypoint);
2415
+ if (!existing.equals(Buffer.from(recoveryEntrypointContents(paths)))) {
2416
+ throw new Error(
2417
+ `refusing to replace modified host recovery entrypoint at ${paths.recoveryEntrypoint}`,
2418
+ );
2419
+ }
2420
+ return true;
2421
+ } catch (error) {
2422
+ if (isMissing(error)) return false;
2423
+ throw error;
2424
+ }
2425
+ }
2426
+
2427
+ function recoveryEntrypointContents(paths: ManagedRuntimePaths): string {
2428
+ const runtime = shellSingleQuote(paths.runtime);
2429
+ const capabilityFloor = shellSingleQuote(paths.minimumRuntimeCapability);
2430
+ const providerState = shellSingleQuote(paths.runtimeProviderState);
2431
+ return `#!/bin/sh
2432
+ # uai-managed-host-recovery-v1
2433
+ set -eu
2434
+
2435
+ runtime=${runtime}
2436
+ capability_floor=${capabilityFloor}
2437
+ provider_state=${providerState}
2438
+
2439
+ fail() {
2440
+ echo "uai-host-recover: $*" >&2
2441
+ exit 126
2442
+ }
2443
+
2444
+ is_stable_version() {
2445
+ value=$1
2446
+ case "$value" in
2447
+ ""|*[!0-9.]*) return 1 ;;
2448
+ esac
2449
+ saved_ifs=$IFS
2450
+ IFS=.
2451
+ set -- $value
2452
+ IFS=$saved_ifs
2453
+ [ "$#" -eq 3 ] || return 1
2454
+ for component in "$@"; do
2455
+ case "$component" in
2456
+ 0|[1-9]|[1-9][0-9]*) ;;
2457
+ *) return 1 ;;
2458
+ esac
2459
+ done
2460
+ }
2461
+
2462
+ previous=$runtime/previous
2463
+ [ -L "$previous" ] || fail "no previous managed runtime is available"
2464
+ version=$(readlink "$previous") || fail "cannot read the previous runtime link"
2465
+ is_stable_version "$version" || fail "the previous runtime link is invalid"
2466
+
2467
+ generation=$runtime/$version
2468
+ if [ ! -d "$generation" ] || [ -L "$generation" ]; then
2469
+ fail "the previous managed runtime directory is invalid"
2470
+ fi
2471
+ floor_required=false
2472
+ if [ -e "$capability_floor" ] || [ -L "$capability_floor" ]; then
2473
+ if [ ! -f "$capability_floor" ] || [ -L "$capability_floor" ]; then
2474
+ fail "the managed runtime capability floor is invalid"
2475
+ fi
2476
+ if ! LC_ALL=C tr -d '[:space:]' < "$capability_floor" |
2477
+ grep -Eq '^\{"schemaVersion":1,"minimumRuntimeCapability":([2-9]|[1-9][0-9]+)\}$'; then
2478
+ fail "the managed runtime capability floor is invalid"
2479
+ fi
2480
+ floor_required=true
2481
+ elif [ -e "$provider_state" ] || [ -L "$provider_state" ]; then
2482
+ if [ ! -f "$provider_state" ] || [ -L "$provider_state" ]; then
2483
+ fail "the container runtime provider state is invalid"
2484
+ fi
2485
+ if ! LC_ALL=C tr -d '[:space:]' < "$provider_state" |
2486
+ grep -Eq '^\{"schemaVersion":1,"statefulProvider":"apple-container","claimedAt":[1-9][0-9]*\}$'; then
2487
+ fail "the container runtime provider state is invalid"
2488
+ fi
2489
+ # A pre-floor apple-container claim is authoritative evidence for the
2490
+ # capability-3 floor that provider requires.
2491
+ required_floor=3
2492
+ floor_required=true
2493
+ fi
2494
+ if [ "$floor_required" = true ]; then
2495
+ if [ -z "\${required_floor:-}" ]; then
2496
+ required_floor=$(LC_ALL=C tr -d '[:space:]' < "$capability_floor" |
2497
+ sed -n 's/^{"schemaVersion":1,"minimumRuntimeCapability":\\([0-9][0-9]*\\)}$/\\1/p')
2498
+ fi
2499
+ case $required_floor in
2500
+ '' | *[!0-9]*) fail "the managed runtime capability floor is invalid" ;;
2501
+ esac
2502
+ capability=$generation/agent/bundled-runtime/capability.json
2503
+ # The TypeScript rollback/install path validates the exact JSON descriptor
2504
+ # and closed asset tree. This stable recovery guard runs only after those
2505
+ # authenticated generation checks and compares the generation's numeric
2506
+ # capability schema against the preserved floor, so a lower-capability
2507
+ # generation can never be launched over provider state that outranks it.
2508
+ if [ ! -f "$capability" ] || [ -L "$capability" ]; then
2509
+ fail "the previous runtime predates this host's provider state"
2510
+ fi
2511
+ generation_capability=$(LC_ALL=C tr -d '[:space:]' < "$capability" |
2512
+ sed -n 's/.*"capabilitySchemaVersion":\\([0-9][0-9]*\\).*/\\1/p')
2513
+ case $generation_capability in
2514
+ '' | *[!0-9]*) fail "the previous runtime capability descriptor is invalid" ;;
2515
+ esac
2516
+ if [ "$generation_capability" -lt "$required_floor" ]; then
2517
+ fail "the previous runtime capability $generation_capability is below this host's floor $required_floor"
2518
+ fi
2519
+ fi
2520
+ launcher_dir=$generation/bin
2521
+ if [ ! -d "$launcher_dir" ] || [ -L "$launcher_dir" ]; then
2522
+ fail "the previous managed runtime launcher directory is invalid"
2523
+ fi
2524
+ launcher=$launcher_dir/uai-host
2525
+ if [ ! -f "$launcher" ] || [ -L "$launcher" ] || [ ! -x "$launcher" ]; then
2526
+ fail "the previous managed runtime launcher is invalid"
2527
+ fi
2528
+
2529
+ exec "$launcher" "$@"
2530
+ `;
2531
+ }
2532
+
2533
+ function shellSingleQuote(value: string): string {
2534
+ return `'${value.split("'").join(`'"'"'`)}'`;
2535
+ }
2536
+
2537
+ async function writeAtomicExecutable(file: string, contents: string): Promise<void> {
2538
+ const temporary = `${file}.${process.pid}.${randomBytes(6).toString("hex")}`;
2539
+ const handle = await open(temporary, "wx", 0o700);
2540
+ try {
2541
+ await handle.writeFile(contents);
2542
+ await handle.sync();
2543
+ } finally {
2544
+ await handle.close();
2545
+ }
2546
+ try {
2547
+ await rename(temporary, file);
2548
+ await syncDirectory(path.dirname(file));
2549
+ } catch (error) {
2550
+ await rm(temporary, { force: true });
2551
+ throw error;
2552
+ }
2553
+ }
2554
+
2555
+ async function removeStableCommands(paths: ManagedRuntimePaths): Promise<void> {
2556
+ await removeOwnedSymlink(paths.recoveryShim, paths.recoveryEntrypoint);
2557
+ await removeOwnedSymlink(
2558
+ paths.shim,
2559
+ path.join(paths.runtime, "current", "bin", "uai-host"),
2560
+ );
2561
+ }
2562
+
2563
+ async function removeOwnedSymlink(
2564
+ link: string,
2565
+ expectedTarget: string,
2566
+ ): Promise<void> {
2567
+ try {
2568
+ const stat = await lstat(link);
2569
+ if (!stat.isSymbolicLink()) return;
2570
+ const target = await readlink(link);
2571
+ if (path.resolve(path.dirname(link), target) === expectedTarget) {
2572
+ await unlink(link);
2573
+ await syncDirectory(path.dirname(link));
2574
+ }
2575
+ } catch (error) {
2576
+ if (!isMissing(error)) throw error;
2577
+ }
2578
+ }
2579
+
2580
+ async function atomicVersionLink(linkPath: string, version: string): Promise<void> {
2581
+ assertStableVersion(version);
2582
+ await atomicSymlink(linkPath, version);
2583
+ }
2584
+
2585
+ async function atomicSymlink(linkPath: string, target: string): Promise<void> {
2586
+ const temporary = path.join(
2587
+ path.dirname(linkPath),
2588
+ `.${path.basename(linkPath)}.${process.pid}.${randomBytes(6).toString("hex")}`,
2589
+ );
2590
+ await symlink(target, temporary);
2591
+ try {
2592
+ await rename(temporary, linkPath);
2593
+ await syncDirectory(path.dirname(linkPath));
2594
+ } catch (error) {
2595
+ await rm(temporary, { force: true });
2596
+ throw error;
2597
+ }
2598
+ }
2599
+
2600
+ async function readVersionLink(linkPath: string, runtimeRoot: string): Promise<string | null> {
2601
+ try {
2602
+ const stat = await lstat(linkPath);
2603
+ if (!stat.isSymbolicLink()) throw new Error(`${linkPath} is not a managed symlink`);
2604
+ const target = await readlink(linkPath);
2605
+ if (target.includes("/") || target.includes(path.sep) || !VERSION.test(target)) {
2606
+ throw new Error(`${linkPath} has an invalid managed-runtime target`);
2607
+ }
2608
+ const targetPath = path.join(runtimeRoot, target);
2609
+ const targetStat = await lstat(targetPath);
2610
+ if (!targetStat.isDirectory() || targetStat.isSymbolicLink()) {
2611
+ throw new Error(`${linkPath} does not target a managed runtime directory`);
2612
+ }
2613
+ return target;
2614
+ } catch (error) {
2615
+ if (isMissing(error)) return null;
2616
+ throw error;
2617
+ }
2618
+ }
2619
+
2620
+ async function recoverRollback(
2621
+ paths: ManagedRuntimePaths,
2622
+ ): Promise<{ fromVersion: string; version: string } | null> {
2623
+ let raw: unknown;
2624
+ try {
2625
+ const stat = await lstat(paths.rollbackJournal);
2626
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_OPERATION_LOCK_BYTES) {
2627
+ throw new Error("managed-host rollback journal is invalid");
2628
+ }
2629
+ raw = JSON.parse(await readFile(paths.rollbackJournal, "utf8"));
2630
+ } catch (error) {
2631
+ if (isMissing(error)) return null;
2632
+ throw new Error(`could not recover interrupted managed-host rollback: ${String(error)}`);
2633
+ }
2634
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
2635
+ throw new Error("managed-host rollback journal is invalid");
2636
+ }
2637
+ const journal = raw as Record<string, unknown>;
2638
+ if (
2639
+ Object.keys(journal).sort().join("\0") !==
2640
+ ["fromVersion", "schemaVersion", "version"].join("\0") ||
2641
+ journal.schemaVersion !== 1 ||
2642
+ typeof journal.fromVersion !== "string" ||
2643
+ typeof journal.version !== "string"
2644
+ ) {
2645
+ throw new Error("managed-host rollback journal is invalid");
2646
+ }
2647
+ assertStableVersion(journal.fromVersion);
2648
+ assertStableVersion(journal.version);
2649
+ // The abandoned generation is deliberately not validated: recovery must
2650
+ // complete a rollback away from a generation that may be the corrupt one.
2651
+ const targetDescriptor = await validateInstalledRuntime(
2652
+ path.join(paths.runtime, journal.version),
2653
+ journal.version,
2654
+ );
2655
+ await assertRuntimeMeetsDurableCapabilityFloor(paths, targetDescriptor);
2656
+ await atomicVersionLink(paths.current, journal.version);
2657
+ await atomicVersionLink(paths.previous, journal.fromVersion);
2658
+ await writeRollbackUpdateHold(paths, journal.fromVersion, journal.version);
2659
+ await removeDurably(paths.rollbackJournal);
2660
+ return { fromVersion: journal.fromVersion, version: journal.version };
2661
+ }
2662
+
2663
+ async function assertRuntimeMeetsDurableCapabilityFloor(
2664
+ paths: ManagedRuntimePaths,
2665
+ descriptor: ResolvedRuntimeDescriptor,
2666
+ ): Promise<void> {
2667
+ const minimum = await readMinimumRuntimeCapability(paths);
2668
+ if (minimum !== null && descriptor.capabilitySchemaVersion < minimum) {
2669
+ throw new Error(
2670
+ `refusing runtime capability ${descriptor.capabilitySchemaVersion}; this host requires capability ${minimum} after the bundled runtime owned task state`,
2671
+ );
2672
+ }
2673
+ }
2674
+
2675
+ async function readMinimumRuntimeCapability(
2676
+ paths: ManagedRuntimePaths,
2677
+ ): Promise<number | null> {
2678
+ let raw: unknown;
2679
+ try {
2680
+ const stat = await lstat(paths.minimumRuntimeCapability);
2681
+ if (
2682
+ !stat.isFile() ||
2683
+ stat.isSymbolicLink() ||
2684
+ stat.size > MAX_OPERATION_LOCK_BYTES
2685
+ ) {
2686
+ throw new Error("managed runtime capability floor is invalid");
2687
+ }
2688
+ raw = JSON.parse(await readFile(paths.minimumRuntimeCapability, "utf8"));
2689
+ } catch (error) {
2690
+ if (isMissing(error)) {
2691
+ const inferred = await inferCapabilityFloorFromProviderState(paths);
2692
+ if (inferred !== null) {
2693
+ await mkdir(path.dirname(paths.minimumRuntimeCapability), {
2694
+ recursive: true,
2695
+ mode: 0o700,
2696
+ });
2697
+ await writeAtomicJson(paths.minimumRuntimeCapability, {
2698
+ schemaVersion: 1,
2699
+ minimumRuntimeCapability: inferred,
2700
+ });
2701
+ }
2702
+ return inferred;
2703
+ }
2704
+ throw error;
2705
+ }
2706
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
2707
+ throw new Error("managed runtime capability floor is invalid");
2708
+ }
2709
+ const marker = raw as Record<string, unknown>;
2710
+ if (
2711
+ Object.keys(marker).sort().join("\0") !==
2712
+ ["minimumRuntimeCapability", "schemaVersion"].join("\0") ||
2713
+ marker.schemaVersion !== 1 ||
2714
+ !Number.isSafeInteger(marker.minimumRuntimeCapability) ||
2715
+ Number(marker.minimumRuntimeCapability) < 2
2716
+ ) {
2717
+ throw new Error("managed runtime capability floor is invalid");
2718
+ }
2719
+ return Number(marker.minimumRuntimeCapability);
2720
+ }
2721
+
2722
+ async function inferCapabilityFloorFromProviderState(
2723
+ paths: ManagedRuntimePaths,
2724
+ ): Promise<number | null> {
2725
+ let raw: unknown;
2726
+ try {
2727
+ const stat = await lstat(paths.runtimeProviderState);
2728
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_OPERATION_LOCK_BYTES) {
2729
+ throw new Error("container runtime provider state is invalid");
2730
+ }
2731
+ raw = JSON.parse(await readFile(paths.runtimeProviderState, "utf8"));
2732
+ } catch (error) {
2733
+ if (isMissing(error)) return null;
2734
+ throw new Error(`container runtime provider state is invalid: ${String(error)}`);
2735
+ }
2736
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
2737
+ throw new Error("container runtime provider state is invalid");
2738
+ }
2739
+ const marker = raw as Record<string, unknown>;
2740
+ if (
2741
+ Object.keys(marker).sort().join("\0") !==
2742
+ ["claimedAt", "schemaVersion", "statefulProvider"].join("\0") ||
2743
+ marker.schemaVersion !== 1 ||
2744
+ marker.statefulProvider !== "apple-container" ||
2745
+ !Number.isSafeInteger(marker.claimedAt) ||
2746
+ Number(marker.claimedAt) <= 0
2747
+ ) {
2748
+ throw new Error("container runtime provider state is invalid");
2749
+ }
2750
+ // The apple-container marker is only writable by a capability-3 runtime.
2751
+ return 3;
2752
+ }
2753
+
2754
+ async function readRollbackUpdateHold(
2755
+ paths: ManagedRuntimePaths,
2756
+ ): Promise<RollbackUpdateHold | null> {
2757
+ try {
2758
+ return parseRollbackUpdateHold(
2759
+ await readBoundedRegularFile(paths.rollbackUpdateHold),
2760
+ );
2761
+ } catch (error) {
2762
+ if (isMissing(error)) return null;
2763
+ throw error;
2764
+ }
2765
+ }
2766
+
2767
+ async function writeRollbackUpdateHold(
2768
+ paths: ManagedRuntimePaths,
2769
+ fromVersion: string,
2770
+ version: string,
2771
+ ): Promise<void> {
2772
+ await writeAtomicJson(paths.rollbackUpdateHold, {
2773
+ schemaVersion: 1,
2774
+ fromVersion,
2775
+ version,
2776
+ createdAt: Date.now(),
2777
+ });
2778
+ }
2779
+
2780
+ async function removeRollbackUpdateHold(
2781
+ paths: ManagedRuntimePaths,
2782
+ expected: RollbackUpdateHold,
2783
+ ): Promise<void> {
2784
+ const current = await readRollbackUpdateHold(paths);
2785
+ if (!current || JSON.stringify(current) !== JSON.stringify(expected)) {
2786
+ throw new Error("managed host rollback update hold changed unexpectedly");
2787
+ }
2788
+ await removeDurably(paths.rollbackUpdateHold);
2789
+ }
2790
+
2791
+ async function pruneRuntimeGenerations(paths: ManagedRuntimePaths): Promise<void> {
2792
+ const keep = new Set(
2793
+ (
2794
+ await Promise.all([
2795
+ readVersionLink(paths.current, paths.runtime),
2796
+ readVersionLink(paths.previous, paths.runtime),
2797
+ ])
2798
+ ).filter((value): value is string => value !== null),
2799
+ );
2800
+ for (const name of await readdir(paths.runtime)) {
2801
+ if (!VERSION.test(name) || keep.has(name)) continue;
2802
+ const candidate = path.join(paths.runtime, name);
2803
+ const stat = await lstat(candidate);
2804
+ if (stat.isDirectory() && !stat.isSymbolicLink()) {
2805
+ await rm(candidate, { recursive: true, force: true });
2806
+ }
2807
+ }
2808
+ await syncDirectory(paths.runtime);
2809
+ }
2810
+
2811
+ async function writeAtomicJson(file: string, value: unknown): Promise<void> {
2812
+ const temporary = `${file}.${process.pid}.${randomBytes(6).toString("hex")}`;
2813
+ const bytes = `${JSON.stringify(value)}\n`;
2814
+ if (Buffer.byteLength(bytes) > MAX_OPERATION_LOCK_BYTES) {
2815
+ throw new Error("managed-host transaction metadata exceeds its byte limit");
2816
+ }
2817
+ const handle = await open(temporary, "wx", 0o600);
2818
+ try {
2819
+ await handle.writeFile(bytes);
2820
+ await handle.sync();
2821
+ } finally {
2822
+ await handle.close();
2823
+ }
2824
+ try {
2825
+ await rename(temporary, file);
2826
+ await syncDirectory(path.dirname(file));
2827
+ } catch (error) {
2828
+ await rm(temporary, { force: true });
2829
+ throw error;
2830
+ }
2831
+ }
2832
+
2833
+ async function withManagedOperationLock<T>(
2834
+ paths: ManagedRuntimePaths,
2835
+ operation: () => Promise<T>,
2836
+ ): Promise<T> {
2837
+ const release = await acquireManagedOperationLock(paths);
2838
+ try {
2839
+ return await operation();
2840
+ } finally {
2841
+ await release();
2842
+ }
2843
+ }
2844
+
2845
+ /** Signal/EXIT cleanup may arrive while its child still owns the service write. */
2846
+ async function withManagedOperationLockWaiting<T>(
2847
+ paths: ManagedRuntimePaths,
2848
+ operation: () => Promise<T>,
2849
+ ): Promise<T> {
2850
+ const deadline = Date.now() + OPERATION_LOCK_WAIT_MS;
2851
+ for (;;) {
2852
+ try {
2853
+ return await withManagedOperationLock(paths, operation);
2854
+ } catch (error) {
2855
+ if (!(error instanceof ManagedOperationBusyError) || Date.now() >= deadline) {
2856
+ throw error;
2857
+ }
2858
+ await new Promise<void>((resolve) => setTimeout(resolve, OPERATION_LOCK_POLL_MS));
2859
+ }
2860
+ }
2861
+ }
2862
+
2863
+ class ManagedOperationBusyError extends Error {}
2864
+
2865
+ async function acquireManagedOperationLock(
2866
+ paths: ManagedRuntimePaths,
2867
+ ): Promise<() => Promise<void>> {
2868
+ const lockParent = path.dirname(paths.operationLock);
2869
+ for (let attempt = 0; attempt < 3; attempt += 1) {
2870
+ const nonce = randomBytes(16).toString("hex");
2871
+ try {
2872
+ await mkdir(paths.operationLock, { mode: 0o700, recursive: false });
2873
+ } catch (error) {
2874
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
2875
+ if (!(await operationLockIsStale(paths.operationLock))) {
2876
+ throw new ManagedOperationBusyError(
2877
+ "another managed host install/update/rollback is already running",
2878
+ );
2879
+ }
2880
+ const quarantine = path.join(
2881
+ lockParent,
2882
+ `.stale-operation-lock.${process.pid}.${randomBytes(6).toString("hex")}`,
2883
+ );
2884
+ try {
2885
+ await rename(paths.operationLock, quarantine);
2886
+ await syncDirectory(lockParent);
2887
+ } catch (renameError) {
2888
+ if (isMissing(renameError)) continue;
2889
+ throw renameError;
2890
+ }
2891
+ await rm(quarantine, { recursive: true, force: true });
2892
+ await syncDirectory(lockParent);
2893
+ continue;
2894
+ }
2895
+
2896
+ await syncDirectory(lockParent);
2897
+ const ownerFile = path.join(paths.operationLock, "owner.json");
2898
+ try {
2899
+ await writeAtomicJson(ownerFile, {
2900
+ schemaVersion: 1,
2901
+ pid: process.pid,
2902
+ startedAt: Date.now(),
2903
+ nonce,
2904
+ });
2905
+ } catch (error) {
2906
+ await rm(paths.operationLock, { recursive: true, force: true });
2907
+ await syncDirectory(lockParent);
2908
+ throw error;
2909
+ }
2910
+ return async () => {
2911
+ let ownsLock = false;
2912
+ try {
2913
+ const owner = parseOperationLockOwner(await readBoundedRegularFile(ownerFile));
2914
+ ownsLock = owner.nonce === nonce && owner.pid === process.pid;
2915
+ } catch (error) {
2916
+ if (!isMissing(error)) throw error;
2917
+ }
2918
+ if (!ownsLock) {
2919
+ throw new Error("managed host operation lock ownership changed unexpectedly");
2920
+ }
2921
+ await rm(paths.operationLock, { recursive: true, force: true });
2922
+ await syncDirectory(lockParent);
2923
+ };
2924
+ }
2925
+ throw new Error("could not safely acquire the managed host operation lock");
2926
+ }
2927
+
2928
+ async function operationLockIsStale(lockPath: string): Promise<boolean> {
2929
+ const stat = await lstat(lockPath);
2930
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
2931
+ throw new Error("managed host operation lock is not a directory");
2932
+ }
2933
+ const ownerFile = path.join(lockPath, "owner.json");
2934
+ try {
2935
+ const owner = parseOperationLockOwner(await readBoundedRegularFile(ownerFile));
2936
+ // Never steal from a live PID based on elapsed time alone: without fencing,
2937
+ // that owner could resume and mutate links after a replacement entered.
2938
+ // A dead PID is safe to reclaim; a future process-incarnation identifier
2939
+ // can additionally distinguish the rare PID-reuse case.
2940
+ return !pidIsAlive(owner.pid);
2941
+ } catch (error) {
2942
+ if (!isMissing(error)) throw error;
2943
+ // A process can die between mkdir and owner.json. Only reclaim that narrow
2944
+ // state after a conservative age; a fresh incomplete lock remains held.
2945
+ return Date.now() - stat.mtimeMs >= INCOMPLETE_LOCK_STALE_MS;
2946
+ }
2947
+ }
2948
+
2949
+ interface OperationLockOwner {
2950
+ readonly schemaVersion: 1;
2951
+ readonly pid: number;
2952
+ readonly startedAt: number;
2953
+ readonly nonce: string;
2954
+ }
2955
+
2956
+ interface ActivationMarker {
2957
+ readonly schemaVersion: 1;
2958
+ readonly fromVersion: string | null;
2959
+ readonly version: string;
2960
+ }
2961
+
2962
+ interface MaintenanceMarker {
2963
+ readonly schemaVersion: 1;
2964
+ readonly operation: "uninstall";
2965
+ readonly phase: "checking" | "teardown" | "cleanup";
2966
+ readonly purgeData: boolean;
2967
+ readonly startedAt: number;
2968
+ }
2969
+
2970
+ interface InstallIntentMarker {
2971
+ readonly schemaVersion: 1;
2972
+ readonly operation: "install";
2973
+ readonly phase: "prepared" | "activated" | "attested";
2974
+ readonly nonce: string;
2975
+ readonly ownerPid: number;
2976
+ readonly ownerUid: number;
2977
+ readonly ownerStartedAt: string;
2978
+ readonly createdAt: number;
2979
+ readonly rootExisted: boolean;
2980
+ readonly currentVersion: string | null;
2981
+ readonly previousVersion: string | null;
2982
+ readonly activationPending: ActivationMarker | null;
2983
+ readonly shimOwnership: "missing" | "managed" | "legacy";
2984
+ readonly recoveryShimExisted: boolean;
2985
+ readonly recoveryEntrypointExisted: boolean;
2986
+ readonly serviceOwnership: "missing" | "managed" | "legacy";
2987
+ readonly serviceManager: ManagedServiceManagerState;
2988
+ readonly priorServiceStop: "none" | "requested" | "stopped";
2989
+ }
2990
+
2991
+ interface LegacyShimTakeoverMarker {
2992
+ readonly schemaVersion: 1;
2993
+ readonly operation: "legacy-shim-takeover";
2994
+ readonly phase: "recorded" | "parked" | "restoring" | "finalizing";
2995
+ readonly originalTarget: string;
2996
+ readonly resolvedTarget: string;
2997
+ readonly packageRoot: string;
2998
+ readonly startedAt: number;
2999
+ }
3000
+
3001
+ interface RollbackUpdateHold {
3002
+ readonly schemaVersion: 1;
3003
+ /** Release the operator rejected. */
3004
+ readonly fromVersion: string;
3005
+ /** Version deliberately restored by rollback. */
3006
+ readonly version: string;
3007
+ readonly createdAt: number;
3008
+ }
3009
+
3010
+ function parseActivationMarker(bytes: Buffer): ActivationMarker {
3011
+ let raw: unknown;
3012
+ try {
3013
+ raw = JSON.parse(bytes.toString("utf8"));
3014
+ } catch {
3015
+ throw new Error("managed host activation marker is invalid");
3016
+ }
3017
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
3018
+ throw new Error("managed host activation marker is invalid");
3019
+ }
3020
+ const marker = raw as Record<string, unknown>;
3021
+ if (
3022
+ Object.keys(marker).sort().join("\0") !==
3023
+ ["fromVersion", "schemaVersion", "version"].join("\0") ||
3024
+ marker.schemaVersion !== 1 ||
3025
+ (marker.fromVersion !== null && typeof marker.fromVersion !== "string") ||
3026
+ typeof marker.version !== "string"
3027
+ ) {
3028
+ throw new Error("managed host activation marker is invalid");
3029
+ }
3030
+ if (marker.fromVersion !== null) assertStableVersion(marker.fromVersion);
3031
+ assertStableVersion(marker.version);
3032
+ return marker as unknown as ActivationMarker;
3033
+ }
3034
+
3035
+ async function readMaintenanceMarker(
3036
+ paths: ManagedRuntimePaths,
3037
+ ): Promise<MaintenanceMarker | null> {
3038
+ try {
3039
+ return parseMaintenanceMarker(
3040
+ await readBoundedRegularFile(paths.maintenancePending),
3041
+ );
3042
+ } catch (error) {
3043
+ if (isMissing(error)) return null;
3044
+ throw error;
3045
+ }
3046
+ }
3047
+
3048
+ function parseMaintenanceMarker(bytes: Buffer): MaintenanceMarker {
3049
+ let raw: unknown;
3050
+ try {
3051
+ raw = JSON.parse(bytes.toString("utf8"));
3052
+ } catch {
3053
+ throw new Error("managed host maintenance marker is invalid");
3054
+ }
3055
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
3056
+ throw new Error("managed host maintenance marker is invalid");
3057
+ }
3058
+ const marker = raw as Record<string, unknown>;
3059
+ if (
3060
+ Object.keys(marker).sort().join("\0") !==
3061
+ ["operation", "phase", "purgeData", "schemaVersion", "startedAt"].join(
3062
+ "\0",
3063
+ ) ||
3064
+ marker.schemaVersion !== 1 ||
3065
+ marker.operation !== "uninstall" ||
3066
+ (marker.phase !== "checking" &&
3067
+ marker.phase !== "teardown" &&
3068
+ marker.phase !== "cleanup") ||
3069
+ typeof marker.purgeData !== "boolean" ||
3070
+ typeof marker.startedAt !== "number" ||
3071
+ !Number.isSafeInteger(marker.startedAt) ||
3072
+ marker.startedAt <= 0
3073
+ ) {
3074
+ throw new Error("managed host maintenance marker is invalid");
3075
+ }
3076
+ return marker as unknown as MaintenanceMarker;
3077
+ }
3078
+
3079
+ async function readInstallIntent(
3080
+ paths: ManagedRuntimePaths,
3081
+ ): Promise<InstallIntentMarker | null> {
3082
+ try {
3083
+ return parseInstallIntent(await readBoundedRegularFile(paths.installIntent));
3084
+ } catch (error) {
3085
+ if (isMissing(error)) return null;
3086
+ throw error;
3087
+ }
3088
+ }
3089
+
3090
+ function parseInstallIntent(bytes: Buffer): InstallIntentMarker {
3091
+ let raw: unknown;
3092
+ try {
3093
+ raw = JSON.parse(bytes.toString("utf8"));
3094
+ } catch {
3095
+ throw new Error("managed host install intent is invalid");
3096
+ }
3097
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
3098
+ throw new Error("managed host install intent is invalid");
3099
+ }
3100
+ const marker = raw as Record<string, unknown>;
3101
+ if (
3102
+ Object.keys(marker).sort().join("\0") !==
3103
+ [
3104
+ "activationPending",
3105
+ "createdAt",
3106
+ "currentVersion",
3107
+ "nonce",
3108
+ "operation",
3109
+ "ownerPid",
3110
+ "ownerStartedAt",
3111
+ "ownerUid",
3112
+ "phase",
3113
+ "previousVersion",
3114
+ "priorServiceStop",
3115
+ "recoveryEntrypointExisted",
3116
+ "recoveryShimExisted",
3117
+ "rootExisted",
3118
+ "schemaVersion",
3119
+ "serviceManager",
3120
+ "serviceOwnership",
3121
+ "shimOwnership",
3122
+ ].join("\0") ||
3123
+ marker.schemaVersion !== 1 ||
3124
+ marker.operation !== "install" ||
3125
+ (marker.phase !== "prepared" &&
3126
+ marker.phase !== "activated" &&
3127
+ marker.phase !== "attested") ||
3128
+ typeof marker.nonce !== "string" ||
3129
+ !INSTALL_INTENT_NONCE.test(marker.nonce) ||
3130
+ typeof marker.ownerPid !== "number" ||
3131
+ !Number.isSafeInteger(marker.ownerPid) ||
3132
+ marker.ownerPid <= 0 ||
3133
+ typeof marker.ownerUid !== "number" ||
3134
+ !Number.isSafeInteger(marker.ownerUid) ||
3135
+ marker.ownerUid < 0 ||
3136
+ typeof marker.ownerStartedAt !== "string" ||
3137
+ marker.ownerStartedAt.length === 0 ||
3138
+ marker.ownerStartedAt.length > 128 ||
3139
+ typeof marker.createdAt !== "number" ||
3140
+ !Number.isSafeInteger(marker.createdAt) ||
3141
+ marker.createdAt <= 0 ||
3142
+ typeof marker.rootExisted !== "boolean" ||
3143
+ (marker.currentVersion !== null &&
3144
+ typeof marker.currentVersion !== "string") ||
3145
+ (marker.previousVersion !== null &&
3146
+ typeof marker.previousVersion !== "string") ||
3147
+ (marker.activationPending !== null &&
3148
+ (typeof marker.activationPending !== "object" ||
3149
+ Array.isArray(marker.activationPending))) ||
3150
+ !new Set(["missing", "managed", "legacy"]).has(
3151
+ marker.shimOwnership as string,
3152
+ ) ||
3153
+ typeof marker.recoveryShimExisted !== "boolean" ||
3154
+ typeof marker.recoveryEntrypointExisted !== "boolean" ||
3155
+ !new Set(["missing", "managed", "legacy"]).has(
3156
+ marker.serviceOwnership as string,
3157
+ ) ||
3158
+ !isServiceManagerState(marker.serviceManager) ||
3159
+ !new Set(["none", "requested", "stopped"]).has(
3160
+ marker.priorServiceStop as string,
3161
+ )
3162
+ ) {
3163
+ throw new Error("managed host install intent is invalid");
3164
+ }
3165
+ if (marker.currentVersion !== null) {
3166
+ assertStableVersion(marker.currentVersion as string);
3167
+ }
3168
+ if (marker.previousVersion !== null) {
3169
+ assertStableVersion(marker.previousVersion as string);
3170
+ }
3171
+ if (marker.activationPending !== null) {
3172
+ parseActivationMarker(Buffer.from(JSON.stringify(marker.activationPending)));
3173
+ }
3174
+ if (
3175
+ marker.serviceOwnership === "missing" &&
3176
+ ((marker.serviceManager as ManagedServiceManagerState).loaded ||
3177
+ (marker.serviceManager as ManagedServiceManagerState).active ||
3178
+ (marker.serviceManager as ManagedServiceManagerState).enabled)
3179
+ ) {
3180
+ throw new Error("managed host install intent is invalid");
3181
+ }
3182
+ return marker as unknown as InstallIntentMarker;
3183
+ }
3184
+
3185
+ function isServiceManagerState(
3186
+ value: unknown,
3187
+ ): value is ManagedServiceManagerState {
3188
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
3189
+ const state = value as Record<string, unknown>;
3190
+ return (
3191
+ Object.keys(state).sort().join("\0") ===
3192
+ ["active", "enabled", "loaded"].join("\0") &&
3193
+ typeof state.loaded === "boolean" &&
3194
+ typeof state.active === "boolean" &&
3195
+ typeof state.enabled === "boolean" &&
3196
+ (!state.active || state.loaded)
3197
+ );
3198
+ }
3199
+
3200
+ function assertServiceDefinitionState(
3201
+ value: unknown,
3202
+ ): asserts value is ManagedServiceDefinitionState {
3203
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3204
+ throw new Error("managed host service definition state is invalid");
3205
+ }
3206
+ const state = value as Record<string, unknown>;
3207
+ if (
3208
+ Object.keys(state).sort().join("\0") !== ["manager", "ownership"].join("\0") ||
3209
+ !new Set(["missing", "managed", "legacy"]).has(state.ownership as string) ||
3210
+ !isServiceManagerState(state.manager)
3211
+ ) {
3212
+ throw new Error("managed host service definition state is invalid");
3213
+ }
3214
+ }
3215
+
3216
+ async function readLegacyShimTakeover(
3217
+ paths: ManagedRuntimePaths,
3218
+ ): Promise<LegacyShimTakeoverMarker | null> {
3219
+ try {
3220
+ return parseLegacyShimTakeover(
3221
+ await readBoundedRegularFile(paths.legacyShimTakeover),
3222
+ );
3223
+ } catch (error) {
3224
+ if (isMissing(error)) return null;
3225
+ throw error;
3226
+ }
3227
+ }
3228
+
3229
+ function parseLegacyShimTakeover(bytes: Buffer): LegacyShimTakeoverMarker {
3230
+ let raw: unknown;
3231
+ try {
3232
+ raw = JSON.parse(bytes.toString("utf8"));
3233
+ } catch {
3234
+ throw new Error("managed host legacy command takeover is invalid");
3235
+ }
3236
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
3237
+ throw new Error("managed host legacy command takeover is invalid");
3238
+ }
3239
+ const marker = raw as Record<string, unknown>;
3240
+ if (
3241
+ Object.keys(marker).sort().join("\0") !==
3242
+ [
3243
+ "operation",
3244
+ "originalTarget",
3245
+ "packageRoot",
3246
+ "phase",
3247
+ "resolvedTarget",
3248
+ "schemaVersion",
3249
+ "startedAt",
3250
+ ].join("\0") ||
3251
+ marker.schemaVersion !== 1 ||
3252
+ marker.operation !== "legacy-shim-takeover" ||
3253
+ !new Set(["recorded", "parked", "restoring", "finalizing"]).has(
3254
+ marker.phase as string,
3255
+ ) ||
3256
+ typeof marker.originalTarget !== "string" ||
3257
+ marker.originalTarget.length === 0 ||
3258
+ Buffer.byteLength(marker.originalTarget) > 2_048 ||
3259
+ typeof marker.resolvedTarget !== "string" ||
3260
+ !path.isAbsolute(marker.resolvedTarget) ||
3261
+ typeof marker.packageRoot !== "string" ||
3262
+ !path.isAbsolute(marker.packageRoot) ||
3263
+ typeof marker.startedAt !== "number" ||
3264
+ !Number.isSafeInteger(marker.startedAt) ||
3265
+ marker.startedAt <= 0
3266
+ ) {
3267
+ throw new Error("managed host legacy command takeover is invalid");
3268
+ }
3269
+ return marker as unknown as LegacyShimTakeoverMarker;
3270
+ }
3271
+
3272
+ function parseRollbackUpdateHold(bytes: Buffer): RollbackUpdateHold {
3273
+ let raw: unknown;
3274
+ try {
3275
+ raw = JSON.parse(bytes.toString("utf8"));
3276
+ } catch {
3277
+ throw new Error("managed host rollback update hold is invalid");
3278
+ }
3279
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
3280
+ throw new Error("managed host rollback update hold is invalid");
3281
+ }
3282
+ const hold = raw as Record<string, unknown>;
3283
+ if (
3284
+ Object.keys(hold).sort().join("\0") !==
3285
+ ["createdAt", "fromVersion", "schemaVersion", "version"].join("\0") ||
3286
+ hold.schemaVersion !== 1 ||
3287
+ typeof hold.fromVersion !== "string" ||
3288
+ typeof hold.version !== "string" ||
3289
+ typeof hold.createdAt !== "number" ||
3290
+ !Number.isSafeInteger(hold.createdAt) ||
3291
+ hold.createdAt <= 0
3292
+ ) {
3293
+ throw new Error("managed host rollback update hold is invalid");
3294
+ }
3295
+ assertStableVersion(hold.fromVersion);
3296
+ assertStableVersion(hold.version);
3297
+ if (hold.fromVersion === hold.version) {
3298
+ throw new Error("managed host rollback update hold is invalid");
3299
+ }
3300
+ return hold as unknown as RollbackUpdateHold;
3301
+ }
3302
+
3303
+ function parseOperationLockOwner(bytes: Buffer): OperationLockOwner {
3304
+ let raw: unknown;
3305
+ try {
3306
+ raw = JSON.parse(bytes.toString("utf8"));
3307
+ } catch {
3308
+ throw new Error("managed host operation lock metadata is invalid");
3309
+ }
3310
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
3311
+ throw new Error("managed host operation lock metadata is invalid");
3312
+ }
3313
+ const owner = raw as Record<string, unknown>;
3314
+ if (
3315
+ Object.keys(owner).sort().join("\0") !==
3316
+ ["nonce", "pid", "schemaVersion", "startedAt"].join("\0") ||
3317
+ owner.schemaVersion !== 1 ||
3318
+ typeof owner.pid !== "number" ||
3319
+ !Number.isSafeInteger(owner.pid) ||
3320
+ owner.pid <= 0 ||
3321
+ typeof owner.startedAt !== "number" ||
3322
+ !Number.isSafeInteger(owner.startedAt) ||
3323
+ owner.startedAt <= 0 ||
3324
+ typeof owner.nonce !== "string" ||
3325
+ !/^[a-f0-9]{32}$/.test(owner.nonce)
3326
+ ) {
3327
+ throw new Error("managed host operation lock metadata is invalid");
3328
+ }
3329
+ return owner as unknown as OperationLockOwner;
3330
+ }
3331
+
3332
+ async function readBoundedRegularFile(
3333
+ file: string,
3334
+ maxBytes = MAX_OPERATION_LOCK_BYTES,
3335
+ ): Promise<Buffer> {
3336
+ const handle = await open(
3337
+ file,
3338
+ fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW,
3339
+ );
3340
+ try {
3341
+ const before = await handle.stat();
3342
+ if (!before.isFile() || before.size > maxBytes) {
3343
+ throw new Error("managed host operation metadata is invalid");
3344
+ }
3345
+ const bytes = await handle.readFile();
3346
+ const after = await handle.stat();
3347
+ if (
3348
+ bytes.length > maxBytes ||
3349
+ before.dev !== after.dev ||
3350
+ before.ino !== after.ino ||
3351
+ after.size !== bytes.length
3352
+ ) {
3353
+ throw new Error("managed host operation metadata changed while it was read");
3354
+ }
3355
+ return bytes;
3356
+ } finally {
3357
+ await handle.close();
3358
+ }
3359
+ }
3360
+
3361
+ function pidIsAlive(pid: number): boolean {
3362
+ try {
3363
+ process.kill(pid, 0);
3364
+ return true;
3365
+ } catch (error) {
3366
+ return (error as NodeJS.ErrnoException).code !== "ESRCH";
3367
+ }
3368
+ }
3369
+
3370
+ function assertOwnerPid(pid: number): void {
3371
+ if (!Number.isSafeInteger(pid) || pid <= 0) {
3372
+ throw new Error("managed host installer owner PID is invalid");
3373
+ }
3374
+ }
3375
+
3376
+ class ProcessIdentityUnavailableError extends Error {}
3377
+
3378
+ /** UID + PID + kernel-recorded start time rejects foreign owners and PID reuse. */
3379
+ function processIdentity(pid: number): ManagedRuntimeOwnerIdentity | null {
3380
+ assertOwnerPid(pid);
3381
+ const result = spawnSync(
3382
+ "/bin/ps",
3383
+ ["-o", "uid=", "-o", "lstart=", "-p", String(pid)],
3384
+ {
3385
+ encoding: "utf8",
3386
+ timeout: 2_000,
3387
+ env: { PATH: "/usr/bin:/bin", LC_ALL: "C" },
3388
+ },
3389
+ );
3390
+ if (result.status !== 0) return null;
3391
+ const value = result.stdout.trim().replace(/\s+/g, " ");
3392
+ const match = /^(\d+) (.+)$/.exec(value);
3393
+ if (
3394
+ !match ||
3395
+ value.length > 160 ||
3396
+ /[^\x20-\x7e]/.test(value)
3397
+ ) {
3398
+ throw new Error("could not identify managed host installer owner process");
3399
+ }
3400
+ const uid = Number(match[1]);
3401
+ const startedAt = match[2]!;
3402
+ if (!Number.isSafeInteger(uid) || uid < 0 || startedAt.length > 128) {
3403
+ throw new Error("could not identify managed host installer owner process");
3404
+ }
3405
+ return { uid, startedAt };
3406
+ }
3407
+
3408
+ function installIntentOwnerIsLive(
3409
+ intent: InstallIntentMarker,
3410
+ identityProbe: (pid: number) => ManagedRuntimeOwnerIdentity | null = processIdentity,
3411
+ ): boolean {
3412
+ if (!pidIsAlive(intent.ownerPid)) return false;
3413
+ const identity = identityProbe(intent.ownerPid);
3414
+ if (!identity) {
3415
+ // kill(0) proved that the PID still exists. A transient ps failure cannot be
3416
+ // interpreted as death or it would authorize rollback over a live install.
3417
+ throw new ProcessIdentityUnavailableError(
3418
+ "could not verify managed host installer owner identity",
3419
+ );
3420
+ }
3421
+ return (
3422
+ identity.uid === intent.ownerUid &&
3423
+ identity.startedAt === intent.ownerStartedAt
3424
+ );
3425
+ }
3426
+
3427
+ async function removeDurably(file: string): Promise<void> {
3428
+ await rm(file, { force: true });
3429
+ await syncDirectory(path.dirname(file));
3430
+ }
3431
+
3432
+ async function syncDirectory(directory: string): Promise<void> {
3433
+ let handle;
3434
+ try {
3435
+ handle = await open(directory, fsConstants.O_RDONLY);
3436
+ await handle.sync();
3437
+ } catch (error) {
3438
+ const code = (error as NodeJS.ErrnoException).code;
3439
+ // Some filesystems/platforms do not expose directory fsync. File fsync and
3440
+ // atomic rename still apply; these are the only portability exceptions.
3441
+ if (code !== "EINVAL" && code !== "ENOTSUP" && code !== "EISDIR" && code !== "ENOENT") {
3442
+ throw error;
3443
+ }
3444
+ } finally {
3445
+ await handle?.close();
3446
+ }
3447
+ }
3448
+
3449
+ async function fetchBounded(
3450
+ fetchImpl: typeof fetch,
3451
+ url: string,
3452
+ maxBytes: number,
3453
+ timeoutMs: number,
3454
+ ): Promise<Buffer> {
3455
+ const response = await fetchImpl(url, {
3456
+ redirect: "follow",
3457
+ credentials: "omit",
3458
+ signal: AbortSignal.timeout(timeoutMs),
3459
+ headers: { "user-agent": "uai-host-updater/1", accept: "application/json" },
3460
+ });
3461
+ if (!response.ok || !response.body) {
3462
+ throw new Error(`release manifest download failed: HTTP ${response.status}`);
3463
+ }
3464
+ const declared = parseContentLength(response.headers.get("content-length"));
3465
+ if (declared !== null && declared > maxBytes) {
3466
+ throw new Error(`release manifest exceeds ${maxBytes} bytes`);
3467
+ }
3468
+ return readResponseBody(response, maxBytes);
3469
+ }
3470
+
3471
+ async function downloadArtifact(
3472
+ fetchImpl: typeof fetch,
3473
+ artifact: ReleaseArtifact,
3474
+ destination: string,
3475
+ timeoutMs: number,
3476
+ ): Promise<void> {
3477
+ if (artifact.size > MAX_RELEASE_ARTIFACT_BYTES) {
3478
+ throw new Error(`release artifact exceeds ${MAX_RELEASE_ARTIFACT_BYTES} bytes`);
3479
+ }
3480
+ const response = await fetchImpl(artifact.url, {
3481
+ redirect: "follow",
3482
+ credentials: "omit",
3483
+ signal: AbortSignal.timeout(timeoutMs),
3484
+ headers: { "user-agent": "uai-host-updater/1", accept: "application/gzip" },
3485
+ });
3486
+ if (!response.ok || !response.body) {
3487
+ throw new Error(`release artifact download failed: HTTP ${response.status}`);
3488
+ }
3489
+ const declared = parseContentLength(response.headers.get("content-length"));
3490
+ if (declared !== null && declared !== artifact.size) {
3491
+ throw new Error(
3492
+ `release artifact content-length mismatch: expected ${artifact.size}, received ${declared}`,
3493
+ );
3494
+ }
3495
+ const handle = await open(destination, "wx", 0o600);
3496
+ let size = 0;
3497
+ try {
3498
+ const reader = response.body.getReader();
3499
+ for (;;) {
3500
+ const { done, value } = await reader.read();
3501
+ if (done) break;
3502
+ size += value.byteLength;
3503
+ if (size > artifact.size || size > MAX_RELEASE_ARTIFACT_BYTES) {
3504
+ await reader.cancel("release artifact exceeds its authenticated size");
3505
+ throw new Error("release artifact exceeds its authenticated size");
3506
+ }
3507
+ await handle.write(value);
3508
+ }
3509
+ await handle.sync();
3510
+ } catch (error) {
3511
+ await handle.close();
3512
+ await rm(destination, { force: true });
3513
+ throw error;
3514
+ }
3515
+ await handle.close();
3516
+ if (size !== artifact.size) {
3517
+ await rm(destination, { force: true });
3518
+ throw new Error(
3519
+ `release artifact size mismatch: expected ${artifact.size}, received ${size}`,
3520
+ );
3521
+ }
3522
+ }
3523
+
3524
+ async function readResponseBody(response: Response, maxBytes: number): Promise<Buffer> {
3525
+ const reader = response.body!.getReader();
3526
+ const chunks: Buffer[] = [];
3527
+ let size = 0;
3528
+ for (;;) {
3529
+ const { done, value } = await reader.read();
3530
+ if (done) break;
3531
+ size += value.byteLength;
3532
+ if (size > maxBytes) {
3533
+ await reader.cancel("response exceeds limit");
3534
+ throw new Error(`response exceeds ${maxBytes} bytes`);
3535
+ }
3536
+ chunks.push(Buffer.from(value));
3537
+ }
3538
+ return Buffer.concat(chunks, size);
3539
+ }
3540
+
3541
+ function parseContentLength(value: string | null): number | null {
3542
+ if (value === null) return null;
3543
+ if (!/^(?:0|[1-9]\d*)$/.test(value)) throw new Error("invalid response content-length");
3544
+ const parsed = Number(value);
3545
+ if (!Number.isSafeInteger(parsed)) throw new Error("invalid response content-length");
3546
+ return parsed;
3547
+ }
3548
+
3549
+ function checkedTimeout(
3550
+ value: number | undefined,
3551
+ fallback: number,
3552
+ maximum: number,
3553
+ ): number {
3554
+ const timeout = value ?? fallback;
3555
+ if (!Number.isSafeInteger(timeout) || timeout <= 0 || timeout > maximum) {
3556
+ throw new Error("managed host update timeout is invalid");
3557
+ }
3558
+ return timeout;
3559
+ }
3560
+
3561
+ function readMacosVersion(): string {
3562
+ const result = spawnSync("/usr/bin/sw_vers", ["-productVersion"], {
3563
+ encoding: "utf8",
3564
+ timeout: 5_000,
3565
+ });
3566
+ const value = result.status === 0 ? result.stdout.trim() : "";
3567
+ if (!/^\d+\.\d+(?:\.\d+)?$/.test(value)) {
3568
+ throw new Error("could not determine the macOS version");
3569
+ }
3570
+ return value;
3571
+ }
3572
+
3573
+ function readGlibcVersion(): string | null {
3574
+ const report = process.report?.getReport() as
3575
+ | { header?: { glibcVersionRuntime?: unknown } }
3576
+ | undefined;
3577
+ const value = report?.header?.glibcVersionRuntime;
3578
+ return typeof value === "string" && /^\d+\.\d+$/.test(value) ? value : null;
3579
+ }
3580
+
3581
+ function assertMinimumVersion(actual: string, minimum: string, label: string): void {
3582
+ const parse = (value: string): number[] => {
3583
+ const match = /^(\d+)\.(\d+)(?:\.(\d+))?/.exec(value);
3584
+ if (!match) throw new Error(`could not determine ${label} version`);
3585
+ return [Number(match[1]), Number(match[2]), Number(match[3] ?? 0)];
3586
+ };
3587
+ const left = parse(actual);
3588
+ const right = parse(minimum);
3589
+ for (let index = 0; index < 3; index += 1) {
3590
+ if (left[index]! > right[index]!) return;
3591
+ if (left[index]! < right[index]!) {
3592
+ throw new Error(`${label} ${minimum} or newer is required; found ${actual}`);
3593
+ }
3594
+ }
3595
+ }
3596
+
3597
+ function parseStableVersion(value: string): readonly [number, number, number] {
3598
+ assertStableVersion(value);
3599
+ return value.split(".").map(Number) as [number, number, number];
3600
+ }
3601
+
3602
+ function assertStableVersion(value: string): void {
3603
+ if (!VERSION.test(value)) throw new Error(`invalid stable host version: ${value}`);
3604
+ }
3605
+
3606
+ function isInside(root: string, candidate: string): boolean {
3607
+ const relative = path.relative(root, candidate);
3608
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
3609
+ }
3610
+
3611
+ async function pathExists(candidate: string): Promise<boolean> {
3612
+ try {
3613
+ await access(candidate, fsConstants.F_OK);
3614
+ return true;
3615
+ } catch (error) {
3616
+ if (isMissing(error)) return false;
3617
+ throw error;
3618
+ }
3619
+ }
3620
+
3621
+ /** Like `pathExists`, but dangling symlinks and partial cleanup trees count. */
3622
+ async function pathEntryExists(candidate: string): Promise<boolean> {
3623
+ try {
3624
+ await lstat(candidate);
3625
+ return true;
3626
+ } catch (error) {
3627
+ if (isMissing(error)) return false;
3628
+ throw error;
3629
+ }
3630
+ }
3631
+
3632
+ function pathExistsSync(candidate: string): boolean {
3633
+ try {
3634
+ lstatSync(candidate);
3635
+ return true;
3636
+ } catch (error) {
3637
+ if (isMissing(error)) return false;
3638
+ throw error;
3639
+ }
3640
+ }
3641
+
3642
+ function isMissing(error: unknown): boolean {
3643
+ return (error as NodeJS.ErrnoException | undefined)?.code === "ENOENT";
3644
+ }