@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
@@ -0,0 +1,289 @@
1
+ // Daemon-side: starts, probes and stops keepers. Exempt from the keeper-side import guard.
2
+ import { spawn } from 'node:child_process';
3
+ import { promises as fs } from 'node:fs';
4
+ import { constants, openSync, fstatSync, readFileSync, closeSync, lstatSync } from 'node:fs';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { keeperOneShot, readKeeperToken } from './client.js';
8
+ import { BundleIntegrityError, ensureBundle, gcBundles, readKeeperManifest } from './bundle.js';
9
+ import { keeperGen } from './identity.js';
10
+ import { encodeLaunchConfig, launchConfigOf } from './launchConfig.js';
11
+ import { acquireStartupLock, liveManifest, manifestProcessLive, pidAlive, processCommand, readPidManifest, releaseStartupLock } from './lock.js';
12
+ import { bundleDirFor, ensureKeeperDirectories, keeperPaths, touchSocket } from './paths.js';
13
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
14
+ /** `keeper.ping` on a fresh connection. Null unless the answer is from this exact keeper generation. */
15
+ export async function pingKeeper(paths, timeoutMs = 250) {
16
+ try {
17
+ const reply = await keeperOneShot(paths.socket, 'keeper.ping', {}, timeoutMs);
18
+ if (reply.identity !== paths.identity || reply.gen !== paths.gen || !Number.isSafeInteger(reply.pid))
19
+ return null;
20
+ return { pid: reply.pid, identity: paths.identity, gen: paths.gen };
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ /** Arguments for the Linux transient unit. `Restart=no`: a crash is handled by the daemon with the right build, never by systemd with stale arguments. */
27
+ export function systemdRunArgs(unit, nodePath, script, config) {
28
+ return ['--user', '--quiet', '--collect', `--unit=${unit}`, '--property=Type=exec', '--property=Restart=no', '--', nodePath, script, config];
29
+ }
30
+ function runQuiet(command, args, timeoutMs) {
31
+ return new Promise((resolve) => {
32
+ const child = spawn(command, args, { stdio: 'ignore' });
33
+ let done = false;
34
+ const finish = (ok) => { if (done)
35
+ return; done = true; clearTimeout(timer); resolve(ok); };
36
+ const timer = setTimeout(() => { child.kill(); finish(false); }, timeoutMs);
37
+ child.on('error', () => finish(false));
38
+ child.on('exit', (code) => finish(code === 0));
39
+ });
40
+ }
41
+ export const systemdAvailable = () => runQuiet('systemctl', ['--user', 'show-environment'], 500);
42
+ export const defaultDistDir = () => fileURLToPath(new URL('../../../keepers/', import.meta.url));
43
+ export class KeeperLauncher {
44
+ options;
45
+ starting = new Map();
46
+ current = new Set();
47
+ constructor(options) {
48
+ this.options = options;
49
+ }
50
+ get distDir() { return this.options.distDir ?? defaultDistDir(); }
51
+ probe(paths, timeoutMs) { return this.options.probe ? this.options.probe(paths) : pingKeeper(paths, timeoutMs); }
52
+ /** Paths of generation `gen` of `kind` (no build needed). */
53
+ pathsFor(kind, gen) {
54
+ const scoped = keeperPaths(this.options, kind, gen);
55
+ try {
56
+ lstatSync(scoped.pidFile);
57
+ return scoped;
58
+ }
59
+ catch (error) {
60
+ if (error.code !== 'ENOENT')
61
+ throw error;
62
+ }
63
+ const legacy = keeperPaths(this.options, kind, gen, { legacyLayout: true });
64
+ let descriptor;
65
+ try {
66
+ descriptor = openSync(legacy.pidFile, constants.O_RDONLY | constants.O_NOFOLLOW);
67
+ const stat = fstatSync(descriptor);
68
+ if (!stat.isFile() || stat.size > 4096 || process.getuid && stat.uid !== process.getuid())
69
+ throw new Error('Unsafe keeper PID manifest');
70
+ const manifest = JSON.parse(readFileSync(descriptor, 'utf8'));
71
+ if (manifest.identity === scoped.identity && manifest.kind === kind && manifest.gen === gen)
72
+ return legacy;
73
+ }
74
+ catch (error) {
75
+ if (error.code !== 'ENOENT')
76
+ throw error;
77
+ }
78
+ finally {
79
+ if (descriptor !== undefined)
80
+ closeSync(descriptor);
81
+ }
82
+ return scoped;
83
+ }
84
+ /** The generation the current build of `kind` runs as (`dist/keepers/<kind>/manifest.json`); starts nothing. */
85
+ async currentGen(kind) {
86
+ return keeperGen((await readKeeperManifest(path.join(this.distDir, kind))).buildVersion);
87
+ }
88
+ /** Generations of `kind` whose pid manifest names a live keeper of that generation (`<root>/run/<gen>`); starts nothing. */
89
+ async liveGenerations(kind) {
90
+ const scoped = keeperPaths(this.options, kind, 'probe');
91
+ const legacy = keeperPaths(this.options, kind, 'probe', { legacyLayout: true });
92
+ const roots = [scoped.root, legacy.root];
93
+ const gens = new Set();
94
+ for (const root of roots)
95
+ for (const gen of await fs.readdir(path.join(root, 'run')).catch(() => [])) {
96
+ const manifest = await readPidManifest(path.join(root, 'run', gen, 'keeper.pid.json')).catch(() => null);
97
+ if (manifest?.identity === scoped.identity && manifest.kind === kind && manifest.gen === gen)
98
+ gens.add(gen);
99
+ }
100
+ const live = [];
101
+ for (const gen of [...gens].sort())
102
+ if (await liveManifest(this.pathsFor(kind, gen)).catch(() => false))
103
+ live.push(gen);
104
+ return live;
105
+ }
106
+ /** Ensures the current build of `kind` is running (snapshot, lock, probe, spawn) and returns it. One start per kind in flight. */
107
+ launch(kind) {
108
+ let pending = this.starting.get(kind);
109
+ if (!pending) {
110
+ pending = this.start(kind).finally(() => { this.starting.delete(kind); });
111
+ this.starting.set(kind, pending);
112
+ }
113
+ return pending;
114
+ }
115
+ async start(kind) {
116
+ const deadline = Date.now() + (this.options.startTimeoutMs ?? 7500);
117
+ const manifest = await readKeeperManifest(path.join(this.distDir, kind));
118
+ const paths = this.pathsFor(kind, keeperGen(manifest.buildVersion));
119
+ await ensureKeeperDirectories(paths);
120
+ // A keeper that already answers needs no snapshot check: it runs from the snapshot it was started with.
121
+ const alive = await this.probe(paths);
122
+ if (alive) {
123
+ this.current.add(manifest.buildVersion);
124
+ return { paths, manifest, bundleDir: bundleDirFor(paths, manifest.buildVersion), pid: alive.pid, launched: false };
125
+ }
126
+ const snapshot = await ensureBundle(path.join(this.distDir, kind), paths.bundlesDir); // BundleIntegrityError refuses the launch
127
+ this.current.add(snapshot.buildVersion);
128
+ const handle = (pid, launched) => ({ paths, manifest, bundleDir: snapshot.bundleDir, pid, launched });
129
+ while (Date.now() < deadline - 500) {
130
+ const found = await this.probe(paths);
131
+ if (found)
132
+ return handle(found.pid, false);
133
+ let lock;
134
+ try {
135
+ lock = await acquireStartupLock(paths.lockPort);
136
+ }
137
+ catch (error) {
138
+ if (error.code !== 'EADDRINUSE')
139
+ throw error;
140
+ await delay(50);
141
+ continue;
142
+ }
143
+ try {
144
+ const winner = await this.probe(paths);
145
+ if (winner)
146
+ return handle(winner.pid, false);
147
+ // A probe timeout is not process death: a busy keeper may be slow to ping, and unlinking its live
148
+ // socket would orphan everything it hosts. The pid manifest is written before the keeper initialises.
149
+ while (await liveManifest(paths)) {
150
+ const slow = await this.probe(paths);
151
+ if (slow)
152
+ return handle(slow.pid, false);
153
+ if (Date.now() >= deadline - 300)
154
+ throw new Error(`Keeper ${kind} is alive but busy or unavailable; retry later`);
155
+ await delay(50);
156
+ }
157
+ await fs.unlink(paths.socket).catch((error) => { if (error.code !== 'ENOENT')
158
+ throw error; });
159
+ await this.spawnKeeper(paths, snapshot.buildVersion, snapshot.bundleDir, deadline);
160
+ while (Date.now() < deadline - 100) {
161
+ const started = await this.probe(paths);
162
+ if (started) {
163
+ void this.gc().catch(() => { });
164
+ return handle(started.pid, true);
165
+ }
166
+ await delay(50);
167
+ }
168
+ throw new Error(`Keeper ${kind} failed to start`);
169
+ }
170
+ finally {
171
+ await releaseStartupLock(lock);
172
+ }
173
+ }
174
+ throw new Error(`Keeper ${kind} startup is busy or timed out`);
175
+ }
176
+ async spawnKeeper(paths, buildVersion, bundleDir, deadline) {
177
+ const script = path.join(bundleDir, 'keeper.mjs'); // always the immutable snapshot, never dist/
178
+ const config = encodeLaunchConfig(launchConfigOf(paths, buildVersion, bundleDir, this.options.umask ?? process.umask()));
179
+ const nodePath = this.options.nodePath ?? process.execPath;
180
+ if ((this.options.platform ?? process.platform) === 'linux' && this.options.useSystemd !== false && await systemdAvailable()) {
181
+ // setsid/detached cannot escape systemd's KillMode=control-group. A separate transient unit gives the keeper
182
+ // (and everything it hosts) its own cgroup so the daemon can restart or upgrade freely.
183
+ const timeout = Math.max(1, Math.min(2000, deadline - Date.now() - 500));
184
+ if (await runQuiet('systemd-run', systemdRunArgs(paths.unit, nodePath, script, config), timeout))
185
+ return;
186
+ // The unit may still be starting. Never fall back to a same-cgroup child on a systemd host.
187
+ while (Date.now() < deadline - 100) {
188
+ if (await this.probe(paths))
189
+ return;
190
+ await delay(50);
191
+ }
192
+ throw new Error(`Cannot start isolated keeper user service ${paths.unit}`);
193
+ }
194
+ // macOS (and Linux without a user systemd): a detached new session. Task 0 verified it survives launchd
195
+ // bootout/bootstrap/kickstart and kill -9 of the daemon (docs/superpowers/reports/2026-10-11-keeper-launch-spike.md).
196
+ // `detached` is what makes it survive; do not add launchd keys or drop it.
197
+ const child = spawn(nodePath, [script, config], { detached: true, stdio: 'ignore' });
198
+ child.on('error', () => { });
199
+ child.unref();
200
+ }
201
+ /**
202
+ * Spec §3.1: a socket that fell back to `/tmp` (its configDir path is too long for sockaddr_un) is touched for every generation
203
+ * of `kind` with a run directory, so a periodic /tmp cleaner (macOS removes files unused for days) never reaps a live keeper's
204
+ * socket. Sockets under configDir are left alone. Returns how many were touched; never throws.
205
+ */
206
+ async touchSockets(kind) {
207
+ const root = keeperPaths(this.options, kind, 'probe').root;
208
+ const gens = new Set([...await fs.readdir(path.join(root, 'run')).catch(() => []), ...await this.liveGenerations(kind)]);
209
+ let touched = 0;
210
+ for (const gen of gens) {
211
+ const paths = this.pathsFor(kind, gen);
212
+ if (paths.socketFallback && await touchSocket(paths).catch(() => false))
213
+ touched += 1;
214
+ }
215
+ return touched;
216
+ }
217
+ /** Removes bundle snapshots no live keeper references (never the daemon's current builds). */
218
+ gc(minAgeMs) {
219
+ return gcBundles({ configDir: this.options.configDir, keep: this.current, minAgeMs });
220
+ }
221
+ /**
222
+ * Stops generation `gen` of `kind`. `graceful` asks the keeper (`keeper.stop`) and waits; it never escalates, so a
223
+ * keeper that refuses stays up. `force` additionally stops the transient unit and finally SIGKILLs the pid from the
224
+ * manifest, but only if that pid is still running this keeper's snapshot (guards against pid reuse). A manifest whose pid
225
+ * is dead or belongs to another program is stale: it is cleaned up and reported as stopped, nothing is signalled.
226
+ */
227
+ async stop(kind, gen, mode, waitMs = 5000) {
228
+ const paths = this.pathsFor(kind, gen);
229
+ const manifest = await readPidManifest(paths.pidFile);
230
+ if (!manifest || manifest.identity !== paths.identity || manifest.kind !== kind || manifest.gen !== gen)
231
+ return { stopped: true, pid: null };
232
+ const pid = manifest.pid;
233
+ if (!await manifestProcessLive(manifest)) { // dead, or the pid now belongs to an unrelated process: the manifest is stale
234
+ await fs.unlink(paths.socket).catch(() => { });
235
+ await fs.unlink(paths.pidFile).catch(() => { });
236
+ return { stopped: true, pid: null };
237
+ }
238
+ const dead = async (ms) => {
239
+ const until = Date.now() + ms;
240
+ while (pidAlive(pid)) {
241
+ if (Date.now() >= until)
242
+ return false;
243
+ await delay(25);
244
+ }
245
+ return true;
246
+ };
247
+ // keeper.stop needs the connection token (the daemon owns the run directory it lives in). Without a usable token
248
+ // the keeper cannot be asked, so after the same identity check the force path uses, SIGTERM it (its handler is the graceful stop).
249
+ let reason;
250
+ let token;
251
+ try {
252
+ token = await readKeeperToken(paths.tokenFile);
253
+ }
254
+ catch (error) {
255
+ reason = `keeper token unavailable: ${error.message}`;
256
+ }
257
+ if (token) {
258
+ try {
259
+ await keeperOneShot(paths.socket, 'keeper.stop', { mode }, 1000, { kind, token });
260
+ }
261
+ catch (error) {
262
+ reason = `keeper.stop failed: ${error.message}`;
263
+ }
264
+ }
265
+ else if (((await processCommand(pid)) ?? '').includes(manifest.bundleDir)) {
266
+ try {
267
+ process.kill(pid, 'SIGTERM');
268
+ }
269
+ catch { /* already gone */ }
270
+ }
271
+ if (!await dead(mode === 'force' ? 1000 : waitMs) && mode === 'force') {
272
+ if (process.platform === 'linux')
273
+ await runQuiet('systemctl', ['--user', 'stop', paths.unit], 2000);
274
+ if (!await dead(500) && ((await processCommand(pid)) ?? '').includes(manifest.bundleDir)) {
275
+ try {
276
+ process.kill(pid, 'SIGKILL');
277
+ }
278
+ catch { /* already gone */ }
279
+ await dead(waitMs);
280
+ }
281
+ }
282
+ if (pidAlive(pid))
283
+ return { stopped: false, pid, ...(reason ? { reason } : {}) };
284
+ await fs.unlink(paths.socket).catch(() => { });
285
+ await fs.unlink(paths.pidFile).catch(() => { });
286
+ return { stopped: true, pid, ...(reason ? { reason } : {}) };
287
+ }
288
+ }
289
+ export { BundleIntegrityError, keeperOneShot };
@@ -0,0 +1,123 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { promises as fs } from 'node:fs';
4
+ import net from 'node:net';
5
+ const MAX_MANIFEST_BYTES = 4096;
6
+ export function pidAlive(pid) {
7
+ if (!Number.isSafeInteger(pid) || pid <= 0)
8
+ return false;
9
+ try {
10
+ process.kill(pid, 0);
11
+ return true;
12
+ }
13
+ catch (error) {
14
+ return error.code === 'EPERM';
15
+ }
16
+ }
17
+ const readProcCmdline = (pid) => fs.readFile(`/proc/${pid}/cmdline`);
18
+ /** A `ps` that has not answered by then is killed; the answer is then "cannot tell" (null), which callers treat conservatively. */
19
+ export const PS_TIMEOUT_MS = 2000;
20
+ function processCommandFromPs(pid) {
21
+ return new Promise((resolve) => execFile('ps', ['-ww', '-p', String(pid), '-o', 'command='], { timeout: PS_TIMEOUT_MS }, (error, stdout) => {
22
+ if (!error) {
23
+ resolve(stdout.trim());
24
+ return;
25
+ }
26
+ resolve(error.code === 1 ? '' : null); // ps exits 1 for "no such process"; killed by the timeout: null
27
+ }));
28
+ }
29
+ /**
30
+ * The full command line of `pid`, '' when no such process, null when it cannot be determined so callers can stay conservative.
31
+ * Linux reads `/proc/<pid>/cmdline` (NUL-separated arguments, joined with spaces): Debian-slim workstation images may have no
32
+ * `ps`, and busybox `ps` exits 1 on unknown flags, which would read as "no such process". macOS uses `ps -ww` (it truncates
33
+ * to the terminal width otherwise).
34
+ */
35
+ export async function processCommand(pid, deps = {}) {
36
+ if ((deps.platform ?? process.platform) !== 'linux')
37
+ return processCommandFromPs(pid);
38
+ const read = deps.readCmdline ?? readProcCmdline;
39
+ try {
40
+ return (await read(pid)).toString('utf8').split('\0').filter((part) => part.length > 0).join(' ');
41
+ }
42
+ catch (error) {
43
+ const code = error.code;
44
+ if (code !== 'ENOENT' && code !== 'ESRCH')
45
+ return null;
46
+ // Without /proc mounted every pid is ENOENT; only a readable /proc/self proves the pid really is gone.
47
+ try {
48
+ await read('self');
49
+ return '';
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ }
56
+ /**
57
+ * Whether the process named by `manifest.pid` is that keeper. The run dir lives under configDir and survives reboots,
58
+ * so a stale manifest's pid may now belong to anything. A keeper is started as `node <bundleDir>/keeper.mjs <config>`,
59
+ * so its command line must contain its bundleDir. If the command cannot be read at all, assume it is alive (never
60
+ * risk unlinking the socket of a live keeper); keepers must therefore not rewrite `process.title`.
61
+ */
62
+ export async function manifestProcessLive(manifest) {
63
+ if (!pidAlive(manifest.pid))
64
+ return false;
65
+ const command = await processCommand(manifest.pid);
66
+ return command === null ? true : command.includes(manifest.bundleDir);
67
+ }
68
+ export async function writePidManifest(pidFile, manifest) {
69
+ const temp = `${pidFile}.${randomUUID()}.tmp`;
70
+ await fs.writeFile(temp, JSON.stringify(manifest), { mode: 0o600 });
71
+ await fs.rename(temp, pidFile);
72
+ }
73
+ function validManifest(value) {
74
+ if (typeof value !== 'object' || value === null)
75
+ return false;
76
+ const m = value;
77
+ return (m.kind === 'terminal' || m.kind === 'acp') && typeof m.gen === 'string' && typeof m.buildVersion === 'string'
78
+ && typeof m.bundleDir === 'string' && typeof m.identity === 'string'
79
+ && Number.isFinite(m.startedAt) && Number.isSafeInteger(m.pid);
80
+ }
81
+ /** The manifest at `pidFile`, null when there is none. Anything malformed throws: it must not be mistaken for "no keeper". */
82
+ export async function readPidManifest(pidFile) {
83
+ try {
84
+ const stat = await fs.lstat(pidFile);
85
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_MANIFEST_BYTES)
86
+ throw new Error('Invalid keeper PID manifest');
87
+ const value = JSON.parse(await fs.readFile(pidFile, 'utf8'));
88
+ if (!validManifest(value))
89
+ throw new Error('Invalid keeper PID manifest');
90
+ return value;
91
+ }
92
+ catch (error) {
93
+ if (error.code === 'ENOENT')
94
+ return null;
95
+ throw error;
96
+ }
97
+ }
98
+ /**
99
+ * True when the manifest names a live process that is exactly this keeper (see `manifestProcessLive`). A probe timeout is not process death:
100
+ * a busy keeper can be slow to ping, and unlinking its live socket would orphan everything it hosts.
101
+ */
102
+ export async function liveManifest(paths) {
103
+ const manifest = await readPidManifest(paths.pidFile);
104
+ if (!manifest)
105
+ return false;
106
+ if (manifest.identity !== paths.identity || manifest.kind !== paths.kind || manifest.gen !== paths.gen)
107
+ throw new Error('Keeper identity mismatch');
108
+ return manifestProcessLive(manifest);
109
+ }
110
+ /**
111
+ * A kernel bind is an atomic cross-process lock with automatic release on process death. It carries no
112
+ * requests and needs no stale-file unlink or ownership-blind cleanup. A rare port collision only serialises startup.
113
+ */
114
+ export function acquireStartupLock(port) {
115
+ return new Promise((resolve, reject) => {
116
+ const lock = net.createServer((socket) => socket.destroy());
117
+ lock.once('error', reject);
118
+ lock.listen({ host: '127.0.0.1', port, exclusive: true }, () => resolve(lock));
119
+ });
120
+ }
121
+ export function releaseStartupLock(lock) {
122
+ return new Promise((resolve) => lock.close(() => resolve()));
123
+ }
@@ -0,0 +1,86 @@
1
+ // PID-reuse-safe process identity for keeper processes (keeper-terminal execs, keeper-acp adapters), and what a restarted keeper does with a group whose keeper died
2
+ // while it ran (spec §4.2, analysis E §6.1). Keeper-side module: node builtins and kit siblings only (the import guard enforces it).
3
+ //
4
+ // A process is identified by (pid, start time). The start time is read once, right after spawn, and later compared EXACTLY:
5
+ // - Linux: `linux:<boot_id>:<starttime>` (field 22 of /proc/<pid>/stat, clock ticks since boot; boot_id makes a reboot never match);
6
+ // - macOS: `darwin:<lstart>` from `ps -o lstart=` under LC_ALL=C and TZ=UTC (an absolute wall-clock second, stable across
7
+ // reboots and time-zone changes).
8
+ // A pid whose start time cannot be read, or reads differently, is NOT the process that was spawned: it is never signalled.
9
+ import { execFile } from 'node:child_process';
10
+ import { readFileSync } from 'node:fs';
11
+ const procStat = (pid) => readFileSync(`/proc/${pid}/stat`, 'utf8');
12
+ const procBootId = () => readFileSync('/proc/sys/kernel/random/boot_id', 'utf8');
13
+ function darwinLstart(pid) {
14
+ return new Promise((resolve) => {
15
+ execFile('/bin/ps', ['-o', 'lstart=', '-p', String(pid)], { timeout: 5000, env: { LC_ALL: 'C', TZ: 'UTC', PATH: '/bin:/usr/bin' } }, (error, stdout) => {
16
+ if (!error) {
17
+ resolve(stdout.trim());
18
+ return;
19
+ }
20
+ resolve(error.code === 1 ? '' : null); // ps exits 1 for "no such process"
21
+ });
22
+ });
23
+ }
24
+ /** `starttime` (field 22) of a /proc/<pid>/stat line; the command name (field 2) may hold spaces and parentheses. */
25
+ export function linuxStartTicks(stat) {
26
+ const close = stat.lastIndexOf(')');
27
+ if (close < 0)
28
+ return null;
29
+ const fields = stat.slice(close + 2).trim().split(/\s+/); // fields[0] is field 3 (state)
30
+ const ticks = fields[19];
31
+ return ticks && /^\d+$/.test(ticks) ? ticks : null;
32
+ }
33
+ /** The start-time token of `pid`, or null when there is no such process or it cannot be read. */
34
+ export async function processStartTime(pid, deps = {}) {
35
+ if (!Number.isSafeInteger(pid) || pid <= 0)
36
+ return null;
37
+ const platform = deps.platform ?? process.platform;
38
+ if (platform === 'linux') {
39
+ try {
40
+ const ticks = linuxStartTicks((deps.readStat ?? procStat)(pid));
41
+ const boot = (deps.readBootId ?? procBootId)().trim();
42
+ return ticks && boot ? `linux:${boot}:${ticks}` : null;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ const lstart = await (deps.psLstart ?? darwinLstart)(pid);
49
+ return lstart ? `${platform}:${lstart.replace(/\s+/g, ' ')}` : null;
50
+ }
51
+ /** True only when `pid` runs now AND started at exactly `startTime` (so it is the process that was spawned, not a reuse of its pid). */
52
+ export async function sameProcess(pid, startTime, deps = {}) {
53
+ if (!startTime)
54
+ return false;
55
+ return (await processStartTime(pid, deps)) === startTime;
56
+ }
57
+ /** Sends `signal` to process group `pgid`; false when the group is gone (or the signal could not be sent). */
58
+ export function signalGroup(pgid, signal) {
59
+ if (!Number.isSafeInteger(pgid) || pgid <= 1)
60
+ return false;
61
+ try {
62
+ process.kill(-pgid, signal);
63
+ return true;
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
69
+ /**
70
+ * Applies `onKeeperLoss` to one orphaned exec (spawned, no exit, its keeper gone). `kill`: only after `sameProcess(pid, startTime)`
71
+ * holds, SIGTERM the group now and SIGKILL it `killGraceMs` later. The SIGKILL goes to the group even when the leader has died
72
+ * from the SIGTERM (members that ignore it must still die): a process group id is not reused while any member lives, so within
73
+ * the grace the id still names our group, or nothing. A group whose leader already ended before the sweep (or whose start time
74
+ * was never recorded) cannot be verified and is left alone: never signal what cannot be proven to be ours.
75
+ */
76
+ export async function settleOrphan(spawn, onKeeperLoss, killGraceMs, deps = {}) {
77
+ if (!(await sameProcess(spawn.pid, spawn.startTime, deps)))
78
+ return 'gone';
79
+ if (onKeeperLoss === 'leave')
80
+ return 'left';
81
+ const send = deps.signal ?? signalGroup;
82
+ send(spawn.pgid, 'SIGTERM');
83
+ const later = deps.later ?? ((ms, run) => { setTimeout(run, ms).unref(); });
84
+ later(killGraceMs, () => { send(spawn.pgid, 'SIGKILL'); });
85
+ return 'killed';
86
+ }
@@ -0,0 +1,76 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { keeperIdentity, keeperUnit } from './identity.js';
5
+ export const KEEPER_PORT_MIN = 18000;
6
+ export const KEEPER_PORT_MAX = 19999;
7
+ /** sockaddr_un.sun_path sizes, including the terminating NUL. */
8
+ export const SOCKADDR_LIMIT = { darwin: 104, linux: 108 };
9
+ export function socketLimit(platform = process.platform) {
10
+ return platform === 'darwin' ? SOCKADDR_LIMIT.darwin : SOCKADDR_LIMIT.linux;
11
+ }
12
+ /** Startup-lock port derived from (identity, gen), inside the keeper range 18000-19999. */
13
+ export function keeperLockPort(identity, gen) {
14
+ const digest = createHash('sha256').update(`${identity}:${gen}`).digest('hex');
15
+ return KEEPER_PORT_MIN + (Number.parseInt(digest.slice(0, 8), 16) % (KEEPER_PORT_MAX - KEEPER_PORT_MIN + 1));
16
+ }
17
+ export function keeperPaths(input, kind, gen, options = {}) {
18
+ const identity = keeperIdentity(input, kind);
19
+ const kindRoot = path.join(path.resolve(input.configDir), 'keepers', kind);
20
+ const root = options.legacyLayout ? kindRoot : path.join(kindRoot, identity);
21
+ const runDir = path.join(root, 'run', gen);
22
+ let socket = path.join(runDir, 'keeper.sock');
23
+ let socketDir = runDir;
24
+ let socketFallback = false;
25
+ // Only the socket may leave configDir. A path of `limit` bytes or more leaves no room for the NUL.
26
+ if (Buffer.byteLength(socket) >= socketLimit(options.platform)) {
27
+ socketDir = path.join(options.fallbackRoot ?? '/tmp', `axom-keeper-${options.uid ?? process.getuid?.() ?? 'user'}`);
28
+ socket = path.join(socketDir, `${identity}-${gen}.sock`);
29
+ socketFallback = true;
30
+ }
31
+ return {
32
+ kind, identity, gen, unit: keeperUnit(kind, identity, gen), root,
33
+ bundlesDir: path.join(root, 'bundles'), runDir, dataDir: path.join(root, 'data'),
34
+ socket, socketFallback, socketDir,
35
+ pidFile: path.join(runDir, 'keeper.pid.json'), tokenFile: path.join(runDir, 'token'),
36
+ lockPort: keeperLockPort(identity, gen),
37
+ };
38
+ }
39
+ export function bundleDirFor(paths, buildVersion) {
40
+ return path.join(paths.bundlesDir, buildVersion);
41
+ }
42
+ /** Creates `dir` (and parents) as a private 0700 directory owned by this user; refuses symlinks and foreign owners. */
43
+ export async function privateDirectory(dir) {
44
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
45
+ const stat = await fs.lstat(dir);
46
+ if (!stat.isDirectory() || stat.isSymbolicLink() || (process.getuid && stat.uid !== process.getuid()))
47
+ throw new Error('Unsafe keeper state directory');
48
+ await fs.chmod(dir, 0o700);
49
+ }
50
+ /** Creates the keeper's private directories (root, data, run/<gen>, and the fallback socket dir if used). */
51
+ export async function ensureKeeperDirectories(paths) {
52
+ const configKeepers = path.dirname(paths.root);
53
+ await privateDirectory(configKeepers);
54
+ for (const dir of [paths.root, paths.bundlesDir, path.dirname(paths.runDir), paths.runDir, paths.dataDir])
55
+ await privateDirectory(dir);
56
+ if (paths.socketFallback)
57
+ await privateDirectory(paths.socketDir);
58
+ }
59
+ /**
60
+ * The daemon calls this on every heartbeat for a fallback socket so periodic /tmp cleaners (macOS removes
61
+ * long-unused files) never reap it. A no-op for sockets under configDir. Returns false when the socket is gone.
62
+ */
63
+ export async function touchSocket(paths) {
64
+ if (!paths.socketFallback)
65
+ return true;
66
+ const now = new Date();
67
+ try {
68
+ await fs.utimes(paths.socket, now, now);
69
+ return true;
70
+ }
71
+ catch (error) {
72
+ if (error.code === 'ENOENT')
73
+ return false;
74
+ throw error;
75
+ }
76
+ }
@@ -0,0 +1,43 @@
1
+ export const FRAME_LIMITS = {
2
+ terminal: { in: 128 * 1024, out: 2 * 1024 * 1024 },
3
+ acp: { in: 32 * 1024 * 1024, out: 32 * 1024 * 1024 },
4
+ };
5
+ /** Frame cap for a connection whose kind is not known (probes, diagnostics). */
6
+ export const UNKNOWN_KIND_LIMITS = { in: 1024 * 1024, out: 1024 * 1024 };
7
+ export const frameLimitsOf = (kind) => (kind ? FRAME_LIMITS[kind] : UNKNOWN_KIND_LIMITS);
8
+ export const MAX_CONNECTIONS = 32;
9
+ /** Largest frame accepted from a connection that has not completed `keeper.hello` (hello and ping are tiny). */
10
+ export const PRE_HELLO_MAX_BYTES = 64 * 1024;
11
+ /** Max records in one pushed `evt` frame. */
12
+ export const MAX_EVT_RECORDS = 256;
13
+ /** The daemon speaks `[N-1, N]` of every kind; protocol upgrades may only remove N-2. */
14
+ export const protocolWindow = (current) => [Math.max(0, current - 1), current];
15
+ export const inProtocolWindow = (window, version) => version >= window[0] && version <= window[1];
16
+ export function isProtocolWindow(value) {
17
+ return Array.isArray(value) && value.length === 2 && Number.isSafeInteger(value[0]) && Number.isSafeInteger(value[1])
18
+ && value[0] >= 0 && value[0] <= value[1];
19
+ }
20
+ /** Methods every keeper answers. `keeper.ping` works before hello; everything else needs a hello. */
21
+ export const GENERIC_METHODS = ['keeper.hello', 'keeper.ping', 'keeper.info', 'keeper.activity', 'keeper.drain', 'keeper.stop', 'keeper.subscribe', 'keeper.unsubscribe'];
22
+ /** Still allowed on a connection to a keeper outside the daemon's protocol window (so policy code can drain/stop/inspect it). */
23
+ export const WINDOW_EXEMPT_METHODS = new Set(['keeper.ping', 'keeper.info', 'keeper.activity', 'keeper.drain', 'keeper.stop']);
24
+ export const ErrorCode = {
25
+ badRequest: 'bad_request',
26
+ unauthenticated: 'unauthenticated',
27
+ badToken: 'bad_token',
28
+ unknownMethod: 'unknown_method',
29
+ unknownStream: 'unknown_stream',
30
+ frameTooLarge: 'frame_too_large',
31
+ /** A method that starts new work was called on a keeper that is draining or stopping. */
32
+ draining: 'draining',
33
+ internal: 'internal',
34
+ };
35
+ /** Thrown by a method handler to answer with a specific wire error code. Any other exception answers `internal`. */
36
+ export class KeeperMethodError extends Error {
37
+ code;
38
+ constructor(code, message) {
39
+ super(message);
40
+ this.code = code;
41
+ this.name = 'KeeperMethodError';
42
+ }
43
+ }