@platless/axom-cli 0.5.9 → 0.6.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/README.md +25 -6
  2. package/dist/axom-patch/cli/README.md +129 -12
  3. package/dist/axom-patch/cli/account-limits/generate.py +15 -12
  4. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
  5. package/dist/axom-patch/cli/claude-account-limits.json +9 -9
  6. package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
  7. package/dist/axom-patch/cli/codex-account-limits.json +11 -11
  8. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  9. package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
  10. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  11. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  12. package/dist/axom-patch/cli/nightly-task.md +55 -13
  13. package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
  14. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  15. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  16. package/dist/axom-patch/cli/probes/zcode-env-account.mjs +81 -0
  17. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
  18. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1687 -0
  19. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +277 -0
  20. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
  21. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
  22. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  23. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  24. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  25. package/dist/axom-patch/cli/runtime.json +19 -19
  26. package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
  27. package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
  28. package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
  29. package/dist/axom-patch/cli/zcode-env-account.json +58 -0
  30. package/dist/axom-patch/cli/zcode-models.json +9 -10
  31. package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -11
  32. package/dist/cli.js +11 -45
  33. package/dist/commands/auth.js +4 -1
  34. package/dist/commands/knowledge.js +47 -0
  35. package/dist/commands/update.js +126 -3
  36. package/dist/commands/workstation.js +101 -164
  37. package/dist/commands/workstationRegistrations.js +116 -0
  38. package/dist/credentialBroker.js +13 -2
  39. package/dist/credentials.js +16 -0
  40. package/dist/install/claude/commands.js +15 -15
  41. package/dist/keepers/terminal/keeper.mjs +1089 -673
  42. package/dist/keepers/terminal/manifest.json +4 -4
  43. package/dist/mcp.js +1 -1
  44. package/dist/native/darwin-x64/axom-quic +0 -0
  45. package/dist/workstation/acp/acpAgents.js +113 -102
  46. package/dist/workstation/acp/acpClient.js +26 -0
  47. package/dist/workstation/acp/builds/install/npm.js +99 -10
  48. package/dist/workstation/acp/initializeResponse.js +6 -1
  49. package/dist/workstation/acp/managedPatches.js +189 -25
  50. package/dist/workstation/acp/managedRuntime.js +314 -0
  51. package/dist/workstation/acp/outbox.js +12 -5
  52. package/dist/workstation/acp/patchAssets.js +40 -0
  53. package/dist/workstation/acp/permission.js +2 -2
  54. package/dist/workstation/acp/processor/checkpoint.js +26 -34
  55. package/dist/workstation/acp/processor/control.js +7 -80
  56. package/dist/workstation/acp/processor/jsonrpc.js +8 -0
  57. package/dist/workstation/acp/processor/runtime.js +5 -81
  58. package/dist/workstation/acp/processor/session.js +54 -82
  59. package/dist/workstation/acp/processor/state.js +7 -13
  60. package/dist/workstation/acp/processor/step.js +3 -20
  61. package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
  62. package/dist/workstation/acp/processor/transport/local.js +19 -3
  63. package/dist/workstation/acp/processor/updates.js +156 -105
  64. package/dist/workstation/acp/processorSession.js +20 -173
  65. package/dist/workstation/acp/registry/catalog.js +65 -39
  66. package/dist/workstation/acp/registry/launch.js +16 -1
  67. package/dist/workstation/acp/switching.js +0 -3
  68. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  69. package/dist/workstation/acp/test/keeperWorld.js +32 -3
  70. package/dist/workstation/acp/test/raw/sources.js +16 -130
  71. package/dist/workstation/acp/test/realAdapter.js +1 -47
  72. package/dist/workstation/acp/test/scriptAgent.js +11 -3
  73. package/dist/workstation/agentFlow.js +1 -1
  74. package/dist/workstation/agentSessions.js +114 -166
  75. package/dist/workstation/channels/agent.js +9 -1
  76. package/dist/workstation/channels/computer.js +27 -4
  77. package/dist/workstation/channels/management.js +130 -15
  78. package/dist/workstation/channels/url.js +1 -1
  79. package/dist/workstation/config.js +33 -9
  80. package/dist/workstation/connection/encryptedChannel.js +4 -1
  81. package/dist/workstation/control/bindingStore.js +204 -0
  82. package/dist/workstation/control/commands.js +125 -0
  83. package/dist/workstation/control/daemonIdentity.js +143 -0
  84. package/dist/workstation/control/deviceCommands.js +191 -0
  85. package/dist/workstation/control/deviceProtocol.js +68 -0
  86. package/dist/workstation/control/enrollment.js +106 -0
  87. package/dist/workstation/control/inspect.js +71 -0
  88. package/dist/workstation/control/locks.js +110 -0
  89. package/dist/workstation/control/protocol.js +40 -0
  90. package/dist/workstation/control/restartGuard.js +46 -0
  91. package/dist/workstation/control/rootFormat.js +16 -0
  92. package/dist/workstation/control/runtimeClient.js +152 -0
  93. package/dist/workstation/control/runtimeHost.js +235 -0
  94. package/dist/workstation/control/serverIdentity.js +35 -0
  95. package/dist/workstation/control/updateTransition.js +91 -0
  96. package/dist/workstation/daemon.js +266 -264
  97. package/dist/workstation/daemonctl.js +226 -25
  98. package/dist/workstation/desktopControl/audio.js +99 -0
  99. package/dist/workstation/desktopControl/automationDevice.js +219 -0
  100. package/dist/workstation/desktopControl/automationJson.js +74 -0
  101. package/dist/workstation/desktopControl/automationLocalState.js +23 -0
  102. package/dist/workstation/desktopControl/automationProtocol.js +253 -0
  103. package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
  104. package/dist/workstation/desktopControl/automationSession.js +81 -0
  105. package/dist/workstation/desktopControl/automationTransport.js +138 -0
  106. package/dist/workstation/desktopControl/capabilities.js +93 -0
  107. package/dist/workstation/desktopControl/captureModes.js +9 -0
  108. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  109. package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
  110. package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
  111. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  112. package/dist/workstation/desktopControl/helperProcess.js +602 -0
  113. package/dist/workstation/desktopControl/hostGrant.js +65 -0
  114. package/dist/workstation/desktopControl/lobby.js +131 -0
  115. package/dist/workstation/desktopControl/macProvider.js +156 -0
  116. package/dist/workstation/desktopControl/media.js +331 -0
  117. package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
  118. package/dist/workstation/desktopControl/nativeProvider.js +1044 -0
  119. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  120. package/dist/workstation/desktopControl/overview.js +58 -0
  121. package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
  122. package/dist/workstation/desktopControl/protocol.js +311 -0
  123. package/dist/workstation/desktopControl/provider.js +1 -0
  124. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  125. package/dist/workstation/desktopControl/rateController.js +226 -0
  126. package/dist/workstation/desktopControl/supervisor.js +1774 -0
  127. package/dist/workstation/desktopControl/transport.js +27 -0
  128. package/dist/workstation/desktopControl/wallpaper.js +98 -0
  129. package/dist/workstation/exec/policy.js +5 -0
  130. package/dist/workstation/files/client.js +10 -1
  131. package/dist/workstation/files/credentials.js +142 -0
  132. package/dist/workstation/files/host.js +9 -2
  133. package/dist/workstation/files/text.js +42 -8
  134. package/dist/workstation/journal.js +5 -2
  135. package/dist/workstation/keepers/acp/client.js +38 -2
  136. package/dist/workstation/keepers/kit/buildVersion.js +35 -11
  137. package/dist/workstation/keepers/kit/bundle.js +21 -17
  138. package/dist/workstation/keepers/kit/client.js +2 -2
  139. package/dist/workstation/keepers/kit/launcher.js +123 -7
  140. package/dist/workstation/keepers/kit/paths.js +2 -1
  141. package/dist/workstation/keepers/legacy.js +16 -2
  142. package/dist/workstation/keepers/policy.js +5 -34
  143. package/dist/workstation/keepers/terminal/client.js +115 -23
  144. package/dist/workstation/keepers/terminal/exec.js +6 -3
  145. package/dist/workstation/keepers/terminal/frames.js +73 -0
  146. package/dist/workstation/keepers/terminal/generations.js +5 -8
  147. package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
  148. package/dist/workstation/keepers/terminal/input.js +146 -0
  149. package/dist/workstation/keepers/terminal/keeper.js +22 -8
  150. package/dist/workstation/keepers/terminal/main.js +1 -1
  151. package/dist/workstation/keepers/terminal/metadata.js +138 -0
  152. package/dist/workstation/keepers/terminal/protocol.js +4 -28
  153. package/dist/workstation/keepers/terminal/pty.js +1 -330
  154. package/dist/workstation/keepers/terminal/screen.js +246 -0
  155. package/dist/workstation/keepers/terminal/sessions.js +526 -0
  156. package/dist/workstation/managedInstall.js +57 -0
  157. package/dist/workstation/oauth/deviceAuthorization.js +31 -17
  158. package/dist/workstation/protocol/agent.js +2 -15
  159. package/dist/workstation/protocol/management.js +113 -5
  160. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  161. package/dist/workstation/rd/artifact.js +120 -0
  162. package/dist/workstation/rd/control.js +117 -0
  163. package/dist/workstation/rd/display.js +29 -0
  164. package/dist/workstation/rd/host.js +213 -0
  165. package/dist/workstation/rd/migrationReadiness.js +46 -0
  166. package/dist/workstation/rd/provider.js +256 -0
  167. package/dist/workstation/rd/sessions.js +391 -0
  168. package/dist/workstation/rd/state.js +57 -0
  169. package/dist/workstation/rd/update.js +168 -0
  170. package/dist/workstation/rd/upkeep.js +80 -0
  171. package/dist/workstation/register.js +27 -37
  172. package/dist/workstation/registrations/enrollment.js +250 -0
  173. package/dist/workstation/registrations/enrollmentPending.js +361 -0
  174. package/dist/workstation/registrations/migration.js +356 -0
  175. package/dist/workstation/registrations/model.js +34 -0
  176. package/dist/workstation/registrations/removal.js +222 -0
  177. package/dist/workstation/registrations/store.js +737 -0
  178. package/dist/workstation/runtime/desktopBinding.js +20 -0
  179. package/dist/workstation/runtime/deviceDesktop.js +193 -0
  180. package/dist/workstation/runtime/deviceRuntime.js +568 -0
  181. package/dist/workstation/runtime/deviceServices.js +117 -0
  182. package/dist/workstation/runtime/events.js +20 -0
  183. package/dist/workstation/runtime/ownership.js +100 -0
  184. package/dist/workstation/runtime/registrationRuntime.js +575 -0
  185. package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
  186. package/dist/workstation/runtime/removalActivity.js +184 -0
  187. package/dist/workstation/service/installer.js +197 -14
  188. package/dist/workstation/service/launchd.js +2 -0
  189. package/dist/workstation/service/systemd.js +1 -0
  190. package/dist/workstation/storageMetrics.js +118 -0
  191. package/dist/workstation/systemMetrics.js +47 -92
  192. package/dist/workstation/toolchain.js +94 -0
  193. package/dist/workstation/tools/browser.js +28 -4
  194. package/dist/workstation/tools/client.js +8 -27
  195. package/dist/workstation/tools/info.js +6 -13
  196. package/dist/workstation/tools/terminalRouter.js +17 -172
  197. package/dist/workstation/tools/validation.js +55 -1
  198. package/dist/workstation/update/archiveEvidence.js +206 -0
  199. package/dist/workstation/update/artifact.js +2 -1
  200. package/dist/workstation/update/coordinator.js +34 -13
  201. package/dist/workstation/update/deviceCompatibility.js +119 -0
  202. package/dist/workstation/update/deviceCoordinator.js +369 -0
  203. package/dist/workstation/update/deviceHealth.js +104 -0
  204. package/dist/workstation/update/deviceHealthProtocol.js +26 -0
  205. package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
  206. package/dist/workstation/update/deviceRollback.js +78 -0
  207. package/dist/workstation/update/globalInstall.js +186 -0
  208. package/dist/workstation/update/legacyBridge.js +414 -0
  209. package/dist/workstation/update/legacyOwner.js +39 -0
  210. package/dist/workstation/update/npmInstaller.js +51 -25
  211. package/dist/workstation/update/privateDurability.js +61 -0
  212. package/dist/workstation/update/rdRollback.js +145 -0
  213. package/dist/workstation/update/recoveryPlan.js +124 -0
  214. package/dist/workstation/update/runUpdate.js +10 -3
  215. package/dist/workstation/update/taskRegistry.js +161 -3
  216. package/dist/workstation/update/watchdog.js +29806 -333
  217. package/dist/workstation/update/watchdogProcess.js +72 -0
  218. package/package.json +11 -7
  219. package/skills/axom/SKILL.md +11 -11
  220. package/skills/axom-definition/SKILL.md +3 -3
  221. package/skills/axom-platform/SKILL.md +3 -3
  222. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  223. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
  224. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  225. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  226. package/dist/commands/guide.js +0 -38
  227. package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
  228. package/dist/runtime-deps/qoder-sdk/package.json +0 -8
  229. package/dist/workstation/acp/delegationMcp.js +0 -24
  230. package/dist/workstation/acp/processor/sideChannel.js +0 -80
  231. package/dist/workstation/acp/qoderStartup.js +0 -45
  232. package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
  233. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
  234. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
  235. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
  236. package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
  237. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  238. package/dist/workstation/qoderAdapter/contract.js +0 -78
  239. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
  240. package/dist/workstation/qoderAdapter/history.js +0 -85
  241. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  242. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  243. package/dist/workstation/qoderAdapter/main.js +0 -95
  244. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  245. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  246. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  247. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  248. package/dist/workstation/qoderAdapter/profile.js +0 -93
  249. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
  250. package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
  251. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
  252. package/dist/workstation/qoderAdapter/session.js +0 -319
  253. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  254. package/dist/workstation/qoderAdapter/subwork.js +0 -85
  255. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  256. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  257. package/dist/workstation/qoderAdapter/usage.js +0 -37
  258. /package/dist/workstation/{qoderAdapter/types.js → desktopControl/automationPreparation.js} +0 -0
@@ -0,0 +1,93 @@
1
+ import { canonicalDesktopJson, decodeCapabilityReport, decodeDesktopManagementFrame } from './protocol.js';
2
+ export const DESKTOP_PROBE_CAP_MS = 4000;
3
+ /** Failure telemetry only: no backend, geometry or input is eligible for admission. */
4
+ const unavailable = () => ({
5
+ protocols: { min: 2, max: 2 }, providerId: 'discovery', providerVersion: 'unknown', configured: true,
6
+ permission: 'not_applicable', health: 'unavailable', reason: 'probe_failed', formats: ['h264-420-v1'], encoderBackends: [],
7
+ input: { pointer: false, physicalKeyboard: false, unicodeText: false },
8
+ displays: [{ id: '0'.repeat(32), label: 'Unavailable', width: 2, height: 2, revision: 1, resize: 'none', modes: [] }],
9
+ });
10
+ export async function capabilitiesForWire(options) {
11
+ if (options.selectedWire < 9)
12
+ return undefined;
13
+ let timer;
14
+ const generation = options.generation ?? 'local';
15
+ if (!options.provider)
16
+ return { generation, revision: 1, desktop: null };
17
+ try {
18
+ const cap = Math.max(1, Math.min(DESKTOP_PROBE_CAP_MS, options.probeCapMs ?? DESKTOP_PROBE_CAP_MS));
19
+ const desktop = await Promise.race([
20
+ Promise.resolve().then(() => options.provider.probe()),
21
+ new Promise(resolve => { timer = setTimeout(() => resolve(unavailable()), cap); timer.unref?.(); }),
22
+ ]);
23
+ const compatible = desktop?.deviceOwnershipProtocol === 1 && options.selectedWire < 10 ? null : desktop;
24
+ const report = decodeCapabilityReport(JSON.stringify({ generation, revision: 1, desktop: compatible && { ...compatible, lobby: true } }));
25
+ decodeDesktopManagementFrame(JSON.stringify({ t: 'workstation_capabilities', ...report }));
26
+ return report;
27
+ }
28
+ catch {
29
+ return { generation, revision: 1, desktop: unavailable() };
30
+ }
31
+ finally {
32
+ clearTimeout(timer);
33
+ }
34
+ }
35
+ export class DesktopCapabilityReporter {
36
+ options;
37
+ current;
38
+ canonical;
39
+ running = false;
40
+ again = false;
41
+ disposed = false;
42
+ probe;
43
+ constructor(options) {
44
+ this.options = options;
45
+ }
46
+ get revision() { return this.current?.revision; }
47
+ refresh() {
48
+ if (this.disposed || this.options.selectedWire < 9)
49
+ return;
50
+ if (this.running) {
51
+ this.again = true;
52
+ return;
53
+ }
54
+ this.running = true;
55
+ void this.report().finally(() => {
56
+ this.running = false;
57
+ if (this.again) {
58
+ this.again = false;
59
+ this.refresh();
60
+ }
61
+ });
62
+ }
63
+ async report() {
64
+ const provider = this.options.provider;
65
+ // A wedged probe remains a single outstanding operation, even across repeated requests.
66
+ const boundedProvider = provider && { ...provider, probe: () => {
67
+ if (!this.probe) {
68
+ const pending = Promise.resolve().then(() => provider.probe());
69
+ this.probe = pending;
70
+ void pending.then(() => { if (this.probe === pending)
71
+ this.probe = undefined; }, () => { if (this.probe === pending)
72
+ this.probe = undefined; });
73
+ }
74
+ return this.probe;
75
+ } };
76
+ const report = await capabilitiesForWire({ ...this.options, provider: boundedProvider });
77
+ if (this.disposed || !report)
78
+ return;
79
+ const canonical = canonicalDesktopJson(report.desktop);
80
+ const revision = this.current ? this.current.revision + (canonical === this.canonical ? 0 : 1) : 1;
81
+ if (!Number.isSafeInteger(revision)) {
82
+ this.dispose();
83
+ return;
84
+ }
85
+ this.canonical = canonical;
86
+ this.current = { ...report, revision };
87
+ try {
88
+ this.options.send(this.current);
89
+ }
90
+ catch { /* a later request/heartbeat retries the snapshot */ }
91
+ }
92
+ dispose() { this.disposed = true; this.again = false; }
93
+ }
@@ -0,0 +1,9 @@
1
+ /** Capture sampling presets preserve the source aspect and never change its display mode. */
2
+ export function captureSamplingModes(width, height, pixelScale = 1) {
3
+ const w = width * pixelScale, h = height * pixelScale;
4
+ const modes = [1280, 1920, 2560, 3840, 4096].map(edge => {
5
+ const scale = Math.min(1, edge / Math.max(w, h), 4096 / w, 4096 / h, Math.sqrt(8847360 / (w * h)));
6
+ return { width: Math.max(2, Math.floor(w * scale / 2) * 2), height: Math.max(2, Math.floor(h * scale / 2) * 2) };
7
+ });
8
+ return modes.filter((mode, index) => modes.findIndex(m => m.width === mode.width && m.height === mode.height) === index);
9
+ }
@@ -0,0 +1,9 @@
1
+ export function captureTargetLabel(label) {
2
+ let result = '';
3
+ for (const char of label.replace(/[\x00-\x1f\x7f]/g, ' ')) {
4
+ if (Buffer.byteLength(result + char) > 128)
5
+ break;
6
+ result += char;
7
+ }
8
+ return result.trim() || 'Window';
9
+ }
@@ -0,0 +1,229 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { bounded } from './helperProcess.js';
3
+ import { nativeReceiptMatches } from './ownershipProtocol.js';
4
+ export class DeviceAdmissionConnection {
5
+ registrationId;
6
+ owner;
7
+ host;
8
+ report;
9
+ generation;
10
+ admissions = new Map();
11
+ commands = new Map();
12
+ highestAcquireSequence = 0;
13
+ open = true;
14
+ accepting = true;
15
+ unwatch;
16
+ constructor(registrationId, owner, host, report) {
17
+ this.registrationId = registrationId;
18
+ this.owner = owner;
19
+ this.host = host;
20
+ this.report = report;
21
+ this.unwatch = owner.observe(() => { if (this.generation && this.open)
22
+ this.event(this.generation, '0'.repeat(32), 'snapshot', 'snapshot'); });
23
+ }
24
+ ready(generation) {
25
+ if (this.generation === generation)
26
+ return;
27
+ this.closeAdmission();
28
+ this.generation = generation;
29
+ this.open = true;
30
+ this.commands.clear();
31
+ this.highestAcquireSequence = 0;
32
+ this.pruneAdmissions();
33
+ this.event(generation, '0'.repeat(32), 'snapshot', 'snapshot');
34
+ }
35
+ closed(generation) { if (this.generation === generation) {
36
+ this.closeAdmission();
37
+ this.generation = undefined;
38
+ } }
39
+ retire(admission) {
40
+ admission.revoked = true;
41
+ admission.committed = false;
42
+ clearTimeout(admission.expiry);
43
+ if (admission.grant) {
44
+ try {
45
+ admission.host.revoke(admission.grant);
46
+ }
47
+ catch { }
48
+ }
49
+ }
50
+ closeAdmission() { this.open = false; for (const admission of this.admissions.values())
51
+ this.retire(admission); }
52
+ resumeAdmission() { if (this.generation)
53
+ this.open = true; }
54
+ holdNewAdmission() { this.accepting = false; }
55
+ resumeNewAdmission() { this.accepting = true; }
56
+ async isolate(admission) {
57
+ this.retire(admission);
58
+ if (admission.pending && !admission.grant)
59
+ throw new Error('physical_isolation_unconfirmed');
60
+ if (admission.grant && !admission.isolated && !await bounded(this.owner.release(admission.grant), 15_000))
61
+ throw new Error('physical_isolation_unconfirmed');
62
+ }
63
+ async isolateOriginal() {
64
+ const outcomes = await Promise.allSettled([...this.admissions.values()].map(admission => this.isolate(admission)));
65
+ this.pruneAdmissions();
66
+ const failures = outcomes.flatMap(outcome => outcome.status === 'rejected' ? [outcome.reason] : []);
67
+ if (failures.length)
68
+ throw new AggregateError(failures, 'physical_isolation_unconfirmed');
69
+ }
70
+ dispose() { this.closeAdmission(); this.unwatch(); }
71
+ key(generation, controllerId) { return `${generation}:${controllerId}`; }
72
+ pruneAdmissions() {
73
+ for (const [key, admission] of this.admissions)
74
+ if (!admission.pending && (!admission.grant || admission.isolated)) {
75
+ clearTimeout(admission.expiry);
76
+ this.admissions.delete(key);
77
+ }
78
+ }
79
+ event(generation, commandId, controllerId, status, reservationCause) {
80
+ if (this.generation === generation)
81
+ this.report({ t: 'desktop_ownership_result', ownershipProtocol: 1, commandId, controllerId,
82
+ deviceGeneration: generation, status, snapshot: this.owner.project(this.registrationId), ...(reservationCause ? { reservationCause } : {}) });
83
+ }
84
+ dispatch(command) {
85
+ if (!this.generation || command.deviceGeneration !== this.generation)
86
+ return Promise.resolve();
87
+ if (command.action === 'release' || command.action === 'cancel')
88
+ return this.perform(command);
89
+ const text = JSON.stringify(command), previous = this.commands.get(command.commandId);
90
+ if (previous)
91
+ return previous.text === text ? previous.result : Promise.reject(new Error('conflict'));
92
+ for (const [key, replay] of this.commands)
93
+ if (this.commands.size >= 1024 && replay.settled)
94
+ this.commands.delete(key);
95
+ if (this.commands.size >= 1024)
96
+ return Promise.reject(new Error('resource_limit'));
97
+ const replay = { text, settled: false, result: Promise.resolve() };
98
+ this.commands.set(command.commandId, replay);
99
+ replay.result = this.perform(command).finally(() => { replay.settled = true; });
100
+ return replay.result;
101
+ }
102
+ async perform(command) {
103
+ const generation = command.deviceGeneration, host = this.host;
104
+ const current = () => this.open && this.generation === generation && host?.current() === true;
105
+ const event = (status) => this.event(generation, command.commandId, command.controllerId, status);
106
+ this.pruneAdmissions();
107
+ const key = this.key(generation, command.controllerId);
108
+ if (command.action === 'cancel') {
109
+ const requestSequence = command.requestSequence;
110
+ if (!Number.isSafeInteger(requestSequence) || requestSequence < 1 || !command.acquireCommandId) {
111
+ event('conflict');
112
+ return;
113
+ }
114
+ this.highestAcquireSequence = Math.max(this.highestAcquireSequence, requestSequence);
115
+ const admission = this.admissions.get(key);
116
+ if (!admission || admission.command.commandId !== command.acquireCommandId || admission.command.requestSequence !== requestSequence) {
117
+ event('cancelled');
118
+ return;
119
+ }
120
+ try {
121
+ await this.isolate(admission);
122
+ event('cancelled');
123
+ }
124
+ catch {
125
+ event('busy');
126
+ }
127
+ this.pruneAdmissions();
128
+ return;
129
+ }
130
+ if (command.action !== 'acquire') {
131
+ const admission = this.admissions.get(key), grant = admission?.grant;
132
+ if (!admission || !grant || grant.deviceEpoch !== command.deviceEpoch || grant.revision !== command.revision) {
133
+ event('conflict');
134
+ return;
135
+ }
136
+ if (command.action === 'release') {
137
+ try {
138
+ await this.isolate(admission);
139
+ event('released');
140
+ }
141
+ catch {
142
+ event('busy');
143
+ }
144
+ this.pruneAdmissions();
145
+ return;
146
+ }
147
+ if (!current()) {
148
+ event('unsupported');
149
+ return;
150
+ }
151
+ if (admission.committed && this.owner.current(grant, host)) {
152
+ event('committed');
153
+ return;
154
+ }
155
+ if (!this.accepting) {
156
+ event('busy');
157
+ return;
158
+ }
159
+ admission.committing = command.commandId;
160
+ let committed = !admission.revoked && (await this.owner.commit(grant)).status === 'granted';
161
+ if (committed && current() && this.owner.current(grant, host)) {
162
+ admission.committed = true;
163
+ clearTimeout(admission.expiry);
164
+ }
165
+ else {
166
+ try {
167
+ await this.isolate(admission);
168
+ }
169
+ catch { }
170
+ committed = false;
171
+ }
172
+ event(committed ? 'committed' : 'busy');
173
+ this.pruneAdmissions();
174
+ return;
175
+ }
176
+ const requestSequence = command.requestSequence;
177
+ if (!Number.isSafeInteger(requestSequence) || requestSequence <= this.highestAcquireSequence) {
178
+ event('conflict');
179
+ return;
180
+ }
181
+ this.highestAcquireSequence = requestSequence;
182
+ if (!host || !current() || !this.accepting) {
183
+ event('unsupported');
184
+ return;
185
+ }
186
+ if (!command.acquisition || this.admissions.has(key) || this.admissions.size >= 1024) {
187
+ event('conflict');
188
+ return;
189
+ }
190
+ const admission = { pending: true, committed: false, revoked: false, isolated: false, host, command };
191
+ this.admissions.set(key, admission);
192
+ admission.expiry = setTimeout(() => { this.retire(admission); if (admission.grant)
193
+ void this.isolate(admission).catch(() => { }); }, 15_000);
194
+ admission.expiry.unref();
195
+ try {
196
+ const result = await this.owner.reserve({ registrationId: this.registrationId, controllerId: command.controllerId, kind: command.kind,
197
+ reservationCause: Object.freeze({ acquireCommandId: command.commandId, contenderControllerId: command.controllerId, deviceGeneration: generation }),
198
+ hostLifetime: host, valid: () => current() && !admission.revoked,
199
+ revoke: () => this.retire(admission),
200
+ admit: async (grant) => { const commandId = admission.committing; return !!commandId && nativeReceiptMatches(host, commandId, grant, 'admit', await host.admit(commandId, grant)); },
201
+ isolate: async () => {
202
+ if (!admission.grant)
203
+ return false;
204
+ const commandId = randomBytes(16).toString('hex');
205
+ admission.isolated = nativeReceiptMatches(host, commandId, admission.grant, 'isolate', await host.isolate(commandId, admission.grant));
206
+ return admission.isolated;
207
+ },
208
+ lost: (_snapshot, cause) => this.event(generation, command.commandId, command.controllerId, 'lost', cause?.deviceGeneration === generation ? cause : undefined),
209
+ }, command.acquisition);
210
+ if (result.status === 'granted')
211
+ admission.grant = result.grant;
212
+ event(result.status === 'granted' ? 'acquired' : result.status);
213
+ }
214
+ finally {
215
+ admission.pending = false;
216
+ this.pruneAdmissions();
217
+ }
218
+ }
219
+ grant(controllerId) {
220
+ const admission = this.generation ? this.admissions.get(this.key(this.generation, controllerId)) : undefined;
221
+ return admission?.grant && admission.committed && !admission.revoked && this.open && this.host?.current() &&
222
+ this.owner.current(admission.grant, this.host) ? admission.grant : undefined;
223
+ }
224
+ current(grant) { return this.grant(grant.controllerId) === grant; }
225
+ activity() {
226
+ return { known: [...this.admissions.values()].every(admission => !admission.pending && (!admission.grant || admission.isolated || this.owner.current(admission.grant))),
227
+ consumers: [...this.admissions.values()].filter(admission => admission.pending || admission.grant && !admission.isolated).length };
228
+ }
229
+ }
@@ -0,0 +1,130 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { deviceAcquisitionSchema, matchesDeviceGrant } from './ownershipProtocol.js';
3
+ export class DeviceOwnership {
4
+ deviceEpoch = randomBytes(32).toString('base64url');
5
+ revision = 0;
6
+ state = 'free';
7
+ owner;
8
+ transition;
9
+ listeners = new Set();
10
+ snapshot() {
11
+ const candidate = this.owner?.candidate;
12
+ return { deviceEpoch: this.deviceEpoch, revision: this.revision, state: this.state,
13
+ controller: candidate ? { registrationId: candidate.registrationId, controllerId: candidate.controllerId, kind: candidate.kind } : null };
14
+ }
15
+ project(registrationId) {
16
+ const snapshot = this.snapshot();
17
+ if (snapshot.controller && snapshot.controller.registrationId !== registrationId)
18
+ snapshot.controller = { registrationId: 'another', controllerId: 'another', kind: 'unknown' };
19
+ return snapshot;
20
+ }
21
+ observe(listener) { this.listeners.add(listener); return () => this.listeners.delete(listener); }
22
+ publish() { for (const listener of this.listeners) {
23
+ try {
24
+ listener();
25
+ }
26
+ catch { }
27
+ } }
28
+ current(grant, hostLifetime) {
29
+ return this.state === 'active' && !!this.owner && matchesDeviceGrant(this.owner.grant, grant) &&
30
+ (hostLifetime === undefined || hostLifetime === this.owner.candidate.hostLifetime) && this.owner.candidate.valid();
31
+ }
32
+ isolate(owner) {
33
+ return owner.cleanup ??= Promise.resolve().then(() => owner.candidate.isolate()).catch(() => false).then(isolated => {
34
+ owner.isolated = isolated;
35
+ if (!isolated)
36
+ owner.cleanup = undefined;
37
+ return isolated;
38
+ });
39
+ }
40
+ async acquire(candidate, request) {
41
+ const reservation = await this.reserve(candidate, request);
42
+ if (reservation.status !== 'granted')
43
+ return reservation;
44
+ return this.commit(reservation.grant);
45
+ }
46
+ async reserve(candidate, request) {
47
+ deviceAcquisitionSchema.parse(request);
48
+ const refused = (status) => ({ status, snapshot: this.snapshot() });
49
+ if (!candidate.valid())
50
+ return refused('conflict');
51
+ if (request.kind === 'takeover' && (request.expectedDeviceEpoch !== this.deviceEpoch || request.expectedRevision !== this.revision))
52
+ return refused('conflict');
53
+ if (this.state === 'draining' || request.kind === 'if_free' && this.owner)
54
+ return refused('busy');
55
+ if (this.revision === Number.MAX_SAFE_INTEGER)
56
+ return refused('busy');
57
+ const old = this.owner;
58
+ this.transition = {};
59
+ this.state = 'draining';
60
+ this.revision++;
61
+ old?.candidate.revoke();
62
+ this.publish();
63
+ const cause = candidate.reservationCause;
64
+ try {
65
+ old?.candidate.lost(this.snapshot(), old.candidate.registrationId === candidate.registrationId &&
66
+ cause?.contenderControllerId === candidate.controllerId && old.candidate.reservationCause?.deviceGeneration === cause?.deviceGeneration ? cause : undefined);
67
+ }
68
+ catch { }
69
+ if (old && !await this.isolate(old))
70
+ return refused('busy');
71
+ if (!candidate.valid()) {
72
+ this.owner = undefined;
73
+ this.state = 'free';
74
+ this.publish();
75
+ return refused('conflict');
76
+ }
77
+ const grant = Object.freeze({ deviceEpoch: this.deviceEpoch, revision: this.revision,
78
+ registrationId: candidate.registrationId, controllerId: candidate.controllerId, authorization: randomBytes(32).toString('base64url') });
79
+ this.owner = { candidate, grant };
80
+ return { status: 'granted', grant };
81
+ }
82
+ async commit(grant) {
83
+ const next = this.owner;
84
+ if (!next || this.state !== 'draining' || next.cleanup || !matchesDeviceGrant(next.grant, grant))
85
+ return { status: 'conflict', snapshot: this.snapshot() };
86
+ const candidate = next.candidate, transition = this.transition;
87
+ let admitted = false;
88
+ try {
89
+ admitted = await candidate.admit(grant);
90
+ }
91
+ catch { }
92
+ if (!admitted || !candidate.valid() || this.transition !== transition) {
93
+ candidate.revoke();
94
+ if (await this.isolate(next) && this.owner === next) {
95
+ this.owner = undefined;
96
+ this.state = 'free';
97
+ }
98
+ this.publish();
99
+ return { status: 'busy', snapshot: this.snapshot() };
100
+ }
101
+ this.state = 'active';
102
+ this.publish();
103
+ return { status: 'granted', grant };
104
+ }
105
+ async release(grant) {
106
+ const owner = this.owner;
107
+ if (!owner || !matchesDeviceGrant(owner.grant, grant))
108
+ return false;
109
+ const transition = this.transition;
110
+ if (this.state === 'active' || !owner.cleanup) {
111
+ this.state = 'draining';
112
+ this.revision++;
113
+ owner.candidate.revoke();
114
+ this.publish();
115
+ }
116
+ const isolated = await this.isolate(owner);
117
+ if (this.owner === owner && this.transition === transition && isolated) {
118
+ this.owner = undefined;
119
+ this.state = 'free';
120
+ this.publish();
121
+ }
122
+ return isolated;
123
+ }
124
+ async retireRegistration(registrationId) {
125
+ const owner = this.owner;
126
+ if (!owner || owner.candidate.registrationId !== registrationId)
127
+ return true;
128
+ return this.release(owner.grant);
129
+ }
130
+ }