@platless/axom-cli 0.3.13 → 0.6.36

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 (320) hide show
  1. package/README.md +18 -5
  2. package/dist/axom-patch/cli/README.md +119 -9
  3. package/dist/axom-patch/cli/account-limits/README.md +27 -0
  4. package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
  5. package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
  6. package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
  7. package/dist/axom-patch/cli/account-limits/generate.py +123 -0
  8. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +81 -0
  9. package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
  10. package/dist/axom-patch/cli/claude-account-limits.json +37 -0
  11. package/dist/axom-patch/cli/codex-account-limits.json +37 -0
  12. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  13. package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
  14. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  15. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  16. package/dist/axom-patch/cli/nightly-task.md +194 -0
  17. package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
  18. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +5 -1
  19. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  20. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  21. package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
  22. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
  23. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1591 -0
  24. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +360 -0
  25. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -5
  26. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -7
  27. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  28. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  29. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  30. package/dist/axom-patch/cli/runtime.json +39 -15
  31. package/dist/axom-patch/cli/zcode-account-limits.json +30 -0
  32. package/dist/axom-patch/cli/zcode-models.json +37 -0
  33. package/dist/axom-patch/cli/zcode-subagent-subwork.json +72 -0
  34. package/dist/cli.js +4 -9
  35. package/dist/commands/knowledge.js +47 -0
  36. package/dist/commands/update.js +65 -3
  37. package/dist/commands/workstation.js +73 -7
  38. package/dist/install/claude/commands.js +13 -13
  39. package/dist/keepers/acp/keeper.mjs +3083 -0
  40. package/dist/keepers/acp/manifest.json +9 -0
  41. package/dist/keepers/terminal/keeper.mjs +8469 -0
  42. package/dist/keepers/terminal/manifest.json +9 -0
  43. package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  44. package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  45. package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
  46. package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
  47. package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
  48. package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
  49. package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
  50. package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
  51. package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
  52. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
  53. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
  54. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
  55. package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  56. package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
  57. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  58. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  59. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  60. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  61. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  62. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  63. package/dist/mcp.js +1 -1
  64. package/dist/native/darwin-arm64/axom-quic +0 -0
  65. package/dist/test/keeperKit.js +67 -0
  66. package/dist/workstation/acp/acpAgents.js +2192 -636
  67. package/dist/workstation/acp/acpClient.js +84 -495
  68. package/dist/workstation/acp/adoption.js +10 -396
  69. package/dist/workstation/acp/binaryInstall.js +100 -27
  70. package/dist/workstation/acp/builds/gc.js +245 -0
  71. package/dist/workstation/acp/builds/install/binary.js +28 -0
  72. package/dist/workstation/acp/builds/install/npm.js +324 -0
  73. package/dist/workstation/acp/builds/install/uv.js +254 -0
  74. package/dist/workstation/acp/builds/migrate.js +195 -0
  75. package/dist/workstation/acp/builds/model.js +128 -0
  76. package/dist/workstation/acp/builds/nodeGate.js +100 -0
  77. package/dist/workstation/acp/builds/store.js +352 -0
  78. package/dist/workstation/acp/commandTableProbe.js +68 -0
  79. package/dist/workstation/acp/configOptions.js +103 -1
  80. package/dist/workstation/acp/envSecrets.js +27 -0
  81. package/dist/workstation/acp/initializeResponse.js +290 -0
  82. package/dist/workstation/acp/installErrors.js +12 -2
  83. package/dist/workstation/acp/installProbe.js +1 -1
  84. package/dist/workstation/acp/managedPatches.js +239 -159
  85. package/dist/workstation/acp/managedRuntime.js +314 -0
  86. package/dist/workstation/acp/outbox.js +330 -0
  87. package/dist/workstation/acp/patchAssets.js +60 -3
  88. package/dist/workstation/acp/permission.js +42 -10
  89. package/dist/workstation/acp/processor/checkpoint.js +78 -0
  90. package/dist/workstation/acp/processor/commands.js +35 -0
  91. package/dist/workstation/acp/processor/control.js +282 -0
  92. package/dist/workstation/acp/processor/emit.js +26 -0
  93. package/dist/workstation/acp/processor/jsonrpc.js +172 -0
  94. package/dist/workstation/acp/processor/memoryLog.js +49 -0
  95. package/dist/workstation/acp/processor/origins.js +48 -0
  96. package/dist/workstation/acp/processor/output.js +67 -0
  97. package/dist/workstation/acp/processor/projection.js +42 -0
  98. package/dist/workstation/acp/processor/record.js +128 -0
  99. package/dist/workstation/acp/processor/runtime.js +105 -0
  100. package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
  101. package/dist/workstation/acp/processor/session.js +496 -0
  102. package/dist/workstation/acp/processor/state.js +67 -0
  103. package/dist/workstation/acp/processor/step.js +442 -0
  104. package/dist/workstation/acp/processor/test/fixtureLog.js +58 -0
  105. package/dist/workstation/acp/processor/test/harness.js +120 -0
  106. package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
  107. package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
  108. package/dist/workstation/acp/processor/transport/generations.js +76 -0
  109. package/dist/workstation/acp/processor/transport/keeper.js +130 -0
  110. package/dist/workstation/acp/processor/transport/local.js +269 -0
  111. package/dist/workstation/acp/processor/transport/types.js +78 -0
  112. package/dist/workstation/acp/processor/updates.js +390 -0
  113. package/dist/workstation/acp/processorSession.js +1348 -0
  114. package/dist/workstation/acp/registry/catalog.js +189 -41
  115. package/dist/workstation/acp/registry/launch.js +76 -119
  116. package/dist/workstation/acp/sessionUpdateMapper.js +18 -7
  117. package/dist/workstation/acp/steering.js +58 -0
  118. package/dist/workstation/acp/switching.js +111 -0
  119. package/dist/workstation/acp/terminalLogin.js +16 -42
  120. package/dist/workstation/acp/terminalLoginSpec.js +41 -0
  121. package/dist/workstation/acp/test/adapterFixtures.js +256 -0
  122. package/dist/workstation/acp/test/daemonLife.js +106 -0
  123. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  124. package/dist/workstation/acp/test/keeperWorld.js +295 -0
  125. package/dist/workstation/acp/test/memoryOutbox.js +55 -0
  126. package/dist/workstation/acp/test/raw/fixtures.js +196 -0
  127. package/dist/workstation/acp/test/raw/sanitize.js +344 -0
  128. package/dist/workstation/acp/test/raw/sources.js +135 -0
  129. package/dist/workstation/acp/test/realAdapter.js +331 -0
  130. package/dist/workstation/acp/test/scriptAgent.js +214 -0
  131. package/dist/workstation/acp/test/sessionFrames.js +42 -0
  132. package/dist/workstation/acp/timedCommand.js +4 -2
  133. package/dist/workstation/acp/uvxInstall.js +72 -122
  134. package/dist/workstation/agentFlow.js +103 -0
  135. package/dist/workstation/agentSessions.js +1289 -544
  136. package/dist/workstation/channels/agent.js +7 -5
  137. package/dist/workstation/channels/computer.js +69 -21
  138. package/dist/workstation/channels/computerExecs.js +200 -0
  139. package/dist/workstation/channels/management.js +172 -8
  140. package/dist/workstation/channels/url.js +1 -1
  141. package/dist/workstation/config.js +16 -4
  142. package/dist/workstation/connection/encryptedChannel.js +4 -1
  143. package/dist/workstation/control/bindingStore.js +200 -0
  144. package/dist/workstation/control/commands.js +124 -0
  145. package/dist/workstation/control/daemonIdentity.js +53 -0
  146. package/dist/workstation/control/enrollment.js +106 -0
  147. package/dist/workstation/control/inspect.js +71 -0
  148. package/dist/workstation/control/locks.js +63 -0
  149. package/dist/workstation/control/protocol.js +40 -0
  150. package/dist/workstation/control/restartGuard.js +46 -0
  151. package/dist/workstation/control/runtimeClient.js +97 -0
  152. package/dist/workstation/control/runtimeHost.js +148 -0
  153. package/dist/workstation/control/serverIdentity.js +35 -0
  154. package/dist/workstation/daemon.js +393 -12
  155. package/dist/workstation/daemonctl.js +103 -14
  156. package/dist/workstation/desktopControl/audio.js +99 -0
  157. package/dist/workstation/desktopControl/capabilities.js +92 -0
  158. package/dist/workstation/desktopControl/captureModes.js +9 -0
  159. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  160. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  161. package/dist/workstation/desktopControl/helperProcess.js +353 -0
  162. package/dist/workstation/desktopControl/lobby.js +129 -0
  163. package/dist/workstation/desktopControl/macProvider.js +155 -0
  164. package/dist/workstation/desktopControl/media.js +331 -0
  165. package/dist/workstation/desktopControl/nativeProvider.js +1018 -0
  166. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  167. package/dist/workstation/desktopControl/overview.js +58 -0
  168. package/dist/workstation/desktopControl/protocol.js +302 -0
  169. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  170. package/dist/workstation/desktopControl/rateController.js +226 -0
  171. package/dist/workstation/desktopControl/supervisor.js +1681 -0
  172. package/dist/workstation/desktopControl/transport.js +27 -0
  173. package/dist/workstation/desktopControl/wallpaper.js +78 -0
  174. package/dist/workstation/exec/attach.js +126 -0
  175. package/dist/workstation/exec/command.js +40 -0
  176. package/dist/workstation/exec/durable.js +517 -0
  177. package/dist/workstation/exec/fallback.js +106 -0
  178. package/dist/workstation/exec/policy.js +837 -0
  179. package/dist/workstation/exec/shaping.js +211 -0
  180. package/dist/workstation/exec.js +3 -112
  181. package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
  182. package/dist/workstation/files/adapters/toolRequests.js +121 -0
  183. package/dist/workstation/files/client.js +455 -0
  184. package/dist/workstation/files/credentials.js +142 -0
  185. package/dist/workstation/files/errors.js +28 -0
  186. package/dist/workstation/files/fence.js +350 -0
  187. package/dist/workstation/files/find/fileIndex.js +153 -0
  188. package/dist/workstation/files/find/fuzzy.js +155 -0
  189. package/dist/workstation/files/host.js +122 -0
  190. package/dist/workstation/files/hostProtocol.js +2 -0
  191. package/dist/workstation/files/media/cache.js +620 -0
  192. package/dist/workstation/files/media/encoding.js +308 -0
  193. package/dist/workstation/files/media/estimator.js +100 -0
  194. package/dist/workstation/files/media/media.js +325 -0
  195. package/dist/workstation/files/media/packedAudio.js +23 -0
  196. package/dist/workstation/files/media/scheduler.js +551 -0
  197. package/dist/workstation/files/media/types.js +5 -0
  198. package/dist/workstation/files/scope.js +45 -0
  199. package/dist/workstation/files/structured.js +284 -0
  200. package/dist/workstation/{fileOps.js → files/text.js} +152 -39
  201. package/dist/workstation/files/uploads.js +351 -0
  202. package/dist/workstation/journal.js +65 -236
  203. package/dist/workstation/keepers/acp/client.js +101 -0
  204. package/dist/workstation/keepers/acp/envelope.js +62 -0
  205. package/dist/workstation/keepers/acp/host.js +419 -0
  206. package/dist/workstation/keepers/acp/main.js +18 -0
  207. package/dist/workstation/keepers/acp/memoryStream.js +121 -0
  208. package/dist/workstation/keepers/acp/params.js +137 -0
  209. package/dist/workstation/keepers/acp/protocol.js +66 -0
  210. package/dist/workstation/keepers/acp/recover.js +120 -0
  211. package/dist/workstation/keepers/acp/session.js +584 -0
  212. package/dist/workstation/keepers/acp/stderr.js +154 -0
  213. package/dist/workstation/keepers/kit/activity.js +103 -0
  214. package/dist/workstation/keepers/kit/bootstrap.js +164 -0
  215. package/dist/workstation/keepers/kit/buildVersion.js +51 -0
  216. package/dist/workstation/keepers/kit/bundle.js +124 -0
  217. package/dist/workstation/keepers/kit/client.js +432 -0
  218. package/dist/workstation/keepers/kit/identity.js +21 -0
  219. package/dist/workstation/keepers/kit/ipc.js +112 -0
  220. package/dist/workstation/keepers/kit/launchConfig.js +8 -0
  221. package/dist/workstation/keepers/kit/launcher.js +289 -0
  222. package/dist/workstation/keepers/kit/lock.js +123 -0
  223. package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
  224. package/dist/workstation/keepers/kit/paths.js +76 -0
  225. package/dist/workstation/keepers/kit/protocol.js +43 -0
  226. package/dist/workstation/keepers/kit/server.js +518 -0
  227. package/dist/workstation/keepers/kit/streamLog.js +697 -0
  228. package/dist/workstation/keepers/legacy.js +296 -0
  229. package/dist/workstation/keepers/policy.js +576 -0
  230. package/dist/workstation/keepers/stop.js +34 -0
  231. package/dist/workstation/keepers/terminal/client.js +213 -0
  232. package/dist/workstation/keepers/terminal/exec.js +981 -0
  233. package/dist/workstation/keepers/terminal/generations.js +129 -0
  234. package/dist/workstation/keepers/terminal/keeper.js +62 -0
  235. package/dist/workstation/keepers/terminal/main.js +43 -0
  236. package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
  237. package/dist/workstation/keepers/terminal/params.js +27 -0
  238. package/dist/workstation/keepers/terminal/protocol.js +56 -0
  239. package/dist/workstation/keepers/terminal/pty.js +330 -0
  240. package/dist/workstation/managedInstall.js +57 -0
  241. package/dist/workstation/protocol/accountLimits.js +47 -0
  242. package/dist/workstation/protocol/agent.js +50 -19
  243. package/dist/workstation/protocol/common.js +30 -0
  244. package/dist/workstation/protocol/computer.js +15 -4
  245. package/dist/workstation/protocol/management.js +141 -4
  246. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  247. package/dist/workstation/rd/artifact.js +119 -0
  248. package/dist/workstation/rd/control.js +119 -0
  249. package/dist/workstation/rd/display.js +29 -0
  250. package/dist/workstation/rd/provider.js +222 -0
  251. package/dist/workstation/rd/sessions.js +391 -0
  252. package/dist/workstation/rd/state.js +45 -0
  253. package/dist/workstation/rd/update.js +171 -0
  254. package/dist/workstation/rd/upkeep.js +38 -0
  255. package/dist/workstation/register.js +27 -37
  256. package/dist/workstation/service/installer.js +104 -13
  257. package/dist/workstation/service/launchd.js +1 -0
  258. package/dist/workstation/storageMetrics.js +118 -0
  259. package/dist/workstation/systemMetrics.js +154 -93
  260. package/dist/workstation/toolchain.js +94 -0
  261. package/dist/workstation/tools/browser.js +28 -4
  262. package/dist/workstation/tools/client.js +9 -1
  263. package/dist/workstation/tools/info.js +50 -0
  264. package/dist/workstation/tools/keeperProcess.js +15 -5
  265. package/dist/workstation/tools/router.js +55 -0
  266. package/dist/workstation/tools/terminalRouter.js +222 -0
  267. package/dist/workstation/tools/validation.js +37 -1
  268. package/dist/workstation/update/artifact.js +2 -1
  269. package/dist/workstation/update/coordinator.js +3 -0
  270. package/dist/workstation/update/globalInstall.js +116 -0
  271. package/dist/workstation/update/npmInstaller.js +49 -23
  272. package/dist/workstation/update/ptyPreflight.js +21 -0
  273. package/dist/workstation/update/rdRollback.js +145 -0
  274. package/dist/workstation/update/runUpdate.js +3 -2
  275. package/dist/workstation/update/taskRegistry.js +105 -11
  276. package/dist/workstation/update/watchdog.js +16790 -334
  277. package/package.json +11 -8
  278. package/skills/axom/SKILL.md +11 -11
  279. package/skills/axom-definition/SKILL.md +3 -3
  280. package/skills/axom-platform/SKILL.md +3 -3
  281. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  282. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -49
  283. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  284. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  285. package/dist/commands/guide.js +0 -38
  286. package/dist/workstation/acp/acpSession.js +0 -1475
  287. package/dist/workstation/acp/contract.js +0 -499
  288. package/dist/workstation/acp/delegationMcp.js +0 -24
  289. package/dist/workstation/acp/draftInterceptor.js +0 -163
  290. package/dist/workstation/acp/qoderStartup.js +0 -45
  291. package/dist/workstation/acp/sessionScope.js +0 -19
  292. package/dist/workstation/filePaths.js +0 -174
  293. package/dist/workstation/fileWorker.js +0 -81
  294. package/dist/workstation/fileWorkerClient.js +0 -121
  295. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -106
  296. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -262
  297. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  298. package/dist/workstation/qoderAdapter/contract.js +0 -78
  299. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -30
  300. package/dist/workstation/qoderAdapter/history.js +0 -87
  301. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  302. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  303. package/dist/workstation/qoderAdapter/main.js +0 -93
  304. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  305. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  306. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  307. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  308. package/dist/workstation/qoderAdapter/profile.js +0 -93
  309. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -49
  310. package/dist/workstation/qoderAdapter/session.js +0 -319
  311. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  312. package/dist/workstation/qoderAdapter/subwork.js +0 -77
  313. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  314. package/dist/workstation/qoderAdapter/taskEvent.js +0 -26
  315. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  316. package/dist/workstation/qoderAdapter/usage.js +0 -37
  317. package/dist/workstation/tools/core.js +0 -50
  318. package/dist/workstation/tools/files.js +0 -525
  319. package/dist/workstation/tools/terminal.js +0 -214
  320. /package/dist/workstation/{qoderAdapter/types.js → desktopControl/provider.js} +0 -0
@@ -1,15 +1,22 @@
1
1
  import { deriveSessionKey, randomBytes } from '../crypto.js';
2
2
  import { connectEncryptedChannel } from '../connection/encryptedChannel.js';
3
3
  import { CLIENT_PACKAGE_NAME, CLIENT_VERSION } from '../version.js';
4
- import { decodeManagementFrame, encodeManagementFrame, MANAGEMENT_WIRE_DRAINING_VERSION, MANAGEMENT_WIRE_LOAD_AVERAGES_VERSION, MANAGEMENT_WIRE_METRICS_VERSION, MANAGEMENT_WIRE_MIN_VERSION, MANAGEMENT_WIRE_PRESSURE_VERSION, MANAGEMENT_WIRE_VERSION, } from '../protocol/management.js';
5
- import { bridgesForWire } from '../protocol/common.js';
4
+ import { decodeManagementFrame, encodeManagementFrame, MANAGEMENT_WIRE_DRAINING_VERSION, MANAGEMENT_WIRE_GPU_VERSION, MANAGEMENT_WIRE_KEEPERS_VERSION, MANAGEMENT_WIRE_LOAD_AVERAGES_VERSION, MANAGEMENT_WIRE_METRICS_VERSION, MANAGEMENT_WIRE_MIN_VERSION, MANAGEMENT_WIRE_PRESSURE_VERSION, MANAGEMENT_WIRE_VERSION, ManagementFrameAdmission, } from '../protocol/management.js';
5
+ import { bridgesForWire, helloRoster } from '../protocol/common.js';
6
6
  import { AGENT_WIRE_MIN_VERSION, AGENT_WIRE_VERSION } from '../protocol/agent.js';
7
7
  import { osInfo } from '../osinfo.js';
8
8
  import { workstationChannelUrl } from './url.js';
9
+ import { DesktopCapabilityReporter } from '../desktopControl/capabilities.js';
9
10
  const protocols = {
10
11
  MANAGEMENT: { min: MANAGEMENT_WIRE_MIN_VERSION, max: MANAGEMENT_WIRE_VERSION },
11
- COMPUTER: { min: 1, max: 4 }, AGENT: { min: AGENT_WIRE_MIN_VERSION, max: AGENT_WIRE_VERSION },
12
+ // COMPUTER v5: durable exec frames (exec_started/attach/status/ack, offsets on exec_output; spec §4.3).
13
+ // COMPUTER v6: this client refuses credential storage to the agent file tools (files/credentials.ts); the server sends
14
+ // file frames only to v6 clients.
15
+ COMPUTER: { min: 1, max: 6 }, AGENT: { min: AGENT_WIRE_MIN_VERSION, max: AGENT_WIRE_VERSION },
12
16
  };
17
+ function toolchainField(toolchain) {
18
+ return toolchain ? { toolchain: { python: toolchain.python, node: toolchain.node } } : {};
19
+ }
13
20
  /** Exported so tests can assert the wire frame's `clientPackage` without opening a socket. */
14
21
  export function buildHelloFrame(options, clientNonce) {
15
22
  return {
@@ -24,7 +31,7 @@ export function buildHelloFrame(options, clientNonce) {
24
31
  */
25
32
  function helloBridges(bridges) {
26
33
  const roster = bridges?.();
27
- return roster === undefined ? undefined : bridgesForWire(roster, 'management_hello');
34
+ return roster === undefined ? undefined : bridgesForWire(helloRoster(roster), 'management_hello');
28
35
  }
29
36
  /** Exported so the roster this answer carries can be asserted without driving a whole channel. */
30
37
  export function buildScanResultFrame(commandId, bridges) {
@@ -57,6 +64,10 @@ export function metricsForManagementWire(metrics, serverManagementWire) {
57
64
  if (!metrics)
58
65
  return metrics;
59
66
  const selectedWire = serverManagementWire ?? 0;
67
+ if (selectedWire < MANAGEMENT_WIRE_GPU_VERSION && metrics.gpus !== undefined) {
68
+ const { gpus: _gpus, ...withoutGpus } = metrics;
69
+ return metricsForManagementWire(withoutGpus, selectedWire);
70
+ }
60
71
  if (selectedWire >= MANAGEMENT_WIRE_LOAD_AVERAGES_VERSION) {
61
72
  const { cpuLoadNormalized: _cpuLoadNormalized, ...rawLoad } = metrics;
62
73
  return rawLoad;
@@ -98,6 +109,51 @@ export function dispatchUpdateCommand(channel, command, onUpdate, serverManageme
98
109
  })
99
110
  .catch(() => { });
100
111
  }
112
+ /**
113
+ * The most a heartbeat waits for its `keepers` sample. The channel builds the next heartbeat only after this one went out, so a sample
114
+ * that hangs (a wedged `ps`, a stuck file read) must never hold the frame: past this it goes out with `keepers: null`.
115
+ */
116
+ export const KEEPERS_SAMPLE_CAP_MS = 1500;
117
+ /**
118
+ * Heartbeat `keepers` for the negotiated wire: absent below 8 (an older server does not know the field), the sampled list from 8 on,
119
+ * `null` when sampling failed or did not answer within `capMs`. Never rejects, never waits longer than `capMs`. Exported for tests.
120
+ */
121
+ export async function keepersForManagementWire(sample, serverManagementWire, capMs = KEEPERS_SAMPLE_CAP_MS) {
122
+ if (!sample || (serverManagementWire ?? 0) < MANAGEMENT_WIRE_KEEPERS_VERSION)
123
+ return {};
124
+ let timer;
125
+ const capped = new Promise((resolve) => { timer = setTimeout(() => resolve(null), capMs); timer.unref?.(); });
126
+ try {
127
+ const sampled = Promise.resolve().then(sample);
128
+ sampled.catch(() => undefined); // a late rejection after the cap must not be unhandled
129
+ return { keepers: await Promise.race([sampled, capped]) };
130
+ }
131
+ catch {
132
+ return { keepers: null };
133
+ }
134
+ finally {
135
+ clearTimeout(timer);
136
+ }
137
+ }
138
+ /**
139
+ * Runs one `keeper_control` and sends its result if the channel is still open. Detached from the frame loop (a replace-now stops
140
+ * keeper processes, which takes seconds). Exported for tests.
141
+ */
142
+ export function dispatchKeeperControl(channel, frame, onKeeperControl) {
143
+ return onKeeperControl(frame)
144
+ .catch((error) => ({
145
+ t: 'keeper_control_result', commandId: frame.commandId, kind: frame.kind, action: frame.action, ok: false, affected: [],
146
+ error: error instanceof Error ? error.message : String(error),
147
+ }))
148
+ .then((result) => {
149
+ if (channel.state !== 'OPEN')
150
+ return;
151
+ try {
152
+ channel.send(result);
153
+ }
154
+ catch { /* the channel closed meanwhile; the server asks again with the same commandId */ }
155
+ });
156
+ }
101
157
  /**
102
158
  * The server nonce to derive this session's key from.
103
159
  *
@@ -125,18 +181,65 @@ export class ManagementChannel {
125
181
  options;
126
182
  /** Undefined until this connection receives management_ready; reset for every reconnect. */
127
183
  serverManagementWire;
184
+ /** The open channel of the current connection, for frames this client sends unprompted ({@link pushRoster}). */
185
+ channel;
186
+ desktopReporter;
187
+ connectionAttempt = 0;
188
+ generation;
128
189
  constructor(options) {
129
190
  this.options = options;
130
191
  }
192
+ refreshDesktopCapabilities(generation) {
193
+ if (generation === undefined || generation === this.generation)
194
+ this.desktopReporter?.refresh();
195
+ }
196
+ /** T10 reports only on its authenticated current management socket. */
197
+ sendDesktopState(generation, frame) {
198
+ if (generation !== this.generation || (this.serverManagementWire ?? 0) < 9 || this.channel?.state !== 'OPEN')
199
+ return false;
200
+ try {
201
+ this.channel.send(frame);
202
+ return true;
203
+ }
204
+ catch {
205
+ return false;
206
+ }
207
+ }
208
+ /**
209
+ * Tell the server about a roster that changed without a scan asking (adapter builds moved by Axom,
210
+ * spec §6.1): the same `external_agents_scan_result` frame a scan answers with, under a client-made
211
+ * command id no scan is waiting on — the server records the roster and completes nothing. `false`
212
+ * when there is no open connection; the next `management_hello` carries {@link ManagementChannelOptions.bridges} then.
213
+ */
214
+ pushRoster(bridges) {
215
+ const channel = this.channel;
216
+ if (!channel || channel.state !== 'OPEN')
217
+ return false;
218
+ try {
219
+ channel.send(buildScanResultFrame(`roster-push-${randomBytes(8).toString('hex')}`, bridges));
220
+ return true;
221
+ }
222
+ catch {
223
+ return false;
224
+ }
225
+ }
131
226
  async connect(signal) {
227
+ const attempt = ++this.connectionAttempt;
228
+ this.desktopReporter?.dispose();
229
+ this.desktopReporter = undefined;
230
+ this.generation = undefined;
231
+ this.channel = undefined;
132
232
  this.serverManagementWire = undefined;
133
233
  const nonce = randomBytes(16);
134
234
  let initialHeartbeat = true;
235
+ const admission = new ManagementFrameAdmission(() => attempt === this.connectionAttempt && !signal?.aborted);
135
236
  const channel = await connectEncryptedChannel({
136
237
  url: workstationChannelUrl(this.options.server, 'management', this.options.allowInsecureWs),
137
238
  signal,
138
239
  encode: encodeManagementFrame,
139
240
  decode: decodeManagementFrame,
241
+ decodeBytes: decodeManagementFrame,
242
+ admitFrame: frame => admission.admit(frame),
140
243
  heartbeat: (seq, sentAtEpochMs) => this.heartbeat(seq, sentAtEpochMs),
141
244
  initialFrame: () => {
142
245
  initialHeartbeat = false;
@@ -156,10 +259,39 @@ export class ManagementChannel {
156
259
  if (initialHeartbeat)
157
260
  throw new Error('Management proof heartbeat was not sent');
158
261
  const ready = await this.nextReady(channel);
262
+ if (attempt !== this.connectionAttempt || channel.state !== 'OPEN') {
263
+ channel.close();
264
+ throw new Error('Management connection superseded');
265
+ }
159
266
  this.serverManagementWire = ready.protocols.MANAGEMENT;
267
+ this.generation = ready.generation;
268
+ this.channel = channel;
269
+ const reporter = new DesktopCapabilityReporter({ selectedWire: this.serverManagementWire, generation: ready.generation,
270
+ provider: this.options.desktopProvider, send: report => {
271
+ if (attempt === this.connectionAttempt && this.channel === channel && channel.state === 'OPEN')
272
+ channel.send({ t: 'workstation_capabilities', ...report });
273
+ } });
274
+ this.desktopReporter = reporter;
275
+ // Starts before the existing awaited update hook; heartbeat and reporting never await discovery.
276
+ reporter.refresh();
277
+ const retire = () => {
278
+ admission.dispose();
279
+ reporter.dispose();
280
+ try {
281
+ this.options.onClosed?.(ready.generation);
282
+ }
283
+ catch { /* transport retirement must complete */ }
284
+ if (this.channel === channel) {
285
+ this.channel = undefined;
286
+ this.generation = undefined;
287
+ this.desktopReporter = undefined;
288
+ this.serverManagementWire = undefined;
289
+ }
290
+ };
291
+ void channel.closed.then(retire, retire);
160
292
  // A readiness record is evidence from this encrypted, proof-of-key management generation — it
161
293
  // is never driven by the local browser/device-authorization flow.
162
- await this.options.onReady?.({ generation: ready.generation, release: ready.release });
294
+ await this.options.onReady?.({ generation: ready.generation, selectedWire: ready.protocols.MANAGEMENT, release: ready.release });
163
295
  const pending = new Map();
164
296
  void this.routeMessages(channel, pending);
165
297
  return {
@@ -181,9 +313,15 @@ export class ManagementChannel {
181
313
  };
182
314
  }
183
315
  async heartbeat(seq, sentAtEpochMs) {
316
+ const selectedWire = this.serverManagementWire;
317
+ // Availability may change without restarting the CLI (Desktop sharing is process-owned).
318
+ // Discovery stays bounded and never delays the heartbeat or creates a capture session.
319
+ this.desktopReporter?.refresh();
320
+ const revision = this.desktopReporter?.revision;
184
321
  const snapshot = await this.options.snapshot();
185
322
  return {
186
323
  t: 'heartbeat', seq, sentAtEpochMs, clientVersion: CLIENT_VERSION,
324
+ ...(this.options.playgroundCwd ? { playgroundCwd: this.options.playgroundCwd } : {}),
187
325
  channels: {
188
326
  MANAGEMENT: snapshot.channels.management,
189
327
  COMPUTER: snapshot.channels.computer,
@@ -191,10 +329,13 @@ export class ManagementChannel {
191
329
  },
192
330
  activeShellTasks: snapshot.activeShellTasks, activeAgentTasks: snapshot.activeAgentTasks,
193
331
  protocols, os: osInfo(),
194
- ...(this.options.metrics && (this.serverManagementWire ?? 0) >= MANAGEMENT_WIRE_METRICS_VERSION
195
- ? { metrics: metricsForManagementWire(this.options.metrics(), this.serverManagementWire) }
332
+ ...toolchainField(this.options.toolchain?.() ?? null),
333
+ ...(this.options.metrics && (selectedWire ?? 0) >= MANAGEMENT_WIRE_METRICS_VERSION
334
+ ? { metrics: metricsForManagementWire(this.options.metrics(), selectedWire) }
196
335
  : {}),
197
- ...(snapshot.update ? { update: statusForManagementWire(snapshot.update, this.serverManagementWire) } : {}),
336
+ ...(snapshot.update ? { update: statusForManagementWire(snapshot.update, selectedWire) } : {}),
337
+ ...await keepersForManagementWire(this.options.keepers, selectedWire, this.options.keepersCapMs),
338
+ ...((selectedWire ?? 0) >= 9 && revision !== undefined ? { capabilitiesRevision: revision } : {}),
198
339
  };
199
340
  }
200
341
  async nextReady(channel) {
@@ -206,6 +347,25 @@ export class ManagementChannel {
206
347
  async routeMessages(channel, pending) {
207
348
  try {
208
349
  for await (const frame of channel.messages) {
350
+ if (channel !== this.channel)
351
+ break;
352
+ if (frame.t === 'workstation_capabilities_request') {
353
+ if ((this.serverManagementWire ?? 0) >= 9 && frame.generation === this.generation)
354
+ this.desktopReporter?.refresh();
355
+ continue;
356
+ }
357
+ if ((frame.t === 'desktop_background' || frame.t === 'desktop_lobby' || frame.t === 'desktop_prepare' || frame.t === 'desktop_cancel') && (this.serverManagementWire ?? 0) >= 9) {
358
+ if (frame.t !== 'desktop_cancel' && frame.deviceGeneration !== this.generation)
359
+ continue;
360
+ if (this.options.onDesktopCommand && this.generation) {
361
+ // Capture the authenticated generation now: later callbacks cannot borrow a reconnect.
362
+ try {
363
+ void this.options.onDesktopCommand(frame, this.generation).catch(() => undefined);
364
+ }
365
+ catch { /* bounded desktop admission failure */ }
366
+ }
367
+ continue;
368
+ }
209
369
  if (frame.t === 'channel_ticket') {
210
370
  const queue = pending.get(frame.channel);
211
371
  queue?.shift()?.resolve(frame.ticket);
@@ -217,6 +377,10 @@ export class ManagementChannel {
217
377
  void dispatchUpdateCommand(channel, frame, this.options.onUpdate, this.serverManagementWire);
218
378
  continue;
219
379
  }
380
+ if (frame.t === 'keeper_control' && this.options.onKeeperControl) {
381
+ void dispatchKeeperControl(channel, frame, this.options.onKeeperControl);
382
+ continue;
383
+ }
220
384
  if (frame.t === 'external_agents_scan' && this.options.onExternalAgentsScan) {
221
385
  channel.send(buildScanResultFrame(frame.commandId, await this.options.onExternalAgentsScan()));
222
386
  }
@@ -11,7 +11,7 @@ function ipv4Octets(hostname) {
11
11
  return octets.every((octet) => octet <= 255) ? octets : null;
12
12
  }
13
13
  /** Deliberately classifies literal names/IPs only: DNS answers must never downgrade transport security. */
14
- function isTrustedPlaintextHost(rawHostname) {
14
+ export function isTrustedPlaintextHost(rawHostname) {
15
15
  const hostname = rawHostname.toLowerCase().replace(/^\[|\]$/g, '');
16
16
  if (TRUSTED_PLAINTEXT_NAMES.has(hostname) || hostname.endsWith('.localhost'))
17
17
  return true;
@@ -1,20 +1,28 @@
1
+ import { loadManagedCliInstallSync, npmExecutionContext } from './managedInstall.js';
1
2
  import { resolveBrowserBinding } from './desktop/binding.js';
2
3
  import { promises as fs } from 'node:fs';
3
4
  import os from 'node:os';
4
5
  import path from 'node:path';
5
6
  import { fileURLToPath } from 'node:url';
6
7
  import yaml from 'js-yaml';
8
+ import { z } from 'zod';
9
+ const localPath = z.string().refine(p => path.isAbsolute(p) && !p.includes('\0'));
10
+ const nativeConfig = z.strictObject({ kind: z.literal('linux-native'), helperPath: localPath, runtimeDir: localPath });
7
11
  export function updateServiceContext(configDir = clientDir(), overrides = {}) {
8
12
  // config.ts lives in dist/workstation/ after packaging, so two levels up is the installed package root.
9
13
  const packageRoot = overrides.packageRoot ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
10
- const nodePath = overrides.nodePath ?? process.execPath;
14
+ const managed = loadManagedCliInstallSync(packageRoot);
15
+ const nodePath = managed?.nodePath ?? overrides.nodePath ?? process.execPath;
16
+ const npmPrefix = managed?.prefix ?? overrides.npmPrefix ?? process.env.AXOM_WORKSTATION_NPM_PREFIX ?? path.resolve(packageRoot, '../../../..');
11
17
  return {
12
18
  configDir,
19
+ ...(managed ? { managed } : {}),
20
+ env: npmExecutionContext({ prefix: npmPrefix, nodePath }, process.env),
13
21
  platform: overrides.platform ?? (process.platform === 'darwin' ? 'darwin' : 'linux'),
14
22
  nodePath,
15
- npmPath: overrides.npmPath ?? process.env.AXOM_WORKSTATION_NPM_PATH ?? path.join(path.dirname(nodePath), 'npm'),
23
+ npmPath: managed?.npmPath ?? overrides.npmPath ?? process.env.AXOM_WORKSTATION_NPM_PATH ?? path.join(path.dirname(nodePath), 'npm'),
16
24
  // packageRoot is under lib/node_modules/@platless; climb to npm's global prefix.
17
- npmPrefix: overrides.npmPrefix ?? process.env.AXOM_WORKSTATION_NPM_PREFIX ?? path.resolve(packageRoot, '../../../..'),
25
+ npmPrefix,
18
26
  packageRoot,
19
27
  };
20
28
  }
@@ -35,7 +43,11 @@ export async function loadClientConfig(dir) {
35
43
  throw new Error('Invalid workstation config: server and workstationId are required; run axom auth again');
36
44
  }
37
45
  const browserBinding = parsed.browserBinding ? resolveBrowserBinding(parsed.browserBinding, {}) : undefined;
38
- return { ...(browserBinding ? { browserBinding } : {}), server: parsed.server, workstationId: parsed.workstationId, ...(Array.isArray(parsed.workspaces) ? { workspaces: parsed.workspaces } : {}) };
46
+ const desktopProvider = parsed.desktopProvider ? nativeConfig.parse({ ...parsed.desktopProvider,
47
+ helperPath: parsed.desktopProvider.helperPath === '/usr/local/bin/axom-desktop-helper' ? '/usr/local/bin/axom-desktop-keeper' : parsed.desktopProvider.helperPath,
48
+ }) : process.env.AXOM_DESKTOP_NATIVE_PROVIDER === '1' ?
49
+ nativeConfig.parse({ kind: 'linux-native', helperPath: '/usr/local/bin/axom-desktop-keeper', runtimeDir: '/run/axom-desktop' }) : undefined;
50
+ return { ...(browserBinding ? { browserBinding } : {}), ...(desktopProvider ? { desktopProvider } : {}), server: parsed.server, workstationId: parsed.workstationId, ...(typeof parsed.ownerUserId === 'string' && parsed.ownerUserId ? { ownerUserId: parsed.ownerUserId } : {}), ...(Array.isArray(parsed.workspaces) ? { workspaces: parsed.workspaces } : {}) };
39
51
  }
40
52
  export async function writeKeys(dir, clientKeyB64, serverPubB64) {
41
53
  await fs.mkdir(dir, { recursive: true });
@@ -279,7 +279,10 @@ export function connectEncryptedChannel(options) {
279
279
  if (!isBinary)
280
280
  return;
281
281
  try {
282
- const frame = options.decode(crypto.open(sessionKey, bytes).toString());
282
+ const plaintext = crypto.open(sessionKey, bytes);
283
+ const frame = options.decodeBytes ? options.decodeBytes(plaintext) : options.decode(plaintext.toString());
284
+ if (options.admitFrame && !options.admitFrame(frame))
285
+ return;
283
286
  const acknowledged = [...outstandingAcks].some((sequence) => options.isHeartbeatAck(frame, sequence));
284
287
  // A correctly encrypted and decoded frame proves the peer is still reachable. Heartbeat
285
288
  // acknowledgements are one such frame, but computer streaming/agent events may legitimately
@@ -0,0 +1,200 @@
1
+ import { assertLockOwnership } from './locks.js';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { promises as fs } from 'node:fs';
4
+ import path from 'node:path';
5
+ import yaml from 'js-yaml';
6
+ import { loadClientConfig } from '../config.js';
7
+ import { privateDirectory } from '../keepers/kit/paths.js';
8
+ import { bindingSchema, operationRecordSchema, LocalControlError } from './protocol.js';
9
+ import { normalizeWorkstationServer } from './serverIdentity.js';
10
+ const IDENTITY_FILES = ['client.key', 'server.pub', 'config.yml'];
11
+ const missing = (error) => error.code === 'ENOENT';
12
+ async function readPrivateFile(file) {
13
+ const stat = await fs.lstat(file);
14
+ if (!stat.isFile() || stat.isSymbolicLink() || (process.getuid && stat.uid !== process.getuid()) || stat.size > 1_048_576)
15
+ throw new Error('Unsafe workstation identity file');
16
+ return fs.readFile(file, 'utf8');
17
+ }
18
+ async function exists(file) {
19
+ try {
20
+ await fs.lstat(file);
21
+ return true;
22
+ }
23
+ catch (error) {
24
+ if (missing(error))
25
+ return false;
26
+ throw error;
27
+ }
28
+ }
29
+ async function atomicWrite(file, value) {
30
+ assertLockOwnership();
31
+ const temporary = `${file}.${randomUUID()}.tmp`;
32
+ try {
33
+ const handle = await fs.open(temporary, 'wx', 0o600);
34
+ try {
35
+ await handle.writeFile(value);
36
+ await handle.sync();
37
+ }
38
+ finally {
39
+ await handle.close();
40
+ }
41
+ await fs.rename(temporary, file);
42
+ }
43
+ finally {
44
+ await fs.rm(temporary, { force: true });
45
+ }
46
+ }
47
+ async function syncDirectory(directory) {
48
+ const handle = await fs.open(directory, 'r');
49
+ try {
50
+ await handle.sync();
51
+ }
52
+ finally {
53
+ await handle.close();
54
+ }
55
+ }
56
+ function validateKeys(keys) {
57
+ for (const value of [keys.clientKey, keys.serverPub]) {
58
+ if (!/^[A-Za-z0-9+/]{43}=$/.test(value) || Buffer.from(value, 'base64').length !== 32 || Buffer.from(value, 'base64').toString('base64') !== value) {
59
+ throw new LocalControlError('CORRUPT_IDENTITY', 'Workstation connection keys are invalid');
60
+ }
61
+ }
62
+ }
63
+ function bindingOf(config) {
64
+ return bindingSchema.parse({ server: normalizeWorkstationServer(config.server), workstationId: config.workstationId, ...(config.ownerUserId ? { ownerUserId: config.ownerUserId } : {}) });
65
+ }
66
+ /** Caller holds the shared operation lock for every mutating method. */
67
+ export class BindingStore {
68
+ directory;
69
+ pending;
70
+ constructor(directory) {
71
+ this.directory = directory;
72
+ this.pending = path.join(directory, 'identity.pending');
73
+ }
74
+ async inspect() {
75
+ const pending = await exists(this.pending);
76
+ const present = await Promise.all(IDENTITY_FILES.map(file => exists(path.join(this.directory, file))));
77
+ if (!present.some(Boolean))
78
+ return { binding: null, revision: 'unregistered', identityState: pending ? 'incomplete' : 'absent' };
79
+ let readableBinding = null;
80
+ let configDigest = 'corrupt';
81
+ try {
82
+ configDigest = createHash('sha256').update(await readPrivateFile(path.join(this.directory, 'config.yml'))).digest('hex');
83
+ await readPrivateFile(path.join(this.directory, 'config.yml'));
84
+ const binding = bindingOf(await loadClientConfig(this.directory));
85
+ readableBinding = binding;
86
+ const keys = { clientKey: (await readPrivateFile(path.join(this.directory, 'client.key'))).trim(), serverPub: (await readPrivateFile(path.join(this.directory, 'server.pub'))).trim() };
87
+ validateKeys(keys);
88
+ const revision = createHash('sha256').update(JSON.stringify(binding)).update('\0').update(keys.clientKey).update('\0').update(keys.serverPub).digest('hex');
89
+ return { binding, revision, identityState: pending ? 'incomplete' : 'ready' };
90
+ }
91
+ catch {
92
+ return { binding: readableBinding, revision: configDigest, identityState: pending ? 'incomplete' : 'corrupt' };
93
+ }
94
+ }
95
+ async history() {
96
+ try {
97
+ const data = JSON.parse(await readPrivateFile(path.join(this.directory, 'binding-history.json')));
98
+ if (!Array.isArray(data))
99
+ throw new Error('Invalid binding history');
100
+ return data.map(value => bindingSchema.parse(value));
101
+ }
102
+ catch (error) {
103
+ if (missing(error))
104
+ return [];
105
+ throw new LocalControlError('CORRUPT_IDENTITY', 'Workstation binding history is unreadable');
106
+ }
107
+ }
108
+ async recover() {
109
+ if (!await exists(this.pending))
110
+ return;
111
+ const stat = await fs.lstat(this.pending);
112
+ if (!stat.isDirectory() || stat.isSymbolicLink() || (process.getuid && stat.uid !== process.getuid()))
113
+ throw new LocalControlError('CORRUPT_IDENTITY', 'Unsafe pending workstation identity');
114
+ let marker;
115
+ try {
116
+ marker = await readPrivateFile(path.join(this.pending, 'ready'));
117
+ }
118
+ catch {
119
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Registration needs verification: incomplete local transaction');
120
+ }
121
+ if (marker === 'clear') {
122
+ await this.finishClear();
123
+ return;
124
+ }
125
+ if (marker !== '1')
126
+ throw new LocalControlError('CORRUPT_IDENTITY', 'Unknown workstation identity transaction');
127
+ const configText = await readPrivateFile(path.join(this.pending, 'config.yml'));
128
+ const config = await loadClientConfig(this.pending);
129
+ bindingOf(config);
130
+ const keys = { clientKey: (await readPrivateFile(path.join(this.pending, 'client.key'))).trim(), serverPub: (await readPrivateFile(path.join(this.pending, 'server.pub'))).trim() };
131
+ validateKeys(keys);
132
+ const operation = operationRecordSchema.parse(JSON.parse(await readPrivateFile(path.join(this.pending, 'operation.json'))));
133
+ let history;
134
+ if (await exists(path.join(this.pending, 'binding-history.json'))) {
135
+ const staged = JSON.parse(await readPrivateFile(path.join(this.pending, 'binding-history.json')));
136
+ if (!Array.isArray(staged))
137
+ throw new LocalControlError('CORRUPT_IDENTITY', 'Invalid staged binding history');
138
+ history = staged.map(value => bindingSchema.parse(value));
139
+ }
140
+ // All staging is validated before any existing identity file changes.
141
+ for (const [name, value] of [['client.key', keys.clientKey], ['server.pub', keys.serverPub], ['config.yml', configText]])
142
+ await atomicWrite(path.join(this.directory, name), value);
143
+ if (history)
144
+ await atomicWrite(path.join(this.directory, 'binding-history.json'), JSON.stringify(history));
145
+ await atomicWrite(path.join(this.directory, 'operation.json'), JSON.stringify(operation));
146
+ await syncDirectory(this.directory);
147
+ await fs.rm(this.pending, { recursive: true });
148
+ await syncDirectory(this.directory);
149
+ }
150
+ async commit(config, keys, operation, repair = false) {
151
+ const binding = bindingOf(config);
152
+ validateKeys(keys);
153
+ const safeOperation = operationRecordSchema.parse(operation);
154
+ await privateDirectory(this.directory);
155
+ await this.recover();
156
+ const current = await this.inspect();
157
+ if (current.identityState === 'corrupt' && !(repair && current.binding && (!current.binding.ownerUserId || current.binding.ownerUserId === binding.ownerUserId) && current.binding.server === binding.server && current.binding.workstationId === binding.workstationId))
158
+ throw new LocalControlError('CORRUPT_IDENTITY', 'Existing workstation identity needs repair');
159
+ const history = await this.history();
160
+ for (const previous of [current.binding, binding])
161
+ if (previous) {
162
+ const i = history.findIndex(item => item.server === previous.server && item.ownerUserId === previous.ownerUserId);
163
+ if (i >= 0)
164
+ history.splice(i, 1);
165
+ history.push(previous);
166
+ }
167
+ await fs.mkdir(this.pending, { mode: 0o700 });
168
+ for (const [name, value] of Object.entries({
169
+ 'config.yml': yaml.dump({ ...config, server: binding.server }), 'client.key': keys.clientKey, 'server.pub': keys.serverPub,
170
+ 'operation.json': JSON.stringify(safeOperation), 'binding-history.json': JSON.stringify(history),
171
+ }))
172
+ await atomicWrite(path.join(this.pending, name), value);
173
+ await atomicWrite(path.join(this.pending, 'ready'), '1');
174
+ await syncDirectory(this.pending);
175
+ await syncDirectory(this.directory);
176
+ await this.recover();
177
+ }
178
+ async clear(expectedRevision) {
179
+ await this.recover();
180
+ const current = await this.inspect();
181
+ if (current.revision !== expectedRevision || current.identityState !== 'ready')
182
+ throw new LocalControlError('BINDING_CONFLICT', 'Workstation identity changed; check it again');
183
+ await fs.mkdir(this.pending, { mode: 0o700 });
184
+ await atomicWrite(path.join(this.pending, 'clear.json'), JSON.stringify(current.binding));
185
+ await atomicWrite(path.join(this.pending, 'ready'), 'clear');
186
+ await syncDirectory(this.pending);
187
+ await syncDirectory(this.directory);
188
+ await this.finishClear();
189
+ }
190
+ async finishClear() {
191
+ const removed = bindingSchema.parse(JSON.parse(await readPrivateFile(path.join(this.pending, 'clear.json'))));
192
+ const history = (await this.history()).filter(item => item.server !== removed.server || item.workstationId !== removed.workstationId);
193
+ await atomicWrite(path.join(this.directory, 'binding-history.json'), JSON.stringify(history));
194
+ for (const name of [...IDENTITY_FILES, 'operation.json'])
195
+ await fs.rm(path.join(this.directory, name), { force: true });
196
+ await syncDirectory(this.directory);
197
+ await fs.rm(this.pending, { recursive: true });
198
+ await syncDirectory(this.directory);
199
+ }
200
+ }