@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
@@ -1,4 +1,16 @@
1
+ import { z } from 'zod';
2
+ import { inspectWorkstation } from '../workstation/control/inspect.js';
3
+ import { requestDeviceRuntimeUpdate, requestRuntimeUpdate } from '../workstation/control/runtimeClient.js';
4
+ import { inspectLocalDevice } from '../workstation/control/deviceCommands.js';
5
+ import { assertDeviceGuard, deviceRuntimeUpdateRequestSchema } from '../workstation/control/deviceProtocol.js';
6
+ import { withWorkstationLock } from '../workstation/control/locks.js';
7
+ import { bindingGuardSchema, LocalControlError } from '../workstation/control/protocol.js';
8
+ import { enrollmentFailure } from '../workstation/control/enrollment.js';
9
+ import { readLocalRequest } from '../workstation/control/commands.js';
10
+ import { deploymentDownloadURL, normalizeWorkstationServer, workstationEndpoint } from '../workstation/control/serverIdentity.js';
1
11
  import { randomUUID } from 'node:crypto';
12
+ import { promises as fs } from 'node:fs';
13
+ import path from 'node:path';
2
14
  import { UsageError, loadConfig, resolveServer } from '../config.js';
3
15
  import { runCommand } from '../output.js';
4
16
  import { clientDir } from '../workstation/config.js';
@@ -13,8 +25,8 @@ function isManifest(value) {
13
25
  }
14
26
  /** Fetches the release the server currently publishes for this exact deployment. */
15
27
  async function fetchManifest(server, fetchImpl) {
16
- const url = new URL('/download/client/manifest.json', server).toString();
17
- const response = await fetchImpl(url);
28
+ const url = workstationEndpoint(server, '/download/client/manifest.json').toString();
29
+ const response = await fetchImpl(url, { redirect: 'error', signal: AbortSignal.timeout(30_000) });
18
30
  if (!response.ok) {
19
31
  throw new Error(`Fetching the release manifest failed with HTTP ${response.status}`);
20
32
  }
@@ -55,6 +67,35 @@ export async function updateAction(options, deps = {}) {
55
67
  version: manifest.version, url: manifest.url, size: manifest.size, sha512: manifest.sha512,
56
68
  };
57
69
  const dir = deps.dir ?? clientDir();
70
+ if (!deps.createCoordinator) {
71
+ let deviceRoot = false;
72
+ for (const name of ['device.json', 'legacy-migration.json']) {
73
+ try {
74
+ await fs.lstat(path.join(dir, name));
75
+ deviceRoot = true;
76
+ }
77
+ catch (error) {
78
+ if (error.code !== 'ENOENT')
79
+ throw error;
80
+ }
81
+ }
82
+ if (deviceRoot) {
83
+ const current = await inspectLocalDevice(dir);
84
+ const result = await updateLocalCli({ schemaVersion: 2, action: 'update', operationId: `manual-${randomUUID()}`, server, guard: { deviceRevision: current.deviceRevision, runtimeGeneration: current.runtime.generation }, expectedRelease: { version: release.version, sha512: release.sha512 } }, dir, fetchImpl);
85
+ if (result.status === 'succeeded' || result.status === 'pending' && !result.error) {
86
+ (deps.log ?? console.log)(result.status === 'pending' ? 'Update requested. Check workstation status for progress.' : `Installed ${release.version}.`);
87
+ return;
88
+ }
89
+ throw new Error(result.error?.message ?? 'The workstation is busy; try again when its work finishes.');
90
+ }
91
+ const current = await inspectWorkstation(dir);
92
+ const result = await updateLocalCli({ operationId: `manual-${randomUUID()}`, server, guard: { revision: current.revision, runtimeGeneration: current.runtime.generation }, expectedRelease: { version: release.version, sha512: release.sha512 } }, dir, fetchImpl);
93
+ if (result.status === 'succeeded' || result.status === 'pending') {
94
+ (deps.log ?? console.log)(result.status === 'pending' ? 'Update requested. Check workstation status for progress.' : `Installed ${release.version}.`);
95
+ return;
96
+ }
97
+ throw new Error(result.error?.message ?? 'The workstation is busy; try again when its work finishes.');
98
+ }
58
99
  const coordinator = deps.createCoordinator ? deps.createCoordinator({ dir, server }) : defaultCoordinator(dir, server);
59
100
  const command = { commandId: `manual-${randomUUID()}`, release, force: false };
60
101
  const status = await coordinator.handle(command);
@@ -78,11 +119,93 @@ export async function updateAction(options, deps = {}) {
78
119
  throw new Error(status.error ?? `Update did not complete (phase: ${status.phase}).`);
79
120
  }
80
121
  }
122
+ const updateRequestSchema = z.object({ operationId: z.string().min(1).max(128), server: z.string(), guard: bindingGuardSchema, expectedRelease: z.object({ version: z.string(), sha512: z.string().regex(/^[a-f0-9]{128}$/i) }).strict() }).strict();
123
+ const deviceUpdateCliSchema = deviceRuntimeUpdateRequestSchema.omit({ release: true }).extend({ expectedRelease: updateRequestSchema.shape.expectedRelease }).strict();
124
+ async function updateDeviceCli(raw, directory, fetchImpl) {
125
+ let operationId = randomUUID();
126
+ try {
127
+ const request = deviceUpdateCliSchema.parse(raw);
128
+ operationId = request.operationId;
129
+ const server = normalizeWorkstationServer(request.server);
130
+ const current = await inspectLocalDevice(directory);
131
+ if (current.runtime.state !== 'running')
132
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Start and verify the original device owner before updating');
133
+ assertDeviceGuard(request.guard, current);
134
+ const release = await fetchManifest(server, fetchImpl);
135
+ if (release.version !== request.expectedRelease.version || release.sha512 !== request.expectedRelease.sha512)
136
+ throw new LocalControlError('BINDING_CONFLICT', 'The published release changed; review it again');
137
+ deploymentDownloadURL(server, release.url);
138
+ const status = await requestDeviceRuntimeUpdate(directory, { schemaVersion: 2, action: 'update', operationId, server, guard: request.guard, release });
139
+ const result = ['BUSY', 'UPDATE_IN_PROGRESS'].includes(status.phase) ? 'busy' : status.phase === 'SUCCEEDED' ? 'succeeded' : ['FAILED', 'ROLLED_BACK'].includes(status.phase) ? 'failed' : 'pending';
140
+ const inspection = await inspectLocalDevice(directory).catch(() => null);
141
+ return { operationId, registrationId: null, status: result, inspection, ...(status.error ? { error: { code: 'SERVICE_FAILED', message: status.error } } : {}) };
142
+ }
143
+ catch (error) {
144
+ const code = error instanceof LocalControlError ? error.code : 'UNSUPPORTED_PROTOCOL';
145
+ return { operationId, registrationId: null, status: code === 'BUSY' ? 'busy' : code === 'OUTCOME_UNKNOWN' ? 'pending' : 'failed', inspection: null, error: { code, message: 'Device update requires the original verified owner and unchanged source/release' } };
146
+ }
147
+ }
148
+ export async function updateLocalCli(raw, directory, fetchImpl = globalThis.fetch) {
149
+ if (raw && typeof raw === 'object' && 'schemaVersion' in raw)
150
+ return updateDeviceCli(raw, directory, fetchImpl);
151
+ try {
152
+ const request = updateRequestSchema.parse(raw);
153
+ const server = normalizeWorkstationServer(request.server);
154
+ const manifest = await fetchManifest(server, fetchImpl);
155
+ if (manifest.version !== request.expectedRelease.version || manifest.sha512 !== request.expectedRelease.sha512)
156
+ throw new LocalControlError('BINDING_CONFLICT', 'The published release changed; review it again');
157
+ manifest.url = deploymentDownloadURL(server, manifest.url);
158
+ const before = await inspectWorkstation(directory);
159
+ if (before.revision !== request.guard.revision || before.runtime.generation !== request.guard.runtimeGeneration)
160
+ throw new LocalControlError('BINDING_CONFLICT', 'Workstation changed; check it again');
161
+ if (before.runtime.state === 'unknown')
162
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Stop and verify the existing daemon before updating');
163
+ if (before.runtime.state === 'running') {
164
+ if (before.runtime.binding?.server !== server)
165
+ throw new LocalControlError('INSTANCE_OCCUPIED', 'Another server owns the running daemon');
166
+ const status = await requestRuntimeUpdate(directory, manifest, request.operationId);
167
+ return { status: ['BUSY', 'UPDATE_IN_PROGRESS'].includes(status.phase) ? 'busy' : status.phase === 'SUCCEEDED' ? 'succeeded' : status.phase === 'FAILED' || status.phase === 'ROLLED_BACK' ? 'failed' : 'pending', inspection: await inspectWorkstation(directory) };
168
+ }
169
+ return await withWorkstationLock(directory, 'installation', () => withWorkstationLock(directory, 'operation', async () => {
170
+ const current = await inspectWorkstation(directory);
171
+ if (current.revision !== request.guard.revision || current.runtime.generation !== request.guard.runtimeGeneration)
172
+ throw new LocalControlError('BINDING_CONFLICT', 'Workstation changed; check it again');
173
+ if (current.runtime.state === 'unknown')
174
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Stop and verify the existing daemon before updating');
175
+ if (current.runtime.state !== 'stopped')
176
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'The original stopped owner changed before offline update admission');
177
+ const status = await createUpdateCoordinator({ dir: directory, server, cliPath: CLI_PATH, serviceLifecycle: 'none', installationLockHeld: true }).handle({ commandId: request.operationId, release: manifest, force: false });
178
+ return { status: ['BUSY', 'UPDATE_IN_PROGRESS'].includes(status.phase) ? 'busy' : status.phase === 'SUCCEEDED' ? 'succeeded' : status.phase === 'FAILED' || status.phase === 'ROLLED_BACK' ? 'failed' : 'pending', inspection: await inspectWorkstation(directory) };
179
+ }));
180
+ }
181
+ catch (error) {
182
+ return enrollmentFailure(error, directory);
183
+ }
184
+ }
81
185
  export function registerUpdateCommand(program) {
82
186
  const serverOption = () => program.opts().server;
83
187
  program
84
188
  .command('update')
85
189
  .description('Update axom-cli to the release the configured server currently publishes')
190
+ .option('--json', 'Emit local management protocol JSON')
191
+ .option('--input-stdin', 'Read the guarded update request from stdin')
86
192
  .option('--version <version>', 'Fail unless the server-published release matches this exact version')
87
- .action((options) => runCommand(() => updateAction({ server: serverOption(), version: options.version })));
193
+ .action((options) => runCommand(async () => {
194
+ if (!options.json)
195
+ return updateAction({ server: serverOption(), version: options.version });
196
+ if (!options.inputStdin)
197
+ throw new UsageError('--json requires --input-stdin');
198
+ let request;
199
+ let result;
200
+ try {
201
+ request = await readLocalRequest(process.stdin);
202
+ result = await updateLocalCli(request, clientDir());
203
+ }
204
+ catch (error) {
205
+ result = await enrollmentFailure(error, clientDir());
206
+ }
207
+ const device = deviceUpdateCliSchema.safeParse(request);
208
+ const id = updateRequestSchema.safeParse(request);
209
+ process.stdout.write(JSON.stringify({ schemaVersion: device.success ? 2 : 1, kind: 'result', operationId: device.success ? device.data.operationId : id.success ? id.data.operationId : randomUUID(), result }) + '\n');
210
+ }));
88
211
  }
@@ -1,171 +1,108 @@
1
- import { hostname } from 'node:os';
2
- import { basename } from 'node:path';
3
- import { loadConfig, resolveServer, UsageError } from '../config.js';
4
- import { accessTokenFor } from '../credentials.js';
5
- import { ensureFreshToken } from '../mcp.js';
1
+ import { randomUUID } from 'node:crypto';
2
+ import { UsageError } from '../config.js';
6
3
  import { runCommand } from '../output.js';
7
- import { clientDir, loadClientConfig } from '../workstation/config.js';
8
- import { resolveWorkstationName, sanitizeName } from '../workstation/names.js';
9
- /**
10
- * Registers this workstation with the server if it has no credential yet, then leaves it alone.
11
- * Returns whether it just registered (`false` when an existing credential meant there was nothing
12
- * to do), so `start` can gate `install()` on that — see its action below for why that gate matters.
13
- *
14
- * Shared by `start` (which then hands off to the OS service) and the bare `axom workstation`
15
- * command (which runs the daemon directly): both need the exact same credential chain, so it lives
16
- * here once rather than twice.
17
- *
18
- * An existing workstation credential boots directly — the common case, and the only sane one for a
19
- * container that gets recreated on every restart: re-registering each boot would rotate keys for no
20
- * reason. Otherwise a usable access token registers this machine — `AXOM_ACCESS_TOKEN` (set by a
21
- * container with no human and no browser) or a stored user token (from `axom auth`), refreshing it
22
- * first if it's stale. With neither available, an interactive terminal falls back to the device
23
- * flow; a headless one gets a hard stop naming the fix.
24
- */
25
- async function ensureRegistered(deps, serverFlag, nameFlag) {
26
- const registered = await loadClientConfig(clientDir()).then(() => true, () => false);
27
- if (registered)
28
- return false;
29
- const config = await loadConfig();
30
- const server = resolveServer(config, serverFlag);
31
- let token = await accessTokenFor(server);
32
- if (!token) {
33
- if (!deps.isInteractive()) {
34
- throw new UsageError('No credential for this workstation. Set AXOM_ACCESS_TOKEN (create one under Settings → Security → ' +
35
- 'Access Tokens), or run `axom auth` on a machine with a browser.');
36
- }
37
- await deps.authenticate(server);
38
- token = await ensureFreshToken(server);
39
- }
40
- const cwd = process.cwd();
41
- await deps.registerWorkstation(server, resolveWorkstationName(nameFlag, process.env, hostname()), token, {
42
- name: sanitizeName(basename(cwd), 'default'),
43
- cwd,
44
- shell: process.env.SHELL ?? '/bin/bash',
45
- });
46
- return true;
47
- }
48
- /**
49
- * Removes `AXOM_ACCESS_TOKEN` from *this process's* copy of the environment, right before the bare
50
- * `axom workstation` command becomes a long-lived daemon.
51
- *
52
- * The daemon spawns agents and shell commands with `{ ...process.env, ... }` (exec.ts, acpClient.ts),
53
- * so anything still in here reaches every model-authored command and every dependency postinstall
54
- * that runs in `/workspace` — a container is precisely where this product runs untrusted code. What
55
- * would leak is not the workstation's own key pair (scoped to one workstation, on disk) but a
56
- * credential that authenticates *as the user*: it can register further workstations under their
57
- * account and, with business scopes, call granted MCP tools as them, until somebody notices and
58
- * revokes it. Fixed once here rather than filtered at each of the two spawn sites, so the next spawn site
59
- * inherits the fix instead of having to remember it.
60
- *
61
- * Nothing after this point needs the value. The daemon authenticates with the file credential
62
- * written at registration (`clientKey`/`serverPub`), and the `container` update lifecycle downloads
63
- * its release over that same authenticated management channel.
64
- *
65
- * Unconditional, not gated on "we just registered": a container that still has its `/data/ws` volume
66
- * boots straight past `ensureRegistered`, and that is exactly the long-lived container this protects.
67
- *
68
- * The token stays in the *container's* environment, so a restarted container still registers — only
69
- * this process's copy is dropped. Every reader of it lives before this line: `accessTokenFor`
70
- * (credentials.ts) reads `process.env.AXOM_ACCESS_TOKEN` on its first line, and its only caller here
71
- * is `ensureRegistered` above; `insufficientScopeError`/`envAccessTokenRejectedError` (mcp.ts) read
72
- * it too, but only from the one-shot `axom execute`/`axom struct` CLI processes, which are separate
73
- * processes with their own untouched copy.
74
- *
75
- * `workstation start` deliberately does not call this: it registers and hands off to launchd/systemd
76
- * rather than becoming the daemon, so dropping the variable would affect only a CLI process that is
77
- * about to exit.
78
- */
79
- function dropAccessTokenFromEnvironment() {
80
- delete process.env.AXOM_ACCESS_TOKEN;
81
- }
82
- /**
83
- * Registers `axom workstation start|stop|restart|status|install|uninstall`, the bare
84
- * `axom workstation` (Docker ENTRYPOINT / debugging foreground run), and the hidden top-level
85
- * `axom __daemon`.
86
- *
87
- * `__daemon` is registered on `program` itself, not on the `workstation` subcommand group: the
88
- * generated launchd/systemd service definitions invoke it as `<cliPath> __daemon`, a bare
89
- * top-level argument (see service/launchd.ts and service/systemd.ts).
90
- */
4
+ import { clientDir } from '../workstation/config.js';
5
+ import { configureRd, rdStatus } from '../workstation/rd/control.js';
6
+ import { runManagedDesktop } from '../workstation/rd/sessions.js';
7
+ import { executeDeviceCommand, inspectLocalDevice } from '../workstation/control/deviceCommands.js';
8
+ import { readLocalRequest } from '../workstation/control/commands.js';
9
+ import { LocalControlError } from '../workstation/control/protocol.js';
10
+ import { deviceEventSchema } from '../workstation/control/deviceProtocol.js';
11
+ import { operationIdSchema } from '../workstation/registrations/enrollmentPending.js';
12
+ import { registrationIdSchema } from '../workstation/registrations/model.js';
13
+ import { addWorkstationRegistration, listWorkstationRegistrations, removeWorkstationRegistration } from './workstationRegistrations.js';
91
14
  export function registerWorkstationCommands(program, deps) {
92
- const workstation = program.command('workstation').description('Manage the local Axomwork workstation daemon');
93
- // Same idiom as `registerUpdateCommand`: the global `--server` is read at action time, not at
94
- // registration time, so `axom --server <url> workstation start` works. `axom auth` only writes
95
- // `defaultServer` when it is unset, so a user who authenticated against localhost first and
96
- // production second would otherwise have no way to keep `start` off localhost.
15
+ const workstation = program.command('workstation').description('Manage this device and its workstation registration collection');
16
+ const registrations = deps.registrations ?? { add: addWorkstationRegistration, list: listWorkstationRegistrations, remove: removeWorkstationRegistration };
97
17
  const serverOption = () => program.opts().server;
98
- // `workstation` (this group) and its `start` subcommand both declare `--server`/`--name` — the
99
- // group needs its own copy so the *bare* command accepts them, and `start` keeps its copy for its
100
- // own `--help`. But for `axom workstation start --server/--name <value>`, only one of the two
101
- // declarations ever actually captures the flag, and it's not always the same one: `program`
102
- // parses its own options over the *entire* remaining argv before `workstation` gets a look, so
103
- // `--server` — also declared on `program`, globally — is always captured there, leaving both
104
- // `options.server` and `workstationOption().server` dead; `--name` has no `program`-level
105
- // declaration, so it falls through to `workstation`, which *does* capture it, leaving `start`'s
106
- // own `options.name` the dead one instead. `start`'s action below reads the whole fallback chain
107
- // so it doesn't matter which link is live for a given flag.
108
- const workstationOption = () => workstation.opts();
109
- workstation
110
- .command('start')
111
- .option('--server <url>', 'Axomwork server URL to register this workstation against')
112
- .option('--name <name>', 'Workstation name (default: AXOM_WORKSTATION_NAME, then the hostname)')
113
- .description('Start the workstation user service')
114
- .action((options) => runCommand(async () => {
115
- // `install()` runs only when this call just registered the workstation, not on every
116
- // `start` — it's not the free idempotent no-op it looks like. On macOS it's bootout, write
117
- // plist, bootstrap, and the `deps.start()` right after it is `kickstart -k`: on an
118
- // already-registered machine that's two restart windows where there used to be one, plus a
119
- // bootout window with a documented stranding mode (`launchdStuck`: the service is not
120
- // registered with launchd right now, so nothing will start it). It would also re-pin the
121
- // service's `node` execPath to whatever ran this `start` — which repairs a machine stranded
122
- // by a removed nvm version only if that's a *good* node; run from an ad-hoc one and it
123
- // manufactures the exact stranding it's meant to cure. That repair belongs to the explicit
124
- // verb that already exists for it, `axom workstation install`, not as a side effect here.
125
- const justRegistered = await ensureRegistered(deps, options.server ?? workstationOption().server ?? serverOption(), options.name ?? workstationOption().name);
126
- if (justRegistered)
127
- await deps.install();
128
- await deps.start();
129
- }));
130
- workstation
131
- .command('stop')
132
- .description('Stop the workstation user service')
133
- .action(() => runCommand(async () => { await deps.stop(); }));
134
- workstation
135
- .command('restart')
136
- .description('Restart the workstation user service')
137
- .action(() => runCommand(async () => { await deps.restart(); }));
138
- workstation
139
- .command('status')
140
- .description('Report whether the workstation daemon is running')
141
- .action(() => runCommand(async () => { deps.log(await deps.status()); }));
142
- workstation
143
- .command('install')
144
- .description('Install and enable the operating-system service')
145
- .action(() => runCommand(async () => { await deps.install(); }));
146
- workstation
147
- .command('uninstall')
148
- .description('Disable and remove the operating-system service')
149
- .action(() => runCommand(async () => { await deps.uninstall(); }));
150
- workstation
151
- .option('--server <url>', 'Axomwork server URL to register against (default: AXOM_SERVER, then the stored default)')
152
- .option('--name <name>', 'Workstation name (default: AXOM_WORKSTATION_NAME, then the hostname)')
153
- .description('Run the workstation daemon in the foreground (Docker ENTRYPOINT / debugging); subcommands manage the OS service')
18
+ const applyRd = deps.configureRd ?? configureRd;
19
+ const rejectSelectors = (options = {}) => {
20
+ const group = workstation.opts();
21
+ if (options.server !== undefined || options.name !== undefined || group.server !== undefined || group.name !== undefined || serverOption() !== undefined)
22
+ throw new UsageError('Workstation lifecycle commands apply to the whole device. Use `axom workstation add [address]` to add a server/account registration, or `axom workstation remove <registrationId>` to remove one.');
23
+ };
24
+ const removed = () => runCommand(async () => { throw new UsageError('This workstation command was removed. Use `axom workstation add [address]`, `list`, or `remove <registrationId>`; start/stop operate on the whole device.'); });
25
+ const dispatch = async (action, options, task, registrationId) => {
26
+ const target = registrationIdSchema.safeParse(registrationId);
27
+ const operation = { operationId: randomUUID(), registrationId: target.success ? target.data : null };
28
+ const captureOperationId = (value) => {
29
+ const parsed = operationIdSchema.safeParse(value);
30
+ if (!parsed.success)
31
+ throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Invalid workstation operation correlation ID');
32
+ operation.operationId = parsed.data;
33
+ };
34
+ if (!options.json) {
35
+ if (options.inputStdin)
36
+ throw new UsageError('--input-stdin requires --json');
37
+ if (options.operationId !== undefined)
38
+ captureOperationId(options.operationId);
39
+ await task(operation);
40
+ return;
41
+ }
42
+ try {
43
+ if (options.operationId !== undefined)
44
+ captureOperationId(options.operationId);
45
+ if (!options.inputStdin) {
46
+ await task(operation);
47
+ return;
48
+ }
49
+ const request = await readLocalRequest(process.stdin);
50
+ if (request && typeof request === 'object' && 'operationId' in request)
51
+ captureOperationId(request.operationId);
52
+ if (!request || typeof request !== 'object' || !('action' in request) || request.action !== action)
53
+ throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Use a matching v2 device command request');
54
+ if (registrationId !== undefined && (!('guard' in request) || !request.guard || typeof request.guard !== 'object' || !('registrationId' in request.guard) || request.guard.registrationId !== registrationId))
55
+ throw new LocalControlError('BINDING_CONFLICT', 'The guarded removal must match the exact registration ID');
56
+ const result = await executeDeviceCommand(request, clientDir());
57
+ if (result.operationId !== operation.operationId)
58
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Original workstation operation reply does not match');
59
+ deps.log(JSON.stringify(deviceEventSchema.parse({ schemaVersion: 2, kind: 'result', operationId: result.operationId, result })));
60
+ if (result.status !== 'succeeded')
61
+ process.exitCode = 1;
62
+ }
63
+ catch (error) {
64
+ const code = error instanceof LocalControlError ? error.code : 'SERVICE_FAILED';
65
+ deps.log(JSON.stringify(deviceEventSchema.parse({ schemaVersion: 2, kind: 'result', operationId: operation.operationId, result: { operationId: operation.operationId, registrationId: operation.registrationId, status: code === 'BUSY' ? 'busy' : code === 'OUTCOME_UNKNOWN' ? 'pending' : ['BINDING_CONFLICT', 'OWNER_MISMATCH'].includes(code) ? 'conflict' : 'failed', inspection: null, error: { code, message: error instanceof LocalControlError ? error.message : 'The workstation operation could not be confirmed' } } })));
66
+ process.exitCode = 1;
67
+ }
68
+ };
69
+ workstation.command('list').description('List local workstation registrations and device state').option('--json', 'Emit the v2 device collection').action(options => runCommand(() => dispatch('inspect', options, () => registrations.list(options))));
70
+ workstation.command('add [address]').description('Add an exact server/account registration without changing ordinary CLI login').option('--name <name>', 'Workstation name').option('--json', 'Emit a v2 correlated result').option('--input-stdin', 'Read a protected v2 enrollment request from stdin').option('--operation-id <id>', 'Operation correlation ID').option('--owner <userId>', 'Require this account identity')
71
+ .action((address, options) => runCommand(() => dispatch('add-registration', options, operation => registrations.add({ ...options, operation, name: options.name ?? workstation.opts().name, server: address ?? serverOption(), expectedOwnerUserId: options.owner }))));
72
+ workstation.command('remove <registrationId>').description('Remove this exact local registration; busy resources require explicit --force').option('--force', 'Retire this registration’s resources').option('--json', 'Emit a v2 correlated result').option('--input-stdin', 'Read the original guarded v2 removal request from stdin').option('--operation-id <id>', 'Retry the original operation ID')
73
+ .action((id, options) => runCommand(() => dispatch('remove-registration', options, operation => registrations.remove(id, { ...options, operation }), id)));
74
+ for (const verb of ['start', 'stop', 'restart', 'install', 'uninstall']) {
75
+ const command = workstation.command(verb).description(`${verb} the whole-device service`).option('--name <name>', 'Removed selector; use workstation add').option('--server <url>', 'Removed selector; use workstation add').option('--json', 'Emit a v2 correlated result').option('--input-stdin', 'Read a guarded v2 whole-device request from stdin');
76
+ if (verb === 'start')
77
+ command.option('--enable-rd', 'Install and enable Linux remote desktop').option('--disable-rd', 'Disable remote desktop').option('--rd-session <mode>', 'Desktop selection: auto, existing, or virtual').option('--rd-display <display>', 'Choose a local X11 display');
78
+ command.action(options => runCommand(async () => {
79
+ rejectSelectors(options);
80
+ const rdOptions = { ...workstation.opts(), ...options };
81
+ const hasRd = rdOptions.enableRd || rdOptions.disableRd || rdOptions.rdSession || rdOptions.rdDisplay;
82
+ if (hasRd && (verb !== 'start' || options.json))
83
+ throw new UsageError('RD flags require a plain `axom workstation start` or foreground command');
84
+ if (options.json && !options.inputStdin)
85
+ throw new UsageError('--json requires --input-stdin for device lifecycle commands');
86
+ if (hasRd)
87
+ await applyRd(clientDir(), rdOptions);
88
+ await dispatch(verb, options, () => deps[verb]());
89
+ }));
90
+ }
91
+ workstation.command('status').description('Report the whole-device service status').action(() => runCommand(async () => { rejectSelectors(); deps.log(await deps.status()); }));
92
+ workstation.command('inspect').requiredOption('--json', 'Emit the v2 device collection').action(() => runCommand(async () => { deps.log(JSON.stringify(await inspectLocalDevice(clientDir()))); }));
93
+ for (const verb of ['auth', 'register', 'forget', 'begin-remove', 'adopt-owner', 'recover', 'set-enabled'])
94
+ workstation.command(verb, { hidden: true }).allowUnknownOption().allowExcessArguments().action(removed);
95
+ const rd = workstation.command('rd').description('Manage Linux remote desktop');
96
+ rd.command('status').action(() => runCommand(async () => { deps.log(JSON.stringify(await rdStatus(clientDir()), null, 2)); }));
97
+ rd.command('disable').action(() => runCommand(() => applyRd(clientDir(), { disableRd: true })));
98
+ workstation.option('--server <url>', 'Removed registration shortcut; use workstation add').option('--name <name>', 'Removed registration shortcut; use workstation add')
99
+ .option('--enable-rd', 'Install and enable Linux remote desktop').option('--disable-rd', 'Disable remote desktop').option('--rd-session <mode>', 'Desktop selection: auto, existing, or virtual').option('--rd-display <display>', 'Choose a local X11 display')
154
100
  .action((options) => runCommand(async () => {
155
- await ensureRegistered(deps, options.server ?? serverOption(), options.name);
156
- dropAccessTokenFromEnvironment();
101
+ rejectSelectors(options);
102
+ await applyRd(clientDir(), options);
103
+ delete process.env.AXOM_ACCESS_TOKEN;
157
104
  await deps.runDaemon({ supervision: deps.detectSupervision() });
158
105
  }));
159
- program.command('__daemon', { hidden: true }).action(() => runCommand(async () => {
160
- // The literal exec target launchd/systemd start: the one call site allowed to opt into
161
- // OS-service treatment. See RunDaemonOptions in daemon.ts for why this must be an explicit
162
- // flag rather than "no `supervision` was passed".
163
- //
164
- // Same reasoning as the bare command's call: this process is about to become the daemon that
165
- // spawns agents and shell commands with its own environment. A service definition is not
166
- // supposed to carry AXOM_ACCESS_TOKEN at all, which makes this usually a no-op — and exactly
167
- // why it belongs here rather than being left to whoever writes the next plist.
168
- dropAccessTokenFromEnvironment();
169
- await deps.runDaemon({ supervisedByServiceManager: true });
170
- }));
106
+ program.command('__rd-desktop', { hidden: true }).requiredOption('--directory <path>').action((options) => runCommand(async () => { delete process.env.AXOM_ACCESS_TOKEN; await runManagedDesktop(options.directory); }));
107
+ program.command('__daemon', { hidden: true }).action(() => runCommand(async () => { delete process.env.AXOM_ACCESS_TOKEN; await deps.runDaemon({ supervisedByServiceManager: true }); }));
171
108
  }
@@ -0,0 +1,116 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { hostname } from 'node:os';
3
+ import { basename } from 'node:path';
4
+ import { loadConfig } from '../config.js';
5
+ import { peekAccessToken, revokeRefreshToken } from '../credentials.js';
6
+ import { resolveAuthServer } from './auth.js';
7
+ import { clientDir } from '../workstation/config.js';
8
+ import { executeDeviceCommand, inspectLocalDevice } from '../workstation/control/deviceCommands.js';
9
+ import { deviceEventSchema, deviceResultSchema, parseDeviceInspection } from '../workstation/control/deviceProtocol.js';
10
+ import { LocalControlError } from '../workstation/control/protocol.js';
11
+ import { normalizeWorkstationServer } from '../workstation/control/serverIdentity.js';
12
+ import { verifyRegistrationAuthority } from '../workstation/registrations/enrollment.js';
13
+ import { authorizeDevice } from '../workstation/oauth/deviceAuthorization.js';
14
+ import { resolveWorkstationName, sanitizeName } from '../workstation/names.js';
15
+ import { operationIdSchema } from '../workstation/registrations/enrollmentPending.js';
16
+ import { registrationIdSchema } from '../workstation/registrations/model.js';
17
+ const defaults = {
18
+ inspect: () => inspectLocalDevice(clientDir()), execute: request => executeDeviceCommand(request, clientDir()),
19
+ authority: verifyRegistrationAuthority, authorize: authorizeDevice, revoke: revokeRefreshToken, log: console.log,
20
+ };
21
+ export async function resolveWorkstationAddServer(explicit) {
22
+ return resolveAuthServer(await loadConfig(), explicit);
23
+ }
24
+ function printResult(result, options, log) {
25
+ result = deviceResultSchema.parse(result);
26
+ if (options.json)
27
+ log(JSON.stringify(deviceEventSchema.parse({ schemaVersion: 2, kind: 'result', operationId: result.operationId, result })));
28
+ else
29
+ log(`${result.status}: ${result.registrationId ?? 'device'} (operation ${result.operationId})${result.error ? ` — ${result.error.code}: ${result.error.message}` : ''}`);
30
+ if (result.status !== 'succeeded')
31
+ process.exitCode = 1;
32
+ }
33
+ export async function listWorkstationRegistrations(options = {}, overrides = {}) {
34
+ const deps = { ...defaults, ...overrides };
35
+ const inspection = parseDeviceInspection(await deps.inspect());
36
+ if (options.json) {
37
+ deps.log(JSON.stringify(inspection));
38
+ return;
39
+ }
40
+ deps.log(`Workstation device: ${inspection.runtime.state}`);
41
+ if (!inspection.registrations.length)
42
+ deps.log('No workstation registrations. Use `axom workstation add [address]`.');
43
+ for (const registration of inspection.registrations)
44
+ deps.log(`${registration.id}\t${registration.server}\taccount=${registration.ownerUserId}\tworkstation=${registration.workstationId}\t${registration.connection}\t${registration.identityState}\t${registration.removalState}`);
45
+ for (const pending of inspection.pending)
46
+ deps.log(`pending: ${pending.id}\t${pending.kind}\t${pending.registrationId ?? 'device'}\t${pending.phase}\t${pending.outcome}`);
47
+ }
48
+ export async function addWorkstationRegistration(options = {}, overrides = {}) {
49
+ const deps = { ...defaults, ...overrides };
50
+ const operation = options.operation ?? { operationId: operationIdSchema.parse(options.operationId ?? randomUUID()), registrationId: null };
51
+ const server = normalizeWorkstationServer(await resolveWorkstationAddServer(options.server));
52
+ const before = parseDeviceInspection(await deps.inspect());
53
+ if (before.runtime.state === 'unknown')
54
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Verify the workstation owner before adding a registration');
55
+ const operationId = operationIdSchema.parse(operation.operationId);
56
+ const hasExplicitAccessToken = Boolean(process.env.AXOM_ACCESS_TOKEN?.trim());
57
+ let token = await peekAccessToken(server);
58
+ let ownerUserId;
59
+ if (token) {
60
+ try {
61
+ ownerUserId = await deps.authority({ server, accessToken: token, expectedOwnerUserId: options.expectedOwnerUserId });
62
+ }
63
+ catch (error) {
64
+ if (!(error instanceof LocalControlError))
65
+ throw error;
66
+ if (error.code !== 'AUTHORIZATION_REQUIRED' && (error.code !== 'OWNER_MISMATCH' || hasExplicitAccessToken))
67
+ throw error;
68
+ token = undefined;
69
+ }
70
+ }
71
+ let issued;
72
+ try {
73
+ if (!token) {
74
+ if (!options.json)
75
+ deps.log('Workstation permission needs a separate approval; unavailable or expired ordinary access credentials are left unchanged.');
76
+ const host = hostname();
77
+ issued = await deps.authorize(server, `Axomwork workstation (${host})`, host, { print: message => { process.stderr.write(message + '\n'); } }, ['workstation'], Object.freeze({ deviceId: `workstation-${randomUUID()}` }));
78
+ token = issued.accessToken;
79
+ ownerUserId = await deps.authority({ server, accessToken: token, expectedOwnerUserId: options.expectedOwnerUserId });
80
+ }
81
+ const cwd = process.cwd();
82
+ const result = await deps.execute({ schemaVersion: 2, action: 'add-registration', operationId,
83
+ guard: { deviceRevision: before.deviceRevision, runtimeGeneration: before.runtime.generation },
84
+ input: { operationId, server, accessToken: token, expectedOwnerUserId: ownerUserId, defaults: { name: resolveWorkstationName(options.name, process.env, hostname()), shell: process.env.SHELL ?? '/bin/sh', workspace: { name: sanitizeName(basename(cwd), 'default'), cwd, shell: process.env.SHELL ?? '/bin/sh' } } },
85
+ });
86
+ printResult(result, options, deps.log);
87
+ }
88
+ finally {
89
+ if (issued)
90
+ await deps.revoke(server, issued.refreshToken);
91
+ }
92
+ }
93
+ export async function removeWorkstationRegistration(id, options = {}, overrides = {}) {
94
+ const deps = { ...defaults, ...overrides };
95
+ const operation = options.operation ?? { operationId: operationIdSchema.parse(options.operationId ?? randomUUID()), registrationId: null };
96
+ const target = registrationIdSchema.safeParse(id);
97
+ if (!target.success) {
98
+ operation.registrationId = null;
99
+ throw new LocalControlError('BINDING_CONFLICT', 'Invalid registration ID; use `axom workstation list`');
100
+ }
101
+ operation.registrationId = target.data;
102
+ const before = parseDeviceInspection(await deps.inspect());
103
+ const registration = before.registrations.find(candidate => candidate.id === id);
104
+ if (!registration)
105
+ throw new LocalControlError('BINDING_CONFLICT', 'No registration matches that exact ID; use `axom workstation list`');
106
+ const pending = before.pending.find(operation => operation.kind === 'remove' && operation.registrationId === id);
107
+ const operationId = operationIdSchema.parse(options.operationId ?? pending?.id ?? operation.operationId);
108
+ operation.operationId = operationId;
109
+ if (before.runtime.state !== 'running' || before.runtime.generation === null) {
110
+ printResult({ operationId, registrationId: id, status: 'pending', inspection: before, error: { code: 'OUTCOME_UNKNOWN', message: pending ? 'Original removal authority is unavailable; preserve its pending operation and data for verification' : 'Removal needs the authenticated running device owner; start the device, inspect, and retry' } }, options, deps.log);
111
+ return;
112
+ }
113
+ printResult(await deps.execute({ schemaVersion: 2, action: 'remove-registration', operationId, force: options.force === true,
114
+ guard: { deviceRevision: before.deviceRevision, runtimeGeneration: before.runtime.generation, registrationId: id, registrationRevision: registration.revision },
115
+ }), options, deps.log);
116
+ }
@@ -30,9 +30,17 @@ export async function requestCredentialBroker(request) {
30
30
  return await sendRequest(brokerRequest, controller.signal);
31
31
  }
32
32
  catch (error) {
33
- if (error instanceof BrokerOperationError)
33
+ if (error instanceof BrokerOperationError) {
34
+ if (request.action === 'peek' && error.message === 'Unsupported credential broker operation.')
35
+ return { ok: true, version: PROTOCOL_VERSION };
34
36
  throw error;
37
+ }
35
38
  throwIfAborted(controller.signal);
39
+ if (request.action === 'peek') {
40
+ if (['ENOENT', 'ECONNREFUSED'].includes(error.code ?? ''))
41
+ return { ok: true, version: PROTOCOL_VERSION };
42
+ throw error;
43
+ }
36
44
  await installAndStartBroker(controller.signal);
37
45
  let lastError;
38
46
  for (let attempt = 0; attempt < 20; attempt += 1) {
@@ -129,6 +137,9 @@ async function handleRequest(raw, connection, credentials) {
129
137
  const accessToken = await credentials.accessTokenFromNativeStore(request.server, request.forceRefresh);
130
138
  response = { ok: true, accessToken, version: PROTOCOL_VERSION };
131
139
  }
140
+ else if (request.action === 'peek') {
141
+ response = { ok: true, accessToken: credentials.peekNativeAccessToken(request.server), version: PROTOCOL_VERSION };
142
+ }
132
143
  else if (request.action === 'revoke') {
133
144
  const persisted = await credentials.loadNativeTokens(request.server);
134
145
  if (persisted?.refreshToken)
@@ -319,7 +330,7 @@ function parseRequest(raw) {
319
330
  const request = JSON.parse(raw);
320
331
  if (request.version !== PROTOCOL_VERSION)
321
332
  throw new Error('Unsupported credential broker protocol version.');
322
- if (request.action !== 'save' && request.action !== 'access' && request.action !== 'clear' && request.action !== 'revoke') {
333
+ if (request.action !== 'save' && request.action !== 'access' && request.action !== 'clear' && request.action !== 'revoke' && request.action !== 'peek') {
323
334
  throw new Error('Unsupported credential broker operation.');
324
335
  }
325
336
  if (typeof request.server !== 'string' || !request.server)