@getmikie/cli 1.1.103 → 1.1.104

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.
package/lib/update.mjs CHANGED
@@ -66,6 +66,8 @@ if (
66
66
  descriptor.transaction_root !== transactionRoot ||
67
67
  descriptor.token !== journal.token ||
68
68
  journal.recovery?.descriptor_sha256 !== expectedDescriptorSha256 ||
69
+ descriptor.final_launcher !== journal.prepared_candidate?.final_launcher ||
70
+ descriptor.final_launcher_sha256 !== journal.prepared_candidate?.final_launcher_sha256 ||
69
71
  ![descriptor.runtime, descriptor.launcher].every(
70
72
  (artifactPath) => typeof artifactPath === "string" && inside(transactionRoot, artifactPath),
71
73
  )
@@ -719,7 +721,9 @@ export async function runUpdateTransaction({
719
721
  if (versionOrder < 0) {
720
722
  throw new Error("registry resolved an older Mikie version");
721
723
  }
722
- if (versionOrder === 0) {
724
+ const reconcileCurrentInstallation =
725
+ versionOrder === 0 && files.requiresFinalLauncherSettlement?.() === true;
726
+ if (versionOrder === 0 && !reconcileCurrentInstallation) {
723
727
  output.writeLine(`Mikie is already current at ${installedCurrentVersion}.`);
724
728
  return { status: "current", version: installedCurrentVersion };
725
729
  }
@@ -757,7 +761,7 @@ export async function runUpdateTransaction({
757
761
  if (wrapperVersion !== resolved.version || candidatePlatformVersion !== resolved.version) {
758
762
  throw new Error("candidate package version verification failed");
759
763
  }
760
- finalLauncher = files.finalLauncher();
764
+ finalLauncher = await files.finalLauncher(candidate, resolved.version);
761
765
  const externalRecovery = await files.recordPreparedCandidate(
762
766
  snapshot,
763
767
  candidate,
@@ -775,6 +779,7 @@ export async function runUpdateTransaction({
775
779
  MIKIE_DISTRIBUTION_MODE: "npm",
776
780
  MIKIE_NPM_UPDATE_TOPOLOGY: UpdateTopology.NpmGlobal,
777
781
  MIKIE_NPM_UPDATE_REPAIR_PHASE: "prepare",
782
+ MIKIE_UPDATE_REPAIR_LAUNCHER_STATE: "prior",
778
783
  MIKIE_UPDATE_REPAIR_CANDIDATE: candidate.binary,
779
784
  MIKIE_UPDATE_REPAIR_LAUNCHER: finalLauncher,
780
785
  },
@@ -810,8 +815,21 @@ export async function runUpdateTransaction({
810
815
  await files.verifyActivatedCandidate(candidate, activatedCandidate);
811
816
  await files.activate(activatedCandidate);
812
817
  await files.checkpointCandidate(snapshot, { provider: true });
813
- const settlementRequired = repairResult.receipt.action === "migrated";
814
- await files.decideCommit(snapshot, { settlementRequired });
818
+ const settlementRequired =
819
+ repairResult.receipt.action === "migrated" ||
820
+ files.requiresFinalLauncherSettlement?.() === true;
821
+ // A skipped receipt does not prove that no client still references a
822
+ // historical launcher. In particular, Claude can report unavailable
823
+ // when hooks/ownership are absent while an MCP-only registration remains.
824
+ // Retain until a later authenticated reconciliation proves both surfaces
825
+ // absent/current instead of deleting a possibly referenced runtime.
826
+ const retainHistoricalRuntimes = ["foreign_conflict", "unavailable"].includes(
827
+ repairResult.receipt.before,
828
+ );
829
+ await files.decideCommit(snapshot, {
830
+ settlementRequired,
831
+ ...(retainHistoricalRuntimes ? { retainHistoricalRuntimes: true } : {}),
832
+ });
815
833
  commitDecided = true;
816
834
  await files.reconcileTrajectoryStore(snapshot);
817
835
  if (settlementRequired) {
@@ -822,6 +840,7 @@ export async function runUpdateTransaction({
822
840
  MIKIE_DISTRIBUTION_MODE: "npm",
823
841
  MIKIE_NPM_UPDATE_TOPOLOGY: UpdateTopology.NpmGlobal,
824
842
  MIKIE_NPM_UPDATE_REPAIR_PHASE: "commit",
843
+ MIKIE_UPDATE_REPAIR_LAUNCHER_STATE: "activated",
825
844
  MIKIE_UPDATE_REPAIR_CANDIDATE: candidate.binary,
826
845
  MIKIE_UPDATE_REPAIR_LAUNCHER: finalLauncher,
827
846
  },
@@ -830,9 +849,12 @@ export async function runUpdateTransaction({
830
849
  throw new Error("candidate owned-adapter settlement failed");
831
850
  }
832
851
  }
833
- await files.retire(snapshot);
852
+ await files.retire(
853
+ snapshot,
854
+ retainHistoricalRuntimes ? { retainHistoricalRuntimes: true } : undefined,
855
+ );
834
856
  return {
835
- status: "updated",
857
+ status: reconcileCurrentInstallation ? "reconciled" : "updated",
836
858
  version: resolved.version,
837
859
  providerState: repairResult.receipt.after,
838
860
  adapterAction: repairResult.skipped
@@ -853,6 +875,8 @@ export async function runUpdateTransaction({
853
875
  MIKIE_DISTRIBUTION_MODE: "npm",
854
876
  MIKIE_NPM_UPDATE_TOPOLOGY: UpdateTopology.NpmGlobal,
855
877
  MIKIE_NPM_UPDATE_REPAIR_PHASE: "rollback",
878
+ MIKIE_UPDATE_REPAIR_LAUNCHER_STATE:
879
+ files.preparedLauncherState?.(snapshot) ?? "prior",
856
880
  MIKIE_UPDATE_REPAIR_CANDIDATE: candidate.binary,
857
881
  MIKIE_UPDATE_REPAIR_LAUNCHER: finalLauncher,
858
882
  },
@@ -868,6 +892,9 @@ export async function runUpdateTransaction({
868
892
  if (!commitDecided && !restoreFailure) {
869
893
  try {
870
894
  await files.restore(snapshot);
895
+ if (snapshot.preparedCandidate) {
896
+ await files.abandonPreparedLauncher?.(snapshot);
897
+ }
871
898
  } catch (restoreError) {
872
899
  restoreFailure = restoreError;
873
900
  }
@@ -1135,6 +1162,33 @@ function bytesFingerprint(existed, bytes) {
1135
1162
  return existed ? `present:${createHash("sha256").update(bytes).digest("hex")}` : "absent";
1136
1163
  }
1137
1164
 
1165
+ async function executableDigest(filePath) {
1166
+ if (typeof filePath !== "string" || !path.isAbsolute(filePath)) {
1167
+ throw new Error("prepared final launcher authority is invalid");
1168
+ }
1169
+ await assertNoLinkedAncestors(filePath);
1170
+ const before = await lstat(filePath);
1171
+ if (
1172
+ !before.isFile() ||
1173
+ before.isSymbolicLink() ||
1174
+ before.size <= 0 ||
1175
+ before.size > MAX_TRANSACTION_TREE_BYTES
1176
+ ) {
1177
+ throw new Error("prepared final launcher authority is invalid");
1178
+ }
1179
+ const bytes = await readFile(filePath);
1180
+ const after = await lstat(filePath);
1181
+ if (
1182
+ !after.isFile() ||
1183
+ after.isSymbolicLink() ||
1184
+ bytes.length !== before.size ||
1185
+ after.size !== before.size
1186
+ ) {
1187
+ throw new Error("prepared final launcher changed");
1188
+ }
1189
+ return sha256Bytes(bytes);
1190
+ }
1191
+
1138
1192
  async function directoryFingerprint(root) {
1139
1193
  const digest = createHash("sha256");
1140
1194
  let entries = 0;
@@ -1263,6 +1317,9 @@ export function createFilesystemAdapter({
1263
1317
  settleProviderRepair,
1264
1318
  reconcileTrajectoryStore,
1265
1319
  activationBoundary,
1320
+ prepareFinalLauncher,
1321
+ retireFinalLauncher,
1322
+ abandonFinalLauncher,
1266
1323
  }) {
1267
1324
  const currentRoots = validateInstallationLayout(currentLayout);
1268
1325
  const currentShims = currentRoots.shims;
@@ -1276,7 +1333,10 @@ export function createFilesystemAdapter({
1276
1333
  !path.isAbsolute(transactionRoot) ||
1277
1334
  (settleProviderRepair !== undefined && typeof settleProviderRepair !== "function") ||
1278
1335
  (reconcileTrajectoryStore !== undefined && typeof reconcileTrajectoryStore !== "function") ||
1279
- (activationBoundary !== undefined && typeof activationBoundary !== "function")
1336
+ (activationBoundary !== undefined && typeof activationBoundary !== "function") ||
1337
+ (prepareFinalLauncher !== undefined && typeof prepareFinalLauncher !== "function") ||
1338
+ (retireFinalLauncher !== undefined && typeof retireFinalLauncher !== "function") ||
1339
+ (abandonFinalLauncher !== undefined && typeof abandonFinalLauncher !== "function")
1280
1340
  ) {
1281
1341
  throw new Error("unsafe Mikie update filesystem path");
1282
1342
  }
@@ -1300,6 +1360,7 @@ export function createFilesystemAdapter({
1300
1360
  }
1301
1361
  const lockPath = `${transactionRoot}.lock`;
1302
1362
  const journalPath = path.join(transactionRoot, "journal.json");
1363
+ let preparedFinalLauncherAuthority = null;
1303
1364
 
1304
1365
  const readLock = async () => {
1305
1366
  try {
@@ -1348,6 +1409,7 @@ export function createFilesystemAdapter({
1348
1409
  platform_fingerprint: snapshot.preparedCandidate.platformFingerprint,
1349
1410
  shims: snapshot.preparedCandidate.shimFingerprints,
1350
1411
  final_launcher: snapshot.preparedCandidate.finalLauncher,
1412
+ final_launcher_sha256: snapshot.preparedCandidate.finalLauncherSha256,
1351
1413
  }
1352
1414
  : null,
1353
1415
  recovery: snapshot.recovery
@@ -1436,7 +1498,10 @@ export function createFilesystemAdapter({
1436
1498
  "provider_rolled_back",
1437
1499
  "commit_decided",
1438
1500
  "commit_decided_no_repair",
1501
+ "commit_decided_retain_runtime",
1502
+ "commit_decided_no_repair_retain_runtime",
1439
1503
  "committed",
1504
+ "committed_retain_runtime",
1440
1505
  ].includes(value.phase) ||
1441
1506
  !Array.isArray(value.installation) ||
1442
1507
  !Array.isArray(value.shims) ||
@@ -1599,6 +1664,7 @@ export function createFilesystemAdapter({
1599
1664
  "platform_fingerprint",
1600
1665
  "shims",
1601
1666
  "final_launcher",
1667
+ "final_launcher_sha256",
1602
1668
  ]) ||
1603
1669
  ![prepared.wrapper_package_json, prepared.platform_package_json, prepared.binary].every(
1604
1670
  (candidatePath) =>
@@ -1619,7 +1685,9 @@ export function createFilesystemAdapter({
1619
1685
  ) ||
1620
1686
  new Set(prepared.shims.map((shim) => path.resolve(shim.path))).size !==
1621
1687
  prepared.shims.length ||
1622
- !sameExecutablePath(prepared.final_launcher, currentLayout.binary)
1688
+ typeof prepared.final_launcher !== "string" ||
1689
+ !path.isAbsolute(prepared.final_launcher) ||
1690
+ !/^[a-f0-9]{64}$/.test(prepared.final_launcher_sha256)
1623
1691
  ) {
1624
1692
  throw new Error("invalid Mikie update transaction journal");
1625
1693
  }
@@ -1632,6 +1700,7 @@ export function createFilesystemAdapter({
1632
1700
  shims: prepared.shims.map((shim) => shim.path),
1633
1701
  shimFingerprints: prepared.shims,
1634
1702
  finalLauncher: prepared.final_launcher,
1703
+ finalLauncherSha256: prepared.final_launcher_sha256,
1635
1704
  };
1636
1705
  const preparedRoots = validateInstallationLayout(preparedCandidate);
1637
1706
  if (
@@ -1648,6 +1717,13 @@ export function createFilesystemAdapter({
1648
1717
  throw new Error("corrupt Mikie staged candidate");
1649
1718
  }
1650
1719
  }
1720
+ if (
1721
+ !sameExecutablePath(preparedCandidate.finalLauncher, currentLayout.binary) &&
1722
+ (await executableDigest(preparedCandidate.finalLauncher)) !==
1723
+ preparedCandidate.finalLauncherSha256
1724
+ ) {
1725
+ throw new Error("prepared final launcher changed");
1726
+ }
1651
1727
  }
1652
1728
  let recovery = null;
1653
1729
  if (value.recovery !== null) {
@@ -1715,7 +1791,10 @@ export function createFilesystemAdapter({
1715
1791
  "candidate_packages",
1716
1792
  "commit_decided",
1717
1793
  "commit_decided_no_repair",
1794
+ "commit_decided_retain_runtime",
1795
+ "commit_decided_no_repair_retain_runtime",
1718
1796
  "committed",
1797
+ "committed_retain_runtime",
1719
1798
  ].includes(value.phase) &&
1720
1799
  (!preparedCandidate || !recovery)
1721
1800
  ) {
@@ -2223,6 +2302,45 @@ export function createFilesystemAdapter({
2223
2302
  }
2224
2303
  };
2225
2304
 
2305
+ const retirePreparedLauncher = async (snapshot) => {
2306
+ if (!retireFinalLauncher) return;
2307
+ const authority = snapshot?.preparedCandidate;
2308
+ if (
2309
+ !authority ||
2310
+ (await executableDigest(authority.finalLauncher)) !== authority.finalLauncherSha256
2311
+ ) {
2312
+ throw new Error("prepared final launcher changed before retirement");
2313
+ }
2314
+ await retireFinalLauncher({
2315
+ path: authority.finalLauncher,
2316
+ sha256: authority.finalLauncherSha256,
2317
+ });
2318
+ };
2319
+
2320
+ const abandonPreparedLauncher = async (snapshot) => {
2321
+ if (!abandonFinalLauncher || !snapshot?.preparedCandidate) return;
2322
+ const authority = snapshot.preparedCandidate;
2323
+ if ((await executableDigest(authority.finalLauncher)) !== authority.finalLauncherSha256) {
2324
+ throw new Error("prepared final launcher changed before abandonment");
2325
+ }
2326
+ await abandonFinalLauncher({
2327
+ path: authority.finalLauncher,
2328
+ sha256: authority.finalLauncherSha256,
2329
+ });
2330
+ };
2331
+
2332
+ const preparedLauncherState = (snapshot) => {
2333
+ const authority = snapshot?.preparedCandidate;
2334
+ if (!authority) throw new Error("prepared final launcher authority is unavailable");
2335
+ if (!sameExecutablePath(authority.finalLauncher, currentLayout.binary)) return "activated";
2336
+ const owner = snapshot.installation.find(
2337
+ (entry) =>
2338
+ sameExecutablePath(entry.root, currentRoots.platformRoot) ||
2339
+ isPathInside(entry.root, currentLayout.binary),
2340
+ );
2341
+ return owner?.activationState === "activated" ? "activated" : "prior";
2342
+ };
2343
+
2226
2344
  const recoverInterrupted = async (lease, expectedJournalToken) => {
2227
2345
  try {
2228
2346
  const metadata = await lstat(transactionRoot);
@@ -2234,7 +2352,8 @@ export function createFilesystemAdapter({
2234
2352
  throw error;
2235
2353
  }
2236
2354
  const snapshot = await hydrateJournal(lease, expectedJournalToken);
2237
- if (snapshot.phase === "commit_decided") {
2355
+ if (["commit_decided", "commit_decided_retain_runtime"].includes(snapshot.phase)) {
2356
+ const retainHistoricalRuntimes = snapshot.phase === "commit_decided_retain_runtime";
2238
2357
  if (!reconcileTrajectoryStore) {
2239
2358
  throw new Error("trajectory-store reconciliation is unavailable");
2240
2359
  }
@@ -2242,25 +2361,39 @@ export function createFilesystemAdapter({
2242
2361
  if (!settleProviderRepair) {
2243
2362
  throw new Error("candidate owned-adapter settlement is unavailable");
2244
2363
  }
2245
- await settleProviderRepair("commit", snapshot);
2246
- snapshot.phase = "committed";
2364
+ await settleProviderRepair("commit", snapshot, "activated");
2365
+ snapshot.phase = retainHistoricalRuntimes ? "committed_retain_runtime" : "committed";
2247
2366
  await writeJournal(snapshot);
2367
+ if (!retainHistoricalRuntimes) await retirePreparedLauncher(snapshot);
2248
2368
  await cleanupCommittedActivation(snapshot);
2249
2369
  await rm(transactionRoot, { recursive: true, force: true });
2250
2370
  return;
2251
2371
  }
2252
- if (snapshot.phase === "commit_decided_no_repair") {
2372
+ if (
2373
+ ["commit_decided_no_repair", "commit_decided_no_repair_retain_runtime"].includes(
2374
+ snapshot.phase,
2375
+ )
2376
+ ) {
2377
+ const retainHistoricalRuntimes =
2378
+ snapshot.phase === "commit_decided_no_repair_retain_runtime";
2253
2379
  if (!reconcileTrajectoryStore) {
2254
2380
  throw new Error("trajectory-store reconciliation is unavailable");
2255
2381
  }
2256
2382
  await reconcileTrajectoryStore(snapshot.preparedCandidate.finalLauncher, snapshot);
2257
- snapshot.phase = "committed";
2383
+ snapshot.phase = retainHistoricalRuntimes ? "committed_retain_runtime" : "committed";
2258
2384
  await writeJournal(snapshot);
2385
+ if (!retainHistoricalRuntimes) await retirePreparedLauncher(snapshot);
2259
2386
  await cleanupCommittedActivation(snapshot);
2260
2387
  await rm(transactionRoot, { recursive: true, force: true });
2261
2388
  return;
2262
2389
  }
2263
2390
  if (snapshot.phase === "committed") {
2391
+ await retirePreparedLauncher(snapshot);
2392
+ await cleanupCommittedActivation(snapshot);
2393
+ await rm(transactionRoot, { recursive: true, force: true });
2394
+ return;
2395
+ }
2396
+ if (snapshot.phase === "committed_retain_runtime") {
2264
2397
  await cleanupCommittedActivation(snapshot);
2265
2398
  await rm(transactionRoot, { recursive: true, force: true });
2266
2399
  return;
@@ -2269,11 +2402,12 @@ export function createFilesystemAdapter({
2269
2402
  if (!settleProviderRepair) {
2270
2403
  throw new Error("candidate owned-adapter settlement is unavailable");
2271
2404
  }
2272
- await settleProviderRepair("rollback", snapshot);
2405
+ await settleProviderRepair("rollback", snapshot, preparedLauncherState(snapshot));
2273
2406
  snapshot.phase = "provider_rolled_back";
2274
2407
  await writeJournal(snapshot);
2275
2408
  }
2276
2409
  await restoreSnapshot(snapshot);
2410
+ await abandonPreparedLauncher(snapshot);
2277
2411
  };
2278
2412
 
2279
2413
  const beginTransaction = async () => {
@@ -2440,11 +2574,45 @@ export function createFilesystemAdapter({
2440
2574
  }
2441
2575
  return snapshot.stagingPrefix;
2442
2576
  },
2443
- finalLauncher() {
2444
- return currentLayout.binary;
2577
+ requiresFinalLauncherSettlement() {
2578
+ return prepareFinalLauncher !== undefined;
2579
+ },
2580
+ preparedLauncherState(snapshot) {
2581
+ return preparedLauncherState(snapshot);
2582
+ },
2583
+ async finalLauncher(candidate, version) {
2584
+ const candidateRoots = validateInstallationLayout(candidate);
2585
+ if (
2586
+ !exactPublicVersion(version) ||
2587
+ !isPathInside(path.join(transactionRoot, "candidate-prefix"), candidateRoots.wrapperRoot) ||
2588
+ !isPathInside(path.join(transactionRoot, "candidate-prefix"), candidateRoots.platformRoot)
2589
+ ) {
2590
+ throw new Error("prepared final launcher authority is invalid");
2591
+ }
2592
+ const finalLauncher = prepareFinalLauncher
2593
+ ? await prepareFinalLauncher(candidate, version)
2594
+ : currentLayout.binary;
2595
+ const candidateSha256 = await executableDigest(candidate.binary);
2596
+ if (
2597
+ typeof finalLauncher !== "string" ||
2598
+ !path.isAbsolute(finalLauncher) ||
2599
+ isPathInside(transactionRoot, finalLauncher) ||
2600
+ (prepareFinalLauncher && (await executableDigest(finalLauncher)) !== candidateSha256)
2601
+ ) {
2602
+ throw new Error("prepared final launcher authority is invalid");
2603
+ }
2604
+ preparedFinalLauncherAuthority = {
2605
+ path: finalLauncher,
2606
+ sha256: candidateSha256,
2607
+ };
2608
+ return finalLauncher;
2445
2609
  },
2446
2610
  async recordPreparedCandidate(snapshot, candidate, finalLauncher) {
2447
2611
  const candidateRoots = validateInstallationLayout(candidate);
2612
+ const authority = preparedFinalLauncherAuthority ?? {
2613
+ path: currentLayout.binary,
2614
+ sha256: await executableDigest(candidate.binary),
2615
+ };
2448
2616
  if (
2449
2617
  !isPathInside(snapshot.stagingPrefix, candidateRoots.wrapperRoot) ||
2450
2618
  !isPathInside(snapshot.stagingPrefix, candidateRoots.platformRoot) ||
@@ -2455,7 +2623,9 @@ export function createFilesystemAdapter({
2455
2623
  !isPathInside(snapshot.stagingPrefix, shim) ||
2456
2624
  path.basename(shim).toLowerCase() !== path.basename(currentShims[index]).toLowerCase(),
2457
2625
  ) ||
2458
- !sameExecutablePath(finalLauncher, currentLayout.binary)
2626
+ !sameExecutablePath(finalLauncher, authority.path) ||
2627
+ (prepareFinalLauncher && (await executableDigest(finalLauncher)) !== authority.sha256) ||
2628
+ (await executableDigest(candidate.binary)) !== authority.sha256
2459
2629
  ) {
2460
2630
  throw new Error("unsafe Mikie staged candidate");
2461
2631
  }
@@ -2482,6 +2652,7 @@ export function createFilesystemAdapter({
2482
2652
  shims: candidateRoots.shims,
2483
2653
  shimFingerprints,
2484
2654
  finalLauncher,
2655
+ finalLauncherSha256: authority.sha256,
2485
2656
  };
2486
2657
  const runtime = path.join(transactionRoot, "recovery-runtime.mjs");
2487
2658
  const launcher = path.join(transactionRoot, "recover.mjs");
@@ -2506,6 +2677,7 @@ export function createFilesystemAdapter({
2506
2677
  node: process.execPath,
2507
2678
  candidate_binary: candidate.binary,
2508
2679
  final_launcher: finalLauncher,
2680
+ final_launcher_sha256: authority.sha256,
2509
2681
  };
2510
2682
  const descriptorBytes = Buffer.from(`${JSON.stringify(recoveryDescriptor)}\n`);
2511
2683
  await atomicWritePrivate(descriptor, descriptorBytes);
@@ -2749,8 +2921,17 @@ export function createFilesystemAdapter({
2749
2921
  await writeJournal(snapshot);
2750
2922
  }
2751
2923
  },
2752
- async decideCommit(snapshot, { settlementRequired = true } = {}) {
2753
- snapshot.phase = settlementRequired ? "commit_decided" : "commit_decided_no_repair";
2924
+ async decideCommit(
2925
+ snapshot,
2926
+ { settlementRequired = true, retainHistoricalRuntimes = false } = {},
2927
+ ) {
2928
+ snapshot.phase = retainHistoricalRuntimes
2929
+ ? settlementRequired
2930
+ ? "commit_decided_retain_runtime"
2931
+ : "commit_decided_no_repair_retain_runtime"
2932
+ : settlementRequired
2933
+ ? "commit_decided"
2934
+ : "commit_decided_no_repair";
2754
2935
  await writeJournal(snapshot);
2755
2936
  },
2756
2937
  async reconcileTrajectoryStore(snapshot) {
@@ -2865,9 +3046,13 @@ export function createFilesystemAdapter({
2865
3046
  await restoreSnapshot(snapshot);
2866
3047
  if (snapshot.autoLease) await releaseLease(snapshot.lease);
2867
3048
  },
2868
- async retire(snapshot) {
2869
- snapshot.phase = "committed";
3049
+ async abandonPreparedLauncher(snapshot) {
3050
+ await abandonPreparedLauncher(snapshot);
3051
+ },
3052
+ async retire(snapshot, { retainHistoricalRuntimes = false } = {}) {
3053
+ snapshot.phase = retainHistoricalRuntimes ? "committed_retain_runtime" : "committed";
2870
3054
  await writeJournal(snapshot);
3055
+ if (!retainHistoricalRuntimes) await retirePreparedLauncher(snapshot);
2871
3056
  await cleanupCommittedActivation(snapshot);
2872
3057
  await rm(snapshot.backupRoot, { recursive: true, force: true });
2873
3058
  if (snapshot.autoLease) await releaseLease(snapshot.lease);
@@ -2908,6 +3093,7 @@ export async function recoverExternalNpmUpdate(transactionRoot, expectedAuthorit
2908
3093
  "node",
2909
3094
  "candidate_binary",
2910
3095
  "final_launcher",
3096
+ "final_launcher_sha256",
2911
3097
  ]) ||
2912
3098
  descriptor.schema_version !== RECOVERY_SCHEMA ||
2913
3099
  descriptor.transaction_root !== transactionRoot ||
@@ -2916,7 +3102,10 @@ export async function recoverExternalNpmUpdate(transactionRoot, expectedAuthorit
2916
3102
  (artifactPath) =>
2917
3103
  typeof artifactPath === "string" && isPathInside(transactionRoot, artifactPath),
2918
3104
  ) ||
2919
- descriptor.final_launcher !== descriptor.current_layout?.binary ||
3105
+ typeof descriptor.final_launcher !== "string" ||
3106
+ !path.isAbsolute(descriptor.final_launcher) ||
3107
+ !/^[a-f0-9]{64}$/.test(descriptor.final_launcher_sha256) ||
3108
+ typeof descriptor.current_layout?.binary !== "string" ||
2920
3109
  descriptor.node !== process.execPath ||
2921
3110
  !/^[a-f0-9]{64}$/.test(descriptor.runtime_sha256) ||
2922
3111
  !/^[a-f0-9]{64}$/.test(descriptor.launcher_sha256) ||
@@ -2947,6 +3136,12 @@ export async function recoverExternalNpmUpdate(transactionRoot, expectedAuthorit
2947
3136
  ) {
2948
3137
  throw new Error("tampered Mikie update recovery authority");
2949
3138
  }
3139
+ if (
3140
+ !sameExecutablePath(descriptor.final_launcher, descriptor.current_layout.binary) &&
3141
+ (await executableDigest(descriptor.final_launcher)) !== descriptor.final_launcher_sha256
3142
+ ) {
3143
+ throw new Error("prepared final launcher changed");
3144
+ }
2950
3145
  const journal = parseJsonNoDuplicateKeys(journalBytes);
2951
3146
  if (journal?.token !== descriptor.token) {
2952
3147
  throw new Error("Mikie update recovery authority does not match the journal");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmikie/cli",
3
- "version": "1.1.103",
3
+ "version": "1.1.104",
4
4
  "description": "Native Mikie relay launcher for MCP clients",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -24,10 +24,10 @@
24
24
  "node": ">=22"
25
25
  },
26
26
  "optionalDependencies": {
27
- "@getmikie/darwin-arm64": "1.1.103",
28
- "@getmikie/darwin-x64": "1.1.103",
29
- "@getmikie/linux-x64-gnu": "1.1.103",
30
- "@getmikie/win32-x64": "1.1.103"
27
+ "@getmikie/darwin-arm64": "1.1.104",
28
+ "@getmikie/darwin-x64": "1.1.104",
29
+ "@getmikie/linux-x64-gnu": "1.1.104",
30
+ "@getmikie/win32-x64": "1.1.104"
31
31
  },
32
32
  "publishConfig": {
33
33
  "access": "public",