@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
@@ -1,14 +1,15 @@
1
1
  import { deriveSessionKey, randomBytes } from '../crypto.js';
2
2
  import { connectEncryptedChannel } from '../connection/encryptedChannel.js';
3
3
  import { CLIENT_PACKAGE_NAME, CLIENT_VERSION } from '../version.js';
4
- import { decodeManagementFrame, encodeManagementFrame, MANAGEMENT_WIRE_DRAINING_VERSION, MANAGEMENT_WIRE_LOAD_AVERAGES_VERSION, MANAGEMENT_WIRE_METRICS_VERSION, MANAGEMENT_WIRE_MIN_VERSION, MANAGEMENT_WIRE_PRESSURE_VERSION, MANAGEMENT_WIRE_VERSION, } from '../protocol/management.js';
5
- import { bridgesForWire } from '../protocol/common.js';
4
+ import { decodeManagementFrame, encodeManagementFrame, MANAGEMENT_WIRE_DRAINING_VERSION, MANAGEMENT_WIRE_GPU_VERSION, MANAGEMENT_WIRE_KEEPERS_VERSION, MANAGEMENT_WIRE_LOAD_AVERAGES_VERSION, MANAGEMENT_WIRE_METRICS_VERSION, MANAGEMENT_WIRE_MIN_VERSION, MANAGEMENT_WIRE_PRESSURE_VERSION, MANAGEMENT_WIRE_VERSION, } from '../protocol/management.js';
5
+ import { bridgesForWire, helloRoster } from '../protocol/common.js';
6
6
  import { AGENT_WIRE_MIN_VERSION, AGENT_WIRE_VERSION } from '../protocol/agent.js';
7
7
  import { osInfo } from '../osinfo.js';
8
8
  import { workstationChannelUrl } from './url.js';
9
9
  const protocols = {
10
10
  MANAGEMENT: { min: MANAGEMENT_WIRE_MIN_VERSION, max: MANAGEMENT_WIRE_VERSION },
11
- COMPUTER: { min: 1, max: 1 }, AGENT: { min: AGENT_WIRE_MIN_VERSION, max: AGENT_WIRE_VERSION },
11
+ // COMPUTER v5: durable exec frames (exec_started/attach/status/ack, offsets on exec_output; spec §4.3).
12
+ COMPUTER: { min: 1, max: 5 }, AGENT: { min: AGENT_WIRE_MIN_VERSION, max: AGENT_WIRE_VERSION },
12
13
  };
13
14
  /** Exported so tests can assert the wire frame's `clientPackage` without opening a socket. */
14
15
  export function buildHelloFrame(options, clientNonce) {
@@ -24,7 +25,7 @@ export function buildHelloFrame(options, clientNonce) {
24
25
  */
25
26
  function helloBridges(bridges) {
26
27
  const roster = bridges?.();
27
- return roster === undefined ? undefined : bridgesForWire(roster, 'management_hello');
28
+ return roster === undefined ? undefined : bridgesForWire(helloRoster(roster), 'management_hello');
28
29
  }
29
30
  /** Exported so the roster this answer carries can be asserted without driving a whole channel. */
30
31
  export function buildScanResultFrame(commandId, bridges) {
@@ -57,6 +58,10 @@ export function metricsForManagementWire(metrics, serverManagementWire) {
57
58
  if (!metrics)
58
59
  return metrics;
59
60
  const selectedWire = serverManagementWire ?? 0;
61
+ if (selectedWire < MANAGEMENT_WIRE_GPU_VERSION && metrics.gpus !== undefined) {
62
+ const { gpus: _gpus, ...withoutGpus } = metrics;
63
+ return metricsForManagementWire(withoutGpus, selectedWire);
64
+ }
60
65
  if (selectedWire >= MANAGEMENT_WIRE_LOAD_AVERAGES_VERSION) {
61
66
  const { cpuLoadNormalized: _cpuLoadNormalized, ...rawLoad } = metrics;
62
67
  return rawLoad;
@@ -98,6 +103,51 @@ export function dispatchUpdateCommand(channel, command, onUpdate, serverManageme
98
103
  })
99
104
  .catch(() => { });
100
105
  }
106
+ /**
107
+ * The most a heartbeat waits for its `keepers` sample. The channel builds the next heartbeat only after this one went out, so a sample
108
+ * that hangs (a wedged `ps`, a stuck file read) must never hold the frame: past this it goes out with `keepers: null`.
109
+ */
110
+ export const KEEPERS_SAMPLE_CAP_MS = 1500;
111
+ /**
112
+ * Heartbeat `keepers` for the negotiated wire: absent below 8 (an older server does not know the field), the sampled list from 8 on,
113
+ * `null` when sampling failed or did not answer within `capMs`. Never rejects, never waits longer than `capMs`. Exported for tests.
114
+ */
115
+ export async function keepersForManagementWire(sample, serverManagementWire, capMs = KEEPERS_SAMPLE_CAP_MS) {
116
+ if (!sample || (serverManagementWire ?? 0) < MANAGEMENT_WIRE_KEEPERS_VERSION)
117
+ return {};
118
+ let timer;
119
+ const capped = new Promise((resolve) => { timer = setTimeout(() => resolve(null), capMs); timer.unref?.(); });
120
+ try {
121
+ const sampled = Promise.resolve().then(sample);
122
+ sampled.catch(() => undefined); // a late rejection after the cap must not be unhandled
123
+ return { keepers: await Promise.race([sampled, capped]) };
124
+ }
125
+ catch {
126
+ return { keepers: null };
127
+ }
128
+ finally {
129
+ clearTimeout(timer);
130
+ }
131
+ }
132
+ /**
133
+ * Runs one `keeper_control` and sends its result if the channel is still open. Detached from the frame loop (a replace-now stops
134
+ * keeper processes, which takes seconds). Exported for tests.
135
+ */
136
+ export function dispatchKeeperControl(channel, frame, onKeeperControl) {
137
+ return onKeeperControl(frame)
138
+ .catch((error) => ({
139
+ t: 'keeper_control_result', commandId: frame.commandId, kind: frame.kind, action: frame.action, ok: false, affected: [],
140
+ error: error instanceof Error ? error.message : String(error),
141
+ }))
142
+ .then((result) => {
143
+ if (channel.state !== 'OPEN')
144
+ return;
145
+ try {
146
+ channel.send(result);
147
+ }
148
+ catch { /* the channel closed meanwhile; the server asks again with the same commandId */ }
149
+ });
150
+ }
101
151
  /**
102
152
  * The server nonce to derive this session's key from.
103
153
  *
@@ -125,9 +175,29 @@ export class ManagementChannel {
125
175
  options;
126
176
  /** Undefined until this connection receives management_ready; reset for every reconnect. */
127
177
  serverManagementWire;
178
+ /** The open channel of the current connection, for frames this client sends unprompted ({@link pushRoster}). */
179
+ channel;
128
180
  constructor(options) {
129
181
  this.options = options;
130
182
  }
183
+ /**
184
+ * Tell the server about a roster that changed without a scan asking (adapter builds moved by Axom,
185
+ * spec §6.1): the same `external_agents_scan_result` frame a scan answers with, under a client-made
186
+ * command id no scan is waiting on — the server records the roster and completes nothing. `false`
187
+ * when there is no open connection; the next `management_hello` carries {@link ManagementChannelOptions.bridges} then.
188
+ */
189
+ pushRoster(bridges) {
190
+ const channel = this.channel;
191
+ if (!channel || channel.state !== 'OPEN')
192
+ return false;
193
+ try {
194
+ channel.send(buildScanResultFrame(`roster-push-${randomBytes(8).toString('hex')}`, bridges));
195
+ return true;
196
+ }
197
+ catch {
198
+ return false;
199
+ }
200
+ }
131
201
  async connect(signal) {
132
202
  this.serverManagementWire = undefined;
133
203
  const nonce = randomBytes(16);
@@ -161,6 +231,10 @@ export class ManagementChannel {
161
231
  // is never driven by the local browser/device-authorization flow.
162
232
  await this.options.onReady?.({ generation: ready.generation, release: ready.release });
163
233
  const pending = new Map();
234
+ this.channel = channel;
235
+ void channel.closed.then(() => { if (this.channel === channel)
236
+ this.channel = undefined; }, () => { if (this.channel === channel)
237
+ this.channel = undefined; });
164
238
  void this.routeMessages(channel, pending);
165
239
  return {
166
240
  generation: ready.generation,
@@ -180,10 +254,11 @@ export class ManagementChannel {
180
254
  }),
181
255
  };
182
256
  }
183
- heartbeat(seq, sentAtEpochMs) {
184
- const snapshot = this.options.snapshot();
257
+ async heartbeat(seq, sentAtEpochMs) {
258
+ const snapshot = await this.options.snapshot();
185
259
  return {
186
260
  t: 'heartbeat', seq, sentAtEpochMs, clientVersion: CLIENT_VERSION,
261
+ ...(this.options.playgroundCwd ? { playgroundCwd: this.options.playgroundCwd } : {}),
187
262
  channels: {
188
263
  MANAGEMENT: snapshot.channels.management,
189
264
  COMPUTER: snapshot.channels.computer,
@@ -195,6 +270,7 @@ export class ManagementChannel {
195
270
  ? { metrics: metricsForManagementWire(this.options.metrics(), this.serverManagementWire) }
196
271
  : {}),
197
272
  ...(snapshot.update ? { update: statusForManagementWire(snapshot.update, this.serverManagementWire) } : {}),
273
+ ...await keepersForManagementWire(this.options.keepers, this.serverManagementWire, this.options.keepersCapMs),
198
274
  };
199
275
  }
200
276
  async nextReady(channel) {
@@ -217,6 +293,10 @@ export class ManagementChannel {
217
293
  void dispatchUpdateCommand(channel, frame, this.options.onUpdate, this.serverManagementWire);
218
294
  continue;
219
295
  }
296
+ if (frame.t === 'keeper_control' && this.options.onKeeperControl) {
297
+ void dispatchKeeperControl(channel, frame, this.options.onKeeperControl);
298
+ continue;
299
+ }
220
300
  if (frame.t === 'external_agents_scan' && this.options.onExternalAgentsScan) {
221
301
  channel.send(buildScanResultFrame(frame.commandId, await this.options.onExternalAgentsScan()));
222
302
  }
@@ -1,3 +1,4 @@
1
+ import { resolveBrowserBinding } from './desktop/binding.js';
1
2
  import { promises as fs } from 'node:fs';
2
3
  import os from 'node:os';
3
4
  import path from 'node:path';
@@ -33,7 +34,8 @@ export async function loadClientConfig(dir) {
33
34
  if (!parsed?.server || !parsed.workstationId) {
34
35
  throw new Error('Invalid workstation config: server and workstationId are required; run axom auth again');
35
36
  }
36
- return { server: parsed.server, workstationId: parsed.workstationId, ...(Array.isArray(parsed.workspaces) ? { workspaces: parsed.workspaces } : {}) };
37
+ const browserBinding = parsed.browserBinding ? resolveBrowserBinding(parsed.browserBinding, {}) : undefined;
38
+ return { ...(browserBinding ? { browserBinding } : {}), server: parsed.server, workstationId: parsed.workstationId, ...(Array.isArray(parsed.workspaces) ? { workspaces: parsed.workspaces } : {}) };
37
39
  }
38
40
  export async function writeKeys(dir, clientKeyB64, serverPubB64) {
39
41
  await fs.mkdir(dir, { recursive: true });
@@ -67,6 +67,8 @@ export function connectEncryptedChannel(options) {
67
67
  let openTimer;
68
68
  let nextSequence = 1;
69
69
  const outstandingAcks = new Set();
70
+ let lastReceivedAt = timers.now();
71
+ let maxHeartbeatLagMs = 0;
70
72
  let resolveClosed;
71
73
  let removeAbortListener = () => { };
72
74
  const closed = new Promise((resolve) => { resolveClosed = resolve; });
@@ -103,30 +105,87 @@ export function connectEncryptedChannel(options) {
103
105
  socket.send(crypto.seal(sessionKey, Buffer.from(options.encode(frame))));
104
106
  };
105
107
  const scheduleHeartbeat = () => {
108
+ const dueAt = timers.now() + heartbeatMs;
106
109
  heartbeatTimer = timers.setTimeout(() => {
107
110
  if (settled)
108
111
  return;
112
+ maxHeartbeatLagMs = Math.max(maxHeartbeatLagMs, timers.now() - dueAt);
109
113
  const sequence = nextSequence++;
110
114
  // Register before send: a synchronous server acknowledgement/work frame emitted from
111
- // `send` must observe and satisfy this precise liveness period.
115
+ // `send` must observe and satisfy this precise liveness period. This happens synchronously,
116
+ // before `options.heartbeat` is even awaited below, so that guarantee holds regardless of
117
+ // how long building the frame takes.
112
118
  outstandingAcks.add(sequence);
113
- try {
114
- send(options.heartbeat(sequence, timers.now()));
115
- }
116
- catch {
117
- outstandingAcks.delete(sequence);
118
- close();
119
- return;
120
- }
121
- // `ws.send` can synchronously deliver close/error through a test double or a transport
122
- // implementation. Never resurrect liveness state after that terminal callback.
123
- if (settled)
124
- return;
125
- if (outstandingAcks.size >= missedAcks) {
126
- close();
127
- return;
128
- }
129
- scheduleHeartbeat();
119
+ // `options.heartbeat` may be async (management's reconciles on-disk update state first), so
120
+ // the rest of this tick's work moves into a promise chain. There is deliberately no separate
121
+ // "did we close while the frame was being built" guard between the await and `send`: `settled`
122
+ // and `state` are only ever flipped together, synchronously, inside `finish()`, so `send`'s own
123
+ // `state !== 'OPEN'` check already covers every case such a guard could -- a second copy of the
124
+ // same condition here would be unreachable, untestable dead code, not defense in depth.
125
+ void (async () => {
126
+ let frame;
127
+ try {
128
+ frame = await options.heartbeat(sequence, timers.now());
129
+ }
130
+ catch (error) {
131
+ // Unlike a `send` failure (below), this can come from `options.heartbeat` itself doing
132
+ // real work before it returns a frame -- management's reconciles on-disk update state
133
+ // first -- so a rejection here can be the *only* sign a whole heartbeat cycle silently
134
+ // gave up. Log it rather than let the channel simply close with no trace of why.
135
+ console.error(`[encrypted-channel] heartbeat frame build failed, closing: ${error instanceof Error ? error.message : String(error)}`);
136
+ outstandingAcks.delete(sequence);
137
+ close();
138
+ return;
139
+ }
140
+ try {
141
+ send(frame);
142
+ }
143
+ catch (error) {
144
+ console.error(`[encrypted-channel] heartbeat send failed, closing: ${error instanceof Error ? error.message : String(error)}`);
145
+ outstandingAcks.delete(sequence);
146
+ close();
147
+ return;
148
+ }
149
+ // `ws.send` can synchronously deliver close/error through a test double or a transport
150
+ // implementation. Never resurrect liveness state after that terminal callback.
151
+ if (settled)
152
+ return;
153
+ if (outstandingAcks.size >= missedAcks) {
154
+ console.warn(`${new Date(timers.now()).toISOString()} [encrypted-channel] ${options.url.pathname} closing after ` +
155
+ `${outstandingAcks.size} unconfirmed heartbeats; silentMs=${Math.max(0, timers.now() - lastReceivedAt)}, ` +
156
+ `timerLagMs=${maxHeartbeatLagMs}`);
157
+ close();
158
+ return;
159
+ }
160
+ scheduleHeartbeat();
161
+ })().catch((error) => {
162
+ // Nothing above is expected to throw past its own try/catch except `close()`'s
163
+ // `socket.close()` (a caller-supplied `WebSocketLike`) and `scheduleHeartbeat`'s
164
+ // `timers.setTimeout` -- both call into code this module does not control. Before
165
+ // `options.heartbeat` could be async, the equivalent synchronous throw escaped this
166
+ // `setTimeout` callback as an uncaught exception, which Node turns into a process exit that
167
+ // the supervisor restarts: loud and recoverable. Now it is a promise rejection instead, and
168
+ // `daemon.ts` registers a process-wide `unhandledRejection` handler that deliberately logs
169
+ // and keeps the daemon alive -- exactly wrong here, because an uncaught throw from
170
+ // `scheduleHeartbeat` in particular leaves `settled` false and `state` stuck at `'OPEN'`
171
+ // with nothing left to ever arm another heartbeat: the channel reports healthy forever while
172
+ // it has silently stopped. Force it terminal by hand instead of trusting whichever call
173
+ // above almost got there. `finish()` alone is synchronous and safe to call unconditionally
174
+ // (and idempotent if `close()` already ran before throwing). It does call two
175
+ // caller-supplied functions -- `timers.clearTimeout` and `removeAbortListener` (which wraps
176
+ // `AbortSignal.removeEventListener` when `options.signal` was given) -- so it is not
177
+ // literally incapable of throwing; it is safe here because production wiring for both is a
178
+ // real timer API and a real `AbortSignal`, neither of which throws from clearing/removing.
179
+ // The socket close is only ever best-effort from here, specifically so
180
+ // this handler's own failure stays impossible even if the very thing that just threw was
181
+ // `socket.close()` itself.
182
+ console.error(`[encrypted-channel] heartbeat cycle failed unrecoverably: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
183
+ finish();
184
+ try {
185
+ socket.close();
186
+ }
187
+ catch { /* best-effort; already reported above */ }
188
+ });
130
189
  }, heartbeatMs);
131
190
  };
132
191
  return new Promise((resolve, reject) => {
@@ -183,7 +242,7 @@ export function connectEncryptedChannel(options) {
183
242
  throw new Error('Encrypted channel needs a session key or handshake');
184
243
  opened = true;
185
244
  if (options.initialFrame)
186
- send(options.initialFrame());
245
+ send(await options.initialFrame());
187
246
  // An initial proof may synchronously trigger a socket close/error. `opened` permits the
188
247
  // proof frame itself, but the public connection is not published until this guard passes.
189
248
  if (settled || connectionSettled)
@@ -193,6 +252,7 @@ export function connectEncryptedChannel(options) {
193
252
  disarmOpenTimer();
194
253
  resolve({
195
254
  send,
255
+ get bufferedAmount() { return socket.bufferedAmount ?? 0; },
196
256
  messages,
197
257
  close,
198
258
  closed,
@@ -226,14 +286,20 @@ export function connectEncryptedChannel(options) {
226
286
  // be the only server traffic while a task runs. Invalid frames take the catch path and never
227
287
  // get to satisfy liveness.
228
288
  outstandingAcks.clear();
289
+ lastReceivedAt = timers.now();
290
+ maxHeartbeatLagMs = 0;
229
291
  if (!acknowledged)
230
292
  messages.push(frame);
231
293
  }
232
294
  catch {
295
+ console.warn(`${new Date(timers.now()).toISOString()} [encrypted-channel] ${options.url.pathname} closing after an invalid encrypted frame`);
233
296
  close();
234
297
  }
235
298
  });
236
- socket.on('close', () => {
299
+ socket.on('close', (code, reason) => {
300
+ if (!settled)
301
+ console.warn(`${new Date(timers.now()).toISOString()} [encrypted-channel] ${options.url.pathname} peer closed; ` +
302
+ `code=${code ?? 'unknown'}, reason=${reason?.toString() || 'none'}`);
237
303
  if (!connectionSettled) {
238
304
  connectionSettled = true;
239
305
  finish();
@@ -242,9 +308,10 @@ export function connectEncryptedChannel(options) {
242
308
  }
243
309
  finish();
244
310
  });
245
- socket.on('error', () => {
311
+ socket.on('error', (error) => {
246
312
  if (settled)
247
313
  return;
314
+ console.warn(`${new Date(timers.now()).toISOString()} [encrypted-channel] ${options.url.pathname} socket error: ${error?.message ?? 'unknown'}`);
248
315
  const rejectOpening = !connectionSettled;
249
316
  // Mark this before close(): ws and deterministic fakes can emit close synchronously.
250
317
  if (rejectOpening)