@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
@@ -0,0 +1,80 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ export class RdUpkeep {
3
+ options;
4
+ pending;
5
+ running = false;
6
+ stopped = false;
7
+ original;
8
+ completion;
9
+ constructor(options) {
10
+ this.options = options;
11
+ }
12
+ async tick() {
13
+ if (this.running || this.stopped)
14
+ return;
15
+ if (this.options.device && (this.options.platform ?? process.platform) !== 'linux')
16
+ return;
17
+ this.running = true;
18
+ this.completion = this.run();
19
+ try {
20
+ await this.completion;
21
+ }
22
+ finally {
23
+ this.running = false;
24
+ }
25
+ }
26
+ async run() {
27
+ if (this.original) {
28
+ try {
29
+ await this.original.release();
30
+ this.original = undefined;
31
+ }
32
+ catch (error) {
33
+ this.options.log?.(`[rd] original maintenance restoration remains pending: ${error instanceof Error ? error.message : 'unknown'}`);
34
+ return;
35
+ }
36
+ }
37
+ try {
38
+ if (this.options.device) {
39
+ this.original = this.options.device.acquireMaintenance({ transactionId: `rd-upkeep:${randomUUID()}` });
40
+ await this.original.ready;
41
+ if ((await this.original.observe()).status !== 'candidate')
42
+ return;
43
+ }
44
+ await this.options.adopt?.();
45
+ this.pending ??= await this.options.synchronize();
46
+ if (this.stopped || !this.pending)
47
+ return;
48
+ const activate = this.pending;
49
+ await this.options.whenIdle(async () => {
50
+ if (this.stopped)
51
+ return;
52
+ try {
53
+ await activate();
54
+ }
55
+ finally {
56
+ this.pending = undefined;
57
+ }
58
+ this.options.refresh();
59
+ });
60
+ }
61
+ catch (error) {
62
+ this.options.log?.(`[rd] automatic update failed: ${error instanceof Error ? error.message : String(error)}`);
63
+ }
64
+ finally {
65
+ if (this.original) {
66
+ try {
67
+ await this.original.release();
68
+ this.original = undefined;
69
+ }
70
+ catch (error) {
71
+ this.options.log?.(`[rd] original maintenance restoration remains pending: ${error instanceof Error ? error.message : 'unknown'}`);
72
+ }
73
+ }
74
+ }
75
+ }
76
+ async stop() { this.stopped = true; await this.completion; if (this.original) {
77
+ await this.original.release();
78
+ this.original = undefined;
79
+ } }
80
+ }
@@ -1,46 +1,36 @@
1
- import { UsageError } from '../config.js';
2
- import { clientDir, loadClientConfig, saveClientConfig, writeKeys } from './config.js';
1
+ import { randomUUID } from 'node:crypto';
2
+ import { clientDir } from './config.js';
3
3
  import { osInfo } from './osinfo.js';
4
- export async function registerWorkstation(server, clientName, token, ws) {
5
- const dir = clientDir();
6
- const existing = await loadClientConfig(dir).catch(() => undefined);
7
- const body = {
8
- name: clientName,
9
- ...(existing?.server === server ? { workstationId: existing.workstationId } : {}),
10
- os: osInfo(),
11
- defaultWorkspace: { name: ws.name, cwd: ws.cwd, shell: ws.shell },
12
- };
4
+ import { workstationEndpoint } from './control/serverIdentity.js';
5
+ import { LocalControlError } from './control/protocol.js';
6
+ import { z } from 'zod';
7
+ export const registerResponseSchema = z.object({ workstationId: z.string().min(1), ownerUserId: z.string().min(1).optional(), clientKey: z.string(), serverPub: z.string() });
8
+ /** No persistence; callers own the operation lock and identity transaction. */
9
+ export async function requestWorkstationRegistration(server, clientName, token, ws, workstationId, shell = ws?.shell ?? '/bin/sh') {
13
10
  let res;
14
11
  try {
15
- res = await fetch(new URL('/workstation/register', server), {
16
- method: 'POST',
12
+ res = await fetch(workstationEndpoint(server, '/workstation/register'), { method: 'POST', redirect: 'error', signal: AbortSignal.timeout(30_000),
17
13
  headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
18
- body: JSON.stringify(body),
14
+ body: JSON.stringify({ name: clientName, ...(workstationId ? { workstationId } : {}), os: osInfo(), shell, ...(ws ? { defaultWorkspace: ws } : {}) }),
19
15
  });
20
16
  }
21
- catch (err) {
22
- throw new Error(`Failed to reach ${server}/workstation/register: ${err instanceof Error ? err.message : err}`);
17
+ catch {
18
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Registration was sent but its outcome could not be confirmed. Check your workstation list before trying again.');
19
+ }
20
+ if (res.status === 409)
21
+ throw new LocalControlError('BINDING_CONFLICT', 'A workstation with this name is already registered. Check Settings → Workstations.');
22
+ if (res.status === 401 || res.status === 403)
23
+ throw new LocalControlError('AUTHORIZATION_REQUIRED', 'Workstation permission is required. Run `axom auth` and select workstation, then try again.');
24
+ if (!res.ok)
25
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Registration could not be confirmed. Check your workstation list.');
26
+ try {
27
+ return registerResponseSchema.parse(await res.json());
23
28
  }
24
- if (!res.ok) {
25
- // A workstation with this name is already registered for this account — most commonly because
26
- // this same machine registered before an axom-cli upgrade and no longer has a local config at
27
- // the new config-directory path (see the client's move from ~/.axom-workstation to
28
- // ~/.axom/workstation). Rebinding the existing row silently would hand this machine's identity
29
- // to whichever caller re-registers next, so the server refuses instead of upserting.
30
- if (res.status === 409) {
31
- throw new UsageError(`A workstation named '${clientName}' is already registered for this account. This is expected `
32
- + `when the same machine re-registers after an axom-cli upgrade. Delete it under Settings → `
33
- + 'Workstations, then run `axom workstation start` again.');
34
- }
35
- if (res.status === 403) {
36
- throw new UsageError('This credential is not authorized to register a workstation. '
37
- + 'Run `axom auth` and select workstation, then try again.');
38
- }
39
- throw new Error(`Registration failed: ${res.status} ${await res.text()}`);
29
+ catch {
30
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Registration response was incomplete. Check your workstation list.');
40
31
  }
41
- const data = (await res.json());
42
- if (!data.workstationId)
43
- throw new Error('Registration response missing workstationId');
44
- await saveClientConfig(dir, { server, workstationId: data.workstationId, workspaces: [{ name: ws.name, cwd: ws.cwd }] });
45
- await writeKeys(dir, data.clientKey, data.serverPub);
32
+ }
33
+ export async function registerWorkstation(server, clientName, token, ws) {
34
+ const { enrollLegacyWorkstation } = await import('./control/enrollment.js');
35
+ await enrollLegacyWorkstation({ server, name: clientName, accessToken: token, workspace: ws, operationId: randomUUID() }, clientDir());
46
36
  }
@@ -0,0 +1,250 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { z } from 'zod';
5
+ import { LocalControlError } from '../control/protocol.js';
6
+ import { assertLockOwnership, withWorkstationLock } from '../control/locks.js';
7
+ import { normalizeWorkstationServer, workstationEndpoint } from '../control/serverIdentity.js';
8
+ import { deriveSessionKey, generateKeyPair } from '../crypto.js';
9
+ import { osInfo } from '../osinfo.js';
10
+ import { RegistrationStore } from './store.js';
11
+ import { registrationKey, registrationSchema } from './model.js';
12
+ import { assertRegistrationEnrollmentAdmission } from './migration.js';
13
+ import { EnrollmentPendingStore, enrollmentDirectory, enrollmentStorageError, keySchema, operationIdSchema, registrationEnrollmentInputSchema, requestDigest, requestSchema } from './enrollmentPending.js';
14
+ const inputSchema = registrationEnrollmentInputSchema;
15
+ async function acquisitionMatches(store, pending) {
16
+ const latest = (await store.completedTombstones()).filter(entry => entry.server === pending.server && entry.ownerUserId === pending.ownerUserId).at(-1);
17
+ return latest ? pending.request.acquisition.kind === 'reattach' && pending.request.proposedWorkstationId === latest.workstationId : pending.request.acquisition.kind === 'create';
18
+ }
19
+ function recoveryProof(store) {
20
+ return {
21
+ committed: async (pending, completed) => {
22
+ try {
23
+ const identity = await store.read(pending.registrationId);
24
+ const registration = identity.registration;
25
+ return registration.id === completed.registrationId && registration.revision === completed.registrationRevision && registration.server === pending.server && registration.ownerUserId === pending.ownerUserId && registration.workstationId === pending.request.proposedWorkstationId && registration.workstationId === completed.workstationId && identity.clientKey.toString('base64') === pending.clientPrivateKey;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ },
31
+ removed: async (completed, candidate) => {
32
+ if (!await store.hasCompletedRemoval({ id: completed.registrationId, server: completed.server, ownerUserId: completed.ownerUserId, workstationId: completed.workstationId }))
33
+ return false;
34
+ if (!candidate)
35
+ return true;
36
+ return acquisitionMatches(store, candidate);
37
+ },
38
+ };
39
+ }
40
+ export async function retireRegistrationEnrollment(root, registration) {
41
+ try {
42
+ assertLockOwnership();
43
+ root = path.resolve(root);
44
+ const parsed = registrationSchema.parse(registration);
45
+ await enrollmentDirectory(root, root, false);
46
+ const store = new RegistrationStore(root);
47
+ const inspection = await store.inspect();
48
+ if (!inspection.known)
49
+ throw enrollmentStorageError();
50
+ const entry = inspection.entries.find(candidate => candidate.id === parsed.id);
51
+ const target = entry?.state === 'ready' ? entry.registration : entry?.target;
52
+ if (target) {
53
+ if (target.server !== parsed.server || target.ownerUserId !== parsed.ownerUserId || target.workstationId !== parsed.workstationId)
54
+ throw enrollmentStorageError();
55
+ if (target.revision !== parsed.revision)
56
+ throw new LocalControlError('BINDING_CONFLICT', 'Registration changed before enrollment cleanup');
57
+ }
58
+ else if (!await store.hasCompletedRemoval({ id: parsed.id, server: parsed.server, ownerUserId: parsed.ownerUserId, workstationId: parsed.workstationId }))
59
+ throw new LocalControlError('BINDING_CONFLICT', 'Registration removal could not be verified');
60
+ await new EnrollmentPendingStore(root, parsed.server, parsed.ownerUserId, recoveryProof(store)).purge(parsed.id, parsed.workstationId);
61
+ }
62
+ catch (error) {
63
+ if (error instanceof LocalControlError)
64
+ throw error;
65
+ throw enrollmentStorageError();
66
+ }
67
+ }
68
+ const identitySchema = z.object({ ownerUserId: z.string().min(1).max(1024).refine(value => !/[\x00-\x1f\x7f-\x9f]/.test(value)) }).strict();
69
+ const lookupSchema = identitySchema.extend({ workstationId: z.string().uuid(), version: z.number().int().min(1).max(2147483646) }).strict();
70
+ const responseSchema = identitySchema.extend({ protocol: z.literal(2), operationId: operationIdSchema, workstationId: z.string().uuid(), serverPub: keySchema }).strict();
71
+ async function requestJson(input, endpoint, body, mutation = false, historical = false) {
72
+ try {
73
+ const response = await fetch(workstationEndpoint(input.server, endpoint), {
74
+ method: body === undefined ? 'GET' : 'POST', headers: { Authorization: `Bearer ${input.accessToken}`, ...(body === undefined ? {} : { 'Content-Type': 'application/json' }) },
75
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }), redirect: 'manual', signal: AbortSignal.timeout(10_000),
76
+ });
77
+ if (!response.ok) {
78
+ await response.body?.cancel();
79
+ if ([401, 403].includes(response.status))
80
+ throw new LocalControlError('AUTHORIZATION_REQUIRED', 'Workstation enrollment requires current workstation authority');
81
+ if (response.status === 409 || (historical && response.status === 404))
82
+ throw new LocalControlError('BINDING_CONFLICT', 'Historical workstation could not be verified or changed; inspect before retrying');
83
+ if ([404, 405, 501].includes(response.status))
84
+ throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'This server requires an upgrade for workstation enrollment');
85
+ if (mutation && response.status >= 500)
86
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Enrollment outcome is unknown; retry with the saved request');
87
+ throw new LocalControlError('NETWORK_FAILED', 'The workstation enrollment request was refused');
88
+ }
89
+ const reader = response.body?.getReader();
90
+ if (!reader)
91
+ throw Error('Missing response');
92
+ const chunks = [];
93
+ let size = 0;
94
+ try {
95
+ while (true) {
96
+ const chunk = await reader.read();
97
+ if (chunk.done)
98
+ break;
99
+ size += chunk.value.length;
100
+ if (size > 65_536)
101
+ throw Error('Response limit');
102
+ chunks.push(chunk.value);
103
+ }
104
+ }
105
+ finally {
106
+ await reader.cancel().catch(() => undefined);
107
+ }
108
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
109
+ }
110
+ catch (error) {
111
+ if (error instanceof LocalControlError)
112
+ throw error;
113
+ throw new LocalControlError(mutation ? 'OUTCOME_UNKNOWN' : 'NETWORK_FAILED', mutation ? 'Enrollment outcome is unknown; retry with the saved request' : 'Workstation authority could not be verified');
114
+ }
115
+ }
116
+ export async function verifyRegistrationAuthority(input) {
117
+ const scopes = z.object({ results: z.array(z.object({ scope: z.string(), allowed: z.boolean() }).strict()).max(64) }).strict().safeParse(await requestJson(input, '/rest/auth/scopes/check', { scopes: ['workstation'] }));
118
+ if (!scopes.success || scopes.data.results.length !== 1 || scopes.data.results[0]?.scope !== 'workstation')
119
+ throw new LocalControlError('NETWORK_FAILED', 'Workstation permission response is invalid');
120
+ if (!scopes.data.results[0].allowed)
121
+ throw new LocalControlError('AUTHORIZATION_REQUIRED', 'Workstation permission is required');
122
+ const identity = identitySchema.safeParse(await requestJson(input, '/workstation/enroll/identity'));
123
+ if (!identity.success)
124
+ throw new LocalControlError('NETWORK_FAILED', 'Workstation owner response is invalid');
125
+ if (input.expectedOwnerUserId !== undefined && input.expectedOwnerUserId !== identity.data.ownerUserId)
126
+ throw new LocalControlError('OWNER_MISMATCH', 'The authenticated workstation owner differs from the expected owner');
127
+ return identity.data.ownerUserId;
128
+ }
129
+ async function enrollLocked(input, root, options) {
130
+ await options.beforeEnrollment?.();
131
+ await assertRegistrationEnrollmentAdmission(root);
132
+ const ownerUserId = await verifyRegistrationAuthority(input);
133
+ const store = new RegistrationStore(root, { afterStage: stage => options.afterStage?.('store-' + stage) ?? Promise.resolve() });
134
+ await store.assertEnrollmentAllowed(input.server, ownerUserId);
135
+ const inspection = await store.inspect();
136
+ if (!inspection.known)
137
+ throw enrollmentStorageError();
138
+ const key = registrationKey(input.server, ownerUserId);
139
+ const existing = inspection.entries.find(entry => registrationKey(entry.state === 'ready' ? entry.registration.server : entry.target.server, entry.state === 'ready' ? entry.registration.ownerUserId : entry.target.ownerUserId) === key);
140
+ if (existing?.state === 'corrupt')
141
+ throw new LocalControlError('CORRUPT_IDENTITY', 'The matching workstation registration needs repair');
142
+ const proof = recoveryProof(store);
143
+ const pendingStore = new EnrollmentPendingStore(root, input.server, ownerUserId, proof, options.afterStage);
144
+ let envelope = await pendingStore.read();
145
+ if (existing?.state === 'ready') {
146
+ if (envelope?.state === 'pending' && envelope.registrationId === existing.id) {
147
+ const identity = await store.read(existing.id);
148
+ if (identity.registration.workstationId !== envelope.request.proposedWorkstationId || identity.clientKey.toString('base64') !== envelope.clientPrivateKey)
149
+ throw enrollmentStorageError();
150
+ try {
151
+ await pendingStore.complete(envelope, existing.registration.revision);
152
+ }
153
+ catch {
154
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Local enrollment recovery completion is uncertain; retry with the saved identity');
155
+ }
156
+ }
157
+ return { operationId: input.operationId, registration: existing.registration, kind: 'reused' };
158
+ }
159
+ if (envelope?.state === 'completed' && !await proof.removed(envelope))
160
+ throw enrollmentStorageError();
161
+ if (!envelope || envelope.state === 'completed') {
162
+ const historical = (await store.completedTombstones()).filter(entry => registrationKey(entry.server, entry.ownerUserId) === key).at(-1);
163
+ let acquisition = { kind: 'create' };
164
+ let proposedWorkstationId = randomUUID();
165
+ if (historical) {
166
+ let metadata;
167
+ try {
168
+ metadata = lookupSchema.parse(await requestJson(input, '/workstation/enroll/' + historical.workstationId, undefined, false, true));
169
+ }
170
+ catch (error) {
171
+ if (error instanceof LocalControlError)
172
+ throw error;
173
+ throw new LocalControlError('BINDING_CONFLICT', 'Historical workstation response could not be verified');
174
+ }
175
+ if (metadata.workstationId !== historical.workstationId || metadata.ownerUserId !== ownerUserId)
176
+ throw new LocalControlError('BINDING_CONFLICT', 'Historical workstation owner or identity differs');
177
+ proposedWorkstationId = metadata.workstationId;
178
+ acquisition = { kind: 'reattach', expectedVersion: metadata.version };
179
+ }
180
+ const keys = generateKeyPair();
181
+ const request = requestSchema.parse({ protocol: 2, operationId: randomUUID(), proposedWorkstationId, clientPublicKey: keys.publicKey.toString('base64'), name: input.defaults.name, os: osInfo(), shell: input.defaults.shell, ...(input.defaults.workspace ? { defaultWorkspace: input.defaults.workspace } : {}), acquisition });
182
+ envelope = { schemaVersion: 1, state: 'pending', server: input.server, ownerUserId, registrationId: randomUUID(), requestDigest: requestDigest(request), request, clientPrivateKey: keys.privateKey.toString('base64') };
183
+ await pendingStore.write(envelope);
184
+ }
185
+ const pending = envelope;
186
+ try {
187
+ await fs.lstat(path.join(root, 'registrations', pending.registrationId, 'retired.json'));
188
+ throw new LocalControlError('BINDING_CONFLICT', 'The pending local identity was removed; verification is required');
189
+ }
190
+ catch (error) {
191
+ if (error.code !== 'ENOENT')
192
+ throw error;
193
+ }
194
+ if (!await acquisitionMatches(store, pending))
195
+ throw enrollmentStorageError();
196
+ await options.afterStage?.('pending-persisted');
197
+ const response = responseSchema.safeParse(await requestJson(input, '/workstation/enroll', pending.request, true));
198
+ if (!response.success || response.data.operationId !== pending.request.operationId || response.data.workstationId !== pending.request.proposedWorkstationId || response.data.ownerUserId !== ownerUserId)
199
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Enrollment response could not be verified; retry with the saved request');
200
+ try {
201
+ deriveSessionKey(Buffer.from(pending.clientPrivateKey, 'base64'), Buffer.from(response.data.serverPub, 'base64'), Buffer.alloc(32), Buffer.alloc(32));
202
+ }
203
+ catch {
204
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Enrollment server key could not be verified; retry with the saved request');
205
+ }
206
+ assertLockOwnership();
207
+ const dataDirectory = path.join(root, 'registrations', pending.registrationId, 'data');
208
+ let registration;
209
+ try {
210
+ registration = await store.commit({ registration: { id: pending.registrationId, server: input.server, ownerUserId, workstationId: response.data.workstationId, dataDirectory, keeperConfigDirectory: dataDirectory }, clientKey: Buffer.from(pending.clientPrivateKey, 'base64'), serverPub: Buffer.from(response.data.serverPub, 'base64'), expectedRevision: null, configuration: { server: input.server, ownerUserId, workstationId: response.data.workstationId, workspaces: pending.request.defaultWorkspace ? [{ name: pending.request.defaultWorkspace.name, cwd: pending.request.defaultWorkspace.cwd }] : [] } });
211
+ await options.afterStage?.('after-local-commit');
212
+ await pendingStore.complete(pending, registration.revision);
213
+ }
214
+ catch {
215
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Local enrollment commit needs recovery; retry with the saved request');
216
+ }
217
+ return { operationId: input.operationId, registration, kind: pending.request.acquisition.kind === 'create' ? 'created' : 'reattached' };
218
+ }
219
+ export async function enrollRegistration(input, root, options = {}) {
220
+ let parsed;
221
+ try {
222
+ parsed = inputSchema.parse(input);
223
+ parsed.server = normalizeWorkstationServer(parsed.server);
224
+ }
225
+ catch {
226
+ throw new LocalControlError('BINDING_CONFLICT', 'Workstation enrollment inputs are invalid');
227
+ }
228
+ root = path.resolve(root);
229
+ try {
230
+ await enrollmentDirectory(root);
231
+ await enrollmentDirectory(root, path.join(root, 'control'));
232
+ try {
233
+ // The live lock owner publishes this file in place; its contents may still be growing.
234
+ const owner = await fs.lstat(path.join(root, 'control/operation.owner.json'));
235
+ if (!owner.isFile() || owner.nlink !== 1 || owner.size > 4096 || (owner.mode & 0o077) !== 0
236
+ || (process.getuid && owner.uid !== process.getuid()))
237
+ throw enrollmentStorageError();
238
+ }
239
+ catch (error) {
240
+ if (error.code !== 'ENOENT')
241
+ throw error;
242
+ }
243
+ return await withWorkstationLock(root, 'operation', () => enrollLocked(parsed, root, options));
244
+ }
245
+ catch (error) {
246
+ if (error instanceof LocalControlError)
247
+ throw error;
248
+ throw enrollmentStorageError();
249
+ }
250
+ }