@platless/axom-cli 0.6.36 → 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 (159) hide show
  1. package/README.md +7 -1
  2. package/dist/axom-patch/cli/README.md +31 -2
  3. package/dist/axom-patch/cli/account-limits/generate.py +2 -2
  4. package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
  5. package/dist/axom-patch/cli/codex-terminal-output.json +2 -2
  6. package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
  7. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +1 -1
  8. package/dist/axom-patch/cli/probes/zcode-env-account.mjs +81 -0
  9. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
  10. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +137 -41
  11. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +5 -88
  12. package/dist/axom-patch/cli/qoder-native-acp.json +23 -23
  13. package/dist/axom-patch/cli/runtime.json +3 -1
  14. package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
  15. package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
  16. package/dist/axom-patch/cli/zcode-account-limits.json +7 -7
  17. package/dist/axom-patch/cli/zcode-env-account.json +58 -0
  18. package/dist/axom-patch/cli/zcode-models.json +9 -9
  19. package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -10
  20. package/dist/cli.js +9 -11
  21. package/dist/commands/auth.js +4 -1
  22. package/dist/commands/update.js +68 -7
  23. package/dist/commands/workstation.js +94 -223
  24. package/dist/commands/workstationRegistrations.js +116 -0
  25. package/dist/credentialBroker.js +13 -2
  26. package/dist/credentials.js +16 -0
  27. package/dist/install/claude/commands.js +2 -2
  28. package/dist/keepers/terminal/keeper.mjs +1089 -673
  29. package/dist/keepers/terminal/manifest.json +4 -4
  30. package/dist/native/darwin-x64/axom-quic +0 -0
  31. package/dist/workstation/acp/acpAgents.js +11 -4
  32. package/dist/workstation/acp/outbox.js +12 -5
  33. package/dist/workstation/acp/permission.js +2 -2
  34. package/dist/workstation/acp/processor/checkpoint.js +4 -3
  35. package/dist/workstation/acp/processor/control.js +2 -2
  36. package/dist/workstation/acp/processor/jsonrpc.js +8 -0
  37. package/dist/workstation/acp/processor/session.js +3 -2
  38. package/dist/workstation/acp/processor/state.js +1 -0
  39. package/dist/workstation/acp/processor/transport/local.js +19 -3
  40. package/dist/workstation/acp/processor/updates.js +25 -0
  41. package/dist/workstation/acp/processorSession.js +12 -1
  42. package/dist/workstation/acp/registry/catalog.js +6 -6
  43. package/dist/workstation/acp/test/keeperWorld.js +4 -3
  44. package/dist/workstation/agentSessions.js +76 -23
  45. package/dist/workstation/channels/agent.js +9 -1
  46. package/dist/workstation/channels/computer.js +27 -4
  47. package/dist/workstation/channels/management.js +36 -5
  48. package/dist/workstation/config.js +18 -6
  49. package/dist/workstation/control/bindingStore.js +4 -0
  50. package/dist/workstation/control/commands.js +1 -0
  51. package/dist/workstation/control/daemonIdentity.js +90 -0
  52. package/dist/workstation/control/deviceCommands.js +191 -0
  53. package/dist/workstation/control/deviceProtocol.js +68 -0
  54. package/dist/workstation/control/locks.js +51 -4
  55. package/dist/workstation/control/rootFormat.js +16 -0
  56. package/dist/workstation/control/runtimeClient.js +60 -5
  57. package/dist/workstation/control/runtimeHost.js +96 -9
  58. package/dist/workstation/control/serverIdentity.js +2 -2
  59. package/dist/workstation/control/updateTransition.js +91 -0
  60. package/dist/workstation/daemon.js +262 -360
  61. package/dist/workstation/daemonctl.js +150 -12
  62. package/dist/workstation/desktopControl/automationDevice.js +219 -0
  63. package/dist/workstation/desktopControl/automationJson.js +74 -0
  64. package/dist/workstation/desktopControl/automationLocalState.js +23 -0
  65. package/dist/workstation/desktopControl/automationPreparation.js +1 -0
  66. package/dist/workstation/desktopControl/automationProtocol.js +253 -0
  67. package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
  68. package/dist/workstation/desktopControl/automationSession.js +81 -0
  69. package/dist/workstation/desktopControl/automationTransport.js +138 -0
  70. package/dist/workstation/desktopControl/capabilities.js +2 -1
  71. package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
  72. package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
  73. package/dist/workstation/desktopControl/deviceSockets.js +2 -2
  74. package/dist/workstation/desktopControl/helperProcess.js +250 -1
  75. package/dist/workstation/desktopControl/hostGrant.js +65 -0
  76. package/dist/workstation/desktopControl/lobby.js +4 -2
  77. package/dist/workstation/desktopControl/macProvider.js +41 -40
  78. package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
  79. package/dist/workstation/desktopControl/nativeProvider.js +31 -5
  80. package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
  81. package/dist/workstation/desktopControl/protocol.js +15 -6
  82. package/dist/workstation/desktopControl/supervisor.js +110 -17
  83. package/dist/workstation/desktopControl/wallpaper.js +26 -6
  84. package/dist/workstation/exec/policy.js +5 -0
  85. package/dist/workstation/files/client.js +10 -1
  86. package/dist/workstation/files/host.js +9 -2
  87. package/dist/workstation/journal.js +5 -2
  88. package/dist/workstation/keepers/acp/client.js +38 -2
  89. package/dist/workstation/keepers/kit/buildVersion.js +35 -11
  90. package/dist/workstation/keepers/kit/client.js +2 -2
  91. package/dist/workstation/keepers/kit/launcher.js +80 -4
  92. package/dist/workstation/keepers/legacy.js +16 -2
  93. package/dist/workstation/keepers/policy.js +5 -34
  94. package/dist/workstation/keepers/terminal/client.js +115 -23
  95. package/dist/workstation/keepers/terminal/exec.js +6 -3
  96. package/dist/workstation/keepers/terminal/frames.js +73 -0
  97. package/dist/workstation/keepers/terminal/generations.js +5 -8
  98. package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
  99. package/dist/workstation/keepers/terminal/input.js +146 -0
  100. package/dist/workstation/keepers/terminal/keeper.js +22 -8
  101. package/dist/workstation/keepers/terminal/main.js +1 -1
  102. package/dist/workstation/keepers/terminal/metadata.js +138 -0
  103. package/dist/workstation/keepers/terminal/protocol.js +4 -28
  104. package/dist/workstation/keepers/terminal/pty.js +1 -330
  105. package/dist/workstation/keepers/terminal/screen.js +246 -0
  106. package/dist/workstation/keepers/terminal/sessions.js +526 -0
  107. package/dist/workstation/oauth/deviceAuthorization.js +31 -17
  108. package/dist/workstation/protocol/management.js +23 -4
  109. package/dist/workstation/rd/artifact.js +4 -3
  110. package/dist/workstation/rd/control.js +3 -5
  111. package/dist/workstation/rd/host.js +213 -0
  112. package/dist/workstation/rd/migrationReadiness.js +46 -0
  113. package/dist/workstation/rd/provider.js +44 -10
  114. package/dist/workstation/rd/state.js +14 -2
  115. package/dist/workstation/rd/update.js +5 -8
  116. package/dist/workstation/rd/upkeep.js +44 -2
  117. package/dist/workstation/registrations/enrollment.js +250 -0
  118. package/dist/workstation/registrations/enrollmentPending.js +361 -0
  119. package/dist/workstation/registrations/migration.js +356 -0
  120. package/dist/workstation/registrations/model.js +34 -0
  121. package/dist/workstation/registrations/removal.js +222 -0
  122. package/dist/workstation/registrations/store.js +737 -0
  123. package/dist/workstation/runtime/desktopBinding.js +20 -0
  124. package/dist/workstation/runtime/deviceDesktop.js +193 -0
  125. package/dist/workstation/runtime/deviceRuntime.js +568 -0
  126. package/dist/workstation/runtime/deviceServices.js +117 -0
  127. package/dist/workstation/runtime/events.js +20 -0
  128. package/dist/workstation/runtime/ownership.js +100 -0
  129. package/dist/workstation/runtime/registrationRuntime.js +575 -0
  130. package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
  131. package/dist/workstation/runtime/removalActivity.js +184 -0
  132. package/dist/workstation/service/installer.js +93 -1
  133. package/dist/workstation/service/launchd.js +1 -0
  134. package/dist/workstation/service/systemd.js +1 -0
  135. package/dist/workstation/systemMetrics.js +3 -0
  136. package/dist/workstation/tools/client.js +8 -27
  137. package/dist/workstation/tools/info.js +6 -13
  138. package/dist/workstation/tools/terminalRouter.js +17 -172
  139. package/dist/workstation/tools/validation.js +55 -1
  140. package/dist/workstation/update/archiveEvidence.js +206 -0
  141. package/dist/workstation/update/coordinator.js +34 -13
  142. package/dist/workstation/update/deviceCompatibility.js +119 -0
  143. package/dist/workstation/update/deviceCoordinator.js +369 -0
  144. package/dist/workstation/update/deviceHealth.js +104 -0
  145. package/dist/workstation/update/deviceHealthProtocol.js +26 -0
  146. package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
  147. package/dist/workstation/update/deviceRollback.js +78 -0
  148. package/dist/workstation/update/globalInstall.js +80 -10
  149. package/dist/workstation/update/legacyBridge.js +414 -0
  150. package/dist/workstation/update/legacyOwner.js +39 -0
  151. package/dist/workstation/update/npmInstaller.js +2 -2
  152. package/dist/workstation/update/privateDurability.js +61 -0
  153. package/dist/workstation/update/recoveryPlan.js +124 -0
  154. package/dist/workstation/update/runUpdate.js +7 -1
  155. package/dist/workstation/update/taskRegistry.js +161 -3
  156. package/dist/workstation/update/watchdog.js +27427 -14410
  157. package/dist/workstation/update/watchdogProcess.js +72 -0
  158. package/package.json +7 -3
  159. package/dist/native/darwin-arm64/axom-quic +0 -0
@@ -1,10 +1,11 @@
1
1
  import os from 'node:os';
2
2
  import { BindingStore } from './control/bindingStore.js';
3
- import { stopRuntime } from './control/runtimeClient.js';
3
+ import { inspectRuntime, stopRuntime } from './control/runtimeClient.js';
4
4
  import { LocalControlError } from './control/protocol.js';
5
5
  import { normalizeWorkstationServer } from './control/serverIdentity.js';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
+ import { randomUUID } from 'node:crypto';
8
9
  import { clientDir, loadClientConfig, updateServiceContext } from './config.js';
9
10
  import { createServiceManager } from './service/installer.js';
10
11
  import { buildServicePath } from './service/types.js';
@@ -12,6 +13,16 @@ import { loginShellPath } from './service/loginShell.js';
12
13
  import { KeeperLauncher } from './keepers/kit/launcher.js';
13
14
  import { stopKeepers } from './keepers/stop.js';
14
15
  import { assertRuntimeRestartSafe } from './control/restartGuard.js';
16
+ import { promises as fs } from 'node:fs';
17
+ import { inspectDeviceRuntime, prepareDeviceRuntimeStop, stopDeviceRuntime } from './control/runtimeClient.js';
18
+ import { assertDeviceUpdateTransitionSafe } from './control/daemonIdentity.js';
19
+ import { RegistrationStore } from './registrations/store.js';
20
+ import { readLegacyRegistrationSources, assertRegistrationEnrollmentAdmission } from './registrations/migration.js';
21
+ import { assertDeviceGuard } from './control/deviceProtocol.js';
22
+ import { resolveRegistrationOwnership } from './runtime/ownership.js';
23
+ import { captureStartupPackageEvidence } from './update/deviceHealth.js';
24
+ import { CLIENT_VERSION } from './version.js';
25
+ import { readDeviceBootstrapRecovery } from './update/recoveryPlan.js';
15
26
  const __filename = fileURLToPath(import.meta.url);
16
27
  /**
17
28
  * The package's real npm bin entry (`package.json`'s `bin.axom`), given the directory a compiled
@@ -58,6 +69,10 @@ export function serviceContext() {
58
69
  ...(managed.managed ? { npmConfigPrefix: managed.managed.prefix } : {}),
59
70
  };
60
71
  }
72
+ async function installationServiceContext(base, shellPath = loginShellPath) {
73
+ const loginPath = await shellPath();
74
+ return loginPath ? { ...base, pathEnv: buildServicePath(`${loginPath}:${process.env.PATH ?? ''}`, path.dirname(base.nodePath)) } : base;
75
+ }
61
76
  /** The workstation's keepers stopped (see {@link DaemonControllerDeps.stopKeepers}); an unregistered client has none. */
62
77
  async function stopRegisteredKeepers() {
63
78
  const configDir = clientDir();
@@ -106,11 +121,7 @@ export function createDaemonController(overrides = {}) {
106
121
  // itself after an update they are not, and only the login shell knows where the user's CLIs
107
122
  // are — see loginShellPath for why reusing the service's own PATH would make a machine
108
123
  // reinstall its own blind spot.
109
- const shellPath = await loginShellPath();
110
- const base = serviceContext();
111
- const context = shellPath
112
- ? { ...base, pathEnv: buildServicePath(`${shellPath}:${process.env.PATH ?? ''}`, path.dirname(base.nodePath)) }
113
- : base;
124
+ const context = await installationServiceContext(serviceContext());
114
125
  await deps.service(context).install({ interactive: true });
115
126
  },
116
127
  // The workstation is stopped by the user: what its keepers host ends too (the daemon first, so nothing relaunches them).
@@ -175,10 +186,137 @@ export function createDaemonController(overrides = {}) {
175
186
  },
176
187
  };
177
188
  }
189
+ export function createDeviceDaemonController(directory, overrides = {}) {
190
+ const context = overrides.context ?? (() => ({ ...serviceContext(), configDir: directory }));
191
+ const service = overrides.service ?? ((input) => createServiceManager(input ?? context()));
192
+ const store = new RegistrationStore(directory);
193
+ const runtime = async () => {
194
+ try {
195
+ return (await inspectDeviceRuntime(directory)).runtime;
196
+ }
197
+ catch {
198
+ const legacy = await inspectRuntime(directory);
199
+ if (legacy.state !== 'stopped')
200
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'The existing owner is not a verified device daemon');
201
+ return { state: 'stopped', generation: null, mode: 'unknown' };
202
+ }
203
+ };
204
+ const checked = async (options) => {
205
+ if (options) {
206
+ const inspection = await inspectDeviceRuntime(directory);
207
+ assertDeviceGuard(options.guard, inspection);
208
+ return inspection.runtime;
209
+ }
210
+ return runtime();
211
+ };
212
+ const terminateKeepers = async () => {
213
+ const inspection = await store.inspect();
214
+ if (!inspection.known || inspection.entries.some(entry => entry.state !== 'ready'))
215
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Cannot verify every owned keeper namespace');
216
+ for (const registration of await store.list()) {
217
+ const sources = await readLegacyRegistrationSources(directory, registration.id);
218
+ const ownership = await resolveRegistrationOwnership(registration, sources);
219
+ const launcher = new KeeperLauncher({ configDir: registration.keeperConfigDirectory, workstationId: registration.workstationId, server: sources?.keeperServer ?? registration.server, ownership });
220
+ const reports = await stopKeepers(launcher);
221
+ if (reports.some(report => !report.result.stopped))
222
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'A workstation keeper has not stopped');
223
+ }
224
+ };
225
+ const finishPrepared = async (generation, mode) => {
226
+ const current = await runtime();
227
+ if (current.state === 'stopped')
228
+ return;
229
+ if (current.generation !== generation)
230
+ throw new LocalControlError('BINDING_CONFLICT', 'A successor device daemon replaced the prepared owner');
231
+ await stopDeviceRuntime(directory, generation, mode);
232
+ };
233
+ return {
234
+ async install(options) {
235
+ const installation = await installationServiceContext(context(), overrides.loginShellPath);
236
+ if (!path.isAbsolute(installation.cliPath) || path.resolve(installation.cliPath) !== installation.cliPath || path.basename(installation.cliPath) !== 'cli.js' || path.basename(path.dirname(installation.cliPath)) !== 'dist')
237
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Installed service entrypoint must identify its exact dist/cli.js package');
238
+ const startup = await captureStartupPackageEvidence(path.dirname(path.dirname(installation.cliPath)), CLIENT_VERSION);
239
+ await assertDeviceUpdateTransitionSafe(directory, startup);
240
+ if (options) {
241
+ const recovery = await readDeviceBootstrapRecovery(directory, startup);
242
+ const transaction = recovery?.transaction;
243
+ if (!transaction || transaction.lifecycle !== 'provision-and-arm' || transaction.phase !== 'RESTARTING' || transaction.installation !== 'started' || transaction.installedRollback || transaction.recoveryRequired || transaction.initiatingDaemonGeneration !== options.updateGeneration)
244
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Service replacement requires the original active device update handoff');
245
+ const current = await inspectDeviceRuntime(directory);
246
+ if (current.runtime.state !== 'running' || current.runtime.generation !== options.updateGeneration)
247
+ throw new LocalControlError('BINDING_CONFLICT', 'The original device update owner changed before service replacement');
248
+ }
249
+ await service(installation).install({ interactive: true });
250
+ },
251
+ async start(options) { if ((await checked(options)).state === 'running')
252
+ return; await assertDeviceUpdateTransitionSafe(directory); await assertRegistrationEnrollmentAdmission(directory); await service().start(); },
253
+ async stop(options) {
254
+ const current = await checked(options);
255
+ const manager = service();
256
+ if (current.generation && current.mode === 'launchd') {
257
+ await prepareDeviceRuntimeStop(directory, current.generation, 'terminate', options?.operationId ?? randomUUID());
258
+ await manager.stop();
259
+ await finishPrepared(current.generation, 'terminate');
260
+ }
261
+ else {
262
+ if (current.generation)
263
+ await stopDeviceRuntime(directory, current.generation, 'terminate');
264
+ await manager.stop();
265
+ }
266
+ if (!current.generation)
267
+ await terminateKeepers();
268
+ },
269
+ async uninstall(options) {
270
+ const current = await checked(options);
271
+ const manager = service();
272
+ if (current.generation && current.mode === 'launchd') {
273
+ await prepareDeviceRuntimeStop(directory, current.generation, 'terminate', options?.operationId ?? randomUUID());
274
+ await manager.uninstall();
275
+ await finishPrepared(current.generation, 'terminate');
276
+ }
277
+ else {
278
+ if (current.generation)
279
+ await stopDeviceRuntime(directory, current.generation, 'terminate');
280
+ await manager.uninstall();
281
+ }
282
+ if (!current.generation)
283
+ await terminateKeepers();
284
+ },
285
+ async restart(options) {
286
+ const current = await checked(options);
287
+ const manager = service();
288
+ if (current.generation) {
289
+ await (overrides.restartSafe ?? (generation => assertRuntimeRestartSafe(directory, generation)))(current.generation);
290
+ if (current.mode === 'launchd') {
291
+ await prepareDeviceRuntimeStop(directory, current.generation, 'detach', options?.operationId ?? randomUUID());
292
+ await manager.stop();
293
+ await finishPrepared(current.generation, 'detach');
294
+ await manager.restart();
295
+ return;
296
+ }
297
+ await stopDeviceRuntime(directory, current.generation, 'detach');
298
+ }
299
+ await manager.restart();
300
+ },
301
+ async status() { const current = await runtime(); return { running: current.state === 'running' }; },
302
+ };
303
+ }
304
+ async function deviceControllerSelected() {
305
+ for (const name of ['device.json', 'legacy-migration.json'])
306
+ try {
307
+ await fs.lstat(path.join(clientDir(), name));
308
+ return true;
309
+ }
310
+ catch (error) {
311
+ if (error.code !== 'ENOENT')
312
+ throw error;
313
+ }
314
+ return false;
315
+ }
178
316
  const controller = createDaemonController();
179
- export const install = () => controller.install();
180
- export const uninstall = () => controller.uninstall();
181
- export const start = () => controller.start();
182
- export const stop = () => controller.stop();
183
- export const restart = () => controller.restart();
184
- export const status = () => controller.status();
317
+ export const install = async () => (await deviceControllerSelected() ? createDeviceDaemonController(clientDir()) : controller).install();
318
+ export const uninstall = async () => (await deviceControllerSelected() ? createDeviceDaemonController(clientDir()) : controller).uninstall();
319
+ export const start = async () => (await deviceControllerSelected() ? createDeviceDaemonController(clientDir()) : controller).start();
320
+ export const stop = async () => (await deviceControllerSelected() ? createDeviceDaemonController(clientDir()) : controller).stop();
321
+ export const restart = async () => (await deviceControllerSelected() ? createDeviceDaemonController(clientDir()) : controller).restart();
322
+ export const status = async () => (await deviceControllerSelected() ? createDeviceDaemonController(clientDir()) : controller).status();
@@ -0,0 +1,219 @@
1
+ import WebSocket from 'ws';
2
+ import { automationSchema, decodeAutomationRequest, decodeAutomationResponse, encodeAutomationBinary } from './automationProtocol.js';
3
+ import { parseAutomationJson } from './automationJson.js';
4
+ import { desktopSocketUrl } from './deviceSockets.js';
5
+ import { bounded, NativeCommandError } from './helperProcess.js';
6
+ import { managedAutomationBinding } from './automationLocalState.js';
7
+ export class AutomationDeviceConnections {
8
+ server;
9
+ provider;
10
+ allowPrivateWs;
11
+ localStateDirectory;
12
+ admission;
13
+ get activeCount() { return this.active.size; }
14
+ active = new Map();
15
+ generation;
16
+ constructor(server, provider, allowPrivateWs = false, localStateDirectory, admission) {
17
+ this.server = server;
18
+ this.provider = provider;
19
+ this.allowPrivateWs = allowPrivateWs;
20
+ this.localStateDirectory = localStateDirectory;
21
+ this.admission = admission;
22
+ }
23
+ ready(generation) { this.close(); this.generation = generation; }
24
+ retired(generation) { if (generation === this.generation) {
25
+ this.close();
26
+ this.generation = undefined;
27
+ } }
28
+ close() { for (const entry of this.active.values())
29
+ entry.close(); this.active.clear(); }
30
+ async prepare(grant, binding, generation) {
31
+ if (generation !== this.generation || binding.route.deviceGeneration !== generation || this.active.size >= 8 || this.active.has(grant))
32
+ throw new Error('stale_generation');
33
+ if (!this.localStateDirectory)
34
+ throw new Error('trusted_storage_unavailable');
35
+ const localBinding = await managedAutomationBinding(this.localStateDirectory, { ...binding, registrationId: this.admission?.registrationId ?? binding.registrationId });
36
+ if (generation !== this.generation || this.active.size >= 8 || this.active.has(grant))
37
+ throw new Error('stale_generation');
38
+ const socket = new WebSocket(desktopSocketUrl(this.server, 'automation', this.allowPrivateWs), {
39
+ headers: { 'X-Axom-Automation-Grant': grant, 'X-Axom-Automation-Features': 'capture_wait_v1' }, followRedirects: false, perMessageDeflate: false, maxPayload: 49152
40
+ });
41
+ let selectedWait = false;
42
+ socket.once('upgrade', response => {
43
+ const selected = response.headers['x-axom-automation-features'];
44
+ if (selected !== undefined && selected !== 'capture_wait_v1') {
45
+ socket.terminate();
46
+ return;
47
+ }
48
+ selectedWait = selected === 'capture_wait_v1';
49
+ });
50
+ let session, retired = false, leaseBusy = false;
51
+ let mode = 'live';
52
+ let statusRequest;
53
+ const pending = new Map();
54
+ let lease;
55
+ const close = () => {
56
+ if (retired)
57
+ return;
58
+ retired = true;
59
+ clearInterval(lease);
60
+ pending.forEach(controller => controller.abort());
61
+ pending.clear();
62
+ this.active.delete(grant);
63
+ socket.terminate();
64
+ void session?.dispose().catch(() => { });
65
+ };
66
+ const current = () => !retired && this.generation === generation;
67
+ this.active.set(grant, { generation, close });
68
+ socket.on('error', close);
69
+ socket.on('close', close);
70
+ const send = async (bytes) => {
71
+ if (!current() || socket.readyState !== WebSocket.OPEN || socket.bufferedAmount > 8388608)
72
+ throw new Error('transport_failed');
73
+ await bounded(new Promise((resolve, reject) => socket.send(bytes, error => error ? reject(error) : resolve())), 5000);
74
+ };
75
+ try {
76
+ await bounded(new Promise((resolve, reject) => { socket.once('open', resolve); socket.once('error', reject); socket.once('close', () => reject(new Error('closed'))); }), 3000);
77
+ if (!this.provider?.prepareAutomation) {
78
+ await send(JSON.stringify({ automationProtocol: 1, operations: [] }));
79
+ return;
80
+ }
81
+ try {
82
+ session = await this.provider.prepareAutomation(localBinding);
83
+ }
84
+ catch {
85
+ await send(JSON.stringify({ automationProtocol: 1, operations: [] }));
86
+ return;
87
+ }
88
+ if (!current()) {
89
+ await session.dispose();
90
+ return;
91
+ }
92
+ await send(JSON.stringify({ automationProtocol: 1, operations: session.operations.filter(method => Object.hasOwn(automationSchema.requests, method) && (method !== 'capture_wait_v1' || selectedWait)) }));
93
+ const backend = session;
94
+ lease = setInterval(() => {
95
+ if (leaseBusy || !current() || mode !== 'live')
96
+ return;
97
+ leaseBusy = true;
98
+ if (this.admission && !this.admission.current(localBinding)) {
99
+ close();
100
+ return;
101
+ }
102
+ void backend.renewAuthenticatedLease(3000).catch(() => { if (mode === 'live')
103
+ close(); }).finally(() => { leaseBusy = false; });
104
+ }, 1000);
105
+ socket.on('message', (bytes, binary) => {
106
+ if (!current() || binary) {
107
+ close();
108
+ return;
109
+ }
110
+ try {
111
+ const text = bytes.toString(), raw = parseAutomationJson(text, 49152).value;
112
+ if (raw.command === 'cancel' && Object.keys(raw).length === 2) {
113
+ pending.get(raw.requestId)?.abort();
114
+ return;
115
+ }
116
+ if (raw.command === 'status_only' || raw.command === 'isolate') {
117
+ if (pending.size >= 64 || pending.has(raw.requestId)) {
118
+ close();
119
+ return;
120
+ }
121
+ mode = 'detaching';
122
+ clearInterval(lease);
123
+ const controller = new AbortController();
124
+ pending.set(raw.requestId, controller);
125
+ const operation = raw.command === 'status_only' ? backend.detachApplicationStatus?.(raw.launchRequestId, raw.canonicalArgsDigest) : backend.isolate?.();
126
+ void (operation ?? Promise.reject(new Error('unsupported'))).then(async (result) => {
127
+ if (raw.command === 'status_only') {
128
+ mode = 'status_only';
129
+ statusRequest = raw.launchRequestId;
130
+ }
131
+ if (current() && !controller.signal.aborted)
132
+ await send(JSON.stringify({ control: raw.command, id: raw.requestId, result }));
133
+ }, async (error) => {
134
+ if (current() && !controller.signal.aborted)
135
+ await send(JSON.stringify({ control: 'prepare_failed', id: raw.requestId, code: error instanceof Error ? error.message : 'service_unavailable', detail: null }));
136
+ }).catch(close).finally(() => pending.delete(raw.requestId));
137
+ return;
138
+ }
139
+ if (raw.command === 'prepare' && Object.keys(raw).length === 3) {
140
+ if (this.admission && !this.admission.current(localBinding))
141
+ throw new Error('control_lost');
142
+ if (mode !== 'live') {
143
+ void send(JSON.stringify({ control: 'prepare_failed', id: raw.requestId, code: 'control_lost', detail: null })).catch(close);
144
+ return;
145
+ }
146
+ if (pending.size >= 64 || pending.has(raw.requestId)) {
147
+ close();
148
+ return;
149
+ }
150
+ const controller = new AbortController();
151
+ pending.set(raw.requestId, controller);
152
+ void backend.prepareAttachment(raw.preparation, { signal: controller.signal }).then(attachment => { if (current() && !controller.signal.aborted)
153
+ return send(JSON.stringify({ control: 'prepared', id: raw.requestId, attachment })); }, error => { if (current() && !controller.signal.aborted)
154
+ return send(JSON.stringify({ control: 'prepare_failed', id: raw.requestId, code: error instanceof Error ? error.message : 'service_unavailable', detail: error instanceof NativeCommandError ? error.detail : null })); }).catch(close).finally(() => pending.delete(raw.requestId));
155
+ return;
156
+ }
157
+ const request = decodeAutomationRequest(text);
158
+ if (this.admission && !this.admission.current(localBinding) && request.method !== 'close')
159
+ throw new Error('control_lost');
160
+ if (mode !== 'live' && (mode !== 'status_only' || request.method !== 'application_status' || request.payload.requestId !== statusRequest)) {
161
+ void send(JSON.stringify({ ...request, payload: { type: 'failure', code: 'control_lost', message: 'Input authority was retired', detail: null } })).catch(close);
162
+ return;
163
+ }
164
+ if (pending.size >= 64 || pending.has(request.requestId)) {
165
+ close();
166
+ return;
167
+ }
168
+ if (request.method === 'capture_wait_v1' && (!selectedWait || !backend.operations.includes(request.method))) {
169
+ void send(JSON.stringify({ ...request, payload: { type: 'failure', code: 'unsupported', message: 'Conditional observation was not negotiated', detail: null } })).catch(close);
170
+ return;
171
+ }
172
+ const controller = new AbortController();
173
+ pending.set(request.requestId, controller);
174
+ void (async () => {
175
+ const reply = await backend.execute(request, { signal: controller.signal });
176
+ if (!current() || controller.signal.aborted)
177
+ return;
178
+ let response = decodeAutomationResponse(JSON.stringify(reply.response), request);
179
+ let binary;
180
+ if (reply.image) {
181
+ const data = response.payload.data, transfer = request.method === 'capture' ? data : request.method === 'capture_wait_v1' ? data.image : data.postImage;
182
+ binary = encodeAutomationBinary(transfer.header, reply.image);
183
+ const captureAgeMs = Date.now() - transfer.metadata.capturedAtEpochMs;
184
+ if (!Number.isSafeInteger(captureAgeMs) || captureAgeMs < 0 || captureAgeMs > 30000) {
185
+ if (request.method !== 'act' && request.method !== 'status')
186
+ throw new Error('stale_observation');
187
+ response = { ...response, payload: { type: 'success', data: { ...data, postImage: null, captureError: 'capture_failed' } } };
188
+ binary = undefined;
189
+ }
190
+ else
191
+ response = { ...response, captureAgeMs };
192
+ }
193
+ await send(JSON.stringify(response));
194
+ if (binary)
195
+ await send(binary);
196
+ })().catch(async (error) => {
197
+ if (!current() || controller.signal.aborted)
198
+ return;
199
+ const code = error instanceof Error && error.message === 'stale_observation' ? 'stale_observation'
200
+ : error instanceof Error && error.message === 'unsupported' ? 'unsupported' : 'service_unavailable';
201
+ try {
202
+ await send(JSON.stringify({ ...request, payload: { type: 'failure', code, message: error instanceof Error ? error.message : code, detail: null } }));
203
+ }
204
+ catch {
205
+ close();
206
+ }
207
+ }).finally(() => pending.delete(request.requestId));
208
+ }
209
+ catch {
210
+ close();
211
+ }
212
+ });
213
+ }
214
+ catch (error) {
215
+ close();
216
+ throw error;
217
+ }
218
+ }
219
+ }
@@ -0,0 +1,74 @@
1
+ import { parseDesktopJson } from './protocol.js';
2
+ export function parseAutomationJson(source, limit) {
3
+ if (typeof source !== 'string' && source.byteLength > limit)
4
+ throw new Error('invalid_automation_protocol');
5
+ const text = typeof source === 'string' ? source : new TextDecoder('utf-8', { fatal: true }).decode(source);
6
+ const value = parseDesktopJson(text, limit, new Set());
7
+ const numbers = new Map();
8
+ let offset = 0;
9
+ const space = () => { while (/[ \t\r\n]/.test(text[offset] ?? '\0'))
10
+ offset++; };
11
+ const string = () => {
12
+ const start = offset++;
13
+ while (offset < text.length) {
14
+ const character = text[offset++];
15
+ if (character === '\\')
16
+ offset++;
17
+ else if (character === '"')
18
+ return JSON.parse(text.slice(start, offset));
19
+ }
20
+ throw new Error('invalid_automation_protocol');
21
+ };
22
+ const visit = (path) => {
23
+ space();
24
+ if (text[offset] === '{') {
25
+ offset++;
26
+ space();
27
+ if (text[offset] === '}') {
28
+ offset++;
29
+ return;
30
+ }
31
+ do {
32
+ space();
33
+ const key = string();
34
+ space();
35
+ offset++;
36
+ visit([...path, key]);
37
+ space();
38
+ if (text[offset] !== ',')
39
+ break;
40
+ offset++;
41
+ } while (true);
42
+ offset++;
43
+ }
44
+ else if (text[offset] === '[') {
45
+ offset++;
46
+ space();
47
+ if (text[offset] === ']') {
48
+ offset++;
49
+ return;
50
+ }
51
+ let index = 0;
52
+ do {
53
+ visit([...path, index++]);
54
+ space();
55
+ if (text[offset] !== ',')
56
+ break;
57
+ offset++;
58
+ } while (true);
59
+ offset++;
60
+ }
61
+ else if (text[offset] === '"')
62
+ string();
63
+ else {
64
+ const start = offset;
65
+ while (offset < text.length && !/[,\]} \t\r\n]/.test(text[offset]))
66
+ offset++;
67
+ const token = text.slice(start, offset);
68
+ if (/^-?[0-9]/.test(token))
69
+ numbers.set(JSON.stringify(path), token);
70
+ }
71
+ };
72
+ visit([]);
73
+ return { value, numbers };
74
+ }
@@ -0,0 +1,23 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstat, mkdir } from 'node:fs/promises';
3
+ import { isAbsolute, join } from 'node:path';
4
+ export async function managedAutomationBinding(configDirectory, binding) {
5
+ const leaseMs = binding.native?.leaseMs;
6
+ if (!Number.isInteger(leaseMs) || leaseMs < 1 || leaseMs > 30000 || !binding.executionId)
7
+ throw new Error('trusted_preparation_unavailable');
8
+ if (!isAbsolute(configDirectory))
9
+ throw new Error('trusted_storage_unavailable');
10
+ const directory = join(configDirectory, 'desktop-automation');
11
+ await mkdir(directory, { mode: 0o700 }).catch(error => { if (error?.code !== 'EEXIST')
12
+ throw error; });
13
+ const status = await lstat(directory);
14
+ if (!status.isDirectory() || status.isSymbolicLink() || status.uid !== process.getuid?.() || (status.mode & 0o077) !== 0)
15
+ throw new Error('trusted_storage_unavailable');
16
+ const route = binding.route;
17
+ const identity = [binding.registrationId ?? 'legacy-single-registration', route.workstation, route.desktopId, route.inputDomainId, route.consumerId, route.deviceGeneration, route.controlGeneration, binding.executionId];
18
+ if (identity.some(value => value === undefined || value === null))
19
+ throw new Error('trusted_preparation_unavailable');
20
+ const key = createHash('sha256').update(JSON.stringify(identity)).digest('hex');
21
+ const domainKey = createHash('sha256').update(JSON.stringify([route.workstation, route.desktopId, route.inputDomainId])).digest('hex');
22
+ return { ...binding, native: { waitLockPath: join(directory, `${domainKey}.wait`), ledgerPath: join(directory, `${key}.jsonl`), leaseMs } };
23
+ }