@platless/axom-cli 0.5.9 → 0.6.52

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 (258) hide show
  1. package/README.md +25 -6
  2. package/dist/axom-patch/cli/README.md +129 -12
  3. package/dist/axom-patch/cli/account-limits/generate.py +15 -12
  4. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
  5. package/dist/axom-patch/cli/claude-account-limits.json +9 -9
  6. package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
  7. package/dist/axom-patch/cli/codex-account-limits.json +11 -11
  8. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  9. package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
  10. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  11. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  12. package/dist/axom-patch/cli/nightly-task.md +55 -13
  13. package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
  14. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  15. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  16. package/dist/axom-patch/cli/probes/zcode-env-account.mjs +81 -0
  17. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
  18. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1687 -0
  19. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +277 -0
  20. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
  21. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
  22. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  23. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  24. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  25. package/dist/axom-patch/cli/runtime.json +19 -19
  26. package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
  27. package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
  28. package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
  29. package/dist/axom-patch/cli/zcode-env-account.json +58 -0
  30. package/dist/axom-patch/cli/zcode-models.json +9 -10
  31. package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -11
  32. package/dist/cli.js +11 -45
  33. package/dist/commands/auth.js +4 -1
  34. package/dist/commands/knowledge.js +47 -0
  35. package/dist/commands/update.js +126 -3
  36. package/dist/commands/workstation.js +101 -164
  37. package/dist/commands/workstationRegistrations.js +116 -0
  38. package/dist/credentialBroker.js +13 -2
  39. package/dist/credentials.js +16 -0
  40. package/dist/install/claude/commands.js +15 -15
  41. package/dist/keepers/terminal/keeper.mjs +1089 -673
  42. package/dist/keepers/terminal/manifest.json +4 -4
  43. package/dist/mcp.js +1 -1
  44. package/dist/native/darwin-x64/axom-quic +0 -0
  45. package/dist/workstation/acp/acpAgents.js +113 -102
  46. package/dist/workstation/acp/acpClient.js +26 -0
  47. package/dist/workstation/acp/builds/install/npm.js +99 -10
  48. package/dist/workstation/acp/initializeResponse.js +6 -1
  49. package/dist/workstation/acp/managedPatches.js +189 -25
  50. package/dist/workstation/acp/managedRuntime.js +314 -0
  51. package/dist/workstation/acp/outbox.js +12 -5
  52. package/dist/workstation/acp/patchAssets.js +40 -0
  53. package/dist/workstation/acp/permission.js +2 -2
  54. package/dist/workstation/acp/processor/checkpoint.js +26 -34
  55. package/dist/workstation/acp/processor/control.js +7 -80
  56. package/dist/workstation/acp/processor/jsonrpc.js +8 -0
  57. package/dist/workstation/acp/processor/runtime.js +5 -81
  58. package/dist/workstation/acp/processor/session.js +54 -82
  59. package/dist/workstation/acp/processor/state.js +7 -13
  60. package/dist/workstation/acp/processor/step.js +3 -20
  61. package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
  62. package/dist/workstation/acp/processor/transport/local.js +19 -3
  63. package/dist/workstation/acp/processor/updates.js +156 -105
  64. package/dist/workstation/acp/processorSession.js +20 -173
  65. package/dist/workstation/acp/registry/catalog.js +65 -39
  66. package/dist/workstation/acp/registry/launch.js +16 -1
  67. package/dist/workstation/acp/switching.js +0 -3
  68. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  69. package/dist/workstation/acp/test/keeperWorld.js +32 -3
  70. package/dist/workstation/acp/test/raw/sources.js +16 -130
  71. package/dist/workstation/acp/test/realAdapter.js +1 -47
  72. package/dist/workstation/acp/test/scriptAgent.js +11 -3
  73. package/dist/workstation/agentFlow.js +1 -1
  74. package/dist/workstation/agentSessions.js +114 -166
  75. package/dist/workstation/channels/agent.js +9 -1
  76. package/dist/workstation/channels/computer.js +27 -4
  77. package/dist/workstation/channels/management.js +130 -15
  78. package/dist/workstation/channels/url.js +1 -1
  79. package/dist/workstation/config.js +33 -9
  80. package/dist/workstation/connection/encryptedChannel.js +4 -1
  81. package/dist/workstation/control/bindingStore.js +204 -0
  82. package/dist/workstation/control/commands.js +125 -0
  83. package/dist/workstation/control/daemonIdentity.js +143 -0
  84. package/dist/workstation/control/deviceCommands.js +191 -0
  85. package/dist/workstation/control/deviceProtocol.js +68 -0
  86. package/dist/workstation/control/enrollment.js +106 -0
  87. package/dist/workstation/control/inspect.js +71 -0
  88. package/dist/workstation/control/locks.js +110 -0
  89. package/dist/workstation/control/protocol.js +40 -0
  90. package/dist/workstation/control/restartGuard.js +46 -0
  91. package/dist/workstation/control/rootFormat.js +16 -0
  92. package/dist/workstation/control/runtimeClient.js +152 -0
  93. package/dist/workstation/control/runtimeHost.js +235 -0
  94. package/dist/workstation/control/serverIdentity.js +35 -0
  95. package/dist/workstation/control/updateTransition.js +91 -0
  96. package/dist/workstation/daemon.js +266 -264
  97. package/dist/workstation/daemonctl.js +226 -25
  98. package/dist/workstation/desktopControl/audio.js +99 -0
  99. package/dist/workstation/desktopControl/automationDevice.js +219 -0
  100. package/dist/workstation/desktopControl/automationJson.js +74 -0
  101. package/dist/workstation/desktopControl/automationLocalState.js +23 -0
  102. package/dist/workstation/desktopControl/automationProtocol.js +253 -0
  103. package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
  104. package/dist/workstation/desktopControl/automationSession.js +81 -0
  105. package/dist/workstation/desktopControl/automationTransport.js +138 -0
  106. package/dist/workstation/desktopControl/capabilities.js +93 -0
  107. package/dist/workstation/desktopControl/captureModes.js +9 -0
  108. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  109. package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
  110. package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
  111. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  112. package/dist/workstation/desktopControl/helperProcess.js +602 -0
  113. package/dist/workstation/desktopControl/hostGrant.js +65 -0
  114. package/dist/workstation/desktopControl/lobby.js +131 -0
  115. package/dist/workstation/desktopControl/macProvider.js +156 -0
  116. package/dist/workstation/desktopControl/media.js +331 -0
  117. package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
  118. package/dist/workstation/desktopControl/nativeProvider.js +1044 -0
  119. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  120. package/dist/workstation/desktopControl/overview.js +58 -0
  121. package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
  122. package/dist/workstation/desktopControl/protocol.js +311 -0
  123. package/dist/workstation/desktopControl/provider.js +1 -0
  124. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  125. package/dist/workstation/desktopControl/rateController.js +226 -0
  126. package/dist/workstation/desktopControl/supervisor.js +1774 -0
  127. package/dist/workstation/desktopControl/transport.js +27 -0
  128. package/dist/workstation/desktopControl/wallpaper.js +98 -0
  129. package/dist/workstation/exec/policy.js +5 -0
  130. package/dist/workstation/files/client.js +10 -1
  131. package/dist/workstation/files/credentials.js +142 -0
  132. package/dist/workstation/files/host.js +9 -2
  133. package/dist/workstation/files/text.js +42 -8
  134. package/dist/workstation/journal.js +5 -2
  135. package/dist/workstation/keepers/acp/client.js +38 -2
  136. package/dist/workstation/keepers/kit/buildVersion.js +35 -11
  137. package/dist/workstation/keepers/kit/bundle.js +21 -17
  138. package/dist/workstation/keepers/kit/client.js +2 -2
  139. package/dist/workstation/keepers/kit/launcher.js +123 -7
  140. package/dist/workstation/keepers/kit/paths.js +2 -1
  141. package/dist/workstation/keepers/legacy.js +16 -2
  142. package/dist/workstation/keepers/policy.js +5 -34
  143. package/dist/workstation/keepers/terminal/client.js +115 -23
  144. package/dist/workstation/keepers/terminal/exec.js +6 -3
  145. package/dist/workstation/keepers/terminal/frames.js +73 -0
  146. package/dist/workstation/keepers/terminal/generations.js +5 -8
  147. package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
  148. package/dist/workstation/keepers/terminal/input.js +146 -0
  149. package/dist/workstation/keepers/terminal/keeper.js +22 -8
  150. package/dist/workstation/keepers/terminal/main.js +1 -1
  151. package/dist/workstation/keepers/terminal/metadata.js +138 -0
  152. package/dist/workstation/keepers/terminal/protocol.js +4 -28
  153. package/dist/workstation/keepers/terminal/pty.js +1 -330
  154. package/dist/workstation/keepers/terminal/screen.js +246 -0
  155. package/dist/workstation/keepers/terminal/sessions.js +526 -0
  156. package/dist/workstation/managedInstall.js +57 -0
  157. package/dist/workstation/oauth/deviceAuthorization.js +31 -17
  158. package/dist/workstation/protocol/agent.js +2 -15
  159. package/dist/workstation/protocol/management.js +113 -5
  160. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  161. package/dist/workstation/rd/artifact.js +120 -0
  162. package/dist/workstation/rd/control.js +117 -0
  163. package/dist/workstation/rd/display.js +29 -0
  164. package/dist/workstation/rd/host.js +213 -0
  165. package/dist/workstation/rd/migrationReadiness.js +46 -0
  166. package/dist/workstation/rd/provider.js +256 -0
  167. package/dist/workstation/rd/sessions.js +391 -0
  168. package/dist/workstation/rd/state.js +57 -0
  169. package/dist/workstation/rd/update.js +168 -0
  170. package/dist/workstation/rd/upkeep.js +80 -0
  171. package/dist/workstation/register.js +27 -37
  172. package/dist/workstation/registrations/enrollment.js +250 -0
  173. package/dist/workstation/registrations/enrollmentPending.js +361 -0
  174. package/dist/workstation/registrations/migration.js +356 -0
  175. package/dist/workstation/registrations/model.js +34 -0
  176. package/dist/workstation/registrations/removal.js +222 -0
  177. package/dist/workstation/registrations/store.js +737 -0
  178. package/dist/workstation/runtime/desktopBinding.js +20 -0
  179. package/dist/workstation/runtime/deviceDesktop.js +193 -0
  180. package/dist/workstation/runtime/deviceRuntime.js +568 -0
  181. package/dist/workstation/runtime/deviceServices.js +117 -0
  182. package/dist/workstation/runtime/events.js +20 -0
  183. package/dist/workstation/runtime/ownership.js +100 -0
  184. package/dist/workstation/runtime/registrationRuntime.js +575 -0
  185. package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
  186. package/dist/workstation/runtime/removalActivity.js +184 -0
  187. package/dist/workstation/service/installer.js +197 -14
  188. package/dist/workstation/service/launchd.js +2 -0
  189. package/dist/workstation/service/systemd.js +1 -0
  190. package/dist/workstation/storageMetrics.js +118 -0
  191. package/dist/workstation/systemMetrics.js +47 -92
  192. package/dist/workstation/toolchain.js +94 -0
  193. package/dist/workstation/tools/browser.js +28 -4
  194. package/dist/workstation/tools/client.js +8 -27
  195. package/dist/workstation/tools/info.js +6 -13
  196. package/dist/workstation/tools/terminalRouter.js +17 -172
  197. package/dist/workstation/tools/validation.js +55 -1
  198. package/dist/workstation/update/archiveEvidence.js +206 -0
  199. package/dist/workstation/update/artifact.js +2 -1
  200. package/dist/workstation/update/coordinator.js +34 -13
  201. package/dist/workstation/update/deviceCompatibility.js +119 -0
  202. package/dist/workstation/update/deviceCoordinator.js +369 -0
  203. package/dist/workstation/update/deviceHealth.js +104 -0
  204. package/dist/workstation/update/deviceHealthProtocol.js +26 -0
  205. package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
  206. package/dist/workstation/update/deviceRollback.js +78 -0
  207. package/dist/workstation/update/globalInstall.js +186 -0
  208. package/dist/workstation/update/legacyBridge.js +414 -0
  209. package/dist/workstation/update/legacyOwner.js +39 -0
  210. package/dist/workstation/update/npmInstaller.js +51 -25
  211. package/dist/workstation/update/privateDurability.js +61 -0
  212. package/dist/workstation/update/rdRollback.js +145 -0
  213. package/dist/workstation/update/recoveryPlan.js +124 -0
  214. package/dist/workstation/update/runUpdate.js +10 -3
  215. package/dist/workstation/update/taskRegistry.js +161 -3
  216. package/dist/workstation/update/watchdog.js +29806 -333
  217. package/dist/workstation/update/watchdogProcess.js +72 -0
  218. package/package.json +11 -7
  219. package/skills/axom/SKILL.md +11 -11
  220. package/skills/axom-definition/SKILL.md +3 -3
  221. package/skills/axom-platform/SKILL.md +3 -3
  222. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  223. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
  224. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  225. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  226. package/dist/commands/guide.js +0 -38
  227. package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
  228. package/dist/runtime-deps/qoder-sdk/package.json +0 -8
  229. package/dist/workstation/acp/delegationMcp.js +0 -24
  230. package/dist/workstation/acp/processor/sideChannel.js +0 -80
  231. package/dist/workstation/acp/qoderStartup.js +0 -45
  232. package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
  233. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
  234. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
  235. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
  236. package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
  237. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  238. package/dist/workstation/qoderAdapter/contract.js +0 -78
  239. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
  240. package/dist/workstation/qoderAdapter/history.js +0 -85
  241. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  242. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  243. package/dist/workstation/qoderAdapter/main.js +0 -95
  244. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  245. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  246. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  247. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  248. package/dist/workstation/qoderAdapter/profile.js +0 -93
  249. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
  250. package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
  251. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
  252. package/dist/workstation/qoderAdapter/session.js +0 -319
  253. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  254. package/dist/workstation/qoderAdapter/subwork.js +0 -85
  255. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  256. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  257. package/dist/workstation/qoderAdapter/usage.js +0 -37
  258. /package/dist/workstation/{qoderAdapter/types.js → desktopControl/automationPreparation.js} +0 -0
@@ -42,8 +42,11 @@ function requireString(value, field, description) {
42
42
  throw new DeviceAuthorizationError(`${description} missing ${field}`);
43
43
  return value;
44
44
  }
45
- function websocketUrl(server) {
46
- const url = new URL('/oauth/device/ws', server);
45
+ function deviceEndpoint(server, endpoint, identity) {
46
+ return identity ? new URL(server + endpoint) : new URL(endpoint, server);
47
+ }
48
+ function websocketUrl(server, identity) {
49
+ const url = deviceEndpoint(server, '/oauth/device/ws', identity);
47
50
  url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
48
51
  return url.toString();
49
52
  }
@@ -89,23 +92,33 @@ function waitForNotification(socket, sessionId, notificationTicket, deadline, no
89
92
  socket.on('error', () => finish('reconnect'));
90
93
  });
91
94
  }
92
- async function exchangeDeviceCode(server, clientId, deviceCode, fetch_, requestedScopes) {
93
- const response = await requestJson(fetch_, new URL('/oauth/token', server), {
95
+ async function exchangeDeviceCode(server, clientId, deviceCode, fetch_, requestedScopes, identity) {
96
+ const response = await requestJson(fetch_, deviceEndpoint(server, '/oauth/token', identity), {
94
97
  method: 'POST',
95
98
  headers: { 'content-type': 'application/x-www-form-urlencoded' },
96
99
  body: new URLSearchParams({
97
100
  grant_type: DEVICE_CODE_GRANT_TYPE,
98
101
  client_id: clientId,
99
102
  device_code: deviceCode,
100
- device_id: await loadDeviceId(),
103
+ device_id: identity?.deviceId ?? await loadDeviceId(),
101
104
  }),
102
105
  }, 200, 'Device token exchange');
103
- return validateTokenResponse(server, response, requestedScopes, fetch_);
106
+ return validateTokenResponse(server, response, requestedScopes, fetch_, identity);
104
107
  }
105
- async function validateTokenResponse(server, response, requestedScopes, fetch_) {
106
- const accessToken = requireString(response.access_token, 'access_token', 'Device token response');
108
+ async function validateTokenResponse(server, response, requestedScopes, fetch_, identity) {
109
+ let accessToken;
110
+ try {
111
+ accessToken = requireString(response.access_token, 'access_token', 'Device token response');
112
+ }
113
+ catch (error) {
114
+ if (identity && typeof response.refresh_token === 'string' && response.refresh_token)
115
+ await revokeRefreshToken(server, response.refresh_token, fetch_);
116
+ throw error;
117
+ }
107
118
  const refreshToken = requireString(response.refresh_token, 'refresh_token', 'Device token response');
108
119
  if (!Number.isFinite(response.expires_in) || response.expires_in <= 0) {
120
+ if (identity)
121
+ await revokeRefreshToken(server, refreshToken, fetch_);
109
122
  throw new DeviceAuthorizationError('Device token response has invalid expires_in');
110
123
  }
111
124
  let scopes;
@@ -118,15 +131,15 @@ async function validateTokenResponse(server, response, requestedScopes, fetch_)
118
131
  }
119
132
  return { accessToken, refreshToken, expiresInSeconds: response.expires_in, scopes };
120
133
  }
121
- async function pollDeviceCode(server, clientId, deviceCode, fetch_, requestedScopes) {
122
- const response = await fetch_(new URL('/oauth/token', server), {
134
+ async function pollDeviceCode(server, clientId, deviceCode, fetch_, requestedScopes, identity) {
135
+ const response = await fetch_(deviceEndpoint(server, '/oauth/token', identity), {
123
136
  method: 'POST',
124
137
  headers: { 'content-type': 'application/x-www-form-urlencoded' },
125
138
  body: new URLSearchParams({
126
139
  grant_type: DEVICE_CODE_GRANT_TYPE,
127
140
  client_id: clientId,
128
141
  device_code: deviceCode,
129
- device_id: await loadDeviceId(),
142
+ device_id: identity?.deviceId ?? await loadDeviceId(),
130
143
  }),
131
144
  });
132
145
  if (response.status === 400) {
@@ -141,13 +154,13 @@ async function pollDeviceCode(server, clientId, deviceCode, fetch_, requestedSco
141
154
  if (response.status !== 200)
142
155
  throw new DeviceAuthorizationError(`Device token polling failed: ${response.status}`);
143
156
  const token = await response.json();
144
- return validateTokenResponse(server, token, requestedScopes, fetch_);
157
+ return validateTokenResponse(server, token, requestedScopes, fetch_, identity);
145
158
  }
146
159
  /**
147
160
  * Uses OAuth Device Authorization for both desktop and headless clients. The notification socket
148
161
  * only carries state; the tokens exist only in this call's return value.
149
162
  */
150
- export async function authorizeDevice(server, clientName, hostname, deps = {}, requestedScopes = DEFAULT_CLI_SCOPES) {
163
+ export async function authorizeDevice(server, clientName, hostname, deps = {}, requestedScopes = DEFAULT_CLI_SCOPES, identity) {
151
164
  const baseUrl = normalizedServer(server);
152
165
  const fetch_ = deps.fetch ?? globalThis.fetch;
153
166
  const print = deps.print ?? console.log;
@@ -155,9 +168,10 @@ export async function authorizeDevice(server, clientName, hostname, deps = {}, r
155
168
  const createWebSocket = deps.createWebSocket ?? ((url) => new WebSocket(url));
156
169
  const now = deps.now ?? Date.now;
157
170
  const wait = deps.sleep ?? sleep;
171
+ const flowIdentity = identity ? Object.freeze({ deviceId: requireString(identity.deviceId, 'deviceId', 'Device authorization identity') }) : undefined;
158
172
  void clientName;
159
173
  const clientId = 'axom-cli';
160
- const authorization = await requestJson(fetch_, new URL('/oauth/device_authorization', baseUrl), {
174
+ const authorization = await requestJson(fetch_, deviceEndpoint(baseUrl, '/oauth/device_authorization', flowIdentity), {
161
175
  method: 'POST',
162
176
  headers: { 'content-type': 'application/x-www-form-urlencoded' },
163
177
  body: new URLSearchParams({ client_id: clientId, hostname, scope: requestedScopes.join(' ') }),
@@ -176,7 +190,7 @@ export async function authorizeDevice(server, clientName, hostname, deps = {}, r
176
190
  while (now() < deadline) {
177
191
  let socket;
178
192
  try {
179
- socket = createWebSocket(websocketUrl(baseUrl));
193
+ socket = createWebSocket(websocketUrl(baseUrl, flowIdentity));
180
194
  }
181
195
  catch {
182
196
  const delay = Math.min(retry.recordFailure(), Math.max(0, deadline - now()));
@@ -187,14 +201,14 @@ export async function authorizeDevice(server, clientName, hostname, deps = {}, r
187
201
  }
188
202
  const status = await waitForNotification(socket, sessionId, notificationTicket, deadline, now, () => retry.recordSuccess());
189
203
  if (status === 'approved') {
190
- const tokens = await exchangeDeviceCode(baseUrl, clientId, deviceCode, fetch_, requestedScopes);
204
+ const tokens = await exchangeDeviceCode(baseUrl, clientId, deviceCode, fetch_, requestedScopes, flowIdentity);
191
205
  return { clientId, ...tokens };
192
206
  }
193
207
  if (status === 'denied')
194
208
  throw new DeviceAuthorizationError('Device authorization was denied');
195
209
  if (status === 'expired')
196
210
  break;
197
- const polled = await pollDeviceCode(baseUrl, clientId, deviceCode, fetch_, requestedScopes);
211
+ const polled = await pollDeviceCode(baseUrl, clientId, deviceCode, fetch_, requestedScopes, flowIdentity);
198
212
  if (polled)
199
213
  return { clientId, ...polled };
200
214
  const pollingFloor = Math.max(1, authorization.interval ?? 5) * 1_000;
@@ -1,5 +1,5 @@
1
1
  import { accountLimitsSchema } from './accountLimits.js';
2
- import { isRuntimeDiagnostics } from '../qoderAdapter/profile.js';
2
+ import { isRuntimeDiagnostics } from './runtimeDiagnostics.js';
3
3
  import { decodeChannelFrame, hasOptional, hasRequired, isBoolean, isBridgeInfoArray, isNullableAuthMethodInfoArray, isNullableString, isNumber, isObject, isString, isStringArray, isStringRecord, } from './common.js';
4
4
  /** Current version and feature floors for the independently negotiated agent protocol. */
5
5
  export const AGENT_WIRE_VERSION = 27;
@@ -14,9 +14,7 @@ export const AGENT_WIRE_SWITCH_IDLE_VERSION = 26;
14
14
  export const AGENT_WIRE_NATIVE_DELETE_VERSION = 24;
15
15
  export const AGENT_WIRE_PROVIDER_TASK_VERSION = 23;
16
16
  export const AGENT_WIRE_RUNTIME_PROFILE_VERSION = 22;
17
- export const AGENT_WIRE_DELEGATION_RENEW_VERSION = 21;
18
17
  export const AGENT_WIRE_TURN_IDENTITY_VERSION = 20;
19
- export const AGENT_WIRE_DELEGATION_MCP_VERSION = 19;
20
18
  /** Scoped replay/history is enabled only when both peers preserve native origin. */
21
19
  export const AGENT_WIRE_SCOPED_ORIGIN_VERSION = 18;
22
20
  /** agent_open.requireSessionLoad: reject unsupported/failed load without session/new fallback. */
@@ -246,9 +244,6 @@ export const AGENT_FRAME_SHAPES = {
246
244
  env: isStringRecord, pathPrepend: isStringArray, agentEnv: isStringRecord, command: isNullableString,
247
245
  args: isStringArray, nativeSessionId: isNullableString,
248
246
  resumeFromSeq: isNumber, backfill: isBoolean, requireSessionLoad: isBoolean,
249
- delegationMcp: (value) => isObject(value) &&
250
- isString(value.url) && isStringRecord(value.headers) && isNumber(value.expiresAtEpochMs) &&
251
- (value.bindingId === undefined || isString(value.bindingId)),
252
247
  },
253
248
  },
254
249
  agent_opened: {
@@ -265,9 +260,6 @@ export const AGENT_FRAME_SHAPES = {
265
260
  // `reloaded` is `true`-or-absent for the same reason as `supportsSteering`: a `false` would
266
261
  // be a second spelling of what its absence already says.
267
262
  reloaded: isVerifiedTrue,
268
- delegationExpiresAtEpochMs: isNumber,
269
- delegationRefreshPending: isVerifiedTrue,
270
- delegationInstanceId: isString, delegationBindingId: isString, delegationRenewalRevision: isNumber,
271
263
  agentInfo: isAgentInfo, supportsImageInput: isVerifiedTrue, configOptions: isNullableSessionConfigOptionArray,
272
264
  // Same guard as `agent_verified.authMethods`, and the same absent-vs-`[]` reading: a
273
265
  // half-decoded method table is worse than none, because the reader would offer a login the
@@ -286,10 +278,6 @@ export const AGENT_FRAME_SHAPES = {
286
278
  },
287
279
  },
288
280
  agent_prompt: { required: { sessionRef: isString, text: isString }, optional: { images: isImageBlockArray, platformTurnId: isString } },
289
- agent_delegation_renew: { required: { id: isString, sessionRef: isString, nativeSessionId: isString,
290
- instanceId: isString, bindingId: isString, renewalRevision: isNumber, expiresAtEpochMs: isNumber } },
291
- agent_delegation_renewed: { required: { id: isString, ok: isBoolean }, optional: { error: isString,
292
- nativeSessionId: isString, instanceId: isString, bindingId: isString, renewalRevision: isNumber, expiresAtEpochMs: isNumber } },
293
281
  agent_answer: { required: { sessionRef: isString, requestId: isString, outcome: isString } },
294
282
  // `content`'s interior is never validated here -- it is ACP's free-form elicitation payload, and
295
283
  // this table is transport, not translator. `isRecord` only rejects a non-object.
@@ -773,7 +761,6 @@ export function outboundFailureReply(frame, reason) {
773
761
  case 'agent_steered': return { t: 'agent_steered', id, ok: false, error };
774
762
  case 'agent_account_read_result': return { t: 'agent_account_read_result', id, ok: false, error };
775
763
  case 'agent_config_option_set': return { t: 'agent_config_option_set', id, ok: false, error };
776
- case 'agent_delegation_renewed': return { t: 'agent_delegation_renewed', id, ok: false, error };
777
764
  case 'agent_switch_idle_result': return { t: 'agent_switch_idle_result', id, ok: false, error };
778
765
  case 'agent_provider_task_stopped': return { t: 'agent_provider_task_stopped', id, state: 'unknown', error };
779
766
  case 'agent_backfill_done':
@@ -790,7 +777,7 @@ export function decodeAgentFrame(text) {
790
777
  if ((frame.state === 'completed' || frame.state === 'failed' || frame.state === 'stopped') && frame.live)
791
778
  throw new TypeError('Invalid provider task terminal live state');
792
779
  if (frame.status !== undefined &&
793
- (frame.status !== frame.state || !['completed', 'failed', 'stopped'].includes(frame.status)))
780
+ (frame.status !== frame.state || !['completed', 'failed', 'stopped'].some(status => status === frame.status)))
794
781
  throw new TypeError('Invalid provider task terminal status');
795
782
  if (frame.summary !== undefined && frame.status === undefined)
796
783
  throw new TypeError('Provider task summary requires terminal status');
@@ -1,3 +1,5 @@
1
+ import { decodeDesktopManagementFrame, parseDesktopJson, DesktopProtocolError } from '../desktopControl/protocol.js';
2
+ import { parseAutomationJson } from '../desktopControl/automationJson.js';
1
3
  import { decodeChannelFrame, hasOptional, hasRequired, isBoolean, isBridgeInfoArray, isNullableString, isNumber, isObject, isString, } from './common.js';
2
4
  export const MANAGEMENT_WIRE_MIN_VERSION = 1;
3
5
  /** Adds the `DRAINING` value to ClientUpdatePhase. */
@@ -12,7 +14,9 @@ export const MANAGEMENT_WIRE_LOAD_AVERAGES_VERSION = 6;
12
14
  export const MANAGEMENT_WIRE_GPU_VERSION = 7;
13
15
  /** Adds `keepers` to heartbeats and the `keeper_control` / `keeper_control_result` pair (spec §8.1, Task 26). */
14
16
  export const MANAGEMENT_WIRE_KEEPERS_VERSION = 8;
15
- export const MANAGEMENT_WIRE_VERSION = MANAGEMENT_WIRE_KEEPERS_VERSION;
17
+ export const MANAGEMENT_WIRE_DESKTOP_VERSION = 9;
18
+ export const MANAGEMENT_WIRE_DEVICE_OWNERSHIP_VERSION = 10;
19
+ export const MANAGEMENT_WIRE_VERSION = MANAGEMENT_WIRE_DEVICE_OWNERSHIP_VERSION;
16
20
  /** Bounds of one heartbeat's `keepers` (spec §8.1). */
17
21
  export const KEEPER_STATUS_MAX_ITEMS = 32;
18
22
  export const KEEPER_LABEL_MAX_CHARS = 64;
@@ -57,6 +61,7 @@ function isUpdateStatus(value) {
57
61
  && hasOptional(value, {
58
62
  commandId: isString, version: isString, releaseSha512: isString, force: isBoolean,
59
63
  activeShellTasks: isNumber, activeAgentTasks: isNumber, error: isString,
64
+ remoteForcePolicy: policy => policy === 'drain-only',
60
65
  });
61
66
  }
62
67
  function isOsInfo(value) {
@@ -128,7 +133,7 @@ function isManagementFrame(value) {
128
133
  }) && hasOptional(value, { bridges: isBridgeInfoArray });
129
134
  case 'management_ready': return hasRequired(value, {
130
135
  serverNonce: isString, generation: isString, protocols: isSelectedProtocolMap,
131
- }) && hasOptional(value, { release: isNullable(isRelease) });
136
+ }) && hasOptional(value, { release: isNullable(isRelease), ownerUserId: isString });
132
137
  case 'heartbeat': return hasRequired(value, {
133
138
  seq: isNumber, sentAtEpochMs: isNumber, clientVersion: isString,
134
139
  channels: (channels) => isObject(channels) && Object.entries(channels).every(([kind, state]) => CHANNEL_KINDS.has(kind) && isChannelState(state)),
@@ -137,14 +142,16 @@ function isManagementFrame(value) {
137
142
  playgroundCwd: isString,
138
143
  protocols: (protocols) => isProtocolRangeMap(protocols, false),
139
144
  os: isNullable(isOsInfo),
145
+ toolchain: (toolchain) => isObject(toolchain) && hasRequired(toolchain, { python: isNullableString, node: isNullableString }),
140
146
  update: isNullable(isUpdateStatus),
141
147
  metrics: isNullable(isWorkstationMetrics),
142
148
  keepers: isNullable((keepers) => Array.isArray(keepers) && keepers.every(isKeeperStatus)),
149
+ capabilitiesRevision: value => Number.isSafeInteger(value) && value > 0,
143
150
  });
144
151
  case 'heartbeat_ack': return hasRequired(value, { seq: isNumber });
145
152
  case 'channel_ticket_request': return hasRequired(value, { channel: isChannelKind });
146
153
  case 'channel_ticket': return hasRequired(value, { channel: isChannelKind, ticket: isString, expiresAtEpochMs: isNumber });
147
- case 'client_update': return hasRequired(value, { commandId: isString, release: isRelease, force: isBoolean });
154
+ case 'client_update': return hasRequired(value, { commandId: isString, release: isRelease, force: isBoolean }) && hasOptional(value, { remoteForcePolicy: value => value === 'drain-only' });
148
155
  case 'client_update_status': return hasRequired(value, { status: isUpdateStatus });
149
156
  case 'external_agents_scan': return hasRequired(value, { commandId: isString });
150
157
  case 'external_agents_scan_result': return hasRequired(value, { commandId: isString, bridges: isBridgeInfoArray });
@@ -157,8 +164,109 @@ function isManagementFrame(value) {
157
164
  }
158
165
  }
159
166
  export function encodeManagementFrame(frame) {
160
- return JSON.stringify(frame);
167
+ const text = JSON.stringify(frame);
168
+ if (frame.t === 'desktop_automation_prepare')
169
+ decodeAutomationPrepare(text);
170
+ if (isDesktopManagementType(frame.t))
171
+ decodeDesktopManagementFrame(text);
172
+ return text;
161
173
  }
162
- export function decodeManagementFrame(text) {
174
+ export function decodeManagementFrame(source) {
175
+ const text = typeof source === 'string' ? source : new TextDecoder('utf-8', { fatal: true }).decode(source);
176
+ // Desktop extensions get the strict lexical/shape guard before any lossy object parsing.
177
+ const parsed = JSON.parse(text);
178
+ if (isObject(parsed) && parsed.t === 'desktop_automation_prepare')
179
+ return decodeAutomationPrepare(text);
180
+ if (isObject(parsed) && isDesktopManagementType(parsed.t))
181
+ return decodeDesktopManagementFrame(text);
182
+ if (isObject(parsed) && parsed.t === 'heartbeat' && Object.hasOwn(parsed, 'capabilitiesRevision'))
183
+ validateHeartbeatRevision(text);
163
184
  return decodeChannelFrame(text, DISCRIMINATORS, isManagementFrame);
164
185
  }
186
+ function decodeAutomationPrepare(text) {
187
+ const value = parseAutomationJson(text, 49152).value;
188
+ if (Object.keys(value).sort().join(',') !== 'automationProtocol,binding,deviceGeneration,grant,t' || value.automationProtocol !== 1 ||
189
+ typeof value.grant !== 'string' || !/^[A-Za-z0-9_-]{43}$/.test(value.grant) ||
190
+ value.binding?.route?.deviceGeneration !== value.deviceGeneration || !value.binding?.native || !value.binding?.preparation || typeof value.binding.executionId !== 'string')
191
+ throw new DesktopProtocolError('Invalid automation preparation');
192
+ return value;
193
+ }
194
+ /** Select only the new root field. JSON.parse already validates syntax; legacy nesting, size,
195
+ * unknown fields and duplicate legacy keys keep their established decoder behavior. */
196
+ function validateHeartbeatRevision(text) {
197
+ let depth = 0, count = 0;
198
+ for (let offset = 0; offset < text.length; offset++) {
199
+ const char = text[offset];
200
+ if (char === '"') {
201
+ const start = offset++;
202
+ while (offset < text.length) {
203
+ if (text[offset] === '\\')
204
+ offset += 2;
205
+ else if (text[offset++] === '"')
206
+ break;
207
+ }
208
+ const end = offset--;
209
+ if (depth !== 1 || JSON.parse(text.slice(start, end)) !== 'capabilitiesRevision')
210
+ continue;
211
+ let tokenStart = end;
212
+ while (/\s/.test(text[tokenStart] ?? ''))
213
+ tokenStart++;
214
+ if (text[tokenStart] !== ':')
215
+ continue; // This string was a legacy field's value.
216
+ let tokenEnd = ++tokenStart;
217
+ while (tokenEnd < text.length && text[tokenEnd] !== ',' && text[tokenEnd] !== '}')
218
+ tokenEnd++;
219
+ const token = text.slice(tokenStart, tokenEnd).trim();
220
+ if (++count !== 1 || !/^[1-9][0-9]*$/.test(token) || !Number.isSafeInteger(Number(token))) {
221
+ throw new DesktopProtocolError('Invalid capabilities revision');
222
+ }
223
+ parseDesktopJson(`{"capabilitiesRevision":${token}}`);
224
+ }
225
+ else if (char === '{' || char === '[')
226
+ depth++;
227
+ else if (char === '}' || char === ']')
228
+ depth--;
229
+ }
230
+ }
231
+ function isDesktopManagementType(type) {
232
+ return typeof type === 'string' && ['desktop_background', 'desktop_lobby', 'desktop_lobby_result', 'workstation_capabilities', 'workstation_capabilities_request', 'desktop_prepare',
233
+ 'desktop_prepare_result', 'desktop_cancel', 'desktop_isolation_result', 'desktop_session_state', 'desktop_ownership', 'desktop_ownership_result'].includes(type);
234
+ }
235
+ export class ManagementFrameAdmission {
236
+ isCurrent;
237
+ ready;
238
+ disposed = false;
239
+ constructor(isCurrent = () => true) {
240
+ this.isCurrent = isCurrent;
241
+ }
242
+ admit(frame) {
243
+ if (this.disposed || !this.isCurrent())
244
+ return false;
245
+ if (frame.t === 'management_ready') {
246
+ if (this.ready || !Number.isInteger(frame.protocols.MANAGEMENT) || frame.protocols.MANAGEMENT < MANAGEMENT_WIRE_MIN_VERSION
247
+ || frame.protocols.MANAGEMENT > MANAGEMENT_WIRE_VERSION || !frame.generation)
248
+ return false;
249
+ this.ready = frame;
250
+ return true;
251
+ }
252
+ if (frame.t === 'heartbeat' && frame.capabilitiesRevision !== undefined)
253
+ return false;
254
+ if (frame.t === 'desktop_ownership')
255
+ return !!this.ready && this.ready.protocols.MANAGEMENT >= 10 && frame.deviceGeneration === this.ready.generation;
256
+ if (frame.t === 'desktop_automation_prepare')
257
+ return !!this.ready && this.ready.protocols.MANAGEMENT >= MANAGEMENT_WIRE_DESKTOP_VERSION && frame.deviceGeneration === this.ready.generation;
258
+ if (!isDesktopManagementType(frame.t))
259
+ return true;
260
+ if (!this.ready || this.ready.protocols.MANAGEMENT < MANAGEMENT_WIRE_DESKTOP_VERSION)
261
+ return false;
262
+ switch (frame.t) {
263
+ case 'workstation_capabilities_request': return frame.generation === this.ready.generation;
264
+ case 'desktop_lobby':
265
+ case 'desktop_background':
266
+ case 'desktop_prepare': return frame.deviceGeneration === this.ready.generation;
267
+ case 'desktop_cancel': return true; // Current socket binding; T10 additionally fences its session/control generation.
268
+ default: return false; // Device-authored reports/results/states cannot be echoed as server activity.
269
+ }
270
+ }
271
+ dispose() { this.disposed = true; }
272
+ }
@@ -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
+ }
@@ -0,0 +1,120 @@
1
+ import { promises as fs, constants } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { gunzipSync } from 'node:zlib';
5
+ import { z } from 'zod';
6
+ import { deploymentDownloadURL } from '../control/serverIdentity.js';
7
+ import { downloadVerified } from '../update/artifact.js';
8
+ import { privateDirectory } from '../keepers/kit/paths.js';
9
+ const legacyRdFiles = ['bin/axom-desktop-keeper', 'bin/axom-input-guardian', 'bin/axom-quic', 'xorg.conf'];
10
+ export const RD_DEVICE_HOST_PLATFORM = 'linux-x64-debian-13-device-host';
11
+ export const rdFiles = [...legacyRdFiles, 'bin/axom-display', 'bin/axom-desktop-session'];
12
+ const sha256 = z.string().regex(/^[a-f0-9]{64}$/);
13
+ export const rdArtifactSchema = z.object({ version: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._+-]{0,127}$/),
14
+ filename: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._+-]*\.(?:tar\.gz|tgz)$/),
15
+ url: z.string(), size: z.number().int().min(1).max(134217728), sha512: z.string().regex(/^[a-f0-9]{128}$/),
16
+ unpackedSize: z.number().int().min(1).max(268435456), ipcVersion: z.union([z.literal(1), z.literal(2)]),
17
+ files: z.record(z.string(), sha256),
18
+ }).refine(artifact => {
19
+ const files = artifact.ipcVersion === 1 ? legacyRdFiles : rdFiles;
20
+ const licenses = artifact.ipcVersion === 2 ? ['opus-LICENSE', 'webp-LICENSE'].filter(file => file in artifact.files) : [];
21
+ return Object.keys(artifact.files).length === files.length + licenses.length && files.every(file => file in artifact.files);
22
+ }, 'RD artifact files do not match its runtime version');
23
+ export function rdPlatform(osRelease, architecture) {
24
+ const values = Object.fromEntries(osRelease.split('\n').map(line => /^([A-Z_]+)=["']?([^"']*)["']?$/.exec(line)).filter(Boolean).map(match => [match[1], match[2]]));
25
+ if (values.ID !== 'debian' || values.VERSION_ID !== '13' || architecture !== 'x64') {
26
+ throw new Error('Remote desktop currently supports Debian 13 amd64; this distribution/architecture has no qualified RD package');
27
+ }
28
+ return RD_DEVICE_HOST_PLATFORM;
29
+ }
30
+ export function unpackRd(bytes, artifact) {
31
+ const archive = gunzipSync(bytes, { maxOutputLength: artifact.unpackedSize + 65536 });
32
+ const files = new Map();
33
+ let unpackedSize = 0;
34
+ let terminated = false;
35
+ for (let offset = 0; offset + 512 <= archive.length;) {
36
+ const header = archive.subarray(offset, offset + 512);
37
+ if (header.every(byte => byte === 0)) {
38
+ if (offset + 1024 > archive.length || archive.subarray(offset).some(byte => byte !== 0))
39
+ throw new Error('RD archive has an invalid terminator or trailing records');
40
+ terminated = true;
41
+ break;
42
+ }
43
+ const field = (start, length) => header.subarray(start, start + length).toString('utf8').replace(/\0.*$/s, '');
44
+ const name = field(0, 100), prefix = field(345, 155), sizeText = field(124, 12).trim();
45
+ const checksum = field(148, 8).trim();
46
+ const actualChecksum = header.reduce((sum, byte, index) => sum + (index >= 148 && index < 156 ? 32 : byte), 0);
47
+ if (field(257, 6) !== 'ustar' || field(263, 2) !== '00' || !/^[0-7]+$/.test(checksum) || Number.parseInt(checksum, 8) !== actualChecksum)
48
+ throw new Error('RD archive has an invalid USTAR header');
49
+ if (prefix || !/^[0-7]+$/.test(sizeText) || !Object.hasOwn(artifact.files, name) || files.has(name) || ![0, 48].includes(header[156])) {
50
+ throw new Error('RD archive contains an unexpected file, path, link or duplicate');
51
+ }
52
+ const size = Number.parseInt(sizeText, 8);
53
+ offset += 512;
54
+ unpackedSize += size;
55
+ if (!Number.isSafeInteger(size) || offset + size > archive.length || unpackedSize > artifact.unpackedSize)
56
+ throw new Error('RD archive exceeds its declared size');
57
+ const contents = archive.subarray(offset, offset + size);
58
+ if (createHash('sha256').update(contents).digest('hex') !== artifact.files[name])
59
+ throw new Error(`RD component checksum mismatch: ${name}`);
60
+ files.set(name, contents);
61
+ offset += Math.ceil(size / 512) * 512;
62
+ }
63
+ if (!terminated || files.size !== Object.keys(artifact.files).length || unpackedSize !== artifact.unpackedSize)
64
+ throw new Error('RD archive is incomplete');
65
+ return files;
66
+ }
67
+ export async function verifyRdInstallation(directory, artifact) {
68
+ for (const name of Object.keys(rdArtifactSchema.parse(artifact).files)) {
69
+ const file = await fs.open(path.join(directory, name), constants.O_RDONLY | constants.O_NOFOLLOW);
70
+ try {
71
+ const info = await file.stat();
72
+ if (!info.isFile() || info.uid !== process.getuid?.() || info.mode & 0o022 || info.size > artifact.unpackedSize)
73
+ throw new Error('Unsafe RD installation');
74
+ if (createHash('sha256').update(await file.readFile()).digest('hex') !== artifact.files[name])
75
+ throw new Error(`RD component checksum mismatch: ${name}`);
76
+ }
77
+ finally {
78
+ await file.close();
79
+ }
80
+ }
81
+ }
82
+ export async function installRd(directory, server, platform, fetchImpl = globalThis.fetch) {
83
+ const response = await fetchImpl(deploymentDownloadURL(server, '/download/client/desktop/manifest.json'), { redirect: 'error', signal: AbortSignal.timeout(15000) });
84
+ if (!response.ok)
85
+ throw new Error(`The server does not publish Linux RD packages (HTTP ${response.status})`);
86
+ const text = await response.text();
87
+ if (Buffer.byteLength(text) > 1048576)
88
+ throw new Error('RD catalog is too large');
89
+ const catalog = z.object({ schemaVersion: z.literal(1), artifacts: z.record(z.string(), rdArtifactSchema) }).parse(JSON.parse(text));
90
+ const artifact = catalog.artifacts[platform];
91
+ if (!artifact)
92
+ throw new Error(`The server has no RD package for ${platform}`);
93
+ if (artifact.url !== `/download/client/desktop/${artifact.filename}`)
94
+ throw new Error('RD artifact URL does not match its filename');
95
+ const target = path.join(directory, 'rd', 'versions', artifact.sha512.slice(0, 32));
96
+ try {
97
+ await verifyRdInstallation(target, artifact);
98
+ return { directory: target, artifact };
99
+ }
100
+ catch (error) {
101
+ if (error.code !== 'ENOENT')
102
+ throw error;
103
+ }
104
+ const downloaded = await downloadVerified(artifact, { server, fetch: fetchImpl });
105
+ await privateDirectory(path.dirname(target));
106
+ const staging = await fs.mkdtemp(path.join(path.dirname(target), '.install-'));
107
+ try {
108
+ const files = unpackRd(await fs.readFile(downloaded.path), artifact);
109
+ await privateDirectory(path.join(staging, 'bin'));
110
+ for (const [name, contents] of files)
111
+ await fs.writeFile(path.join(staging, name), contents, { flag: 'wx', mode: name.startsWith('bin/') ? 0o700 : 0o600 });
112
+ await verifyRdInstallation(staging, artifact);
113
+ await fs.rename(staging, target);
114
+ return { directory: target, artifact };
115
+ }
116
+ finally {
117
+ await downloaded.cleanup();
118
+ await fs.rm(staging, { recursive: true, force: true });
119
+ }
120
+ }
@@ -0,0 +1,117 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ import { z } from 'zod';
6
+ import { loadConfig, resolveServer } from '../../config.js';
7
+ import { loadClientConfig } from '../config.js';
8
+ import { normalizeWorkstationServer } from '../control/serverIdentity.js';
9
+ import { withPrivateLock } from '../control/locks.js';
10
+ import { installRd, rdPlatform } from './artifact.js';
11
+ import { readRdState, writeRdState } from './state.js';
12
+ import { chooseSession, discoverSessions } from './sessions.js';
13
+ const execute = promisify(execFile);
14
+ export function rdSessionSelection(previous, options) {
15
+ const session = z.enum(['auto', 'existing', 'virtual']).parse(options.rdSession ?? previous?.session ?? 'auto');
16
+ const display = options.rdDisplay ?? (options.rdSession === undefined ? previous?.display : undefined);
17
+ if (display !== undefined && !/^:\d+(?:\.\d+)?$/.test(display))
18
+ throw new Error('--rd-display must be a local X11 display such as :10');
19
+ if (session === 'virtual' && display)
20
+ throw new Error('--rd-display selects an existing session and cannot be combined with --rd-session virtual');
21
+ return { session, display };
22
+ }
23
+ export async function configureRd(directory, options) {
24
+ if (!options.enableRd && !options.disableRd && options.rdSession === undefined && options.rdDisplay === undefined)
25
+ return;
26
+ if (process.platform !== 'linux' || process.getuid?.() === 0)
27
+ throw new Error('Linux RD must be configured and run as the workstation user, not root');
28
+ if (options.enableRd && options.disableRd)
29
+ throw new Error('--enable-rd and --disable-rd cannot be combined');
30
+ if (!options.enableRd && !options.disableRd)
31
+ throw new Error('--rd-session and --rd-display require --enable-rd');
32
+ await withPrivateLock(path.join(directory, 'rd', 'install'), async () => {
33
+ const previous = await readRdState(directory);
34
+ const registered = await loadClientConfig(directory).catch(error => {
35
+ if (error.code !== 'ENOENT')
36
+ throw error;
37
+ return undefined;
38
+ });
39
+ const server = normalizeWorkstationServer(options.server ?? registered?.server ?? resolveServer(await loadConfig()));
40
+ if (options.disableRd) {
41
+ await writeRdState(directory, previous ? { ...previous, enabled: false } : { schemaVersion: 2, enabled: false, artifactSource: server, session: 'auto' });
42
+ return;
43
+ }
44
+ const { session, display } = rdSessionSelection(previous, options);
45
+ const platform = rdPlatform(await fs.readFile('/etc/os-release', 'utf8'), process.arch);
46
+ const installed = await installRd(directory, server, platform);
47
+ await ensureDependencies(installed.directory, session, display, installed.artifact);
48
+ await writeRdState(directory, { schemaVersion: 2, enabled: true, artifactSource: server, session, ...(display ? { display } : {}),
49
+ installation: installed.directory, artifact: installed.artifact });
50
+ console.log(`Remote desktop enabled (${session}, ${installed.artifact.version}); capture starts only when an authorized viewer connects`);
51
+ });
52
+ }
53
+ async function ensureDependencies(installation, session, display, artifact) {
54
+ let runtimeMissing = false;
55
+ try {
56
+ await execute(path.join(installation, 'bin/axom-desktop-keeper'), ['--self-test'], { timeout: 5000, maxBuffer: 8192 });
57
+ }
58
+ catch {
59
+ runtimeMissing = true;
60
+ }
61
+ const selected = chooseSession(await discoverSessions(), { session, display });
62
+ const packages = runtimeMissing ? ['libavcodec61', 'libavutil59', 'libswscale8', 'libgstreamer-plugins-bad1.0-0', 'libx11-6', 'libxext6', 'libxdamage1', 'libxfixes3', 'libxtst6', 'libxrandr2', 'libxcomposite1', 'zlib1g'] : [];
63
+ if (artifact?.ipcVersion === 2)
64
+ packages.push('python3', 'x11-xserver-utils', 'x11-utils', 'xauth', 'libpulse0', 'pulseaudio', 'pulseaudio-utils', 'libasound2-plugins');
65
+ if (!selected)
66
+ packages.push('xserver-xorg-core', 'xserver-xorg-video-dummy', 'xfce4-session', 'xfwm4', 'xfce4-panel', 'xfdesktop4', 'xfce4-settings', 'thunar', 'xfce4-terminal', 'dbus-x11', 'xauth', 'x11-utils', 'fonts-dejavu-core');
67
+ if (!packages.length)
68
+ return;
69
+ const missing = [];
70
+ for (const name of packages) {
71
+ try {
72
+ const { stdout } = await execute('dpkg-query', ['-W', '-f=${db:Status-Status}', name], { timeout: 2000, maxBuffer: 4096 });
73
+ if (stdout.trim() !== 'installed')
74
+ missing.push(name);
75
+ }
76
+ catch {
77
+ missing.push(name);
78
+ }
79
+ }
80
+ if (missing.length) {
81
+ console.log(`Installing remote desktop dependencies: ${missing.join(', ')}`);
82
+ try {
83
+ await execute('sudo', ['-n', 'apt-get', 'install', '-y', '--no-install-recommends', ...missing], { timeout: 180000, maxBuffer: 1048576 });
84
+ }
85
+ catch {
86
+ throw new Error(`Install the required RD packages, then retry: sudo apt-get install --no-install-recommends ${missing.join(' ')}`);
87
+ }
88
+ }
89
+ await execute(path.join(installation, 'bin/axom-desktop-keeper'), ['--self-test'], { timeout: 5000, maxBuffer: 8192 });
90
+ if (artifact?.ipcVersion === 2) {
91
+ for (const command of ['axom-display', 'axom-desktop-session'])
92
+ await execute(path.join(installation, 'bin', command), ['--self-test'], { timeout: 5000, maxBuffer: 8192 });
93
+ }
94
+ }
95
+ export async function rdStatus(directory) {
96
+ const state = await readRdState(directory);
97
+ if (!state)
98
+ return { enabled: false, installed: false };
99
+ let observed;
100
+ let update;
101
+ try {
102
+ observed = JSON.parse(await fs.readFile(path.join(directory, 'rd', 'status.json'), 'utf8'));
103
+ }
104
+ catch (error) {
105
+ if (error.code !== 'ENOENT')
106
+ throw error;
107
+ }
108
+ try {
109
+ update = JSON.parse(await fs.readFile(path.join(directory, 'rd', 'update-status.json'), 'utf8'));
110
+ }
111
+ catch (error) {
112
+ if (error.code !== 'ENOENT')
113
+ throw error;
114
+ }
115
+ return { enabled: state.enabled, installed: state.installation !== undefined, version: state.artifact?.version, selection: state.session, artifactSource: state.artifactSource,
116
+ ...(state.display ? { requestedDisplay: state.display } : {}), ...(state.enabled ? { observed, update } : {}) };
117
+ }
@@ -0,0 +1,29 @@
1
+ import { captureSamplingModes } from '../desktopControl/captureModes.js';
2
+ export function parseXrandr(text) {
3
+ const screen = /current (\d+) x (\d+)/.exec(text);
4
+ const outputs = [...text.matchAll(/^(\S+) connected(?: primary)? (\d+)x(\d+)\+(\d+)\+(\d+).*$/gm)];
5
+ if (!screen || outputs.length !== 1 || outputs[0][4] !== '0' || outputs[0][5] !== '0' ||
6
+ screen[1] !== outputs[0][2] || screen[2] !== outputs[0][3])
7
+ throw new Error('No consistent single-output X11 desktop is available');
8
+ const output = outputs[0];
9
+ const section = text.slice(output.index + output[0].length).split(/^\S/m)[0];
10
+ const modes = [...section.matchAll(/^\s+(\d+)x(\d+)(?:\S*)\s/gm)]
11
+ .map(match => ({ width: Number(match[1]), height: Number(match[2]) }))
12
+ .filter(mode => mode.width >= 2 && mode.height >= 2 && mode.width <= 4096 && mode.height <= 4096 && mode.width % 2 === 0 && mode.height % 2 === 0);
13
+ return { output: output[1], width: Number(output[2]), height: Number(output[3]),
14
+ modes: modes.filter((mode, index) => modes.findIndex(other => other.width === mode.width && other.height === mode.height) === index) };
15
+ }
16
+ export function desktopDisplayControl(target, actual, managed) {
17
+ if (target.kind !== 'window' && target.width === actual.width && target.height === actual.height &&
18
+ target.width % 2 === 0 && target.height % 2 === 0 && !/^rdp\d+$/i.test(actual.output)) {
19
+ if (managed && /^DUMMY\d+$/i.test(actual.output))
20
+ return { ...target, overview: true, resize: 'dynamic',
21
+ resizeRange: { minWidth: 320, minHeight: 320, maxWidth: 4096, maxHeight: 4096 },
22
+ modes: [{ width: 1280, height: 720 }, { width: 1920, height: 1080 }, { width: 1024, height: 768 }, { width: 720, height: 1280 }] };
23
+ if (actual.modes.length > 1)
24
+ return { ...target, overview: true, resize: 'modes', modes: actual.modes };
25
+ }
26
+ return { ...target, overview: true, resize: 'dynamic', captureViewport: true, capturePixelScale: 1,
27
+ resizeRange: { minWidth: 2, minHeight: 2, maxWidth: target.width & ~1, maxHeight: target.height & ~1 },
28
+ modes: captureSamplingModes(target.width, target.height) };
29
+ }