@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,50 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ /*
5
+ * `tools.info`, aggregated by the daemon. Consumers (analysis C §3.6): Workbench app.mjs (version), CoworkService
6
+ * (files / mediaPreviewVersion / fileFindVersion / home / userName), files.mjs (roots / downloads / terminal),
7
+ * terminal.mjs (home, terminal / terminalError), browser.mjs (browser* fields). Every field keeps the value and meaning
8
+ * the legacy tools keeper gave it (tools/core.ts at the merge-base, since deleted, + tools/client.ts), and answering it never
9
+ * starts any keeper:
10
+ * - file fields come from the files host's last `ready` (or are computed here before the first one); `files: true`
11
+ * is reported even while the host restarts, so a probe never mistakes the machine for an old CLI;
12
+ * - terminal fields come from keeper-terminal's hello capabilities when that keeper is already running (connecting to it,
13
+ * never starting it: keepers/terminal/client.ts `terminalStatus`), otherwise from a cached node-pty load check
14
+ * (update/ptyPreflight.ts);
15
+ * - browser fields come from the browser-kind ToolsClient, whose `tools.info` only returns metadata.
16
+ */
17
+ /** The legacy keeper always sent this (tools/core.ts at the merge-base); the browser metadata never removed it. */
18
+ export const BROWSER_DISABLED_ERROR = 'Browser tools are not enabled in this release';
19
+ export async function aggregateToolsInfo(sources) {
20
+ const [files, browser, terminal] = await Promise.all([sources.files(), sources.browser('tools.info', {}), sources.terminal()]);
21
+ return {
22
+ version: 1,
23
+ terminal: terminal.terminal,
24
+ browser: false,
25
+ ...files,
26
+ ...(terminal.terminalError ? { terminalError: terminal.terminalError } : {}),
27
+ browserError: BROWSER_DISABLED_ERROR,
28
+ ...browser,
29
+ };
30
+ }
31
+ /**
32
+ * The file fields before any files host was ready: the same canonical home and roots the host computes
33
+ * (scope.ts `configuredRoots`), except that a root which cannot be resolved is left out instead of failing the probe.
34
+ */
35
+ export async function computeFilesInfo(home, roots) {
36
+ const canonicalHome = await fs.realpath(home).catch(() => home);
37
+ const canonical = new Set([canonicalHome]);
38
+ for (const root of roots) {
39
+ try {
40
+ const real = await fs.realpath(root);
41
+ if ((await fs.stat(real)).isDirectory())
42
+ canonical.add(real);
43
+ }
44
+ catch { /* skipped, as the daemon skips it when starting the host */ }
45
+ }
46
+ return {
47
+ files: true, readOnlyFilesVersion: 1, filesystemFilesVersion: 1, mediaPreviewVersion: 2, fileFindVersion: 1,
48
+ home: canonicalHome, userName: os.userInfo().username, roots: [...canonical], downloads: path.join(canonicalHome, 'Downloads'),
49
+ };
50
+ }
@@ -0,0 +1,124 @@
1
+ import net from 'node:net';
2
+ import { promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { BrowserCore } from './browser.js';
6
+ import os from 'node:os';
7
+ import { privateDirectory } from './client.js';
8
+ import { object, TOOL_REQUEST_BYTES, TOOL_RESULT_BYTES } from './validation.js';
9
+ export async function runKeeper(config) {
10
+ // Only the browser keeper runs from here (spec §9): keeper-terminal owns PTYs and the files host owns files.
11
+ if (config.kind !== 'browser')
12
+ throw new Error('Only the browser keeper runs from this entry');
13
+ await privateDirectory(path.dirname(config.socket));
14
+ const pidFile = path.join(path.dirname(config.socket), 'keeper.pid.json');
15
+ await fs.writeFile(pidFile, JSON.stringify({ pid: process.pid, identity: config.identity }), { mode: 0o600 });
16
+ const core = await BrowserCore.create({ home: config.home ?? os.homedir(), profileRoot: config.profileRoot, executablePath: config.executablePath, proxy: config.proxy });
17
+ const sockets = new Set();
18
+ let pending = 0;
19
+ const server = net.createServer({ allowHalfOpen: true }, socket => {
20
+ if (sockets.size >= 32) {
21
+ socket.destroy();
22
+ return;
23
+ }
24
+ sockets.add(socket);
25
+ socket.setEncoding('utf8');
26
+ socket.setTimeout(8000, () => socket.destroy());
27
+ let buffer = '';
28
+ let received = false;
29
+ socket.on('error', () => socket.destroy());
30
+ socket.on('close', () => sockets.delete(socket));
31
+ socket.on('data', chunk => {
32
+ if (received) {
33
+ socket.destroy();
34
+ return;
35
+ }
36
+ buffer += chunk;
37
+ if (Buffer.byteLength(buffer) > TOOL_REQUEST_BYTES) {
38
+ socket.destroy();
39
+ return;
40
+ }
41
+ const newline = buffer.indexOf('\n');
42
+ if (newline < 0)
43
+ return;
44
+ received = true;
45
+ let frame;
46
+ try {
47
+ const value = JSON.parse(buffer.slice(0, newline));
48
+ if (!object(value) || typeof value.id !== 'string' || value.id.length > 128 || !value.id.length || typeof value.method !== 'string' || !object(value.params) || Object.keys(value).some(key => !['id', 'method', 'params'].includes(key)) || buffer.slice(newline + 1).length)
49
+ throw new Error('Invalid keeper request');
50
+ frame = value;
51
+ }
52
+ catch {
53
+ socket.destroy();
54
+ return;
55
+ }
56
+ const reply = (ok, result, error) => {
57
+ const payload = JSON.stringify({ id: frame.id, ok, result, error }) + '\n';
58
+ if (socket.destroyed)
59
+ return;
60
+ if (Buffer.byteLength(payload) > TOOL_RESULT_BYTES) {
61
+ socket.destroy();
62
+ return;
63
+ }
64
+ socket.end(payload);
65
+ };
66
+ if (pending >= 32) {
67
+ reply(false, {}, 'Tools keeper is busy');
68
+ return;
69
+ }
70
+ pending++;
71
+ void (async () => {
72
+ try {
73
+ const result = frame.method === 'keeper.ping' ? { pid: process.pid, identity: config.identity } : await core.dispatch(frame.method, frame.params);
74
+ reply(true, result, null);
75
+ }
76
+ catch (error) {
77
+ reply(false, {}, (error instanceof Error ? error.message : 'Workstation tool failed').slice(0, 1024));
78
+ }
79
+ finally {
80
+ pending--;
81
+ }
82
+ })();
83
+ });
84
+ socket.on('end', () => { if (!received)
85
+ socket.destroy(); });
86
+ });
87
+ try {
88
+ // A private umask applies before bind; chmod after listen alone would leave a brief broad mode.
89
+ process.umask(0o077);
90
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(config.socket, resolve); });
91
+ await fs.chmod(config.socket, 0o600);
92
+ }
93
+ catch (error) {
94
+ await core.dispose();
95
+ throw error;
96
+ }
97
+ return { async stop() {
98
+ for (const socket of sockets)
99
+ socket.destroy();
100
+ await new Promise(resolve => server.close(() => resolve()));
101
+ await core.dispose();
102
+ await fs.unlink(config.socket).catch(() => { });
103
+ await fs.unlink(pidFile).catch(() => { });
104
+ } };
105
+ }
106
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
107
+ let config;
108
+ try {
109
+ config = JSON.parse(Buffer.from(process.argv[2] ?? '', 'base64').toString('utf8'));
110
+ }
111
+ catch { /* below */ }
112
+ // Anti-resurrection (spec §9): only the browser keeper still starts from this entry. A terminal-kind config (kind 'terminal',
113
+ // or no kind from older CLIs) comes from a legacy tools keeper unit restarting (`Restart=on-failure`) or an old launcher:
114
+ // keeper-terminal owns PTYs now and the files host owns files, so exit cleanly (0, which systemd does not restart) before
115
+ // touching the socket, the pid manifest or any state.
116
+ if (config?.kind !== 'browser')
117
+ process.exit(0);
118
+ const keeper = await runKeeper(config);
119
+ let stopping = false;
120
+ const stop = () => { if (stopping)
121
+ return; stopping = true; void keeper.stop().finally(() => process.exit(0)); };
122
+ process.on('SIGTERM', stop);
123
+ process.on('SIGINT', stop);
124
+ }
@@ -0,0 +1,55 @@
1
+ import { isFilesToolMethod } from '../files/adapters/toolRequests.js';
2
+ import { keys, validateRequest } from './validation.js';
3
+ export function routeTool(method) {
4
+ if (method === 'tools.info')
5
+ return 'info';
6
+ if (method === 'browser.sharedText')
7
+ return 'shared-text';
8
+ if (isFilesToolMethod(method))
9
+ return 'files';
10
+ if (method.startsWith('terminal.'))
11
+ return 'terminal';
12
+ if (method.startsWith('browser.'))
13
+ return 'browser';
14
+ return 'unsupported';
15
+ }
16
+ /** A probe is answered even while the workstation drains and is not work a restart would interrupt. */
17
+ export function isProbeTool(method) { return routeTool(method) === 'info'; }
18
+ /**
19
+ * Answered while draining and not counted as a shell task: probes, and `terminal.*` (spec §4.1: a PTY lives in its keeper, not in
20
+ * the daemon, so a restart does not interrupt it; counting every poll made the activity flicker).
21
+ */
22
+ export function isUnaccountedTool(method) {
23
+ const route = routeTool(method);
24
+ return route === 'info' || route === 'terminal';
25
+ }
26
+ export class WorkstationTools {
27
+ deps;
28
+ constructor(deps) {
29
+ this.deps = deps;
30
+ }
31
+ /** One `tool_request` (not `browser.sharedText`). Rejects with the `tool_result.error` text. */
32
+ async request(method, params) {
33
+ // Validated at the daemon boundary as well as by whoever runs it.
34
+ validateRequest(method, params);
35
+ switch (routeTool(method)) {
36
+ case 'info':
37
+ keys(params, []);
38
+ return this.deps.info();
39
+ case 'files': return this.deps.files.tool(method, params);
40
+ case 'terminal':
41
+ if (!this.deps.terminal)
42
+ throw new Error('Workstation terminal keeper is not configured');
43
+ return this.deps.terminal(method, params);
44
+ case 'browser':
45
+ if (!this.deps.browser)
46
+ throw new Error('Workstation browser keeper is not configured');
47
+ return this.deps.browser(method, params);
48
+ default: throw new Error('Unsupported workstation tool');
49
+ }
50
+ }
51
+ /** One `file_*_request`; never rejects (errors are result frames). */
52
+ runFrame(request, signal) {
53
+ return this.deps.files.runFrame(request, signal);
54
+ }
55
+ }
@@ -0,0 +1,222 @@
1
+ import { accessSync, constants, statSync } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { CREATE_KEYS, PTY_OPERATIONS } from '../keepers/terminal/pty.js';
5
+ import { PTY_SESSION_LIMIT } from '../keepers/terminal/protocol.js';
6
+ import { TERMINAL_NOT_FOUND } from '../keepers/legacy.js';
7
+ import { identifier, integer, keys } from './validation.js';
8
+ const LEGACY = 'legacy';
9
+ /** The single-keeper form of {@link KitGenerations}: one generation named `kit`, running when it answers a hello. */
10
+ function singleKit(kit) {
11
+ return {
12
+ servingGeneration: async () => ({ gen: 'kit', client: kit }),
13
+ running: async () => [{ gen: 'kit', client: kit }],
14
+ };
15
+ }
16
+ export class TerminalRouter {
17
+ deps;
18
+ owners = new Map();
19
+ kits;
20
+ /** Clients of the generations seen in the last listing, by generation. */
21
+ clients = new Map();
22
+ constructor(deps) {
23
+ this.deps = deps;
24
+ if (!deps.kits && !deps.kit)
25
+ throw new Error('TerminalRouter needs keeper-terminal');
26
+ this.kits = deps.kits ?? singleKit(deps.kit);
27
+ }
28
+ /** The current owner map (tests and diagnostics): `legacy`, or a generation (`kit` with a single keeper-terminal). */
29
+ ownerOf(id) { return this.owners.get(id); }
30
+ async request(method, params) {
31
+ const op = method.startsWith('terminal.') ? method.slice('terminal.'.length) : '';
32
+ if (op === 'list') {
33
+ keys(params, []);
34
+ return { sessions: await this.list() };
35
+ }
36
+ if (op === 'create')
37
+ return { ...await this.create(params) };
38
+ if (!PTY_OPERATIONS.has(op))
39
+ throw new Error('Unsupported workstation tool');
40
+ const id = identifier(params, 'id');
41
+ const owner = await this.resolveOwner(id);
42
+ if (!owner)
43
+ throw new Error(TERMINAL_NOT_FOUND);
44
+ const kit = owner === LEGACY ? undefined : this.clients.get(owner);
45
+ if (owner !== LEGACY && !kit)
46
+ throw new Error(TERMINAL_NOT_FOUND);
47
+ try {
48
+ const result = owner === LEGACY
49
+ ? await this.deps.legacy.request(method, params)
50
+ : { ...await kit[op].call(kit, params) };
51
+ if (op === 'close')
52
+ this.owners.delete(id);
53
+ return result;
54
+ }
55
+ catch (error) {
56
+ // The owner no longer has it (closed elsewhere, keeper restarted): forget it; the next request looks it up again.
57
+ if (error instanceof Error && error.message === TERMINAL_NOT_FOUND)
58
+ this.owners.delete(id);
59
+ throw error;
60
+ }
61
+ }
62
+ async list() {
63
+ const { legacy, kits } = await this.refresh();
64
+ const seen = new Set((legacy ?? []).map((session) => session.id));
65
+ const out = [...(legacy ?? [])];
66
+ for (const sessions of kits.values())
67
+ for (const session of sessions ?? [])
68
+ if (!seen.has(session.id)) {
69
+ seen.add(session.id);
70
+ out.push(session);
71
+ }
72
+ return out;
73
+ }
74
+ /** Lists every side once (keeper-terminal generations only if running) and refreshes the owners of every side that answered. */
75
+ async refresh() {
76
+ const [legacy, kits] = await Promise.all([this.legacySessions(), this.kitRefresh()]);
77
+ if (legacy)
78
+ this.replaceOwners(LEGACY, legacy);
79
+ else if (!this.deps.legacy?.live)
80
+ this.replaceOwners(LEGACY, []);
81
+ return { legacy, kits };
82
+ }
83
+ /**
84
+ * Lists every running keeper-terminal generation (never starts one) and refreshes their owners: a generation that answered gets
85
+ * exactly its sessions; one that did not answer keeps its owners; one that is no longer running loses them (its PTYs are gone).
86
+ */
87
+ async kitRefresh() {
88
+ const listed = new Map();
89
+ let running;
90
+ try {
91
+ running = await this.kits.running();
92
+ }
93
+ catch {
94
+ return listed;
95
+ }
96
+ await Promise.all(running.map(async ({ gen, client }) => {
97
+ this.clients.set(gen, client);
98
+ listed.set(gen, await this.sessionsOf(client));
99
+ }));
100
+ for (const [gen, sessions] of listed)
101
+ if (sessions)
102
+ this.replaceOwners(gen, sessions);
103
+ for (const [id, owner] of [...this.owners])
104
+ if (owner !== LEGACY && !listed.has(owner))
105
+ this.owners.delete(id);
106
+ for (const gen of [...this.clients.keys()])
107
+ if (!listed.has(gen))
108
+ this.clients.delete(gen);
109
+ return listed;
110
+ }
111
+ replaceOwners(side, sessions) {
112
+ for (const [id, owner] of this.owners)
113
+ if (owner === side)
114
+ this.owners.delete(id);
115
+ for (const session of sessions)
116
+ if (side === LEGACY || !this.owners.has(session.id))
117
+ this.owners.set(session.id, side);
118
+ }
119
+ /** The legacy keeper's sessions; `sessions()` probes it first (read-only), so a daemon that just started finds it too. */
120
+ async legacySessions() {
121
+ return this.deps.legacy ? this.deps.legacy.sessions() : null;
122
+ }
123
+ /** One generation's sessions if it is already running; null when it is not (or did not answer): nothing is started here. */
124
+ async sessionsOf(client) {
125
+ try {
126
+ if (!await client.runningHello())
127
+ return null;
128
+ return (await client.list()).sessions;
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ }
134
+ async resolveOwner(id) {
135
+ const known = this.owners.get(id);
136
+ if ((known !== undefined && known !== LEGACY && this.clients.has(known)) || (known === LEGACY && this.deps.legacy?.live))
137
+ return known;
138
+ await this.refresh();
139
+ return this.owners.get(id);
140
+ }
141
+ async create(params) {
142
+ // The legacy keeper's checks, wording and order (tools/terminal.ts at the merge-base: id, clientId, keys, cols, rows, name,
143
+ // then existing, limit, cwd), all before the cwd is resolved.
144
+ const id = identifier(params, 'id');
145
+ identifier(params, 'clientId');
146
+ keys(params, [...CREATE_KEYS]);
147
+ integer(params, 'cols', 20, 300, 100);
148
+ integer(params, 'rows', 5, 100, 30);
149
+ const name = params.name;
150
+ if (name !== undefined && (typeof name !== 'string' || !name.length || name.length > 128 || /[\x00-\x1f]/.test(name)))
151
+ throw new Error('Invalid terminal name');
152
+ const legacy = this.deps.legacy;
153
+ const knownOwner = this.owners.get(id);
154
+ if (legacy && (knownOwner === undefined || knownOwner === LEGACY)) {
155
+ // The legacy keeper never gains a session after the upgrade (creates go to keeper-terminal), so the ids of its first
156
+ // successful list answer "is this a retry of one of its sessions?" for good — even while it is busy or hung.
157
+ if (legacy.knownIds === null) {
158
+ const sessions = await legacy.sessions(); // bounded; null at once when it is busy or absent
159
+ if (sessions)
160
+ this.replaceOwners(LEGACY, sessions);
161
+ }
162
+ if (legacy.live && legacy.knownIds?.has(id))
163
+ return legacy.request('terminal.create', params);
164
+ }
165
+ // Duplicate and limit first (the legacy order), against the keeper-terminal generations already running: an idempotent create
166
+ // (in whichever generation has the id) costs no files-host round trip. keeper-terminal re-checks both when it really creates.
167
+ const listed = await this.kitRefresh();
168
+ for (const [gen, sessions] of listed) {
169
+ const existing = sessions?.find((session) => session.id === id);
170
+ if (existing) {
171
+ this.owners.set(id, gen);
172
+ return existing;
173
+ }
174
+ }
175
+ const serving = await this.kits.servingGeneration();
176
+ const running = listed.get(serving.gen);
177
+ if (running && running.length >= PTY_SESSION_LIMIT)
178
+ throw new Error('Terminal session limit exceeded');
179
+ if (params.cwd !== undefined && typeof params.cwd !== 'string')
180
+ throw new Error('Invalid cwd');
181
+ const launch = await this.deps.launch(params.cwd);
182
+ const created = await serving.client.create({ ...params, ...launch });
183
+ this.clients.set(serving.gen, serving.client);
184
+ this.owners.set(id, serving.gen);
185
+ return created;
186
+ }
187
+ }
188
+ /**
189
+ * The shell a new PTY runs (as a login shell, `-l`): what the legacy keeper chose (`$SHELL`, else the account's shell, else
190
+ * `/bin/sh`), restricted to an absolute path to an executable file, which keeper-terminal requires.
191
+ */
192
+ export function terminalShell(env = process.env) {
193
+ let account;
194
+ try {
195
+ account = os.userInfo().shell ?? undefined;
196
+ }
197
+ catch { /* no passwd entry (some containers) */ }
198
+ for (const candidate of [env.SHELL, account, '/bin/sh']) {
199
+ if (!candidate || !path.isAbsolute(candidate))
200
+ continue;
201
+ try {
202
+ if (statSync(candidate).isFile()) {
203
+ accessSync(candidate, constants.X_OK);
204
+ return candidate;
205
+ }
206
+ }
207
+ catch { /* next */ }
208
+ }
209
+ return '/bin/sh';
210
+ }
211
+ /**
212
+ * The environment a new PTY gets: the daemon's own (the legacy keeper gave its own process.env, which was the daemon's when it
213
+ * was spawned), never the access token (commands/workstation.ts already drops it from the daemon; dropped again here so a
214
+ * future spawn path cannot leak it). keeper-terminal adds TERM and COLORTERM.
215
+ */
216
+ export function terminalEnv(env = process.env) {
217
+ const out = {};
218
+ for (const [name, value] of Object.entries(env))
219
+ if (value !== undefined && name !== 'AXOM_ACCESS_TOKEN')
220
+ out[name] = value;
221
+ return out;
222
+ }
@@ -0,0 +1,86 @@
1
+ /** Bounds apply on both the encrypted channel and private keeper IPC. */
2
+ export const TOOL_REQUEST_BYTES = 128 * 1024;
3
+ export const TOOL_RESULT_BYTES = 2 * 1024 * 1024;
4
+ export const CHUNK_BYTES = 64 * 1024;
5
+ export const TOOL_METHODS = new Set([
6
+ 'browser.list', 'browser.create', 'browser.attach', 'browser.frame', 'browser.navigate', 'browser.input', 'browser.takeover', 'browser.detach', 'browser.close',
7
+ 'tools.info', 'terminal.list', 'terminal.create', 'terminal.attach', 'terminal.poll',
8
+ 'terminal.write', 'terminal.resize', 'terminal.takeover', 'terminal.detach', 'terminal.close',
9
+ 'files.media.prepare', 'files.media.read', 'files.media.cancel',
10
+ 'files.media.open', 'files.media.status', 'files.media.seek', 'files.media.interest', 'files.media.object', 'files.media.release',
11
+ 'files.list', 'files.find', 'files.read', 'files.mkdir', 'files.rename', 'files.delete',
12
+ 'files.upload.begin', 'files.upload.chunk', 'files.upload.finish', 'files.upload.cancel',
13
+ ]);
14
+ export function object(value) {
15
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
16
+ }
17
+ export function keys(params, allowed) {
18
+ if (Object.keys(params).some(key => !allowed.includes(key)))
19
+ throw new Error('Unexpected tool parameter');
20
+ }
21
+ export function string(params, name, max = 4096, fallback) {
22
+ const value = params[name] === undefined ? fallback : params[name];
23
+ if (typeof value !== 'string' || !value.length || value.includes('\0') || value.length > max)
24
+ throw new Error(`Invalid ${name}`);
25
+ return value;
26
+ }
27
+ export function identifier(params, name) {
28
+ const value = string(params, name, 128);
29
+ if (!/^[A-Za-z0-9_.:-]+$/.test(value))
30
+ throw new Error(`Invalid ${name}`);
31
+ return value;
32
+ }
33
+ export function integer(params, name, min, max, fallback) {
34
+ const value = params[name] === undefined ? fallback : params[name];
35
+ if (!Number.isSafeInteger(value) || value < min || value > max)
36
+ throw new Error(`Invalid ${name}`);
37
+ return value;
38
+ }
39
+ export function base64(value) {
40
+ if (typeof value !== 'string' || value.length > Math.ceil(CHUNK_BYTES / 3) * 4 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value))
41
+ throw new Error('Invalid upload chunk');
42
+ const bytes = Buffer.from(value, 'base64');
43
+ if (bytes.length > CHUNK_BYTES || bytes.toString('base64') !== value)
44
+ throw new Error('Invalid upload chunk');
45
+ return bytes;
46
+ }
47
+ export function validateRequest(method, params) {
48
+ if (typeof method !== 'string' || !TOOL_METHODS.has(method))
49
+ throw new Error('Unsupported workstation tool');
50
+ if (!object(params) || Buffer.byteLength(JSON.stringify(params)) > TOOL_REQUEST_BYTES)
51
+ throw new Error('Invalid or oversized tool parameters');
52
+ const shape = MEDIA_SHAPES[method];
53
+ if (shape) {
54
+ keys(params, [...shape, 'readOnly', 'filesystem']);
55
+ string(params, 'path');
56
+ identifier(params, 'consumerId');
57
+ if (method !== 'files.media.open' && !/^[a-f0-9]{64}$/.test(string(params, 'version', 64)))
58
+ throw new Error('Invalid version');
59
+ for (const flag of ['readOnly', 'filesystem', 'hls', 'playing']) {
60
+ if (params[flag] !== undefined && typeof params[flag] !== 'boolean')
61
+ throw new Error(`Invalid ${flag}`);
62
+ }
63
+ if (method === 'files.media.open' && typeof params.hls !== 'boolean')
64
+ throw new Error('Invalid hls');
65
+ if (method === 'files.media.seek' || method === 'files.media.interest')
66
+ integer(params, 'positionMs', 0, 21_600_000);
67
+ if (method === 'files.media.seek')
68
+ integer(params, 'revision', 0, Number.MAX_SAFE_INTEGER);
69
+ if (method === 'files.media.interest' && typeof params.playing !== 'boolean')
70
+ throw new Error('Invalid playing');
71
+ if (method === 'files.media.object') {
72
+ if (!/^[A-Za-z0-9_-]+$/.test(string(params, 'objectId', 128)))
73
+ throw new Error('Invalid objectId');
74
+ integer(params, 'offset', 0, Number.MAX_SAFE_INTEGER);
75
+ integer(params, 'length', 1, 256 * 1024);
76
+ }
77
+ }
78
+ }
79
+ const MEDIA_SHAPES = {
80
+ 'files.media.open': ['path', 'consumerId', 'hls'],
81
+ 'files.media.status': ['path', 'version', 'consumerId'],
82
+ 'files.media.seek': ['path', 'version', 'consumerId', 'positionMs', 'revision'],
83
+ 'files.media.interest': ['path', 'version', 'consumerId', 'positionMs', 'playing'],
84
+ 'files.media.object': ['path', 'version', 'consumerId', 'objectId', 'offset', 'length'],
85
+ 'files.media.release': ['path', 'version', 'consumerId'],
86
+ };
@@ -93,6 +93,14 @@ export class UpdateCoordinator {
93
93
  await this.transition({ ...this.status, phase: 'SUCCEEDED' });
94
94
  }
95
95
  async ensureInitialized() {
96
+ // Concurrent callers must share one in-flight attempt (assigned synchronously below, before
97
+ // any `await`), but a *rejected* attempt must not stay memoized: a transient failure here --
98
+ // the state dir being unwritable while the daemon died mid-update, say -- would otherwise wedge
99
+ // this coordinator forever, since every future `handle`/`snapshotAsync`/`managementReady` call
100
+ // awaits the same rejected promise and never gets a chance to retry. Clearing `this.initialized`
101
+ // only happens inside this same promise's own `.catch`, i.e. strictly after it has settled, so
102
+ // callers that started awaiting it while it was still in flight all observe the same outcome;
103
+ // only a call that arrives *after* it has rejected gets to start a fresh attempt.
96
104
  if (!this.initialized)
97
105
  this.initialized = this.options.state.load().then(async (saved) => {
98
106
  if (!saved)
@@ -113,7 +121,28 @@ export class UpdateCoordinator {
113
121
  catch { /* a broken logger must not fail a boot */ }
114
122
  return;
115
123
  }
124
+ // Any other non-terminal phase was written by a process that no longer exists: the execute()
125
+ // that owned it died with the previous daemon, so nothing will ever advance it. Adopting it
126
+ // verbatim makes handle() answer every later command with UPDATE_IN_PROGRESS while the
127
+ // heartbeat repeats the stale phase to the server forever -- the same permanent wedge the
128
+ // RESTARTING branch above exists to prevent, reachable by killing the daemon at any earlier
129
+ // point (a laptop shut mid-download, an operator's restart mid-drain). RESTARTING is the one
130
+ // deliberate exception in every lifecycle: the successor is *expected* to adopt it and let
131
+ // management_ready promote it (or the armed watchdog fail it).
132
+ if (!isTerminalPhase(saved.phase) && saved.phase !== 'RESTARTING') {
133
+ this.status = { ...saved, phase: 'FAILED', error: `process restarted during '${saved.phase}'; the update never completed` };
134
+ await this.options.state.save(this.status);
135
+ // Reported like any other transition, for the same reason the container branch is.
136
+ try {
137
+ this.options.onPhase?.(this.status);
138
+ }
139
+ catch { /* a broken logger must not fail a boot */ }
140
+ return;
141
+ }
116
142
  this.status = saved;
143
+ }).catch((error) => {
144
+ this.initialized = null;
145
+ throw error;
117
146
  });
118
147
  await this.initialized;
119
148
  }
@@ -224,6 +253,9 @@ export class UpdateCoordinator {
224
253
  this.options.tasks.accepting = true;
225
254
  return this.transition(this.base(command, 'SUCCEEDED'));
226
255
  }
256
+ // Keeper-hosted agent sessions outlive this process: detach them first (head checkpoint, adapter untouched — no
257
+ // `session/cancel`, no failed turn), so the next daemon adopts them where they are (spec §5.5.5).
258
+ await this.options.tasks.detachDurableSessions();
227
259
  await this.options.restart();
228
260
  return restarting;
229
261
  }
@@ -1,5 +1,6 @@
1
1
  import { spawn as nodeSpawn } from 'node:child_process';
2
2
  import { redactPaths, redactSecrets } from './redact.js';
3
+ import { verifyOptionalPty } from './watchdog.js';
3
4
  const STDERR_LIMIT = 4096;
4
5
  function absolute(value, field) {
5
6
  if (!value.startsWith('/') || /[\u0000-\u001f\u007f]/.test(value))
@@ -47,6 +48,7 @@ export class NpmInstaller {
47
48
  reject(new Error(`npm install failed: ${sanitizeNpmError(stderr) || `exit ${code ?? signal ?? 'unknown'}`}`));
48
49
  });
49
50
  });
51
+ await verifyOptionalPty(this.options.npmPrefix, this.spawn);
50
52
  }
51
53
  /** Packs the installed package locally; `--json` gives npm's own name/version validation input. */
52
54
  async pack(packageRoot, destination) {
@@ -0,0 +1,45 @@
1
+ import { createRequire } from 'node:module';
2
+ import path from 'node:path';
3
+ import { PTY_UNAVAILABLE_ERROR } from '../keepers/terminal/protocol.js';
4
+ const require = createRequire(import.meta.url);
5
+ /**
6
+ * Says once, at daemon start, when the installed CLI's optional `node-pty` does not load. There is
7
+ * no repair to attempt: the pinned node-pty ships prebuilds for every desktop target, so a load
8
+ * failure means an unsupported platform (musl, an older glibc), not a missing build step.
9
+ */
10
+ export async function preflightOptionalPty(context, deps = {}) {
11
+ const installedRoot = path.join(context.npmPrefix, process.platform === 'win32' ? 'node_modules' : 'lib/node_modules', '@platless/axom-cli');
12
+ // Source checkouts and custom unpacked installations load their own node-pty; nothing to report.
13
+ if (path.resolve(installedRoot) !== path.resolve(context.packageRoot))
14
+ return;
15
+ const addon = path.join(installedRoot, 'node_modules/node-pty');
16
+ try {
17
+ (deps.load ?? require)(addon);
18
+ }
19
+ catch (error) {
20
+ // An explicitly omitted optional dependency is not a problem worth a warning.
21
+ if (error.code === 'MODULE_NOT_FOUND' && String(error.message).includes(addon))
22
+ return;
23
+ (deps.warn ?? console.warn)('[daemon] Optional node-pty does not load on this platform; the terminal tool stays unavailable.');
24
+ }
25
+ }
26
+ export { PTY_UNAVAILABLE_ERROR };
27
+ /**
28
+ * A cached load check of the node-pty copy the terminal would use (the same `require('node-pty')` from this package),
29
+ * done at most once per daemon. Answering `tools.info` must never start a keeper just to learn this.
30
+ */
31
+ export function cachedPtyStatus(load = () => require('node-pty')) {
32
+ let status;
33
+ return () => {
34
+ if (status)
35
+ return status;
36
+ try {
37
+ load();
38
+ status = { terminal: true };
39
+ }
40
+ catch {
41
+ status = { terminal: false, terminalError: PTY_UNAVAILABLE_ERROR };
42
+ }
43
+ return status;
44
+ };
45
+ }