@platless/axom-cli 0.3.13 → 0.6.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (320) hide show
  1. package/README.md +18 -5
  2. package/dist/axom-patch/cli/README.md +119 -9
  3. package/dist/axom-patch/cli/account-limits/README.md +27 -0
  4. package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
  5. package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
  6. package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
  7. package/dist/axom-patch/cli/account-limits/generate.py +123 -0
  8. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +81 -0
  9. package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
  10. package/dist/axom-patch/cli/claude-account-limits.json +37 -0
  11. package/dist/axom-patch/cli/codex-account-limits.json +37 -0
  12. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  13. package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
  14. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  15. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  16. package/dist/axom-patch/cli/nightly-task.md +194 -0
  17. package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
  18. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +5 -1
  19. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  20. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  21. package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
  22. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
  23. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1591 -0
  24. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +360 -0
  25. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -5
  26. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -7
  27. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  28. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  29. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  30. package/dist/axom-patch/cli/runtime.json +39 -15
  31. package/dist/axom-patch/cli/zcode-account-limits.json +30 -0
  32. package/dist/axom-patch/cli/zcode-models.json +37 -0
  33. package/dist/axom-patch/cli/zcode-subagent-subwork.json +72 -0
  34. package/dist/cli.js +4 -9
  35. package/dist/commands/knowledge.js +47 -0
  36. package/dist/commands/update.js +65 -3
  37. package/dist/commands/workstation.js +73 -7
  38. package/dist/install/claude/commands.js +13 -13
  39. package/dist/keepers/acp/keeper.mjs +3083 -0
  40. package/dist/keepers/acp/manifest.json +9 -0
  41. package/dist/keepers/terminal/keeper.mjs +8469 -0
  42. package/dist/keepers/terminal/manifest.json +9 -0
  43. package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  44. package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  45. package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
  46. package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
  47. package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
  48. package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
  49. package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
  50. package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
  51. package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
  52. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
  53. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
  54. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
  55. package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  56. package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
  57. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  58. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  59. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  60. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  61. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  62. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  63. package/dist/mcp.js +1 -1
  64. package/dist/native/darwin-arm64/axom-quic +0 -0
  65. package/dist/test/keeperKit.js +67 -0
  66. package/dist/workstation/acp/acpAgents.js +2192 -636
  67. package/dist/workstation/acp/acpClient.js +84 -495
  68. package/dist/workstation/acp/adoption.js +10 -396
  69. package/dist/workstation/acp/binaryInstall.js +100 -27
  70. package/dist/workstation/acp/builds/gc.js +245 -0
  71. package/dist/workstation/acp/builds/install/binary.js +28 -0
  72. package/dist/workstation/acp/builds/install/npm.js +324 -0
  73. package/dist/workstation/acp/builds/install/uv.js +254 -0
  74. package/dist/workstation/acp/builds/migrate.js +195 -0
  75. package/dist/workstation/acp/builds/model.js +128 -0
  76. package/dist/workstation/acp/builds/nodeGate.js +100 -0
  77. package/dist/workstation/acp/builds/store.js +352 -0
  78. package/dist/workstation/acp/commandTableProbe.js +68 -0
  79. package/dist/workstation/acp/configOptions.js +103 -1
  80. package/dist/workstation/acp/envSecrets.js +27 -0
  81. package/dist/workstation/acp/initializeResponse.js +290 -0
  82. package/dist/workstation/acp/installErrors.js +12 -2
  83. package/dist/workstation/acp/installProbe.js +1 -1
  84. package/dist/workstation/acp/managedPatches.js +239 -159
  85. package/dist/workstation/acp/managedRuntime.js +314 -0
  86. package/dist/workstation/acp/outbox.js +330 -0
  87. package/dist/workstation/acp/patchAssets.js +60 -3
  88. package/dist/workstation/acp/permission.js +42 -10
  89. package/dist/workstation/acp/processor/checkpoint.js +78 -0
  90. package/dist/workstation/acp/processor/commands.js +35 -0
  91. package/dist/workstation/acp/processor/control.js +282 -0
  92. package/dist/workstation/acp/processor/emit.js +26 -0
  93. package/dist/workstation/acp/processor/jsonrpc.js +172 -0
  94. package/dist/workstation/acp/processor/memoryLog.js +49 -0
  95. package/dist/workstation/acp/processor/origins.js +48 -0
  96. package/dist/workstation/acp/processor/output.js +67 -0
  97. package/dist/workstation/acp/processor/projection.js +42 -0
  98. package/dist/workstation/acp/processor/record.js +128 -0
  99. package/dist/workstation/acp/processor/runtime.js +105 -0
  100. package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
  101. package/dist/workstation/acp/processor/session.js +496 -0
  102. package/dist/workstation/acp/processor/state.js +67 -0
  103. package/dist/workstation/acp/processor/step.js +442 -0
  104. package/dist/workstation/acp/processor/test/fixtureLog.js +58 -0
  105. package/dist/workstation/acp/processor/test/harness.js +120 -0
  106. package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
  107. package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
  108. package/dist/workstation/acp/processor/transport/generations.js +76 -0
  109. package/dist/workstation/acp/processor/transport/keeper.js +130 -0
  110. package/dist/workstation/acp/processor/transport/local.js +269 -0
  111. package/dist/workstation/acp/processor/transport/types.js +78 -0
  112. package/dist/workstation/acp/processor/updates.js +390 -0
  113. package/dist/workstation/acp/processorSession.js +1348 -0
  114. package/dist/workstation/acp/registry/catalog.js +189 -41
  115. package/dist/workstation/acp/registry/launch.js +76 -119
  116. package/dist/workstation/acp/sessionUpdateMapper.js +18 -7
  117. package/dist/workstation/acp/steering.js +58 -0
  118. package/dist/workstation/acp/switching.js +111 -0
  119. package/dist/workstation/acp/terminalLogin.js +16 -42
  120. package/dist/workstation/acp/terminalLoginSpec.js +41 -0
  121. package/dist/workstation/acp/test/adapterFixtures.js +256 -0
  122. package/dist/workstation/acp/test/daemonLife.js +106 -0
  123. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  124. package/dist/workstation/acp/test/keeperWorld.js +295 -0
  125. package/dist/workstation/acp/test/memoryOutbox.js +55 -0
  126. package/dist/workstation/acp/test/raw/fixtures.js +196 -0
  127. package/dist/workstation/acp/test/raw/sanitize.js +344 -0
  128. package/dist/workstation/acp/test/raw/sources.js +135 -0
  129. package/dist/workstation/acp/test/realAdapter.js +331 -0
  130. package/dist/workstation/acp/test/scriptAgent.js +214 -0
  131. package/dist/workstation/acp/test/sessionFrames.js +42 -0
  132. package/dist/workstation/acp/timedCommand.js +4 -2
  133. package/dist/workstation/acp/uvxInstall.js +72 -122
  134. package/dist/workstation/agentFlow.js +103 -0
  135. package/dist/workstation/agentSessions.js +1289 -544
  136. package/dist/workstation/channels/agent.js +7 -5
  137. package/dist/workstation/channels/computer.js +69 -21
  138. package/dist/workstation/channels/computerExecs.js +200 -0
  139. package/dist/workstation/channels/management.js +172 -8
  140. package/dist/workstation/channels/url.js +1 -1
  141. package/dist/workstation/config.js +16 -4
  142. package/dist/workstation/connection/encryptedChannel.js +4 -1
  143. package/dist/workstation/control/bindingStore.js +200 -0
  144. package/dist/workstation/control/commands.js +124 -0
  145. package/dist/workstation/control/daemonIdentity.js +53 -0
  146. package/dist/workstation/control/enrollment.js +106 -0
  147. package/dist/workstation/control/inspect.js +71 -0
  148. package/dist/workstation/control/locks.js +63 -0
  149. package/dist/workstation/control/protocol.js +40 -0
  150. package/dist/workstation/control/restartGuard.js +46 -0
  151. package/dist/workstation/control/runtimeClient.js +97 -0
  152. package/dist/workstation/control/runtimeHost.js +148 -0
  153. package/dist/workstation/control/serverIdentity.js +35 -0
  154. package/dist/workstation/daemon.js +393 -12
  155. package/dist/workstation/daemonctl.js +103 -14
  156. package/dist/workstation/desktopControl/audio.js +99 -0
  157. package/dist/workstation/desktopControl/capabilities.js +92 -0
  158. package/dist/workstation/desktopControl/captureModes.js +9 -0
  159. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  160. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  161. package/dist/workstation/desktopControl/helperProcess.js +353 -0
  162. package/dist/workstation/desktopControl/lobby.js +129 -0
  163. package/dist/workstation/desktopControl/macProvider.js +155 -0
  164. package/dist/workstation/desktopControl/media.js +331 -0
  165. package/dist/workstation/desktopControl/nativeProvider.js +1018 -0
  166. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  167. package/dist/workstation/desktopControl/overview.js +58 -0
  168. package/dist/workstation/desktopControl/protocol.js +302 -0
  169. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  170. package/dist/workstation/desktopControl/rateController.js +226 -0
  171. package/dist/workstation/desktopControl/supervisor.js +1681 -0
  172. package/dist/workstation/desktopControl/transport.js +27 -0
  173. package/dist/workstation/desktopControl/wallpaper.js +78 -0
  174. package/dist/workstation/exec/attach.js +126 -0
  175. package/dist/workstation/exec/command.js +40 -0
  176. package/dist/workstation/exec/durable.js +517 -0
  177. package/dist/workstation/exec/fallback.js +106 -0
  178. package/dist/workstation/exec/policy.js +837 -0
  179. package/dist/workstation/exec/shaping.js +211 -0
  180. package/dist/workstation/exec.js +3 -112
  181. package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
  182. package/dist/workstation/files/adapters/toolRequests.js +121 -0
  183. package/dist/workstation/files/client.js +455 -0
  184. package/dist/workstation/files/credentials.js +142 -0
  185. package/dist/workstation/files/errors.js +28 -0
  186. package/dist/workstation/files/fence.js +350 -0
  187. package/dist/workstation/files/find/fileIndex.js +153 -0
  188. package/dist/workstation/files/find/fuzzy.js +155 -0
  189. package/dist/workstation/files/host.js +122 -0
  190. package/dist/workstation/files/hostProtocol.js +2 -0
  191. package/dist/workstation/files/media/cache.js +620 -0
  192. package/dist/workstation/files/media/encoding.js +308 -0
  193. package/dist/workstation/files/media/estimator.js +100 -0
  194. package/dist/workstation/files/media/media.js +325 -0
  195. package/dist/workstation/files/media/packedAudio.js +23 -0
  196. package/dist/workstation/files/media/scheduler.js +551 -0
  197. package/dist/workstation/files/media/types.js +5 -0
  198. package/dist/workstation/files/scope.js +45 -0
  199. package/dist/workstation/files/structured.js +284 -0
  200. package/dist/workstation/{fileOps.js → files/text.js} +152 -39
  201. package/dist/workstation/files/uploads.js +351 -0
  202. package/dist/workstation/journal.js +65 -236
  203. package/dist/workstation/keepers/acp/client.js +101 -0
  204. package/dist/workstation/keepers/acp/envelope.js +62 -0
  205. package/dist/workstation/keepers/acp/host.js +419 -0
  206. package/dist/workstation/keepers/acp/main.js +18 -0
  207. package/dist/workstation/keepers/acp/memoryStream.js +121 -0
  208. package/dist/workstation/keepers/acp/params.js +137 -0
  209. package/dist/workstation/keepers/acp/protocol.js +66 -0
  210. package/dist/workstation/keepers/acp/recover.js +120 -0
  211. package/dist/workstation/keepers/acp/session.js +584 -0
  212. package/dist/workstation/keepers/acp/stderr.js +154 -0
  213. package/dist/workstation/keepers/kit/activity.js +103 -0
  214. package/dist/workstation/keepers/kit/bootstrap.js +164 -0
  215. package/dist/workstation/keepers/kit/buildVersion.js +51 -0
  216. package/dist/workstation/keepers/kit/bundle.js +124 -0
  217. package/dist/workstation/keepers/kit/client.js +432 -0
  218. package/dist/workstation/keepers/kit/identity.js +21 -0
  219. package/dist/workstation/keepers/kit/ipc.js +112 -0
  220. package/dist/workstation/keepers/kit/launchConfig.js +8 -0
  221. package/dist/workstation/keepers/kit/launcher.js +289 -0
  222. package/dist/workstation/keepers/kit/lock.js +123 -0
  223. package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
  224. package/dist/workstation/keepers/kit/paths.js +76 -0
  225. package/dist/workstation/keepers/kit/protocol.js +43 -0
  226. package/dist/workstation/keepers/kit/server.js +518 -0
  227. package/dist/workstation/keepers/kit/streamLog.js +697 -0
  228. package/dist/workstation/keepers/legacy.js +296 -0
  229. package/dist/workstation/keepers/policy.js +576 -0
  230. package/dist/workstation/keepers/stop.js +34 -0
  231. package/dist/workstation/keepers/terminal/client.js +213 -0
  232. package/dist/workstation/keepers/terminal/exec.js +981 -0
  233. package/dist/workstation/keepers/terminal/generations.js +129 -0
  234. package/dist/workstation/keepers/terminal/keeper.js +62 -0
  235. package/dist/workstation/keepers/terminal/main.js +43 -0
  236. package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
  237. package/dist/workstation/keepers/terminal/params.js +27 -0
  238. package/dist/workstation/keepers/terminal/protocol.js +56 -0
  239. package/dist/workstation/keepers/terminal/pty.js +330 -0
  240. package/dist/workstation/managedInstall.js +57 -0
  241. package/dist/workstation/protocol/accountLimits.js +47 -0
  242. package/dist/workstation/protocol/agent.js +50 -19
  243. package/dist/workstation/protocol/common.js +30 -0
  244. package/dist/workstation/protocol/computer.js +15 -4
  245. package/dist/workstation/protocol/management.js +141 -4
  246. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  247. package/dist/workstation/rd/artifact.js +119 -0
  248. package/dist/workstation/rd/control.js +119 -0
  249. package/dist/workstation/rd/display.js +29 -0
  250. package/dist/workstation/rd/provider.js +222 -0
  251. package/dist/workstation/rd/sessions.js +391 -0
  252. package/dist/workstation/rd/state.js +45 -0
  253. package/dist/workstation/rd/update.js +171 -0
  254. package/dist/workstation/rd/upkeep.js +38 -0
  255. package/dist/workstation/register.js +27 -37
  256. package/dist/workstation/service/installer.js +104 -13
  257. package/dist/workstation/service/launchd.js +1 -0
  258. package/dist/workstation/storageMetrics.js +118 -0
  259. package/dist/workstation/systemMetrics.js +154 -93
  260. package/dist/workstation/toolchain.js +94 -0
  261. package/dist/workstation/tools/browser.js +28 -4
  262. package/dist/workstation/tools/client.js +9 -1
  263. package/dist/workstation/tools/info.js +50 -0
  264. package/dist/workstation/tools/keeperProcess.js +15 -5
  265. package/dist/workstation/tools/router.js +55 -0
  266. package/dist/workstation/tools/terminalRouter.js +222 -0
  267. package/dist/workstation/tools/validation.js +37 -1
  268. package/dist/workstation/update/artifact.js +2 -1
  269. package/dist/workstation/update/coordinator.js +3 -0
  270. package/dist/workstation/update/globalInstall.js +116 -0
  271. package/dist/workstation/update/npmInstaller.js +49 -23
  272. package/dist/workstation/update/ptyPreflight.js +21 -0
  273. package/dist/workstation/update/rdRollback.js +145 -0
  274. package/dist/workstation/update/runUpdate.js +3 -2
  275. package/dist/workstation/update/taskRegistry.js +105 -11
  276. package/dist/workstation/update/watchdog.js +16790 -334
  277. package/package.json +11 -8
  278. package/skills/axom/SKILL.md +11 -11
  279. package/skills/axom-definition/SKILL.md +3 -3
  280. package/skills/axom-platform/SKILL.md +3 -3
  281. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  282. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -49
  283. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  284. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  285. package/dist/commands/guide.js +0 -38
  286. package/dist/workstation/acp/acpSession.js +0 -1475
  287. package/dist/workstation/acp/contract.js +0 -499
  288. package/dist/workstation/acp/delegationMcp.js +0 -24
  289. package/dist/workstation/acp/draftInterceptor.js +0 -163
  290. package/dist/workstation/acp/qoderStartup.js +0 -45
  291. package/dist/workstation/acp/sessionScope.js +0 -19
  292. package/dist/workstation/filePaths.js +0 -174
  293. package/dist/workstation/fileWorker.js +0 -81
  294. package/dist/workstation/fileWorkerClient.js +0 -121
  295. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -106
  296. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -262
  297. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  298. package/dist/workstation/qoderAdapter/contract.js +0 -78
  299. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -30
  300. package/dist/workstation/qoderAdapter/history.js +0 -87
  301. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  302. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  303. package/dist/workstation/qoderAdapter/main.js +0 -93
  304. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  305. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  306. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  307. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  308. package/dist/workstation/qoderAdapter/profile.js +0 -93
  309. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -49
  310. package/dist/workstation/qoderAdapter/session.js +0 -319
  311. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  312. package/dist/workstation/qoderAdapter/subwork.js +0 -77
  313. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  314. package/dist/workstation/qoderAdapter/taskEvent.js +0 -26
  315. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  316. package/dist/workstation/qoderAdapter/usage.js +0 -37
  317. package/dist/workstation/tools/core.js +0 -50
  318. package/dist/workstation/tools/files.js +0 -525
  319. package/dist/workstation/tools/terminal.js +0 -214
  320. /package/dist/workstation/{qoderAdapter/types.js → desktopControl/provider.js} +0 -0
@@ -0,0 +1,57 @@
1
+ import { lstatSync, readFileSync, realpathSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { z } from 'zod';
4
+ import { LocalControlError } from './control/protocol.js';
5
+ const schema = z.object({ schemaVersion: z.literal(1), source: z.literal('managed'), prefix: z.string(), nodePath: z.string(), npmPath: z.string(), cliPath: z.string(), entryPath: z.string(), runtimeVersion: z.string().regex(/^24\.\d+\.\d+$/), architecture: z.enum(['arm64', 'x64']) }).strict();
6
+ function plannedRealPath(value) {
7
+ try {
8
+ return realpathSync(value);
9
+ }
10
+ catch (error) {
11
+ if (error.code !== 'ENOENT' || path.dirname(value) === value)
12
+ throw error;
13
+ return path.join(plannedRealPath(path.dirname(value)), path.basename(value));
14
+ }
15
+ }
16
+ /** This synchronous variant is shared by the existing synchronous service-context constructors. */
17
+ export function loadManagedCliInstallSync(packageRoot) {
18
+ let root;
19
+ try {
20
+ root = realpathSync(packageRoot);
21
+ }
22
+ catch (error) {
23
+ if (error.code === 'ENOENT')
24
+ return null;
25
+ throw error;
26
+ }
27
+ const prefix = path.resolve(root, '../../../..');
28
+ if (root !== path.join(prefix, 'lib/node_modules/@platless/axom-cli'))
29
+ return null;
30
+ const file = path.join(prefix, 'install.json');
31
+ let found = false;
32
+ try {
33
+ const stat = lstatSync(file);
34
+ found = true;
35
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 16384 || (process.getuid && stat.uid !== process.getuid()) || (stat.mode & 0o077))
36
+ throw Error('Unsafe metadata');
37
+ const value = schema.parse(JSON.parse(readFileSync(file, 'utf8')));
38
+ const home = path.resolve(prefix, '../..');
39
+ const runtime = path.join(home, '.axom/runtime/node', `${value.runtimeVersion}-darwin-${value.architecture}`);
40
+ if (prefix !== path.join(home, '.axom/cli') || realpathSync(value.prefix) !== prefix || realpathSync(value.cliPath) !== path.join(root, 'dist/cli.js') || realpathSync(value.nodePath) !== path.join(runtime, 'bin/node') || realpathSync(value.npmPath) !== path.join(runtime, 'bin/npm') || plannedRealPath(value.entryPath) !== path.join(home, '.local/bin/axom'))
41
+ throw Error('Metadata does not describe this installation');
42
+ return value;
43
+ }
44
+ catch (error) {
45
+ if (!found && error.code === 'ENOENT')
46
+ return null;
47
+ throw new LocalControlError('UNSUPPORTED_INSTALLATION', 'The managed CLI installation needs repair');
48
+ }
49
+ }
50
+ export async function loadManagedCliInstall(packageRoot) { return loadManagedCliInstallSync(packageRoot); }
51
+ export function npmExecutionContext(install, inherited) {
52
+ if (!install)
53
+ return { ...inherited };
54
+ const result = { ...inherited, NPM_CONFIG_PREFIX: install.prefix, PATH: `${path.dirname(install.nodePath)}:${inherited.PATH ?? '/usr/bin:/bin'}` };
55
+ delete result.npm_config_prefix;
56
+ return result;
57
+ }
@@ -0,0 +1,47 @@
1
+ /** Bounded, plain-JS wire validation: usable inside the deterministic ACP processor. */
2
+ export const ACCOUNT_LIMITS_READ_TIMEOUT_MS = 180_000;
3
+ const object = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
4
+ const text = (v, min = 0, max = 160) => typeof v === 'string' && v.length >= min && v.length <= max;
5
+ const time = (v) => typeof v === 'string' && /^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/.test(v) && Number.isFinite(new Date(v).getTime());
6
+ const oneOf = (v, values) => typeof v === 'string' && values.includes(v);
7
+ function parse(value) {
8
+ const valid = object(value) && value.schemaVersion === 1 && text(value.provider, 1, 64)
9
+ && (value.updateMode === undefined || oneOf(value.updateMode, ['snapshot', 'delta']))
10
+ && oneOf(value.billingMode, ['subscription', 'non_subscription', 'unknown'])
11
+ && oneOf(value.availability, ['available', 'unavailable', 'unknown', 'not_applicable']) && time(value.observedAt)
12
+ && (value.plan === null || object(value.plan) && text(value.plan.id) && text(value.plan.label))
13
+ && (value.accountKey === undefined || text(value.accountKey, 0, 128))
14
+ && Array.isArray(value.limits) && value.limits.length <= 32 && value.limits.every(row => object(row)
15
+ && text(row.id, 1) && text(row.label) && oneOf(row.kind, ['subscription', 'spend', 'rate'])
16
+ && (row.windowSeconds === null || typeof row.windowSeconds === 'number' && Number.isInteger(row.windowSeconds) && row.windowSeconds > 0)
17
+ && (row.period == null || text(row.period))
18
+ && (row.key === undefined || row.key === null || text(row.key, 1, 64))
19
+ && (row.usedPercent === null || typeof row.usedPercent === 'number' && Number.isFinite(row.usedPercent) && row.usedPercent >= 0 && row.usedPercent <= 100)
20
+ && (row.resetsAt === null || time(row.resetsAt)) && oneOf(row.status, ['allowed', 'warning', 'blocked', 'unknown'])
21
+ && (row.active == null || typeof row.active === 'boolean') && (row.scope == null || object(row.scope))
22
+ && ['used', 'limit', 'unit', 'currency'].every(key => row[key] === undefined || text(row[key]))
23
+ && (row.exponent === undefined || typeof row.exponent === 'number' && Number.isInteger(row.exponent) && row.exponent >= 0 && row.exponent <= 12))
24
+ && new Set(value.limits.map(row => row.id)).size === value.limits.length
25
+ && Array.isArray(value.balances) && value.balances.length <= 16 && value.balances.every(row => object(row)
26
+ && text(row.id, 1) && text(row.label) && text(row.unit) && (row.remaining === null || text(row.remaining))
27
+ && (row.currency === undefined || text(row.currency)) && (row.unlimited === undefined || typeof row.unlimited === 'boolean')
28
+ && (row.exponent === undefined || typeof row.exponent === 'number' && Number.isInteger(row.exponent) && row.exponent >= 0 && row.exponent <= 12))
29
+ && new Set(value.balances.map(row => row.id)).size === value.balances.length
30
+ && (value._meta === undefined || object(value._meta)) && JSON.stringify(value).length <= 65_536;
31
+ if (!valid)
32
+ throw new Error('Invalid account quota snapshot');
33
+ return { ...value, _meta: value._meta ?? {} };
34
+ }
35
+ export const accountLimitsSchema = {
36
+ parse,
37
+ safeParse(value) {
38
+ try {
39
+ return { success: true, data: parse(value) };
40
+ }
41
+ catch {
42
+ return { success: false, error: new Error('Invalid account quota snapshot') };
43
+ }
44
+ },
45
+ };
46
+ export const ACCOUNT_LIMITS_READ = '_axom/account/rateLimits/read';
47
+ export const ACCOUNT_LIMITS_UPDATED = '_axom/account/rateLimits/updated';
@@ -1,14 +1,20 @@
1
- import { isRuntimeDiagnostics } from '../qoderAdapter/profile.js';
1
+ import { accountLimitsSchema } from './accountLimits.js';
2
+ import { isRuntimeDiagnostics } from './runtimeDiagnostics.js';
2
3
  import { decodeChannelFrame, hasOptional, hasRequired, isBoolean, isBridgeInfoArray, isNullableAuthMethodInfoArray, isNullableString, isNumber, isObject, isString, isStringArray, isStringRecord, } from './common.js';
3
4
  /** Current version and feature floors for the independently negotiated agent protocol. */
4
- export const AGENT_WIRE_VERSION = 24;
5
+ export const AGENT_WIRE_VERSION = 27;
6
+ /**
7
+ * `agent_switch_idle` / `agent_switch_idle_result` (spec 2026-10-11 §6.4, Task 25): "move idle sessions" of one agent to another
8
+ * adapter build. A server-to-client request: the server sends it only to a client advertising this version (an older one has no
9
+ * case for it and the request would only die at its timeout).
10
+ */
11
+ export const AGENT_WIRE_ACCOUNT_LIMITS_VERSION = 27;
12
+ export const AGENT_WIRE_SWITCH_IDLE_VERSION = 26;
5
13
  /** `agent_delete_native` / `agent_native_deleted`: purge a session Axom created on an external agent. */
6
14
  export const AGENT_WIRE_NATIVE_DELETE_VERSION = 24;
7
15
  export const AGENT_WIRE_PROVIDER_TASK_VERSION = 23;
8
16
  export const AGENT_WIRE_RUNTIME_PROFILE_VERSION = 22;
9
- export const AGENT_WIRE_DELEGATION_RENEW_VERSION = 21;
10
17
  export const AGENT_WIRE_TURN_IDENTITY_VERSION = 20;
11
- export const AGENT_WIRE_DELEGATION_MCP_VERSION = 19;
12
18
  /** Scoped replay/history is enabled only when both peers preserve native origin. */
13
19
  export const AGENT_WIRE_SCOPED_ORIGIN_VERSION = 18;
14
20
  /** agent_open.requireSessionLoad: reject unsupported/failed load without session/new fallback. */
@@ -207,7 +213,7 @@ export const AGENT_WIRE_ELICITATION_VERSION = 16;
207
213
  *
208
214
  * Declarative on purpose: the required-field half of this table is the *only* statement anywhere of
209
215
  * which fields the Kotlin twin will refuse to decode without, and the contract suite
210
- * (`acp/contract.ts`'s `assertRequiredFieldsEnforced`) reads it directly to prove each one is really
216
+ * (`protocol/agentRequiredFields.test.ts`'s `assertRequiredFieldsEnforced`) reads it directly to prove each one is really
211
217
  * enforced. Left as inline `hasRequired({...})` calls inside a `switch`, that list is unreadable from
212
218
  * a test, so the only way to check it would be a hand-maintained copy in the test file — which is
213
219
  * precisely how `agent_authenticate.methodId` came to have no guard at all while a report still
@@ -229,6 +235,7 @@ export const AGENT_FRAME_SHAPES = {
229
235
  optional: { sessions: (sessions) => Array.isArray(sessions) && sessions.every(isAgentResumeSession) },
230
236
  },
231
237
  heartbeat_ack: { required: { seq: isNumber } },
238
+ agent_reopen_request: { required: { sessionRef: isString, reason: isString } },
232
239
  agent_open: {
233
240
  required: {
234
241
  id: isString, sessionRef: isString, agentKey: isString, bridge: isString, cwd: isString, shell: isString,
@@ -237,9 +244,6 @@ export const AGENT_FRAME_SHAPES = {
237
244
  env: isStringRecord, pathPrepend: isStringArray, agentEnv: isStringRecord, command: isNullableString,
238
245
  args: isStringArray, nativeSessionId: isNullableString,
239
246
  resumeFromSeq: isNumber, backfill: isBoolean, requireSessionLoad: isBoolean,
240
- delegationMcp: (value) => isObject(value) &&
241
- isString(value.url) && isStringRecord(value.headers) && isNumber(value.expiresAtEpochMs) &&
242
- (value.bindingId === undefined || isString(value.bindingId)),
243
247
  },
244
248
  },
245
249
  agent_opened: {
@@ -256,9 +260,6 @@ export const AGENT_FRAME_SHAPES = {
256
260
  // `reloaded` is `true`-or-absent for the same reason as `supportsSteering`: a `false` would
257
261
  // be a second spelling of what its absence already says.
258
262
  reloaded: isVerifiedTrue,
259
- delegationExpiresAtEpochMs: isNumber,
260
- delegationRefreshPending: isVerifiedTrue,
261
- delegationInstanceId: isString, delegationBindingId: isString, delegationRenewalRevision: isNumber,
262
263
  agentInfo: isAgentInfo, supportsImageInput: isVerifiedTrue, configOptions: isNullableSessionConfigOptionArray,
263
264
  // Same guard as `agent_verified.authMethods`, and the same absent-vs-`[]` reading: a
264
265
  // half-decoded method table is worse than none, because the reader would offer a login the
@@ -272,13 +273,11 @@ export const AGENT_FRAME_SHAPES = {
272
273
  // Vendor-verbatim and uninterpreted here (decision L1) -- any object shape is accepted, the
273
274
  // same lenience `agent_event.payload` gets via `isRecord`.
274
275
  activityRaw: isRecord,
276
+ // Task 21 (spec §5.5.4): `true`-or-absent, and the two id lists.
277
+ durable: isVerifiedTrue, pendingRequestIds: isStringArray, steeredIds: isStringArray,
275
278
  },
276
279
  },
277
280
  agent_prompt: { required: { sessionRef: isString, text: isString }, optional: { images: isImageBlockArray, platformTurnId: isString } },
278
- agent_delegation_renew: { required: { id: isString, sessionRef: isString, nativeSessionId: isString,
279
- instanceId: isString, bindingId: isString, renewalRevision: isNumber, expiresAtEpochMs: isNumber } },
280
- agent_delegation_renewed: { required: { id: isString, ok: isBoolean }, optional: { error: isString,
281
- nativeSessionId: isString, instanceId: isString, bindingId: isString, renewalRevision: isNumber, expiresAtEpochMs: isNumber } },
282
281
  agent_answer: { required: { sessionRef: isString, requestId: isString, outcome: isString } },
283
282
  // `content`'s interior is never validated here -- it is ACP's free-form elicitation payload, and
284
283
  // this table is transport, not translator. `isRecord` only rejects a non-object.
@@ -319,6 +318,9 @@ export const AGENT_FRAME_SHAPES = {
319
318
  },
320
319
  // Same lenience as `agent_list_native` above, for the same reason: the session manager wants to
321
320
  // answer a malformed query with its own `ok: false` rather than have the transport drop it.
321
+ agent_account_read: { required: { id: isString, bridge: isString, agentKey: isString, query: () => true } },
322
+ agent_account_read_result: { required: { id: isString, ok: isBoolean }, optional: { snapshot: (value) => accountLimitsSchema.safeParse(value).success, error: isString } },
323
+ agent_account_update: { required: { agentKey: isString, snapshot: (value) => accountLimitsSchema.safeParse(value).success } },
322
324
  agent_verify: {
323
325
  required: { id: isString, bridge: isString, query: () => true },
324
326
  // 与 `query` 那条**刻意的宽松**相反,这里是严格的:一份畸形的 query 还有个能回
@@ -398,8 +400,35 @@ export const AGENT_FRAME_SHAPES = {
398
400
  agent_provider_task_stopped: { required: { id: isTaskId, state: isProviderStopState },
399
401
  optional: { error: isTaskSummary } },
400
402
  agent_async_task_stop: { required: { sessionRef: isString, asyncTaskId: isString } },
403
+ agent_switch_idle: { required: { id: isString, bridge: isString }, optional: { toBuild: isString } },
404
+ agent_switch_idle_result: {
405
+ required: { id: isString, ok: isBoolean },
406
+ optional: { registered: isCount, immediate: isCount, unmovable: isSwitchIdleUnmovableArray, error: isNullableString },
407
+ },
401
408
  };
402
409
  const DISCRIMINATORS = new Set(Object.keys(AGENT_FRAME_SHAPES));
410
+ /**
411
+ * `agent_reopen_request{sessionRef, reason}` (spec 2026-10-11 §6.3, C→S): a session that is idle and has a pending switch (an adapter
412
+ * update, a keeper-acp generation change) asks the server to send `agent_open{requireSessionLoad:true}` with the session's fresh
413
+ * environment — a switch needs the spawn secrets only an `agent_open` carries. Task 24 decides when; the manager's
414
+ * `requestReopen` sends it, and only to a server advertising {@link AGENT_WIRE_REOPEN_REQUEST_VERSION}: an older server cannot
415
+ * decode it, and eight undecodable frames in a row close the agent channel (spec §10: there the switch waits for a natural reopen).
416
+ */
417
+ export const AGENT_WIRE_REOPEN_REQUEST_VERSION = 25;
418
+ /**
419
+ * The same version (Task 22): the server acknowledges as soon as a subagent relation releases the watermark it capped, and stops
420
+ * wasting its 32-event ack count on capped attempts — so a sent relation-pinned outbox entry may count against the sending
421
+ * window again (`SessionOutbox.inflight`). Below it the entries keep not counting once sent (fix round 1 of Task 20).
422
+ */
423
+ export const AGENT_WIRE_RELATION_ACK_VERSION = 25;
424
+ export const AGENT_REOPEN_REQUEST_SHAPE = AGENT_FRAME_SHAPES.agent_reopen_request;
425
+ export function agentReopenRequest(sessionRef, reason) {
426
+ return { t: 'agent_reopen_request', sessionRef, reason };
427
+ }
428
+ function isCount(value) { return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0; }
429
+ function isSwitchIdleUnmovableArray(value) {
430
+ return Array.isArray(value) && value.every((entry) => isObject(entry) && isString(entry.sessionRef) && isString(entry.reason));
431
+ }
403
432
  function isRecord(value) { return isObject(value); }
404
433
  function isTaskId(value) {
405
434
  return typeof value === 'string' && value.length > 0 &&
@@ -422,12 +451,13 @@ function isProviderStopState(value) {
422
451
  }
423
452
  function isAgentResumeSession(value) {
424
453
  return isObject(value)
425
- && hasRequired(value, { sessionRef: isString, journalMaxSeq: isNumber, agentAlive: isBoolean });
454
+ && hasRequired(value, { sessionRef: isString, journalMaxSeq: isNumber, agentAlive: isBoolean })
455
+ && (value.pendingRequestIds === undefined || isStringArray(value.pendingRequestIds));
426
456
  }
427
457
  /**
428
458
  * Accepts only the two outcomes this client can produce. `startedNewTurn` is refused on purpose: it
429
459
  * is the extension's third outcome, and receiving one means a peer invented a disposition for a
430
- * message the server has already put on its timeline — see `acpSession.ts`'s `mapSteerOutcome`.
460
+ * message the server has already put on its timeline — see `acp/steering.ts`'s `mapSteerOutcome`.
431
461
  */
432
462
  function isSteerOutcome(value) { return value === 'injected' || value === 'promptRequired'; }
433
463
  /** Accepts `true` and nothing else — the whole point of `authenticated` having no `false`. */
@@ -729,8 +759,9 @@ export function outboundFailureReply(frame, reason) {
729
759
  case 'agent_authenticated': return { t: 'agent_authenticated', id, ok: false, error };
730
760
  case 'agent_installed': return { t: 'agent_installed', id, ok: false, error };
731
761
  case 'agent_steered': return { t: 'agent_steered', id, ok: false, error };
762
+ case 'agent_account_read_result': return { t: 'agent_account_read_result', id, ok: false, error };
732
763
  case 'agent_config_option_set': return { t: 'agent_config_option_set', id, ok: false, error };
733
- case 'agent_delegation_renewed': return { t: 'agent_delegation_renewed', id, ok: false, error };
764
+ case 'agent_switch_idle_result': return { t: 'agent_switch_idle_result', id, ok: false, error };
734
765
  case 'agent_provider_task_stopped': return { t: 'agent_provider_task_stopped', id, state: 'unknown', error };
735
766
  case 'agent_backfill_done':
736
767
  case 'agent_backfill_events': return { t: 'agent_backfill_done', id, ok: false, count: 0, error };
@@ -746,7 +777,7 @@ export function decodeAgentFrame(text) {
746
777
  if ((frame.state === 'completed' || frame.state === 'failed' || frame.state === 'stopped') && frame.live)
747
778
  throw new TypeError('Invalid provider task terminal live state');
748
779
  if (frame.status !== undefined &&
749
- (frame.status !== frame.state || !['completed', 'failed', 'stopped'].includes(frame.status)))
780
+ (frame.status !== frame.state || !['completed', 'failed', 'stopped'].some(status => status === frame.status)))
750
781
  throw new TypeError('Invalid provider task terminal status');
751
782
  if (frame.summary !== undefined && frame.status === undefined)
752
783
  throw new TypeError('Provider task summary requires terminal status');
@@ -26,6 +26,18 @@ export const isNullableAuthMethodInfoArray = (value) => value === null || (Array
26
26
  export function isMissingPrerequisite(value) {
27
27
  return isObject(value) && hasRequired(value, { label: isString }) && hasOptional(value, { hint: isString });
28
28
  }
29
+ const isCount = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
30
+ export function isBridgeCurrentBuild(value) {
31
+ return isObject(value) && hasRequired(value, { id: isString, version: isString }) && hasOptional(value, { patched: isTrueOnly });
32
+ }
33
+ export function isBridgeBuild(value) {
34
+ return isObject(value)
35
+ && hasRequired(value, { id: isString, version: isString, sessions: isCount, idle: isCount, busy: isCount, unmovable: isCount })
36
+ && hasOptional(value, { patched: isTrueOnly, current: isTrueOnly, busyReasons: isStringArray, unmovableReasons: isStringArray });
37
+ }
38
+ export function isBridgeSwitchPending(value) {
39
+ return isObject(value) && hasRequired(value, { remaining: isCount }) && hasOptional(value, { toBuild: isString });
40
+ }
29
41
  export function isBridgeInfo(value) {
30
42
  return isObject(value)
31
43
  && hasRequired(value, { type: isString })
@@ -66,6 +78,11 @@ export function isBridgeInfo(value) {
66
78
  // A peer that sent one would be publishing "we looked and it cannot steer" through a key
67
79
  // whose absence already says exactly that, so refusing it keeps one spelling per fact.
68
80
  supportsSteering: isTrueOnly,
81
+ supportsAccountLimits: isTrueOnly,
82
+ // Task 25 (spec §6.4): scan-time build data. Optional: absent is "not looked at".
83
+ currentBuild: isBridgeCurrentBuild,
84
+ builds: (value) => Array.isArray(value) && value.every(isBridgeBuild),
85
+ switchPending: isBridgeSwitchPending,
69
86
  });
70
87
  }
71
88
  export function isBridgeInfoArray(value) {
@@ -103,6 +120,19 @@ export function bridgesForWire(bridges, frame) {
103
120
  }
104
121
  return kept;
105
122
  }
123
+ /**
124
+ * The roster as a hello carries it: without the scan-only build fields (`currentBuild`/`builds`/`switchPending`, spec §6.4). The
125
+ * daemon keeps one cached roster, which a scan refreshes with those fields; a later reconnect's hello would otherwise carry a
126
+ * snapshot of sessions and pending moves that is stale by then. Only the scan answers them, fresh.
127
+ */
128
+ export function helloRoster(bridges) {
129
+ return bridges.map((bridge) => {
130
+ if (bridge.currentBuild === undefined && bridge.builds === undefined && bridge.switchPending === undefined)
131
+ return bridge;
132
+ const { currentBuild: _current, builds: _builds, switchPending: _pending, ...rest } = bridge;
133
+ return rest;
134
+ });
135
+ }
106
136
  function partitionWireSafeBridges(bridges) {
107
137
  let parsed;
108
138
  try {
@@ -6,6 +6,7 @@ const DISCRIMINATORS = new Set([
6
6
  'desktop_capability', 'desktop_open', 'desktop_opened', 'desktop_data', 'desktop_credit', 'desktop_close',
7
7
  'computer_hello', 'computer_ready', 'heartbeat', 'heartbeat_ack',
8
8
  'exec_request', 'exec_output', 'exec_exit', 'exec_cancel', 'tool_request', 'tool_result',
9
+ 'exec_started', 'exec_ack', 'exec_attach', 'exec_status',
9
10
  'file_read_request', 'file_read_result', 'file_write_request', 'file_write_result',
10
11
  'file_ls_request', 'file_ls_result', 'file_mkdir_request', 'file_mkdir_result',
11
12
  ]);
@@ -16,6 +17,9 @@ const fileErrorCodes = new Set([
16
17
  ]);
17
18
  const isNonnegativeInteger = (value) => Number.isSafeInteger(value) && value >= 0;
18
19
  const isPositiveInteger = (value) => Number.isSafeInteger(value) && value >= 1;
20
+ /** A record offset, or -1 for "none" (exec_status / exec_ack). */
21
+ const isOffsetOrNone = (value) => Number.isSafeInteger(value) && value >= -1;
22
+ const EXEC_STATES = new Set(['running', 'exited', 'unknown']);
19
23
  const isSha256 = (value) => typeof value === 'string' && /^[0-9a-f]{64}$/.test(value);
20
24
  const onlyKeys = (value, keys) => Object.keys(value).every(key => keys.includes(key));
21
25
  function isFileError(value) {
@@ -66,12 +70,19 @@ function isComputerFrame(value) {
66
70
  case 'exec_request': return hasRequired(value, {
67
71
  id: isString, command: isString, shell: isString, cwd: isString, env: isStringRecord,
68
72
  pathPrepend: isStringArray, streaming: isBoolean,
69
- }) && hasOptional(value, { timeoutSeconds: isNullableNumber });
70
- case 'exec_output': return hasRequired(value, { id: isString, stream: isString, chunk: isString });
73
+ }) && hasOptional(value, { timeoutSeconds: isNullableNumber, label: isString, jobId: isString });
74
+ case 'exec_started': return hasRequired(value, { id: isString, durable: isBoolean, startedAtMs: isNumber })
75
+ && hasOptional(value, { pid: isNonnegativeInteger });
76
+ case 'exec_output': return hasRequired(value, { id: isString, stream: isString, chunk: isString })
77
+ && hasOptional(value, { offset: isNonnegativeInteger, recvAtMs: isNumber });
78
+ case 'exec_ack': return hasRequired(value, { id: isString, throughOffset: isOffsetOrNone });
71
79
  case 'exec_exit': return hasRequired(value, {
72
80
  id: isString, exitCode: isNullableNumber, stdout: isString, stderr: isString,
73
81
  timedOut: isBoolean, truncated: isBoolean,
74
- });
82
+ }) && hasOptional(value, { signal: isString, cancelled: isBoolean, gaps: isNonnegativeInteger });
83
+ case 'exec_attach': return hasRequired(value, { id: isString, fromOffset: isNonnegativeInteger });
84
+ case 'exec_status': return hasRequired(value, { id: isString, state: isString, headOffset: isOffsetOrNone, committedOffset: isOffsetOrNone })
85
+ && EXEC_STATES.has(value.state);
75
86
  case 'tool_request': return hasRequired(value, { id: isString, method: isString, params: isObject })
76
87
  && value.id.length > 0 && value.id.length <= 128
77
88
  && value.method.length > 0 && value.method.length <= 64
@@ -82,7 +93,7 @@ function isComputerFrame(value) {
82
93
  && (value.ok ? value.error === null : typeof value.error === 'string' && value.error.length > 0)
83
94
  && Buffer.byteLength(JSON.stringify(value)) <= TOOL_RESULT_BYTES
84
95
  && onlyKeys(value, ['t', 'id', 'ok', 'result', 'error']);
85
- case 'exec_cancel': return hasRequired(value, { id: isString });
96
+ case 'exec_cancel': return hasRequired(value, { id: isString }) && hasOptional(value, { signal: isString });
86
97
  case 'file_read_request': return hasRequired(value, {
87
98
  id: isString, root: isString, path: isString, startLine: isPositiveInteger, maxLines: isPositiveInteger,
88
99
  }) && value.maxLines <= 2000 && onlyKeys(value, ['t', 'id', 'root', 'path', 'startLine', 'maxLines']);
@@ -1,3 +1,4 @@
1
+ import { decodeDesktopManagementFrame, parseDesktopJson, DesktopProtocolError } from '../desktopControl/protocol.js';
1
2
  import { decodeChannelFrame, hasOptional, hasRequired, isBoolean, isBridgeInfoArray, isNullableString, isNumber, isObject, isString, } from './common.js';
2
3
  export const MANAGEMENT_WIRE_MIN_VERSION = 1;
3
4
  /** Adds the `DRAINING` value to ClientUpdatePhase. */
@@ -8,11 +9,21 @@ export const MANAGEMENT_WIRE_METRICS_VERSION = 4;
8
9
  export const MANAGEMENT_WIRE_PRESSURE_VERSION = 5;
9
10
  /** Replaces normalized CPU load with raw 1/5/15-minute load averages and CPU capacity. */
10
11
  export const MANAGEMENT_WIRE_LOAD_AVERAGES_VERSION = 6;
11
- export const MANAGEMENT_WIRE_VERSION = MANAGEMENT_WIRE_LOAD_AVERAGES_VERSION;
12
+ /** Adds per-GPU utilization and GPU memory to heartbeat metrics. */
13
+ export const MANAGEMENT_WIRE_GPU_VERSION = 7;
14
+ /** Adds `keepers` to heartbeats and the `keeper_control` / `keeper_control_result` pair (spec §8.1, Task 26). */
15
+ export const MANAGEMENT_WIRE_KEEPERS_VERSION = 8;
16
+ export const MANAGEMENT_WIRE_DESKTOP_VERSION = 9;
17
+ export const MANAGEMENT_WIRE_VERSION = MANAGEMENT_WIRE_DESKTOP_VERSION;
18
+ /** Bounds of one heartbeat's `keepers` (spec §8.1). */
19
+ export const KEEPER_STATUS_MAX_ITEMS = 32;
20
+ export const KEEPER_LABEL_MAX_CHARS = 64;
21
+ export const KEEPER_STATUS_MAX_ENTRIES = 16;
22
+ export const KEEPER_AFFECTED_MAX_ITEMS = 256;
12
23
  const DISCRIMINATORS = new Set([
13
24
  'management_hello', 'management_ready', 'heartbeat', 'heartbeat_ack',
14
25
  'channel_ticket_request', 'channel_ticket', 'client_update', 'client_update_status',
15
- 'external_agents_scan', 'external_agents_scan_result',
26
+ 'external_agents_scan', 'external_agents_scan_result', 'keeper_control', 'keeper_control_result',
16
27
  ]);
17
28
  const CHANNEL_KINDS = new Set(['MANAGEMENT', 'COMPUTER', 'AGENT']);
18
29
  const CHANNEL_STATES = new Set(['DISCONNECTED', 'CONNECTING', 'CONNECTED', 'RECONNECTING']);
@@ -63,6 +74,15 @@ function isStoragePoolMetrics(value) {
63
74
  availableBytes: isNumber,
64
75
  });
65
76
  }
77
+ function isGpuMetrics(value) {
78
+ return isObject(value) && hasRequired(value, { id: isString, name: isString })
79
+ && hasOptional(value, {
80
+ utilization: isNullable(isNumber),
81
+ memoryTotalBytes: isNullable(isNumber),
82
+ memoryUsedBytes: isNullable(isNumber),
83
+ coreCount: isNullable(isNumber),
84
+ });
85
+ }
66
86
  function isWorkstationMetrics(value) {
67
87
  return isObject(value) && hasRequired(value, {
68
88
  cpuUtilization: isNullable(isNumber),
@@ -76,8 +96,31 @@ function isWorkstationMetrics(value) {
76
96
  cpuLoad15: isNullable(isNumber),
77
97
  cpuLogicalCount: isNullable(isNumber),
78
98
  memoryPressure: isNullable(isNumber),
99
+ gpus: isNullable((gpus) => Array.isArray(gpus) && gpus.every(isGpuMetrics)),
79
100
  });
80
101
  }
102
+ const KEEPER_KINDS = new Set(['terminal', 'acp']);
103
+ const KEEPER_STATES = new Set(['running', 'starting', 'stopped', 'unavailable']);
104
+ const KEEPER_REPLACEMENTS = new Set(['none', 'available', 'required', 'mandatory']);
105
+ const KEEPER_ITEM_TYPES = new Set(['pty', 'exec', 'session']);
106
+ const KEEPER_ACTIONS = new Set(['replace-when-idle', 'replace-now', 'cancel']);
107
+ const oneOf = (set) => (value) => typeof value === 'string' && set.has(value);
108
+ const isPending = (value) => value === null || value === 'when-idle';
109
+ function isKeeperItem(value) {
110
+ return isObject(value) && hasRequired(value, { type: oneOf(KEEPER_ITEM_TYPES), id: isString, label: isString, state: isString, sinceEpochMs: isNumber })
111
+ && hasOptional(value, { jobId: isString });
112
+ }
113
+ function isKeeperStatus(value) {
114
+ return isObject(value) && hasRequired(value, {
115
+ kind: oneOf(KEEPER_KINDS), gen: isString, state: oneOf(KEEPER_STATES), protocolVersion: isNumber, buildVersion: isString,
116
+ replacement: oneOf(KEEPER_REPLACEMENTS),
117
+ metrics: (metrics) => isObject(metrics) && Object.values(metrics).every(isNumber),
118
+ items: (items) => Array.isArray(items) && items.length <= KEEPER_STATUS_MAX_ITEMS && items.every(isKeeperItem),
119
+ }) && hasOptional(value, { legacy: isBoolean, pending: isPending });
120
+ }
121
+ function isKeeperAffected(value) {
122
+ return isObject(value) && hasRequired(value, { type: oneOf(KEEPER_ITEM_TYPES), id: isString, label: isString, gen: isString });
123
+ }
81
124
  function isManagementFrame(value) {
82
125
  switch (value.t) {
83
126
  case 'management_hello': return hasRequired(value, {
@@ -93,10 +136,14 @@ function isManagementFrame(value) {
93
136
  channels: (channels) => isObject(channels) && Object.entries(channels).every(([kind, state]) => CHANNEL_KINDS.has(kind) && isChannelState(state)),
94
137
  activeShellTasks: isNumber, activeAgentTasks: isNumber,
95
138
  }) && hasOptional(value, {
139
+ playgroundCwd: isString,
96
140
  protocols: (protocols) => isProtocolRangeMap(protocols, false),
97
141
  os: isNullable(isOsInfo),
142
+ toolchain: (toolchain) => isObject(toolchain) && hasRequired(toolchain, { python: isNullableString, node: isNullableString }),
98
143
  update: isNullable(isUpdateStatus),
99
144
  metrics: isNullable(isWorkstationMetrics),
145
+ keepers: isNullable((keepers) => Array.isArray(keepers) && keepers.every(isKeeperStatus)),
146
+ capabilitiesRevision: value => Number.isSafeInteger(value) && value > 0,
100
147
  });
101
148
  case 'heartbeat_ack': return hasRequired(value, { seq: isNumber });
102
149
  case 'channel_ticket_request': return hasRequired(value, { channel: isChannelKind });
@@ -105,12 +152,102 @@ function isManagementFrame(value) {
105
152
  case 'client_update_status': return hasRequired(value, { status: isUpdateStatus });
106
153
  case 'external_agents_scan': return hasRequired(value, { commandId: isString });
107
154
  case 'external_agents_scan_result': return hasRequired(value, { commandId: isString, bridges: isBridgeInfoArray });
155
+ case 'keeper_control': return hasRequired(value, { commandId: isString, kind: oneOf(KEEPER_KINDS), action: oneOf(KEEPER_ACTIONS) });
156
+ case 'keeper_control_result': return hasRequired(value, {
157
+ commandId: isString, kind: oneOf(KEEPER_KINDS), action: oneOf(KEEPER_ACTIONS), ok: isBoolean,
158
+ affected: (affected) => Array.isArray(affected) && affected.every(isKeeperAffected),
159
+ }) && hasOptional(value, { pending: isPending, error: isString, affectedIncomplete: (flag) => flag === true });
108
160
  default: return false;
109
161
  }
110
162
  }
111
163
  export function encodeManagementFrame(frame) {
112
- return JSON.stringify(frame);
164
+ const text = JSON.stringify(frame);
165
+ if (isDesktopManagementType(frame.t))
166
+ decodeDesktopManagementFrame(text);
167
+ return text;
113
168
  }
114
- export function decodeManagementFrame(text) {
169
+ export function decodeManagementFrame(source) {
170
+ const text = typeof source === 'string' ? source : new TextDecoder('utf-8', { fatal: true }).decode(source);
171
+ // Desktop extensions get the strict lexical/shape guard before any lossy object parsing.
172
+ const parsed = JSON.parse(text);
173
+ if (isObject(parsed) && isDesktopManagementType(parsed.t))
174
+ return decodeDesktopManagementFrame(text);
175
+ if (isObject(parsed) && parsed.t === 'heartbeat' && Object.hasOwn(parsed, 'capabilitiesRevision'))
176
+ validateHeartbeatRevision(text);
115
177
  return decodeChannelFrame(text, DISCRIMINATORS, isManagementFrame);
116
178
  }
179
+ /** Select only the new root field. JSON.parse already validates syntax; legacy nesting, size,
180
+ * unknown fields and duplicate legacy keys keep their established decoder behavior. */
181
+ function validateHeartbeatRevision(text) {
182
+ let depth = 0, count = 0;
183
+ for (let offset = 0; offset < text.length; offset++) {
184
+ const char = text[offset];
185
+ if (char === '"') {
186
+ const start = offset++;
187
+ while (offset < text.length) {
188
+ if (text[offset] === '\\')
189
+ offset += 2;
190
+ else if (text[offset++] === '"')
191
+ break;
192
+ }
193
+ const end = offset--;
194
+ if (depth !== 1 || JSON.parse(text.slice(start, end)) !== 'capabilitiesRevision')
195
+ continue;
196
+ let tokenStart = end;
197
+ while (/\s/.test(text[tokenStart] ?? ''))
198
+ tokenStart++;
199
+ if (text[tokenStart] !== ':')
200
+ continue; // This string was a legacy field's value.
201
+ let tokenEnd = ++tokenStart;
202
+ while (tokenEnd < text.length && text[tokenEnd] !== ',' && text[tokenEnd] !== '}')
203
+ tokenEnd++;
204
+ const token = text.slice(tokenStart, tokenEnd).trim();
205
+ if (++count !== 1 || !/^[1-9][0-9]*$/.test(token) || !Number.isSafeInteger(Number(token))) {
206
+ throw new DesktopProtocolError('Invalid capabilities revision');
207
+ }
208
+ parseDesktopJson(`{"capabilitiesRevision":${token}}`);
209
+ }
210
+ else if (char === '{' || char === '[')
211
+ depth++;
212
+ else if (char === '}' || char === ']')
213
+ depth--;
214
+ }
215
+ }
216
+ function isDesktopManagementType(type) {
217
+ return typeof type === 'string' && ['desktop_background', 'desktop_lobby', 'desktop_lobby_result', 'workstation_capabilities', 'workstation_capabilities_request', 'desktop_prepare',
218
+ 'desktop_prepare_result', 'desktop_cancel', 'desktop_session_state'].includes(type);
219
+ }
220
+ export class ManagementFrameAdmission {
221
+ isCurrent;
222
+ ready;
223
+ disposed = false;
224
+ constructor(isCurrent = () => true) {
225
+ this.isCurrent = isCurrent;
226
+ }
227
+ admit(frame) {
228
+ if (this.disposed || !this.isCurrent())
229
+ return false;
230
+ if (frame.t === 'management_ready') {
231
+ if (this.ready || !Number.isInteger(frame.protocols.MANAGEMENT) || frame.protocols.MANAGEMENT < MANAGEMENT_WIRE_MIN_VERSION
232
+ || frame.protocols.MANAGEMENT > MANAGEMENT_WIRE_VERSION || !frame.generation)
233
+ return false;
234
+ this.ready = frame;
235
+ return true;
236
+ }
237
+ if (frame.t === 'heartbeat' && frame.capabilitiesRevision !== undefined)
238
+ return false;
239
+ if (!isDesktopManagementType(frame.t))
240
+ return true;
241
+ if (!this.ready || this.ready.protocols.MANAGEMENT < MANAGEMENT_WIRE_DESKTOP_VERSION)
242
+ return false;
243
+ switch (frame.t) {
244
+ case 'workstation_capabilities_request': return frame.generation === this.ready.generation;
245
+ case 'desktop_lobby':
246
+ case 'desktop_background':
247
+ case 'desktop_prepare': return frame.deviceGeneration === this.ready.generation;
248
+ case 'desktop_cancel': return true; // Current socket binding; T10 additionally fences its session/control generation.
249
+ default: return false; // Device-authored reports/results/states cannot be echoed as server activity.
250
+ }
251
+ }
252
+ dispose() { this.disposed = true; }
253
+ }
@@ -0,0 +1,15 @@
1
+ export function isRuntimeDiagnostics(value) {
2
+ if (!value || typeof value !== 'object' || Array.isArray(value))
3
+ return false;
4
+ const input = value;
5
+ const bounded = (value, max) => typeof value === 'string' &&
6
+ value.trim().length > 0 && value.length <= max && !/[\x00-\x1f\x7f]/.test(value);
7
+ if (!bounded(input.generation, 256) || !Number.isSafeInteger(input.revision) ||
8
+ input.revision < 0 || typeof input.available !== 'boolean' ||
9
+ !input.launch || typeof input.launch !== 'object')
10
+ return false;
11
+ const launch = input.launch;
12
+ return ['command-override', 'registry', 'registry-fallback', 'direct'].includes(launch.source) &&
13
+ bounded(launch.executable, 4096) && bounded(launch.platform, 64) && bounded(launch.architecture, 64) &&
14
+ launch.transport === 'stdio' && (input.profile === undefined || (typeof input.profile === 'object' && input.profile !== null && !Array.isArray(input.profile)));
15
+ }