@platless/axom-cli 0.5.9 → 0.6.52

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 (258) hide show
  1. package/README.md +25 -6
  2. package/dist/axom-patch/cli/README.md +129 -12
  3. package/dist/axom-patch/cli/account-limits/generate.py +15 -12
  4. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
  5. package/dist/axom-patch/cli/claude-account-limits.json +9 -9
  6. package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
  7. package/dist/axom-patch/cli/codex-account-limits.json +11 -11
  8. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  9. package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
  10. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  11. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  12. package/dist/axom-patch/cli/nightly-task.md +55 -13
  13. package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
  14. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  15. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  16. package/dist/axom-patch/cli/probes/zcode-env-account.mjs +81 -0
  17. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
  18. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1687 -0
  19. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +277 -0
  20. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
  21. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
  22. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  23. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  24. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  25. package/dist/axom-patch/cli/runtime.json +19 -19
  26. package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
  27. package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
  28. package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
  29. package/dist/axom-patch/cli/zcode-env-account.json +58 -0
  30. package/dist/axom-patch/cli/zcode-models.json +9 -10
  31. package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -11
  32. package/dist/cli.js +11 -45
  33. package/dist/commands/auth.js +4 -1
  34. package/dist/commands/knowledge.js +47 -0
  35. package/dist/commands/update.js +126 -3
  36. package/dist/commands/workstation.js +101 -164
  37. package/dist/commands/workstationRegistrations.js +116 -0
  38. package/dist/credentialBroker.js +13 -2
  39. package/dist/credentials.js +16 -0
  40. package/dist/install/claude/commands.js +15 -15
  41. package/dist/keepers/terminal/keeper.mjs +1089 -673
  42. package/dist/keepers/terminal/manifest.json +4 -4
  43. package/dist/mcp.js +1 -1
  44. package/dist/native/darwin-x64/axom-quic +0 -0
  45. package/dist/workstation/acp/acpAgents.js +113 -102
  46. package/dist/workstation/acp/acpClient.js +26 -0
  47. package/dist/workstation/acp/builds/install/npm.js +99 -10
  48. package/dist/workstation/acp/initializeResponse.js +6 -1
  49. package/dist/workstation/acp/managedPatches.js +189 -25
  50. package/dist/workstation/acp/managedRuntime.js +314 -0
  51. package/dist/workstation/acp/outbox.js +12 -5
  52. package/dist/workstation/acp/patchAssets.js +40 -0
  53. package/dist/workstation/acp/permission.js +2 -2
  54. package/dist/workstation/acp/processor/checkpoint.js +26 -34
  55. package/dist/workstation/acp/processor/control.js +7 -80
  56. package/dist/workstation/acp/processor/jsonrpc.js +8 -0
  57. package/dist/workstation/acp/processor/runtime.js +5 -81
  58. package/dist/workstation/acp/processor/session.js +54 -82
  59. package/dist/workstation/acp/processor/state.js +7 -13
  60. package/dist/workstation/acp/processor/step.js +3 -20
  61. package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
  62. package/dist/workstation/acp/processor/transport/local.js +19 -3
  63. package/dist/workstation/acp/processor/updates.js +156 -105
  64. package/dist/workstation/acp/processorSession.js +20 -173
  65. package/dist/workstation/acp/registry/catalog.js +65 -39
  66. package/dist/workstation/acp/registry/launch.js +16 -1
  67. package/dist/workstation/acp/switching.js +0 -3
  68. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  69. package/dist/workstation/acp/test/keeperWorld.js +32 -3
  70. package/dist/workstation/acp/test/raw/sources.js +16 -130
  71. package/dist/workstation/acp/test/realAdapter.js +1 -47
  72. package/dist/workstation/acp/test/scriptAgent.js +11 -3
  73. package/dist/workstation/agentFlow.js +1 -1
  74. package/dist/workstation/agentSessions.js +114 -166
  75. package/dist/workstation/channels/agent.js +9 -1
  76. package/dist/workstation/channels/computer.js +27 -4
  77. package/dist/workstation/channels/management.js +130 -15
  78. package/dist/workstation/channels/url.js +1 -1
  79. package/dist/workstation/config.js +33 -9
  80. package/dist/workstation/connection/encryptedChannel.js +4 -1
  81. package/dist/workstation/control/bindingStore.js +204 -0
  82. package/dist/workstation/control/commands.js +125 -0
  83. package/dist/workstation/control/daemonIdentity.js +143 -0
  84. package/dist/workstation/control/deviceCommands.js +191 -0
  85. package/dist/workstation/control/deviceProtocol.js +68 -0
  86. package/dist/workstation/control/enrollment.js +106 -0
  87. package/dist/workstation/control/inspect.js +71 -0
  88. package/dist/workstation/control/locks.js +110 -0
  89. package/dist/workstation/control/protocol.js +40 -0
  90. package/dist/workstation/control/restartGuard.js +46 -0
  91. package/dist/workstation/control/rootFormat.js +16 -0
  92. package/dist/workstation/control/runtimeClient.js +152 -0
  93. package/dist/workstation/control/runtimeHost.js +235 -0
  94. package/dist/workstation/control/serverIdentity.js +35 -0
  95. package/dist/workstation/control/updateTransition.js +91 -0
  96. package/dist/workstation/daemon.js +266 -264
  97. package/dist/workstation/daemonctl.js +226 -25
  98. package/dist/workstation/desktopControl/audio.js +99 -0
  99. package/dist/workstation/desktopControl/automationDevice.js +219 -0
  100. package/dist/workstation/desktopControl/automationJson.js +74 -0
  101. package/dist/workstation/desktopControl/automationLocalState.js +23 -0
  102. package/dist/workstation/desktopControl/automationProtocol.js +253 -0
  103. package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
  104. package/dist/workstation/desktopControl/automationSession.js +81 -0
  105. package/dist/workstation/desktopControl/automationTransport.js +138 -0
  106. package/dist/workstation/desktopControl/capabilities.js +93 -0
  107. package/dist/workstation/desktopControl/captureModes.js +9 -0
  108. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  109. package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
  110. package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
  111. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  112. package/dist/workstation/desktopControl/helperProcess.js +602 -0
  113. package/dist/workstation/desktopControl/hostGrant.js +65 -0
  114. package/dist/workstation/desktopControl/lobby.js +131 -0
  115. package/dist/workstation/desktopControl/macProvider.js +156 -0
  116. package/dist/workstation/desktopControl/media.js +331 -0
  117. package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
  118. package/dist/workstation/desktopControl/nativeProvider.js +1044 -0
  119. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  120. package/dist/workstation/desktopControl/overview.js +58 -0
  121. package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
  122. package/dist/workstation/desktopControl/protocol.js +311 -0
  123. package/dist/workstation/desktopControl/provider.js +1 -0
  124. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  125. package/dist/workstation/desktopControl/rateController.js +226 -0
  126. package/dist/workstation/desktopControl/supervisor.js +1774 -0
  127. package/dist/workstation/desktopControl/transport.js +27 -0
  128. package/dist/workstation/desktopControl/wallpaper.js +98 -0
  129. package/dist/workstation/exec/policy.js +5 -0
  130. package/dist/workstation/files/client.js +10 -1
  131. package/dist/workstation/files/credentials.js +142 -0
  132. package/dist/workstation/files/host.js +9 -2
  133. package/dist/workstation/files/text.js +42 -8
  134. package/dist/workstation/journal.js +5 -2
  135. package/dist/workstation/keepers/acp/client.js +38 -2
  136. package/dist/workstation/keepers/kit/buildVersion.js +35 -11
  137. package/dist/workstation/keepers/kit/bundle.js +21 -17
  138. package/dist/workstation/keepers/kit/client.js +2 -2
  139. package/dist/workstation/keepers/kit/launcher.js +123 -7
  140. package/dist/workstation/keepers/kit/paths.js +2 -1
  141. package/dist/workstation/keepers/legacy.js +16 -2
  142. package/dist/workstation/keepers/policy.js +5 -34
  143. package/dist/workstation/keepers/terminal/client.js +115 -23
  144. package/dist/workstation/keepers/terminal/exec.js +6 -3
  145. package/dist/workstation/keepers/terminal/frames.js +73 -0
  146. package/dist/workstation/keepers/terminal/generations.js +5 -8
  147. package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
  148. package/dist/workstation/keepers/terminal/input.js +146 -0
  149. package/dist/workstation/keepers/terminal/keeper.js +22 -8
  150. package/dist/workstation/keepers/terminal/main.js +1 -1
  151. package/dist/workstation/keepers/terminal/metadata.js +138 -0
  152. package/dist/workstation/keepers/terminal/protocol.js +4 -28
  153. package/dist/workstation/keepers/terminal/pty.js +1 -330
  154. package/dist/workstation/keepers/terminal/screen.js +246 -0
  155. package/dist/workstation/keepers/terminal/sessions.js +526 -0
  156. package/dist/workstation/managedInstall.js +57 -0
  157. package/dist/workstation/oauth/deviceAuthorization.js +31 -17
  158. package/dist/workstation/protocol/agent.js +2 -15
  159. package/dist/workstation/protocol/management.js +113 -5
  160. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  161. package/dist/workstation/rd/artifact.js +120 -0
  162. package/dist/workstation/rd/control.js +117 -0
  163. package/dist/workstation/rd/display.js +29 -0
  164. package/dist/workstation/rd/host.js +213 -0
  165. package/dist/workstation/rd/migrationReadiness.js +46 -0
  166. package/dist/workstation/rd/provider.js +256 -0
  167. package/dist/workstation/rd/sessions.js +391 -0
  168. package/dist/workstation/rd/state.js +57 -0
  169. package/dist/workstation/rd/update.js +168 -0
  170. package/dist/workstation/rd/upkeep.js +80 -0
  171. package/dist/workstation/register.js +27 -37
  172. package/dist/workstation/registrations/enrollment.js +250 -0
  173. package/dist/workstation/registrations/enrollmentPending.js +361 -0
  174. package/dist/workstation/registrations/migration.js +356 -0
  175. package/dist/workstation/registrations/model.js +34 -0
  176. package/dist/workstation/registrations/removal.js +222 -0
  177. package/dist/workstation/registrations/store.js +737 -0
  178. package/dist/workstation/runtime/desktopBinding.js +20 -0
  179. package/dist/workstation/runtime/deviceDesktop.js +193 -0
  180. package/dist/workstation/runtime/deviceRuntime.js +568 -0
  181. package/dist/workstation/runtime/deviceServices.js +117 -0
  182. package/dist/workstation/runtime/events.js +20 -0
  183. package/dist/workstation/runtime/ownership.js +100 -0
  184. package/dist/workstation/runtime/registrationRuntime.js +575 -0
  185. package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
  186. package/dist/workstation/runtime/removalActivity.js +184 -0
  187. package/dist/workstation/service/installer.js +197 -14
  188. package/dist/workstation/service/launchd.js +2 -0
  189. package/dist/workstation/service/systemd.js +1 -0
  190. package/dist/workstation/storageMetrics.js +118 -0
  191. package/dist/workstation/systemMetrics.js +47 -92
  192. package/dist/workstation/toolchain.js +94 -0
  193. package/dist/workstation/tools/browser.js +28 -4
  194. package/dist/workstation/tools/client.js +8 -27
  195. package/dist/workstation/tools/info.js +6 -13
  196. package/dist/workstation/tools/terminalRouter.js +17 -172
  197. package/dist/workstation/tools/validation.js +55 -1
  198. package/dist/workstation/update/archiveEvidence.js +206 -0
  199. package/dist/workstation/update/artifact.js +2 -1
  200. package/dist/workstation/update/coordinator.js +34 -13
  201. package/dist/workstation/update/deviceCompatibility.js +119 -0
  202. package/dist/workstation/update/deviceCoordinator.js +369 -0
  203. package/dist/workstation/update/deviceHealth.js +104 -0
  204. package/dist/workstation/update/deviceHealthProtocol.js +26 -0
  205. package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
  206. package/dist/workstation/update/deviceRollback.js +78 -0
  207. package/dist/workstation/update/globalInstall.js +186 -0
  208. package/dist/workstation/update/legacyBridge.js +414 -0
  209. package/dist/workstation/update/legacyOwner.js +39 -0
  210. package/dist/workstation/update/npmInstaller.js +51 -25
  211. package/dist/workstation/update/privateDurability.js +61 -0
  212. package/dist/workstation/update/rdRollback.js +145 -0
  213. package/dist/workstation/update/recoveryPlan.js +124 -0
  214. package/dist/workstation/update/runUpdate.js +10 -3
  215. package/dist/workstation/update/taskRegistry.js +161 -3
  216. package/dist/workstation/update/watchdog.js +29806 -333
  217. package/dist/workstation/update/watchdogProcess.js +72 -0
  218. package/package.json +11 -7
  219. package/skills/axom/SKILL.md +11 -11
  220. package/skills/axom-definition/SKILL.md +3 -3
  221. package/skills/axom-platform/SKILL.md +3 -3
  222. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  223. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
  224. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  225. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  226. package/dist/commands/guide.js +0 -38
  227. package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
  228. package/dist/runtime-deps/qoder-sdk/package.json +0 -8
  229. package/dist/workstation/acp/delegationMcp.js +0 -24
  230. package/dist/workstation/acp/processor/sideChannel.js +0 -80
  231. package/dist/workstation/acp/qoderStartup.js +0 -45
  232. package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
  233. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
  234. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
  235. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
  236. package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
  237. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  238. package/dist/workstation/qoderAdapter/contract.js +0 -78
  239. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
  240. package/dist/workstation/qoderAdapter/history.js +0 -85
  241. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  242. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  243. package/dist/workstation/qoderAdapter/main.js +0 -95
  244. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  245. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  246. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  247. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  248. package/dist/workstation/qoderAdapter/profile.js +0 -93
  249. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
  250. package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
  251. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
  252. package/dist/workstation/qoderAdapter/session.js +0 -319
  253. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  254. package/dist/workstation/qoderAdapter/subwork.js +0 -85
  255. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  256. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  257. package/dist/workstation/qoderAdapter/usage.js +0 -37
  258. /package/dist/workstation/{qoderAdapter/types.js → desktopControl/automationPreparation.js} +0 -0
@@ -0,0 +1,46 @@
1
+ import { deviceHealthExpectationSchema, deviceHealthMatches, deviceHealthWitnessSchema, queryDeviceHealth, runtimeFilesDigest } from './deviceHealth.js';
2
+ import { deviceRollbackCompleted, readDeviceBootstrapRecovery } from './recoveryPlan.js';
3
+ export function createDeviceRecoveryTick(options) {
4
+ let running = false, attempts = 0;
5
+ const binding = options.bootstrap?.transaction;
6
+ const expectation = binding?.targetEvidence ? deviceHealthExpectationSchema.parse({ transactionId: binding.transactionId, challenge: binding.challenge,
7
+ initiatingDaemonGeneration: binding.initiatingDaemonGeneration, origin: binding.origin, release: binding.release, resolvedSource: binding.resolvedSource,
8
+ runtimeDigest: runtimeFilesDigest(binding.targetEvidence.runtimeFiles),
9
+ }) : undefined;
10
+ return async () => {
11
+ if (running || options.signal.aborted || !options.initialized())
12
+ return;
13
+ running = true;
14
+ try {
15
+ const original = options.lease();
16
+ if (original && options.bootstrap?.transaction.installedRollback) {
17
+ if (await deviceRollbackCompleted(options.root, options.startup, options.bootstrap.transaction.transactionId)) {
18
+ await original.release(async () => {
19
+ if (!await deviceRollbackCompleted(options.root, options.startup, options.bootstrap.transaction.transactionId))
20
+ throw Error('Original rollback completion changed during admission restoration');
21
+ });
22
+ options.released(original);
23
+ }
24
+ return;
25
+ }
26
+ if (original) {
27
+ if (!binding || !expectation || binding.origin.kind !== 'local' || options.bootstrap?.plan || binding.lifecycle === 'provision-and-arm' || binding.initiatingDaemonGeneration === options.generation || original.transactionId !== binding.transactionId || attempts >= 5)
28
+ return;
29
+ attempts += 1;
30
+ const current = await readDeviceBootstrapRecovery(options.root, options.startup);
31
+ if (!current || current.plan || current.transaction.installedRollback || current.transaction.transactionId !== binding.transactionId || current.transaction.challenge !== binding.challenge || JSON.stringify(current.transaction.origin) !== JSON.stringify(binding.origin) || JSON.stringify(current.transaction.release) !== JSON.stringify(binding.release) || current.transaction.resolvedSource !== binding.resolvedSource || current.transaction.initiatingDaemonGeneration !== binding.initiatingDaemonGeneration)
32
+ throw Error('Original local successor recovery binding changed');
33
+ if (options.signal.aborted || !options.initialized() || options.lease() !== original)
34
+ return;
35
+ const witness = deviceHealthWitnessSchema.parse(await queryDeviceHealth(options.root, expectation));
36
+ if (!deviceHealthMatches(expectation, witness) || witness.daemonGeneration !== options.generation)
37
+ throw Error('Private control did not confirm the exact original successor');
38
+ return;
39
+ }
40
+ await options.reconcile();
41
+ }
42
+ finally {
43
+ running = false;
44
+ }
45
+ };
46
+ }
@@ -0,0 +1,78 @@
1
+ import path from 'node:path';
2
+ import { deviceHealthExpectationSchema, deviceInstalledRollbackSchema, deviceRollbackReceiptSchema, runtimeFilesDigest } from './deviceHealthProtocol.js';
3
+ import { readArchiveEvidence } from './archiveEvidence.js';
4
+ import { readPrivateUpdateBytes, updateTransactionKey } from './recoveryPlan.js';
5
+ import { runtimeRequest } from '../control/runtimeClient.js';
6
+ import { LocalControlError } from '../control/protocol.js';
7
+ import { writeDurablePrivateUpdate } from './privateDurability.js';
8
+ export { deviceRollbackReceiptSchema } from './deviceHealthProtocol.js';
9
+ export function rollbackHandoffPath(root, transactionId) { return path.join(root, 'update', `rollback-handoff-${updateTransactionKey(transactionId)}.json`); }
10
+ export async function writePrivateDeviceEvidence(file, value) {
11
+ await writeDurablePrivateUpdate(file, JSON.stringify(value) + '\n');
12
+ }
13
+ function receiptMatches(expected, raw) {
14
+ const receipt = deviceRollbackReceiptSchema.parse(raw);
15
+ const { schemaVersion: _schemaVersion, prepared: _prepared, daemonGeneration: _generation, ...binding } = receipt;
16
+ if (receipt.daemonGeneration === expected.initiatingDaemonGeneration || JSON.stringify(deviceHealthExpectationSchema.parse(binding)) !== JSON.stringify(expected))
17
+ throw Error('Original target cleanup receipt changed its private recovery binding');
18
+ return receipt;
19
+ }
20
+ export async function assertDeviceRollbackReceipt(root, expected, raw) {
21
+ const receipt = receiptMatches(expected, raw);
22
+ const durable = receiptMatches(expected, JSON.parse((await readPrivateUpdateBytes(rollbackHandoffPath(root, expected.transactionId), 65536)).toString('utf8')));
23
+ if (JSON.stringify(receipt) !== JSON.stringify(durable))
24
+ throw Error('Original target cleanup receipt was not durably confirmed');
25
+ }
26
+ export async function queryDeviceRollbackHandoff(root, expected) {
27
+ return runtimeRequest(root, 'prepare-update-rollback', undefined, { request: expected }, 20_000);
28
+ }
29
+ export async function prepareDeviceRollbackHandoff(options, raw) {
30
+ const expected = deviceHealthExpectationSchema.parse(raw);
31
+ const { deviceTransactionSchema } = await import('./deviceCoordinator.js');
32
+ const transaction = deviceTransactionSchema.parse(JSON.parse((await readPrivateUpdateBytes(path.join(options.root, 'update/device-transaction.json'), 33_554_432)).toString('utf8')));
33
+ const actual = deviceHealthExpectationSchema.parse({ transactionId: transaction.transactionId, challenge: transaction.challenge, initiatingDaemonGeneration: transaction.initiatingDaemonGeneration, origin: transaction.origin, release: transaction.release, resolvedSource: transaction.resolvedSource, runtimeDigest: transaction.targetEvidence ? runtimeFilesDigest(transaction.targetEvidence.runtimeFiles) : '' });
34
+ if (JSON.stringify(expected) !== JSON.stringify(actual) || options.generation === expected.initiatingDaemonGeneration || options.startup.version !== expected.release.version || options.startup.runtimeDigest !== expected.runtimeDigest || !['RESTARTING', 'SUCCEEDED', 'FAILED'].includes(transaction.phase))
35
+ throw new LocalControlError('BINDING_CONFLICT', 'Only the exact original target daemon can prepare this rollback');
36
+ try {
37
+ const receipt = receiptMatches(expected, JSON.parse((await readPrivateUpdateBytes(rollbackHandoffPath(options.root, expected.transactionId), 65536)).toString('utf8')));
38
+ if (receipt.daemonGeneration !== options.generation)
39
+ throw Error('A replacement daemon cannot reuse another original cleanup receipt');
40
+ await writePrivateDeviceEvidence(rollbackHandoffPath(options.root, expected.transactionId), receipt);
41
+ return receipt;
42
+ }
43
+ catch (error) {
44
+ if (error.code !== 'ENOENT')
45
+ throw error;
46
+ }
47
+ const lease = options.lease();
48
+ if (lease.transactionId !== transaction.transactionId)
49
+ throw new LocalControlError('BINDING_CONFLICT', 'Original maintenance transaction changed');
50
+ await lease.ready;
51
+ if ((await lease.observe()).status !== 'candidate')
52
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Original target work or unknown obligations prevent automatic rollback');
53
+ await lease.handoff();
54
+ const receipt = deviceRollbackReceiptSchema.parse({ ...actual, schemaVersion: 1, prepared: true, daemonGeneration: options.generation });
55
+ await writePrivateDeviceEvidence(rollbackHandoffPath(options.root, expected.transactionId), receipt);
56
+ return receipt;
57
+ }
58
+ export async function persistDeviceRollbackPhase(root, expected, phase) {
59
+ const { deviceTransactionSchema } = await import('./deviceCoordinator.js');
60
+ const file = path.join(root, 'update/device-transaction.json');
61
+ const transaction = deviceTransactionSchema.parse(JSON.parse((await readPrivateUpdateBytes(file, 33_554_432)).toString('utf8')));
62
+ if (transaction.transactionId !== expected.transactionId || transaction.challenge !== expected.challenge || transaction.release.sha512 !== expected.release.sha512 || transaction.resolvedSource !== expected.resolvedSource)
63
+ throw Error('Original private rollback transaction changed');
64
+ await writePrivateDeviceEvidence(file, deviceTransactionSchema.parse({ ...transaction, phase, recoveryRequired: phase === 'FAILED' }));
65
+ }
66
+ export async function persistDeviceInstalledRollback(recovery, rawHandoff) {
67
+ await assertDeviceRollbackReceipt(recovery.root, recovery.expectation, rawHandoff);
68
+ const archive = await readArchiveEvidence(recovery.rollback.archive);
69
+ if (archive.sha512 !== recovery.rollback.sha512 || archive.size !== recovery.rollback.size || archive.version !== recovery.rollback.version)
70
+ throw Error('Frozen rollback package changed before installed evidence persistence');
71
+ const { deviceTransactionSchema } = await import('./deviceCoordinator.js');
72
+ const file = path.join(recovery.root, 'update/device-transaction.json');
73
+ const transaction = deviceTransactionSchema.parse(JSON.parse((await readPrivateUpdateBytes(file, 33_554_432)).toString('utf8')));
74
+ if (transaction.transactionId !== recovery.expectation.transactionId || transaction.challenge !== recovery.expectation.challenge || transaction.resolvedSource !== recovery.expectation.resolvedSource)
75
+ throw Error('Original installed rollback transaction changed');
76
+ const installedRollback = deviceInstalledRollbackSchema.parse({ schemaVersion: 1, transactionId: transaction.transactionId, archive, handoff: rawHandoff });
77
+ await writePrivateDeviceEvidence(file, deviceTransactionSchema.parse({ ...transaction, phase: 'RESTARTING', recoveryRequired: true, installedRollback }));
78
+ }
@@ -0,0 +1,186 @@
1
+ import { spawn as nodeSpawn } from 'node:child_process';
2
+ import { promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+ /**
5
+ * `npm install --global <archive>` reinstalls an already-present package by renaming the old
6
+ * install directory aside ("retire") and extracting the new one in its place. On at least one
7
+ * observed host (a container whose target directory sits on the image's read-only layers, backed
8
+ * by `fuse-overlayfs`) that retire-then-extract sequence is unreliable: `npm`'s own tar/arborist
9
+ * step intermittently reports `TAR_ENTRY_ERROR EEXIST` on the package's top-level `package.json`,
10
+ * immediately followed by `ENOENT` when npm reads the same path back — on the *same* path, even
11
+ * starting from an empty directory, and reproducibly across repeated attempts. A plain `tar`
12
+ * extraction of the identical archive onto the identical path, and a non-global `npm install` run
13
+ * inside an already-extracted directory, both succeed every time: the bug is specific to npm's
14
+ * own global-package retire/link machinery, not to the archive, the dependencies, or the
15
+ * filesystem's ability to write there at all.
16
+ *
17
+ * This is therefore a fallback, not the primary path: it only runs after a real `npm install
18
+ * --global` attempt has already failed, and it reimplements only what that attempt was observably
19
+ * unable to do reliably --- replace the target directory's contents and its `bin` entry --- using
20
+ * the two operations proven to work. It intentionally does not run on Windows, where npm's global
21
+ * `bin` entries are shim files (`.cmd`/`.ps1`), not symlinks, and this path has no evidence either
22
+ * way.
23
+ */
24
+ export async function attemptGlobalInstallFallback(npmPath, npmPrefix, archive, env, spawn = nodeSpawn) {
25
+ if (process.platform === 'win32')
26
+ throw new Error('No tested fallback for a global npm install on Windows');
27
+ const manifest = await readArchivePackageJson(archive, spawn);
28
+ const name = manifest.name;
29
+ if (typeof name !== 'string' || name.length === 0)
30
+ throw new Error('Archive package.json has no usable "name"');
31
+ const bin = manifest.bin;
32
+ const target = path.join(npmPrefix, 'lib/node_modules', name);
33
+ await fs.mkdir(path.dirname(target), { recursive: true });
34
+ // Finish extraction and dependency resolution before changing the installed package. Keep the
35
+ // previous tree and command entries until every replacement succeeds, including bin linking.
36
+ const staging = await fs.mkdtemp(path.join(path.dirname(target), '.axom-install-'));
37
+ const candidate = path.join(staging, 'candidate');
38
+ const previous = path.join(staging, 'previous');
39
+ let retired = false;
40
+ let replaced = false;
41
+ let retainRecovery = false;
42
+ const commands = [];
43
+ try {
44
+ await fs.mkdir(candidate);
45
+ await run(spawn, 'tar', ['-xzf', archive, '-C', candidate, '--strip-components=1'], {});
46
+ const installEnv = { ...process.env, ...env };
47
+ delete installEnv.NPM_CONFIG_PREFIX;
48
+ delete installEnv.npm_config_prefix;
49
+ await run(spawn, npmPath, ['install', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund'], { cwd: candidate, env: installEnv });
50
+ try {
51
+ await fs.rename(target, previous);
52
+ retired = true;
53
+ }
54
+ catch (error) {
55
+ if (error.code !== 'ENOENT')
56
+ throw error;
57
+ }
58
+ await fs.rename(candidate, target);
59
+ replaced = true;
60
+ await relinkBinaries(npmPrefix, target, name, bin, staging, commands);
61
+ }
62
+ catch (error) {
63
+ const restoreErrors = [];
64
+ for (const { link, backup } of commands.reverse()) {
65
+ try {
66
+ await fs.rm(link, { force: true });
67
+ if (backup)
68
+ await fs.rename(backup, link);
69
+ }
70
+ catch (restoreError) {
71
+ restoreErrors.push(restoreError);
72
+ }
73
+ }
74
+ // A command restoration failure must not prevent restoring the package itself.
75
+ try {
76
+ if (replaced)
77
+ await fs.rm(target, { recursive: true, force: true });
78
+ }
79
+ catch (restoreError) {
80
+ restoreErrors.push(restoreError);
81
+ }
82
+ try {
83
+ if (retired)
84
+ await fs.rename(previous, target);
85
+ }
86
+ catch (restoreError) {
87
+ restoreErrors.push(restoreError);
88
+ }
89
+ if (restoreErrors.length) {
90
+ retainRecovery = true;
91
+ throw new AggregateError([error, ...restoreErrors], `Global install fallback failed and its previous installation could not be restored; recovery files retained at ${staging}`);
92
+ }
93
+ throw error;
94
+ }
95
+ finally {
96
+ if (!retainRecovery)
97
+ await fs.rm(staging, { recursive: true, force: true }).catch(() => undefined);
98
+ }
99
+ }
100
+ async function readArchivePackageJson(archive, spawn) {
101
+ const raw = await capture(spawn, 'tar', ['-xzf', archive, '-O', 'package/package.json'], {});
102
+ try {
103
+ return JSON.parse(raw);
104
+ }
105
+ catch {
106
+ throw new Error('Archive package.json is not valid JSON');
107
+ }
108
+ }
109
+ /** Reproduces npm's own `bin` linking: a relative symlink from `<prefix>/bin/<name>` to the script. */
110
+ async function relinkBinaries(npmPrefix, target, packageName, bin, staging, commands) {
111
+ const entries = binEntries(packageName, bin);
112
+ if (entries.length === 0)
113
+ return;
114
+ const binDir = path.join(npmPrefix, 'bin');
115
+ await fs.mkdir(binDir, { recursive: true });
116
+ for (const [command, relativeScript] of entries) {
117
+ const script = path.join(target, relativeScript);
118
+ const link = path.join(binDir, command);
119
+ let backup;
120
+ try {
121
+ const existing = await fs.lstat(link);
122
+ if (existing.isDirectory())
123
+ throw new Error('Refusing to replace a directory with an npm command');
124
+ backup = path.join(staging, `bin-${commands.length}`);
125
+ await fs.rename(link, backup);
126
+ }
127
+ catch (error) {
128
+ if (error.code !== 'ENOENT')
129
+ throw error;
130
+ }
131
+ commands.push({ link, backup });
132
+ await fs.symlink(path.relative(binDir, script), link);
133
+ await fs.chmod(script, 0o755).catch(() => undefined);
134
+ }
135
+ }
136
+ /** Exported for direct unit testing; same resolution npm itself applies to a package.json `bin` field. */
137
+ export function binEntries(packageName, bin) {
138
+ if (typeof bin === 'string') {
139
+ const short = packageName.startsWith('@') ? packageName.split('/')[1] ?? packageName : packageName;
140
+ return [[short, bin]];
141
+ }
142
+ if (bin && typeof bin === 'object') {
143
+ return Object.entries(bin).filter((entry) => typeof entry[1] === 'string');
144
+ }
145
+ return [];
146
+ }
147
+ function run(spawn, command, args, options) {
148
+ return new Promise((resolve, reject) => {
149
+ let child;
150
+ try {
151
+ child = spawn(command, args, { ...options, shell: false, stdio: 'ignore', windowsHide: true });
152
+ }
153
+ catch (error) {
154
+ reject(error instanceof Error ? error : new Error(String(error)));
155
+ return;
156
+ }
157
+ child.once('error', reject);
158
+ child.once('close', (code, signal) => {
159
+ if (code === 0)
160
+ resolve();
161
+ else
162
+ reject(new Error(`${command} failed: exit ${code ?? signal ?? 'unknown'}`));
163
+ });
164
+ });
165
+ }
166
+ function capture(spawn, command, args, options) {
167
+ return new Promise((resolve, reject) => {
168
+ let child;
169
+ try {
170
+ child = spawn(command, args, { ...options, shell: false, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
171
+ }
172
+ catch (error) {
173
+ reject(error instanceof Error ? error : new Error(String(error)));
174
+ return;
175
+ }
176
+ let stdout = '';
177
+ child.stdout?.on('data', (chunk) => { stdout += chunk.toString(); });
178
+ child.once('error', reject);
179
+ child.once('close', (code, signal) => {
180
+ if (code === 0)
181
+ resolve(stdout);
182
+ else
183
+ reject(new Error(`${command} failed: exit ${code ?? signal ?? 'unknown'}`));
184
+ });
185
+ });
186
+ }