@platless/axom-cli 0.1.0 → 0.5.9

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 (281) hide show
  1. package/README.md +1 -1
  2. package/dist/authScopes.js +19 -0
  3. package/dist/axom-patch/cli/README.md +132 -0
  4. package/dist/axom-patch/cli/account-limits/README.md +27 -0
  5. package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
  6. package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
  7. package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
  8. package/dist/axom-patch/cli/account-limits/generate.py +120 -0
  9. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
  10. package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
  11. package/dist/axom-patch/cli/claude-account-limits.json +37 -0
  12. package/dist/axom-patch/cli/codex-account-limits.json +37 -0
  13. package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
  14. package/dist/axom-patch/cli/nightly-task.md +152 -0
  15. package/dist/axom-patch/cli/pi-steering.json +37 -0
  16. package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
  17. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +38 -0
  18. package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
  19. package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
  20. package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
  21. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +61 -0
  22. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
  23. package/dist/axom-patch/cli/qoder-background-terminal.md +82 -0
  24. package/dist/axom-patch/cli/qoder-foreground-output.md +42 -0
  25. package/dist/axom-patch/cli/qoder-steering.json +52 -0
  26. package/dist/axom-patch/cli/qoder-steering.md +30 -0
  27. package/dist/axom-patch/cli/qoder-subagent-origin.json +73 -0
  28. package/dist/axom-patch/cli/qoder-subagent-subwork.md +73 -0
  29. package/dist/axom-patch/cli/runtime.json +63 -0
  30. package/dist/axom-patch/cli/steering/claude-steering-js.patch +89 -0
  31. package/dist/axom-patch/cli/steering/claude-steering-types.patch +28 -0
  32. package/dist/axom-patch/cli/steering/codex-steering.patch +57 -0
  33. package/dist/axom-patch/cli/steering/pi-steering.js +79 -0
  34. package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
  35. package/dist/axom-patch/cli/zcode-models.json +38 -0
  36. package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
  37. package/dist/cli.js +36 -1
  38. package/dist/commands/auth.js +4 -13
  39. package/dist/commands/execute.js +4 -4
  40. package/dist/commands/record.js +6 -6
  41. package/dist/commands/struct.js +4 -4
  42. package/dist/commands/workstation.js +2 -2
  43. package/dist/credentials.js +2 -2
  44. package/dist/install/claude/commands.js +13 -13
  45. package/dist/keepers/acp/keeper.mjs +3083 -0
  46. package/dist/keepers/acp/manifest.json +9 -0
  47. package/dist/keepers/terminal/keeper.mjs +8469 -0
  48. package/dist/keepers/terminal/manifest.json +9 -0
  49. package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  50. package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  51. package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
  52. package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
  53. package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
  54. package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
  55. package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
  56. package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
  57. package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
  58. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
  59. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
  60. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
  61. package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  62. package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
  63. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  64. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  65. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  66. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  67. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  68. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  69. package/dist/mcp.js +32 -14
  70. package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
  71. package/dist/runtime-deps/qoder-sdk/package.json +8 -0
  72. package/dist/test/keeperKit.js +67 -0
  73. package/dist/workstation/acp/acpAgents.js +2514 -681
  74. package/dist/workstation/acp/acpClient.js +95 -318
  75. package/dist/workstation/acp/activityBusy.js +43 -0
  76. package/dist/workstation/acp/activityReporting.js +54 -0
  77. package/dist/workstation/acp/adoption.js +28 -356
  78. package/dist/workstation/acp/authUrlSniffer.js +36 -0
  79. package/dist/workstation/acp/binaryInstall.js +100 -27
  80. package/dist/workstation/acp/builds/gc.js +245 -0
  81. package/dist/workstation/acp/builds/install/binary.js +28 -0
  82. package/dist/workstation/acp/builds/install/npm.js +235 -0
  83. package/dist/workstation/acp/builds/install/uv.js +254 -0
  84. package/dist/workstation/acp/builds/migrate.js +195 -0
  85. package/dist/workstation/acp/builds/model.js +128 -0
  86. package/dist/workstation/acp/builds/nodeGate.js +100 -0
  87. package/dist/workstation/acp/builds/store.js +352 -0
  88. package/dist/workstation/acp/commandTableProbe.js +68 -0
  89. package/dist/workstation/acp/configOptions.js +103 -1
  90. package/dist/workstation/acp/delegationMcp.js +24 -0
  91. package/dist/workstation/acp/envSecrets.js +27 -0
  92. package/dist/workstation/acp/initializeResponse.js +285 -0
  93. package/dist/workstation/acp/installErrors.js +12 -2
  94. package/dist/workstation/acp/installProbe.js +1 -1
  95. package/dist/workstation/acp/managedPatches.js +144 -0
  96. package/dist/workstation/acp/outbox.js +330 -0
  97. package/dist/workstation/acp/patchAssets.js +80 -0
  98. package/dist/workstation/acp/permission.js +335 -37
  99. package/dist/workstation/acp/processor/checkpoint.js +87 -0
  100. package/dist/workstation/acp/processor/commands.js +35 -0
  101. package/dist/workstation/acp/processor/control.js +355 -0
  102. package/dist/workstation/acp/processor/emit.js +26 -0
  103. package/dist/workstation/acp/processor/jsonrpc.js +172 -0
  104. package/dist/workstation/acp/processor/memoryLog.js +49 -0
  105. package/dist/workstation/acp/processor/origins.js +48 -0
  106. package/dist/workstation/acp/processor/output.js +67 -0
  107. package/dist/workstation/acp/processor/projection.js +42 -0
  108. package/dist/workstation/acp/processor/record.js +128 -0
  109. package/dist/workstation/acp/processor/runtime.js +181 -0
  110. package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
  111. package/dist/workstation/acp/processor/session.js +525 -0
  112. package/dist/workstation/acp/processor/sideChannel.js +80 -0
  113. package/dist/workstation/acp/processor/state.js +74 -0
  114. package/dist/workstation/acp/processor/step.js +459 -0
  115. package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
  116. package/dist/workstation/acp/processor/test/harness.js +120 -0
  117. package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
  118. package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
  119. package/dist/workstation/acp/processor/transport/generations.js +76 -0
  120. package/dist/workstation/acp/processor/transport/keeper.js +130 -0
  121. package/dist/workstation/acp/processor/transport/local.js +269 -0
  122. package/dist/workstation/acp/processor/transport/types.js +78 -0
  123. package/dist/workstation/acp/processor/updates.js +364 -0
  124. package/dist/workstation/acp/processorSession.js +1512 -0
  125. package/dist/workstation/acp/qoderStartup.js +45 -0
  126. package/dist/workstation/acp/registry/catalog.js +385 -4
  127. package/dist/workstation/acp/registry/detect.js +55 -0
  128. package/dist/workstation/acp/registry/launch.js +61 -119
  129. package/dist/workstation/acp/sessionUpdateMapper.js +56 -27
  130. package/dist/workstation/acp/steering.js +58 -0
  131. package/dist/workstation/acp/switching.js +114 -0
  132. package/dist/workstation/acp/terminalLogin.js +171 -0
  133. package/dist/workstation/acp/terminalLoginSpec.js +41 -0
  134. package/dist/workstation/acp/test/adapterFixtures.js +256 -0
  135. package/dist/workstation/acp/test/daemonLife.js +106 -0
  136. package/dist/workstation/acp/test/fakeAgent.js +185 -9
  137. package/dist/workstation/acp/test/keeperWorld.js +267 -0
  138. package/dist/workstation/acp/test/memoryOutbox.js +55 -0
  139. package/dist/workstation/acp/test/raw/fixtures.js +196 -0
  140. package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
  141. package/dist/workstation/acp/test/raw/sanitize.js +344 -0
  142. package/dist/workstation/acp/test/raw/sources.js +249 -0
  143. package/dist/workstation/acp/test/realAdapter.js +377 -0
  144. package/dist/workstation/acp/test/scriptAgent.js +206 -0
  145. package/dist/workstation/acp/test/sessionFrames.js +42 -0
  146. package/dist/workstation/acp/timedCommand.js +4 -2
  147. package/dist/workstation/acp/uvxInstall.js +72 -122
  148. package/dist/workstation/acp/validate.js +33 -5
  149. package/dist/workstation/agentFlow.js +103 -0
  150. package/dist/workstation/agentSessions.js +1686 -348
  151. package/dist/workstation/channels/agent.js +14 -6
  152. package/dist/workstation/channels/computer.js +203 -7
  153. package/dist/workstation/channels/computerExecs.js +200 -0
  154. package/dist/workstation/channels/management.js +86 -6
  155. package/dist/workstation/config.js +3 -1
  156. package/dist/workstation/connection/encryptedChannel.js +88 -21
  157. package/dist/workstation/daemon.js +309 -6
  158. package/dist/workstation/daemonctl.js +28 -2
  159. package/dist/workstation/desktop/binding.js +36 -0
  160. package/dist/workstation/desktop/bridge.js +212 -0
  161. package/dist/workstation/desktop/flow-control.js +52 -0
  162. package/dist/workstation/desktop/focused-text.js +130 -0
  163. package/dist/workstation/desktop/protocol.js +35 -0
  164. package/dist/workstation/exec/attach.js +126 -0
  165. package/dist/workstation/exec/command.js +40 -0
  166. package/dist/workstation/exec/durable.js +517 -0
  167. package/dist/workstation/exec/fallback.js +106 -0
  168. package/dist/workstation/exec/policy.js +837 -0
  169. package/dist/workstation/exec/shaping.js +211 -0
  170. package/dist/workstation/exec.js +3 -112
  171. package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
  172. package/dist/workstation/files/adapters/toolRequests.js +121 -0
  173. package/dist/workstation/files/client.js +455 -0
  174. package/dist/workstation/files/errors.js +28 -0
  175. package/dist/workstation/files/fence.js +350 -0
  176. package/dist/workstation/files/find/fileIndex.js +153 -0
  177. package/dist/workstation/files/find/fuzzy.js +155 -0
  178. package/dist/workstation/files/host.js +122 -0
  179. package/dist/workstation/files/hostProtocol.js +2 -0
  180. package/dist/workstation/files/media/cache.js +620 -0
  181. package/dist/workstation/files/media/encoding.js +308 -0
  182. package/dist/workstation/files/media/estimator.js +100 -0
  183. package/dist/workstation/files/media/media.js +325 -0
  184. package/dist/workstation/files/media/packedAudio.js +23 -0
  185. package/dist/workstation/files/media/scheduler.js +551 -0
  186. package/dist/workstation/files/media/types.js +5 -0
  187. package/dist/workstation/files/scope.js +45 -0
  188. package/dist/workstation/files/structured.js +284 -0
  189. package/dist/workstation/files/text.js +356 -0
  190. package/dist/workstation/files/uploads.js +351 -0
  191. package/dist/workstation/journal.js +65 -236
  192. package/dist/workstation/keepers/acp/client.js +101 -0
  193. package/dist/workstation/keepers/acp/envelope.js +62 -0
  194. package/dist/workstation/keepers/acp/host.js +419 -0
  195. package/dist/workstation/keepers/acp/main.js +18 -0
  196. package/dist/workstation/keepers/acp/memoryStream.js +121 -0
  197. package/dist/workstation/keepers/acp/params.js +137 -0
  198. package/dist/workstation/keepers/acp/protocol.js +66 -0
  199. package/dist/workstation/keepers/acp/recover.js +120 -0
  200. package/dist/workstation/keepers/acp/session.js +584 -0
  201. package/dist/workstation/keepers/acp/stderr.js +154 -0
  202. package/dist/workstation/keepers/kit/activity.js +103 -0
  203. package/dist/workstation/keepers/kit/bootstrap.js +164 -0
  204. package/dist/workstation/keepers/kit/buildVersion.js +51 -0
  205. package/dist/workstation/keepers/kit/bundle.js +120 -0
  206. package/dist/workstation/keepers/kit/client.js +432 -0
  207. package/dist/workstation/keepers/kit/identity.js +21 -0
  208. package/dist/workstation/keepers/kit/ipc.js +112 -0
  209. package/dist/workstation/keepers/kit/launchConfig.js +8 -0
  210. package/dist/workstation/keepers/kit/launcher.js +249 -0
  211. package/dist/workstation/keepers/kit/lock.js +123 -0
  212. package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
  213. package/dist/workstation/keepers/kit/paths.js +75 -0
  214. package/dist/workstation/keepers/kit/protocol.js +43 -0
  215. package/dist/workstation/keepers/kit/server.js +518 -0
  216. package/dist/workstation/keepers/kit/streamLog.js +697 -0
  217. package/dist/workstation/keepers/legacy.js +296 -0
  218. package/dist/workstation/keepers/policy.js +576 -0
  219. package/dist/workstation/keepers/stop.js +34 -0
  220. package/dist/workstation/keepers/terminal/client.js +213 -0
  221. package/dist/workstation/keepers/terminal/exec.js +981 -0
  222. package/dist/workstation/keepers/terminal/generations.js +129 -0
  223. package/dist/workstation/keepers/terminal/keeper.js +62 -0
  224. package/dist/workstation/keepers/terminal/main.js +43 -0
  225. package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
  226. package/dist/workstation/keepers/terminal/params.js +27 -0
  227. package/dist/workstation/keepers/terminal/protocol.js +56 -0
  228. package/dist/workstation/keepers/terminal/pty.js +330 -0
  229. package/dist/workstation/oauth/deviceAuthorization.js +21 -12
  230. package/dist/workstation/protocol/accountLimits.js +47 -0
  231. package/dist/workstation/protocol/agent.js +201 -8
  232. package/dist/workstation/protocol/common.js +36 -0
  233. package/dist/workstation/protocol/computer.js +107 -7
  234. package/dist/workstation/protocol/management.js +50 -2
  235. package/dist/workstation/qoderAdapter/backgroundProcess.js +118 -0
  236. package/dist/workstation/qoderAdapter/backgroundTerminal.js +193 -0
  237. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
  238. package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
  239. package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
  240. package/dist/workstation/qoderAdapter/contract.js +78 -0
  241. package/dist/workstation/qoderAdapter/foregroundOutput.js +36 -0
  242. package/dist/workstation/qoderAdapter/history.js +85 -0
  243. package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
  244. package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
  245. package/dist/workstation/qoderAdapter/main.js +95 -0
  246. package/dist/workstation/qoderAdapter/mapper.js +117 -0
  247. package/dist/workstation/qoderAdapter/observerPlugin.js +245 -0
  248. package/dist/workstation/qoderAdapter/observerReceiver.js +187 -0
  249. package/dist/workstation/qoderAdapter/permissions.js +61 -0
  250. package/dist/workstation/qoderAdapter/profile.js +93 -0
  251. package/dist/workstation/qoderAdapter/sdkDriver.js +48 -0
  252. package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
  253. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
  254. package/dist/workstation/qoderAdapter/session.js +319 -0
  255. package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
  256. package/dist/workstation/qoderAdapter/subwork.js +85 -0
  257. package/dist/workstation/qoderAdapter/taskControl.js +38 -0
  258. package/dist/workstation/qoderAdapter/taskLedger.js +163 -0
  259. package/dist/workstation/qoderAdapter/types.js +1 -0
  260. package/dist/workstation/qoderAdapter/usage.js +37 -0
  261. package/dist/workstation/register.js +4 -0
  262. package/dist/workstation/systemMetrics.js +110 -1
  263. package/dist/workstation/tools/browser.js +394 -0
  264. package/dist/workstation/tools/client.js +277 -0
  265. package/dist/workstation/tools/info.js +50 -0
  266. package/dist/workstation/tools/keeperProcess.js +124 -0
  267. package/dist/workstation/tools/router.js +55 -0
  268. package/dist/workstation/tools/terminalRouter.js +222 -0
  269. package/dist/workstation/tools/validation.js +86 -0
  270. package/dist/workstation/update/coordinator.js +32 -0
  271. package/dist/workstation/update/npmInstaller.js +2 -0
  272. package/dist/workstation/update/ptyPreflight.js +45 -0
  273. package/dist/workstation/update/taskRegistry.js +141 -23
  274. package/dist/workstation/update/watchdog.js +66 -0
  275. package/package.json +20 -7
  276. package/skills/axom/SKILL.md +55 -17
  277. package/skills/axom-definition/SKILL.md +32 -32
  278. package/skills/axom-platform/SKILL.md +39 -39
  279. package/dist/workstation/acp/acpSession.js +0 -1047
  280. package/dist/workstation/acp/contract.js +0 -494
  281. package/dist/workstation/acp/draftInterceptor.js +0 -154
@@ -0,0 +1,249 @@
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 path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { keeperOneShot, readKeeperToken } from './client.js';
7
+ import { BundleIntegrityError, ensureBundle, gcBundles, readKeeperManifest } from './bundle.js';
8
+ import { keeperGen } from './identity.js';
9
+ import { encodeLaunchConfig, launchConfigOf } from './launchConfig.js';
10
+ import { acquireStartupLock, liveManifest, manifestProcessLive, pidAlive, processCommand, readPidManifest, releaseStartupLock } from './lock.js';
11
+ import { bundleDirFor, ensureKeeperDirectories, keeperPaths, touchSocket } from './paths.js';
12
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
13
+ /** `keeper.ping` on a fresh connection. Null unless the answer is from this exact keeper generation. */
14
+ export async function pingKeeper(paths, timeoutMs = 250) {
15
+ try {
16
+ const reply = await keeperOneShot(paths.socket, 'keeper.ping', {}, timeoutMs);
17
+ if (reply.identity !== paths.identity || reply.gen !== paths.gen || !Number.isSafeInteger(reply.pid))
18
+ return null;
19
+ return { pid: reply.pid, identity: paths.identity, gen: paths.gen };
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ /** 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. */
26
+ export function systemdRunArgs(unit, nodePath, script, config) {
27
+ return ['--user', '--quiet', '--collect', `--unit=${unit}`, '--property=Type=exec', '--property=Restart=no', '--', nodePath, script, config];
28
+ }
29
+ function runQuiet(command, args, timeoutMs) {
30
+ return new Promise((resolve) => {
31
+ const child = spawn(command, args, { stdio: 'ignore' });
32
+ let done = false;
33
+ const finish = (ok) => { if (done)
34
+ return; done = true; clearTimeout(timer); resolve(ok); };
35
+ const timer = setTimeout(() => { child.kill(); finish(false); }, timeoutMs);
36
+ child.on('error', () => finish(false));
37
+ child.on('exit', (code) => finish(code === 0));
38
+ });
39
+ }
40
+ export const systemdAvailable = () => runQuiet('systemctl', ['--user', 'show-environment'], 500);
41
+ export const defaultDistDir = () => fileURLToPath(new URL('../../../keepers/', import.meta.url));
42
+ export class KeeperLauncher {
43
+ options;
44
+ starting = new Map();
45
+ current = new Set();
46
+ constructor(options) {
47
+ this.options = options;
48
+ }
49
+ get distDir() { return this.options.distDir ?? defaultDistDir(); }
50
+ probe(paths, timeoutMs) { return this.options.probe ? this.options.probe(paths) : pingKeeper(paths, timeoutMs); }
51
+ /** Paths of generation `gen` of `kind` (no build needed). */
52
+ pathsFor(kind, gen) { return keeperPaths(this.options, kind, gen); }
53
+ /** The generation the current build of `kind` runs as (`dist/keepers/<kind>/manifest.json`); starts nothing. */
54
+ async currentGen(kind) {
55
+ return keeperGen((await readKeeperManifest(path.join(this.distDir, kind))).buildVersion);
56
+ }
57
+ /** Generations of `kind` whose pid manifest names a live keeper of that generation (`<root>/run/<gen>`); starts nothing. */
58
+ async liveGenerations(kind) {
59
+ const runRoot = path.join(this.pathsFor(kind, 'probe').root, 'run');
60
+ const gens = await fs.readdir(runRoot).catch(() => []);
61
+ const live = [];
62
+ for (const gen of gens.sort())
63
+ if (await liveManifest(this.pathsFor(kind, gen)).catch(() => false))
64
+ live.push(gen);
65
+ return live;
66
+ }
67
+ /** Ensures the current build of `kind` is running (snapshot, lock, probe, spawn) and returns it. One start per kind in flight. */
68
+ launch(kind) {
69
+ let pending = this.starting.get(kind);
70
+ if (!pending) {
71
+ pending = this.start(kind).finally(() => { this.starting.delete(kind); });
72
+ this.starting.set(kind, pending);
73
+ }
74
+ return pending;
75
+ }
76
+ async start(kind) {
77
+ const deadline = Date.now() + (this.options.startTimeoutMs ?? 7500);
78
+ const manifest = await readKeeperManifest(path.join(this.distDir, kind));
79
+ const paths = keeperPaths(this.options, kind, keeperGen(manifest.buildVersion));
80
+ await ensureKeeperDirectories(paths);
81
+ // A keeper that already answers needs no snapshot check: it runs from the snapshot it was started with.
82
+ const alive = await this.probe(paths);
83
+ if (alive) {
84
+ this.current.add(manifest.buildVersion);
85
+ return { paths, manifest, bundleDir: bundleDirFor(paths, manifest.buildVersion), pid: alive.pid, launched: false };
86
+ }
87
+ const snapshot = await ensureBundle(path.join(this.distDir, kind), paths.bundlesDir); // BundleIntegrityError refuses the launch
88
+ this.current.add(snapshot.buildVersion);
89
+ const handle = (pid, launched) => ({ paths, manifest, bundleDir: snapshot.bundleDir, pid, launched });
90
+ while (Date.now() < deadline - 500) {
91
+ const found = await this.probe(paths);
92
+ if (found)
93
+ return handle(found.pid, false);
94
+ let lock;
95
+ try {
96
+ lock = await acquireStartupLock(paths.lockPort);
97
+ }
98
+ catch (error) {
99
+ if (error.code !== 'EADDRINUSE')
100
+ throw error;
101
+ await delay(50);
102
+ continue;
103
+ }
104
+ try {
105
+ const winner = await this.probe(paths);
106
+ if (winner)
107
+ return handle(winner.pid, false);
108
+ // A probe timeout is not process death: a busy keeper may be slow to ping, and unlinking its live
109
+ // socket would orphan everything it hosts. The pid manifest is written before the keeper initialises.
110
+ while (await liveManifest(paths)) {
111
+ const slow = await this.probe(paths);
112
+ if (slow)
113
+ return handle(slow.pid, false);
114
+ if (Date.now() >= deadline - 300)
115
+ throw new Error(`Keeper ${kind} is alive but busy or unavailable; retry later`);
116
+ await delay(50);
117
+ }
118
+ await fs.unlink(paths.socket).catch((error) => { if (error.code !== 'ENOENT')
119
+ throw error; });
120
+ await this.spawnKeeper(paths, snapshot.buildVersion, snapshot.bundleDir, deadline);
121
+ while (Date.now() < deadline - 100) {
122
+ const started = await this.probe(paths);
123
+ if (started) {
124
+ void this.gc().catch(() => { });
125
+ return handle(started.pid, true);
126
+ }
127
+ await delay(50);
128
+ }
129
+ throw new Error(`Keeper ${kind} failed to start`);
130
+ }
131
+ finally {
132
+ await releaseStartupLock(lock);
133
+ }
134
+ }
135
+ throw new Error(`Keeper ${kind} startup is busy or timed out`);
136
+ }
137
+ async spawnKeeper(paths, buildVersion, bundleDir, deadline) {
138
+ const script = path.join(bundleDir, 'keeper.mjs'); // always the immutable snapshot, never dist/
139
+ const config = encodeLaunchConfig(launchConfigOf(paths, buildVersion, bundleDir, this.options.umask ?? process.umask()));
140
+ const nodePath = this.options.nodePath ?? process.execPath;
141
+ if ((this.options.platform ?? process.platform) === 'linux' && this.options.useSystemd !== false && await systemdAvailable()) {
142
+ // setsid/detached cannot escape systemd's KillMode=control-group. A separate transient unit gives the keeper
143
+ // (and everything it hosts) its own cgroup so the daemon can restart or upgrade freely.
144
+ const timeout = Math.max(1, Math.min(2000, deadline - Date.now() - 500));
145
+ if (await runQuiet('systemd-run', systemdRunArgs(paths.unit, nodePath, script, config), timeout))
146
+ return;
147
+ // The unit may still be starting. Never fall back to a same-cgroup child on a systemd host.
148
+ while (Date.now() < deadline - 100) {
149
+ if (await this.probe(paths))
150
+ return;
151
+ await delay(50);
152
+ }
153
+ throw new Error(`Cannot start isolated keeper user service ${paths.unit}`);
154
+ }
155
+ // macOS (and Linux without a user systemd): a detached new session. Task 0 verified it survives launchd
156
+ // bootout/bootstrap/kickstart and kill -9 of the daemon (docs/superpowers/reports/2026-10-11-keeper-launch-spike.md).
157
+ // `detached` is what makes it survive; do not add launchd keys or drop it.
158
+ const child = spawn(nodePath, [script, config], { detached: true, stdio: 'ignore' });
159
+ child.on('error', () => { });
160
+ child.unref();
161
+ }
162
+ /**
163
+ * Spec §3.1: a socket that fell back to `/tmp` (its configDir path is too long for sockaddr_un) is touched for every generation
164
+ * of `kind` with a run directory, so a periodic /tmp cleaner (macOS removes files unused for days) never reaps a live keeper's
165
+ * socket. Sockets under configDir are left alone. Returns how many were touched; never throws.
166
+ */
167
+ async touchSockets(kind) {
168
+ const runRoot = path.join(this.pathsFor(kind, 'probe').root, 'run');
169
+ let touched = 0;
170
+ for (const gen of await fs.readdir(runRoot).catch(() => [])) {
171
+ const paths = this.pathsFor(kind, gen);
172
+ if (paths.socketFallback && await touchSocket(paths).catch(() => false))
173
+ touched += 1;
174
+ }
175
+ return touched;
176
+ }
177
+ /** Removes bundle snapshots no live keeper references (never the daemon's current builds). */
178
+ gc(minAgeMs) {
179
+ return gcBundles({ configDir: this.options.configDir, keep: this.current, minAgeMs });
180
+ }
181
+ /**
182
+ * Stops generation `gen` of `kind`. `graceful` asks the keeper (`keeper.stop`) and waits; it never escalates, so a
183
+ * keeper that refuses stays up. `force` additionally stops the transient unit and finally SIGKILLs the pid from the
184
+ * manifest, but only if that pid is still running this keeper's snapshot (guards against pid reuse). A manifest whose pid
185
+ * is dead or belongs to another program is stale: it is cleaned up and reported as stopped, nothing is signalled.
186
+ */
187
+ async stop(kind, gen, mode, waitMs = 5000) {
188
+ const paths = this.pathsFor(kind, gen);
189
+ const manifest = await readPidManifest(paths.pidFile);
190
+ if (!manifest || manifest.identity !== paths.identity || manifest.kind !== kind || manifest.gen !== gen)
191
+ return { stopped: true, pid: null };
192
+ const pid = manifest.pid;
193
+ if (!await manifestProcessLive(manifest)) { // dead, or the pid now belongs to an unrelated process: the manifest is stale
194
+ await fs.unlink(paths.socket).catch(() => { });
195
+ await fs.unlink(paths.pidFile).catch(() => { });
196
+ return { stopped: true, pid: null };
197
+ }
198
+ const dead = async (ms) => {
199
+ const until = Date.now() + ms;
200
+ while (pidAlive(pid)) {
201
+ if (Date.now() >= until)
202
+ return false;
203
+ await delay(25);
204
+ }
205
+ return true;
206
+ };
207
+ // keeper.stop needs the connection token (the daemon owns the run directory it lives in). Without a usable token
208
+ // the keeper cannot be asked, so after the same identity check the force path uses, SIGTERM it (its handler is the graceful stop).
209
+ let reason;
210
+ let token;
211
+ try {
212
+ token = await readKeeperToken(paths.tokenFile);
213
+ }
214
+ catch (error) {
215
+ reason = `keeper token unavailable: ${error.message}`;
216
+ }
217
+ if (token) {
218
+ try {
219
+ await keeperOneShot(paths.socket, 'keeper.stop', { mode }, 1000, { kind, token });
220
+ }
221
+ catch (error) {
222
+ reason = `keeper.stop failed: ${error.message}`;
223
+ }
224
+ }
225
+ else if (((await processCommand(pid)) ?? '').includes(manifest.bundleDir)) {
226
+ try {
227
+ process.kill(pid, 'SIGTERM');
228
+ }
229
+ catch { /* already gone */ }
230
+ }
231
+ if (!await dead(mode === 'force' ? 1000 : waitMs) && mode === 'force') {
232
+ if (process.platform === 'linux')
233
+ await runQuiet('systemctl', ['--user', 'stop', paths.unit], 2000);
234
+ if (!await dead(500) && ((await processCommand(pid)) ?? '').includes(manifest.bundleDir)) {
235
+ try {
236
+ process.kill(pid, 'SIGKILL');
237
+ }
238
+ catch { /* already gone */ }
239
+ await dead(waitMs);
240
+ }
241
+ }
242
+ if (pidAlive(pid))
243
+ return { stopped: false, pid, ...(reason ? { reason } : {}) };
244
+ await fs.unlink(paths.socket).catch(() => { });
245
+ await fs.unlink(paths.pidFile).catch(() => { });
246
+ return { stopped: true, pid, ...(reason ? { reason } : {}) };
247
+ }
248
+ }
249
+ 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,75 @@
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 root = path.join(path.resolve(input.configDir), 'keepers', kind);
20
+ const runDir = path.join(root, 'run', gen);
21
+ let socket = path.join(runDir, 'keeper.sock');
22
+ let socketDir = runDir;
23
+ let socketFallback = false;
24
+ // Only the socket may leave configDir. A path of `limit` bytes or more leaves no room for the NUL.
25
+ if (Buffer.byteLength(socket) >= socketLimit(options.platform)) {
26
+ socketDir = path.join(options.fallbackRoot ?? '/tmp', `axom-keeper-${options.uid ?? process.getuid?.() ?? 'user'}`);
27
+ socket = path.join(socketDir, `${identity}-${gen}.sock`);
28
+ socketFallback = true;
29
+ }
30
+ return {
31
+ kind, identity, gen, unit: keeperUnit(kind, identity, gen), root,
32
+ bundlesDir: path.join(root, 'bundles'), runDir, dataDir: path.join(root, 'data'),
33
+ socket, socketFallback, socketDir,
34
+ pidFile: path.join(runDir, 'keeper.pid.json'), tokenFile: path.join(runDir, 'token'),
35
+ lockPort: keeperLockPort(identity, gen),
36
+ };
37
+ }
38
+ export function bundleDirFor(paths, buildVersion) {
39
+ return path.join(paths.bundlesDir, buildVersion);
40
+ }
41
+ /** Creates `dir` (and parents) as a private 0700 directory owned by this user; refuses symlinks and foreign owners. */
42
+ export async function privateDirectory(dir) {
43
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
44
+ const stat = await fs.lstat(dir);
45
+ if (!stat.isDirectory() || stat.isSymbolicLink() || (process.getuid && stat.uid !== process.getuid()))
46
+ throw new Error('Unsafe keeper state directory');
47
+ await fs.chmod(dir, 0o700);
48
+ }
49
+ /** Creates the keeper's private directories (root, data, run/<gen>, and the fallback socket dir if used). */
50
+ export async function ensureKeeperDirectories(paths) {
51
+ const configKeepers = path.dirname(paths.root);
52
+ await privateDirectory(configKeepers);
53
+ for (const dir of [paths.root, paths.bundlesDir, path.dirname(paths.runDir), paths.runDir, paths.dataDir])
54
+ await privateDirectory(dir);
55
+ if (paths.socketFallback)
56
+ await privateDirectory(paths.socketDir);
57
+ }
58
+ /**
59
+ * The daemon calls this on every heartbeat for a fallback socket so periodic /tmp cleaners (macOS removes
60
+ * long-unused files) never reap it. A no-op for sockets under configDir. Returns false when the socket is gone.
61
+ */
62
+ export async function touchSocket(paths) {
63
+ if (!paths.socketFallback)
64
+ return true;
65
+ const now = new Date();
66
+ try {
67
+ await fs.utimes(paths.socket, now, now);
68
+ return true;
69
+ }
70
+ catch (error) {
71
+ if (error.code === 'ENOENT')
72
+ return false;
73
+ throw error;
74
+ }
75
+ }
@@ -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
+ }