@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,188 +1,33 @@
1
1
  import { accessSync, constants, statSync } from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { CREATE_KEYS, PTY_OPERATIONS } from '../keepers/terminal/pty.js';
5
- import { PTY_SESSION_LIMIT } from '../keepers/terminal/protocol.js';
6
- import { TERMINAL_NOT_FOUND } from '../keepers/legacy.js';
7
- import { identifier, integer, keys } from './validation.js';
8
- const LEGACY = 'legacy';
9
- /** The single-keeper form of {@link KitGenerations}: one generation named `kit`, running when it answers a hello. */
10
- function singleKit(kit) {
11
- return {
12
- servingGeneration: async () => ({ gen: 'kit', client: kit }),
13
- running: async () => [{ gen: 'kit', client: kit }],
14
- };
15
- }
4
+ import { validateRequest } from './validation.js';
16
5
  export class TerminalRouter {
17
6
  deps;
18
- owners = new Map();
19
- kits;
20
- /** Clients of the generations seen in the last listing, by generation. */
21
- clients = new Map();
22
7
  constructor(deps) {
23
8
  this.deps = deps;
24
- if (!deps.kits && !deps.kit)
25
- throw new Error('TerminalRouter needs keeper-terminal');
26
- this.kits = deps.kits ?? singleKit(deps.kit);
27
9
  }
28
- /** The current owner map (tests and diagnostics): `legacy`, or a generation (`kit` with a single keeper-terminal). */
29
- ownerOf(id) { return this.owners.get(id); }
30
10
  async request(method, params) {
31
- const op = method.startsWith('terminal.') ? method.slice('terminal.'.length) : '';
32
- if (op === 'list') {
33
- keys(params, []);
34
- return { sessions: await this.list() };
11
+ validateRequest(method, params);
12
+ const op = method.slice('terminal.'.length);
13
+ const kit = this.deps.kit;
14
+ if (op === 'list')
15
+ return { ...await this.list() };
16
+ if (op === 'create') {
17
+ const existing = (await this.list()).sessions.find(session => session.id === params.id);
18
+ if (existing)
19
+ return { ...existing };
20
+ const launch = await this.deps.launch(params.cwd);
21
+ return { ...await kit.create({ id: params.id, ...launch }) };
35
22
  }
36
- if (op === 'create')
37
- return { ...await this.create(params) };
38
- if (!PTY_OPERATIONS.has(op))
23
+ if (!['attach', 'poll', 'resize', 'input', 'scroll', 'detach', 'close'].includes(op))
39
24
  throw new Error('Unsupported workstation tool');
40
- const id = identifier(params, 'id');
41
- const owner = await this.resolveOwner(id);
42
- if (!owner)
43
- throw new Error(TERMINAL_NOT_FOUND);
44
- const kit = owner === LEGACY ? undefined : this.clients.get(owner);
45
- if (owner !== LEGACY && !kit)
46
- throw new Error(TERMINAL_NOT_FOUND);
47
- try {
48
- const result = owner === LEGACY
49
- ? await this.deps.legacy.request(method, params)
50
- : { ...await kit[op].call(kit, params) };
51
- if (op === 'close')
52
- this.owners.delete(id);
53
- return result;
54
- }
55
- catch (error) {
56
- // The owner no longer has it (closed elsewhere, keeper restarted): forget it; the next request looks it up again.
57
- if (error instanceof Error && error.message === TERMINAL_NOT_FOUND)
58
- this.owners.delete(id);
59
- throw error;
60
- }
25
+ const call = kit[op];
26
+ return { ...await call.call(kit, params) };
61
27
  }
62
28
  async list() {
63
- const { legacy, kits } = await this.refresh();
64
- const seen = new Set((legacy ?? []).map((session) => session.id));
65
- const out = [...(legacy ?? [])];
66
- for (const sessions of kits.values())
67
- for (const session of sessions ?? [])
68
- if (!seen.has(session.id)) {
69
- seen.add(session.id);
70
- out.push(session);
71
- }
72
- return out;
73
- }
74
- /** Lists every side once (keeper-terminal generations only if running) and refreshes the owners of every side that answered. */
75
- async refresh() {
76
- const [legacy, kits] = await Promise.all([this.legacySessions(), this.kitRefresh()]);
77
- if (legacy)
78
- this.replaceOwners(LEGACY, legacy);
79
- else if (!this.deps.legacy?.live)
80
- this.replaceOwners(LEGACY, []);
81
- return { legacy, kits };
82
- }
83
- /**
84
- * Lists every running keeper-terminal generation (never starts one) and refreshes their owners: a generation that answered gets
85
- * exactly its sessions; one that did not answer keeps its owners; one that is no longer running loses them (its PTYs are gone).
86
- */
87
- async kitRefresh() {
88
- const listed = new Map();
89
- let running;
90
- try {
91
- running = await this.kits.running();
92
- }
93
- catch {
94
- return listed;
95
- }
96
- await Promise.all(running.map(async ({ gen, client }) => {
97
- this.clients.set(gen, client);
98
- listed.set(gen, await this.sessionsOf(client));
99
- }));
100
- for (const [gen, sessions] of listed)
101
- if (sessions)
102
- this.replaceOwners(gen, sessions);
103
- for (const [id, owner] of [...this.owners])
104
- if (owner !== LEGACY && !listed.has(owner))
105
- this.owners.delete(id);
106
- for (const gen of [...this.clients.keys()])
107
- if (!listed.has(gen))
108
- this.clients.delete(gen);
109
- return listed;
110
- }
111
- replaceOwners(side, sessions) {
112
- for (const [id, owner] of this.owners)
113
- if (owner === side)
114
- this.owners.delete(id);
115
- for (const session of sessions)
116
- if (side === LEGACY || !this.owners.has(session.id))
117
- this.owners.set(session.id, side);
118
- }
119
- /** The legacy keeper's sessions; `sessions()` probes it first (read-only), so a daemon that just started finds it too. */
120
- async legacySessions() {
121
- return this.deps.legacy ? this.deps.legacy.sessions() : null;
122
- }
123
- /** One generation's sessions if it is already running; null when it is not (or did not answer): nothing is started here. */
124
- async sessionsOf(client) {
125
- try {
126
- if (!await client.runningHello())
127
- return null;
128
- return (await client.list()).sessions;
129
- }
130
- catch {
131
- return null;
132
- }
133
- }
134
- async resolveOwner(id) {
135
- const known = this.owners.get(id);
136
- if ((known !== undefined && known !== LEGACY && this.clients.has(known)) || (known === LEGACY && this.deps.legacy?.live))
137
- return known;
138
- await this.refresh();
139
- return this.owners.get(id);
140
- }
141
- async create(params) {
142
- // The legacy keeper's checks, wording and order (tools/terminal.ts at the merge-base: id, clientId, keys, cols, rows, name,
143
- // then existing, limit, cwd), all before the cwd is resolved.
144
- const id = identifier(params, 'id');
145
- identifier(params, 'clientId');
146
- keys(params, [...CREATE_KEYS]);
147
- integer(params, 'cols', 20, 300, 100);
148
- integer(params, 'rows', 5, 100, 30);
149
- const name = params.name;
150
- if (name !== undefined && (typeof name !== 'string' || !name.length || name.length > 128 || /[\x00-\x1f]/.test(name)))
151
- throw new Error('Invalid terminal name');
152
- const legacy = this.deps.legacy;
153
- const knownOwner = this.owners.get(id);
154
- if (legacy && (knownOwner === undefined || knownOwner === LEGACY)) {
155
- // The legacy keeper never gains a session after the upgrade (creates go to keeper-terminal), so the ids of its first
156
- // successful list answer "is this a retry of one of its sessions?" for good — even while it is busy or hung.
157
- if (legacy.knownIds === null) {
158
- const sessions = await legacy.sessions(); // bounded; null at once when it is busy or absent
159
- if (sessions)
160
- this.replaceOwners(LEGACY, sessions);
161
- }
162
- if (legacy.live && legacy.knownIds?.has(id))
163
- return legacy.request('terminal.create', params);
164
- }
165
- // Duplicate and limit first (the legacy order), against the keeper-terminal generations already running: an idempotent create
166
- // (in whichever generation has the id) costs no files-host round trip. keeper-terminal re-checks both when it really creates.
167
- const listed = await this.kitRefresh();
168
- for (const [gen, sessions] of listed) {
169
- const existing = sessions?.find((session) => session.id === id);
170
- if (existing) {
171
- this.owners.set(id, gen);
172
- return existing;
173
- }
174
- }
175
- const serving = await this.kits.servingGeneration();
176
- const running = listed.get(serving.gen);
177
- if (running && running.length >= PTY_SESSION_LIMIT)
178
- throw new Error('Terminal session limit exceeded');
179
- if (params.cwd !== undefined && typeof params.cwd !== 'string')
180
- throw new Error('Invalid cwd');
181
- const launch = await this.deps.launch(params.cwd);
182
- const created = await serving.client.create({ ...params, ...launch });
183
- this.clients.set(serving.gen, serving.client);
184
- this.owners.set(id, serving.gen);
185
- return created;
29
+ // The typed client is non-launching and distinguishes proven absence from an unreachable owner.
30
+ return this.deps.kit.list();
186
31
  }
187
32
  }
188
33
  /**
@@ -5,7 +5,7 @@ export const CHUNK_BYTES = 64 * 1024;
5
5
  export const TOOL_METHODS = new Set([
6
6
  'browser.list', 'browser.create', 'browser.attach', 'browser.frame', 'browser.navigate', 'browser.input', 'browser.takeover', 'browser.detach', 'browser.close',
7
7
  'tools.info', 'terminal.list', 'terminal.create', 'terminal.attach', 'terminal.poll',
8
- 'terminal.write', 'terminal.resize', 'terminal.takeover', 'terminal.detach', 'terminal.close',
8
+ 'terminal.input', 'terminal.resize', 'terminal.scroll', 'terminal.detach', 'terminal.close',
9
9
  'files.media.prepare', 'files.media.read', 'files.media.cancel',
10
10
  'files.media.open', 'files.media.status', 'files.media.seek', 'files.media.interest', 'files.media.object', 'files.media.release',
11
11
  'files.list', 'files.find', 'files.read', 'files.mkdir', 'files.rename', 'files.delete',
@@ -49,6 +49,8 @@ export function validateRequest(method, params) {
49
49
  throw new Error('Unsupported workstation tool');
50
50
  if (!object(params) || Buffer.byteLength(JSON.stringify(params)) > TOOL_REQUEST_BYTES)
51
51
  throw new Error('Invalid or oversized tool parameters');
52
+ if (method.startsWith('terminal.'))
53
+ validateTerminal(method, params);
52
54
  const shape = MEDIA_SHAPES[method];
53
55
  if (shape) {
54
56
  keys(params, [...shape, 'readOnly', 'filesystem']);
@@ -84,3 +86,55 @@ const MEDIA_SHAPES = {
84
86
  'files.media.object': ['path', 'version', 'consumerId', 'objectId', 'offset', 'length'],
85
87
  'files.media.release': ['path', 'version', 'consumerId'],
86
88
  };
89
+ /** Native terminal kinds have exact flat shapes. Geometry freshness is checked against the keeper's live grid. */
90
+ function validateTerminal(method, params) {
91
+ const ref = ['id', 'incarnation', 'clientId'];
92
+ const kinds = { key: ['usage', 'down', 'modifiers'], text: ['text'], paste: ['text'], pointer: ['x', 'y', 'geometry', 'modifiers'], button: ['x', 'y', 'geometry', 'button', 'down', 'modifiers'], release: [] };
93
+ const shapes = {
94
+ list: [], create: ['id', 'cwd'], attach: [...ref, 'cols', 'rows'], poll: [...ref, 'after'], resize: [...ref, 'cols', 'rows'],
95
+ scroll: [...ref, 'lines', 'x', 'y', 'modifiers', 'geometry'], detach: ref, close: ['id', 'incarnation'],
96
+ };
97
+ const op = method.slice('terminal.'.length);
98
+ let shape = shapes[op];
99
+ if (op === 'input') {
100
+ if (typeof params.kind !== 'string' || !Object.hasOwn(kinds, params.kind))
101
+ throw new Error('Invalid kind');
102
+ shape = [...ref, 'kind', ...kinds[params.kind]];
103
+ }
104
+ if (!shape)
105
+ throw new Error('Unsupported workstation tool');
106
+ keys(params, shape);
107
+ for (const name of shape) {
108
+ if (name === 'cwd') {
109
+ if (params.cwd !== undefined)
110
+ string(params, name);
111
+ continue;
112
+ }
113
+ if (!(name in params))
114
+ throw new Error(`Missing ${name}`);
115
+ if (['id', 'incarnation', 'clientId'].includes(name))
116
+ identifier(params, name);
117
+ else if (name === 'cols')
118
+ integer(params, name, 20, 300);
119
+ else if (name === 'rows')
120
+ integer(params, name, 5, 100);
121
+ else if (name === 'after' || name === 'geometry')
122
+ integer(params, name, name === 'geometry' ? 1 : 0, Number.MAX_SAFE_INTEGER);
123
+ else if (name === 'x')
124
+ integer(params, name, 0, 299);
125
+ else if (name === 'y')
126
+ integer(params, name, 0, 99);
127
+ else if (name === 'lines')
128
+ integer(params, name, -Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
129
+ else if (name === 'usage')
130
+ integer(params, name, 0, 65535);
131
+ else if (name === 'button')
132
+ integer(params, name, 0, 2);
133
+ else if (name === 'modifiers')
134
+ integer(params, name, 0, 15);
135
+ else if (name === 'down' && typeof params[name] !== 'boolean')
136
+ throw new Error('Invalid down');
137
+ else if (name === 'text' && (typeof params.text !== 'string' || Buffer.byteLength(params.text) > 65536))
138
+ throw new Error('Invalid text');
139
+ }
140
+ }
@@ -0,0 +1,206 @@
1
+ import { constants, promises as fs } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { gunzipSync } from 'node:zlib';
4
+ import { z } from 'zod';
5
+ export const rootReaderSchema = z.object({ schemaVersion: z.literal(1), rootFormat: z.literal(2), legacySourceMapping: z.literal(true), coverageEvidence: z.boolean().optional() }).strict();
6
+ export const archiveEvidenceSchema = z.object({
7
+ name: z.literal('@platless/axom-cli'), version: z.string().min(1).max(128), size: z.number().int().positive().max(536_870_912), sha512: z.string().regex(/^[a-f0-9]{128}$/), reader: rootReaderSchema,
8
+ runtimeFiles: z.array(z.object({ path: z.string().min(1).max(4096).refine(value => (value === 'package.json' || value.startsWith('dist/')) && !/[^\x20-\x7e]|\\/.test(value) && value.split('/').every(part => part && part !== '.' && part !== '..')), size: z.number().int().min(0).max(134_217_728), sha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict()).min(2).max(65_536),
9
+ }).strict().refine(value => value.runtimeFiles.some(file => file.path === 'package.json') && value.runtimeFiles.some(file => file.path === 'dist/cli.js') && new Set(value.runtimeFiles.map(file => file.path.toLowerCase())).size === value.runtimeFiles.length);
10
+ const archiveLimit = 536_870_912;
11
+ const expandedLimit = 1_073_741_824;
12
+ const memberLimit = 134_217_728;
13
+ const metadataLimit = 1_048_576;
14
+ const memberCountLimit = 65_536;
15
+ const recovery = 'Compatible root-format-2 reader recovery required; preserve the archive and update evidence and install an explicitly compatible CLI';
16
+ function invalid() { throw new Error(recovery); }
17
+ function stringField(header, start, length) {
18
+ const field = header.subarray(start, start + length);
19
+ const end = field.indexOf(0);
20
+ const bytes = end < 0 ? field : field.subarray(0, end);
21
+ const value = bytes.toString('utf8');
22
+ if (!Buffer.from(value).equals(bytes))
23
+ invalid();
24
+ return value;
25
+ }
26
+ function octal(header, start, length) {
27
+ const value = stringField(header, start, length).trim();
28
+ if (!/^[0-7]+$/.test(value))
29
+ invalid();
30
+ const parsed = Number.parseInt(value, 8);
31
+ if (!Number.isSafeInteger(parsed) || parsed < 0)
32
+ invalid();
33
+ return parsed;
34
+ }
35
+ function canonical(value, directory) {
36
+ if (directory && value.endsWith('/'))
37
+ value = value.slice(0, -1);
38
+ if (!value || value.length > 4096 || /[^\x20-\x7e]|\\/.test(value) || !value.startsWith('package/') && value !== 'package')
39
+ invalid();
40
+ if (value.split('/').length > 64 || value.split('/').some(part => !part || part === '.' || part === '..'))
41
+ invalid();
42
+ return value;
43
+ }
44
+ function paxFields(content) {
45
+ const fields = new Map();
46
+ let offset = 0;
47
+ while (offset < content.length) {
48
+ const space = content.indexOf(32, offset);
49
+ if (space < offset || space - offset > 10)
50
+ invalid();
51
+ const declared = content.subarray(offset, space).toString('ascii');
52
+ if (!/^[1-9][0-9]*$/.test(declared))
53
+ invalid();
54
+ const length = Number(declared);
55
+ if (!Number.isSafeInteger(length) || length <= space - offset + 2 || offset + length > content.length || content[offset + length - 1] !== 10)
56
+ invalid();
57
+ const field = content.subarray(space + 1, offset + length - 1);
58
+ const equals = field.indexOf(61);
59
+ if (equals < 1)
60
+ invalid();
61
+ const key = field.subarray(0, equals).toString('utf8');
62
+ const valueBytes = field.subarray(equals + 1);
63
+ const value = valueBytes.toString('utf8');
64
+ if (!Buffer.from(value).equals(valueBytes) || fields.has(key) || !['path', 'size', 'mtime', 'atime', 'ctime', 'uid', 'gid', 'uname', 'gname', 'SCHILY.dev', 'SCHILY.ino', 'SCHILY.nlink'].includes(key))
65
+ invalid();
66
+ fields.set(key, value);
67
+ offset += length;
68
+ }
69
+ return fields;
70
+ }
71
+ function decimal(value) {
72
+ if (!/^(0|[1-9][0-9]*)$/.test(value))
73
+ invalid();
74
+ const parsed = Number(value);
75
+ if (!Number.isSafeInteger(parsed) || parsed < 0)
76
+ invalid();
77
+ return parsed;
78
+ }
79
+ export async function readArchiveEvidence(archive) {
80
+ const handle = await fs.open(archive, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
81
+ let bytes;
82
+ try {
83
+ const stat = await handle.stat();
84
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size < 1 || stat.size > archiveLimit)
85
+ invalid();
86
+ bytes = await handle.readFile();
87
+ const after = await handle.stat();
88
+ if (bytes.length !== stat.size || stat.size !== after.size || stat.mtimeMs !== after.mtimeMs || stat.ctimeMs !== after.ctimeMs)
89
+ invalid();
90
+ }
91
+ finally {
92
+ await handle.close();
93
+ }
94
+ let tar;
95
+ try {
96
+ tar = gunzipSync(bytes, { maxOutputLength: expandedLimit });
97
+ }
98
+ catch {
99
+ invalid();
100
+ }
101
+ if (tar.length % 512)
102
+ invalid();
103
+ const seen = new Set();
104
+ const files = new Set();
105
+ const directories = new Set();
106
+ const spellings = new Map();
107
+ const runtimeFiles = [];
108
+ let metadata;
109
+ let pending;
110
+ let longName;
111
+ let offset = 0;
112
+ let ended = false;
113
+ while (offset + 512 <= tar.length) {
114
+ const header = tar.subarray(offset, offset + 512);
115
+ if (header.every(value => value === 0)) {
116
+ if (pending || longName || offset + 1024 > tar.length || !tar.subarray(offset).every(value => value === 0))
117
+ invalid();
118
+ ended = true;
119
+ break;
120
+ }
121
+ let checksum = 0;
122
+ for (let index = 0; index < 512; index += 1)
123
+ checksum += index >= 148 && index < 156 ? 32 : header[index];
124
+ if (checksum !== octal(header, 148, 8))
125
+ invalid();
126
+ if (!['ustar', 'ustar '].includes(stringField(header, 257, 6)))
127
+ invalid();
128
+ const type = stringField(header, 156, 1);
129
+ if (!['', '0', '5', 'x', 'L'].includes(type))
130
+ invalid();
131
+ const rawSize = octal(header, 124, 12);
132
+ const contentSize = type === 'x' || type === 'L' ? rawSize : pending?.has('size') ? decimal(pending.get('size')) : rawSize;
133
+ if (contentSize > memberLimit || (type === 'x' || type === 'L') && contentSize > metadataLimit)
134
+ invalid();
135
+ const start = offset + 512;
136
+ const end = start + contentSize;
137
+ if (end > tar.length)
138
+ invalid();
139
+ const content = tar.subarray(start, end);
140
+ offset = start + Math.ceil(contentSize / 512) * 512;
141
+ if (type === 'x') {
142
+ if (pending || longName)
143
+ invalid();
144
+ pending = paxFields(content);
145
+ continue;
146
+ }
147
+ if (type === 'L') {
148
+ if (pending || longName || !content.length || content[content.length - 1] !== 0)
149
+ invalid();
150
+ longName = stringField(content, 0, content.length);
151
+ continue;
152
+ }
153
+ const prefix = stringField(header, 345, 155);
154
+ const name = canonical(pending?.get('path') ?? longName ?? `${prefix ? prefix + '/' : ''}${stringField(header, 0, 100)}`, type === '5');
155
+ pending = undefined;
156
+ longName = undefined;
157
+ if (seen.has(name) || seen.size >= memberCountLimit || type === '5' && contentSize !== 0 || name === 'package' && type !== '5')
158
+ invalid();
159
+ const parts = name.split('/');
160
+ for (let index = 1; index <= parts.length; index += 1) {
161
+ const prefixName = parts.slice(0, index).join('/');
162
+ const alias = prefixName.toLowerCase();
163
+ const previous = spellings.get(alias);
164
+ if (previous && previous !== prefixName || index < parts.length && files.has(alias))
165
+ invalid();
166
+ spellings.set(alias, prefixName);
167
+ if (index < parts.length)
168
+ directories.add(alias);
169
+ }
170
+ const alias = name.toLowerCase();
171
+ if (type === '5') {
172
+ if (files.has(alias))
173
+ invalid();
174
+ directories.add(alias);
175
+ }
176
+ else {
177
+ if (directories.has(alias))
178
+ invalid();
179
+ files.add(alias);
180
+ }
181
+ seen.add(name);
182
+ if (type === '5')
183
+ continue;
184
+ if (name === 'package/package.json') {
185
+ if (contentSize > metadataLimit)
186
+ invalid();
187
+ try {
188
+ metadata = JSON.parse(content.toString('utf8'));
189
+ }
190
+ catch {
191
+ invalid();
192
+ }
193
+ }
194
+ if (name === 'package/package.json' || name.startsWith('package/dist/'))
195
+ runtimeFiles.push({ path: name.slice(8), size: contentSize, sha256: createHash('sha256').update(content).digest('hex') });
196
+ }
197
+ if (!ended || typeof metadata !== 'object' || !metadata || Array.isArray(metadata))
198
+ invalid();
199
+ const value = metadata;
200
+ const reader = rootReaderSchema.safeParse(value.axomWorkstationRootReader);
201
+ if (value.name !== '@platless/axom-cli' || typeof value.version !== 'string' || !value.version || value.version.length > 128 || !reader.success)
202
+ invalid();
203
+ if (!value.bin || typeof value.bin !== 'object' || Array.isArray(value.bin) || value.bin.axom !== 'dist/cli.js' || !runtimeFiles.some(entry => entry.path === 'dist/cli.js'))
204
+ invalid();
205
+ return { name: value.name, version: value.version, size: bytes.length, sha512: createHash('sha512').update(bytes).digest('hex'), reader: reader.data, runtimeFiles: runtimeFiles.sort((left, right) => left.path.localeCompare(right.path)) };
206
+ }
@@ -1,3 +1,4 @@
1
+ import { deploymentDownloadURL } from '../control/serverIdentity.js';
1
2
  import { createHash, timingSafeEqual } from 'node:crypto';
2
3
  import { createWriteStream, promises as fs } from 'node:fs';
3
4
  import { tmpdir } from 'node:os';
@@ -16,7 +17,7 @@ function safeRelease(release, server) {
16
17
  if ((configured.protocol !== 'http:' && configured.protocol !== 'https:') || configured.username || configured.password) {
17
18
  throw new Error('Configured server URL must be an HTTP(S) origin without credentials');
18
19
  }
19
- const url = new URL(release.url, configured);
20
+ const url = new URL(deploymentDownloadURL(server, release.url));
20
21
  // The release descriptor arrives over the authenticated management connection, but keeping the
21
22
  // download on that exact origin prevents an injected relative/absolute URL from turning the
22
23
  // client into a credentialed downloader. HTTP remains supported for private-network development
@@ -22,6 +22,11 @@ export class UpdateCoordinator {
22
22
  constructor(options) {
23
23
  this.options = options;
24
24
  }
25
+ legacyTasks() {
26
+ if (this.options.deviceAdmission || !this.options.tasks)
27
+ throw new Error('Legacy task admission is not available for a device update');
28
+ return this.options.tasks;
29
+ }
25
30
  snapshot() { return { ...this.status }; }
26
31
  async snapshotAsync() { await this.ensureInitialized(); return this.snapshot(); }
27
32
  async handle(command) {
@@ -70,6 +75,8 @@ export class UpdateCoordinator {
70
75
  */
71
76
  async managementReady(release, generation) {
72
77
  await this.ensureInitialized();
78
+ if (this.options.deviceAdmission)
79
+ return;
73
80
  const readiness = this.options.readiness;
74
81
  if (!release)
75
82
  return;
@@ -149,6 +156,7 @@ export class UpdateCoordinator {
149
156
  async transition(status) {
150
157
  this.status = status;
151
158
  await this.options.state.save(status);
159
+ await this.options.persistDevicePhase?.(status);
152
160
  // After the persist, and swallowed: a logger is a witness to the update, never a participant
153
161
  // that can fail one.
154
162
  try {
@@ -167,16 +175,23 @@ export class UpdateCoordinator {
167
175
  try {
168
176
  await this.transition(this.base(command, 'REQUESTED'));
169
177
  let drained = null;
170
- if (command.force) {
171
- await this.options.tasks.stopAll();
178
+ if (this.options.deviceAdmission) {
179
+ if (command.force)
180
+ throw new Error('Destructive all-device force requires a separately confirmed local action');
181
+ await this.transition(this.base(command, 'DRAINING'));
182
+ if (!await this.options.deviceAdmission.observe())
183
+ return this.transition(this.base(command, 'BUSY'));
184
+ }
185
+ else if (command.force) {
186
+ await this.legacyTasks().stopAll();
172
187
  }
173
188
  else {
174
189
  // Close admission before the persisted transition can yield: no command may enter in the
175
190
  // gap between announcing DRAINING and establishing the barrier it promises.
176
- this.options.tasks.accepting = false;
191
+ this.legacyTasks().accepting = false;
177
192
  await this.transition(this.base(command, 'DRAINING'));
178
193
  drainCancellation = new AbortController();
179
- drained = this.options.tasks.waitForIdle(this.options.drainTimeoutMs ?? DEFAULT_UPDATE_DRAIN_TIMEOUT_MS, drainCancellation.signal);
194
+ drained = this.legacyTasks().waitForIdle(this.options.drainTimeoutMs ?? DEFAULT_UPDATE_DRAIN_TIMEOUT_MS, drainCancellation.signal);
180
195
  }
181
196
  await this.transition(this.base(command, 'DOWNLOADING'));
182
197
  if (this.options.downloader)
@@ -187,10 +202,13 @@ export class UpdateCoordinator {
187
202
  artifact = await downloadVerified(command.release, this.options.artifact);
188
203
  }
189
204
  await this.transition(this.base(command, 'VERIFYING'));
205
+ await this.options.verifyArtifact?.(artifact, command);
190
206
  if (drained && !await drained) {
191
- this.options.tasks.accepting = true;
192
- return this.transition({ ...this.base(command, 'BUSY'), ...this.options.tasks.snapshot() });
207
+ this.legacyTasks().accepting = true;
208
+ return this.transition({ ...this.base(command, 'BUSY'), ...this.legacyTasks().snapshot() });
193
209
  }
210
+ if (this.options.deviceAdmission && !await this.options.deviceAdmission.observe())
211
+ return this.transition(this.base(command, 'BUSY'));
194
212
  // This is intentionally after a verified new archive exists but before npm mutates the
195
213
  // installed package. A missing/invalid last-known-good archive must leave the client alone.
196
214
  rollback = await this.options.rollback?.prepare(command) ?? null;
@@ -214,6 +232,8 @@ export class UpdateCoordinator {
214
232
  // the successor loading INSTALLING and heartbeating a non-terminal phase forever, which the
215
233
  // server never resolves and which blocks every later update command.
216
234
  const restarting = await this.transition(this.base(command, 'RESTARTING'));
235
+ if (this.options.deviceAdmission && this.options.restart)
236
+ await this.options.deviceAdmission.handoff();
217
237
  // Deliberately not inside the try/catch that restores the package: a service definition we
218
238
  // failed to rewrite does not make the installed package wrong, and rolling npm back over a
219
239
  // provisioning failure would be a much bigger hammer than the problem.
@@ -228,8 +248,8 @@ export class UpdateCoordinator {
228
248
  }
229
249
  catch (provisionError) {
230
250
  this.options.onProvisionFailure?.(provisionError);
231
- if (!command.force)
232
- this.options.tasks.accepting = true;
251
+ if (!command.force && !this.options.deviceAdmission)
252
+ this.legacyTasks().accepting = true;
233
253
  return this.transition({ ...this.base(command, 'FAILED'), error: failure(provisionError) });
234
254
  }
235
255
  if (!this.options.restart) {
@@ -249,19 +269,20 @@ export class UpdateCoordinator {
249
269
  // SUCCEEDED is the honest phase: the install did succeed. What has not happened is the
250
270
  // restart, which the caller is the one positioned to say — `axom update` prints it, and
251
271
  // the server-driven case sees the new version only after the operator restarts.
252
- if (!command.force)
253
- this.options.tasks.accepting = true;
272
+ if (!command.force && !this.options.deviceAdmission)
273
+ this.legacyTasks().accepting = true;
254
274
  return this.transition(this.base(command, 'SUCCEEDED'));
255
275
  }
256
276
  // Keeper-hosted agent sessions outlive this process: detach them first (head checkpoint, adapter untouched — no
257
277
  // `session/cancel`, no failed turn), so the next daemon adopts them where they are (spec §5.5.5).
258
- await this.options.tasks.detachDurableSessions();
278
+ if (!this.options.deviceAdmission)
279
+ await this.legacyTasks().detachDurableSessions();
259
280
  await this.options.restart();
260
281
  return restarting;
261
282
  }
262
283
  catch (error) {
263
- if (!command.force)
264
- this.options.tasks.accepting = true;
284
+ if (!command.force && !this.options.deviceAdmission)
285
+ this.legacyTasks().accepting = true;
265
286
  return this.transition({ ...this.base(command, 'FAILED'), error: failure(error) });
266
287
  }
267
288
  finally {