@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
@@ -0,0 +1,65 @@
1
+ import { promises as fs, constants } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { z } from 'zod';
5
+ export const deviceHostGrantSchema = z.strictObject({ schemaVersion: z.literal(2), hostGeneration: z.string().min(1).max(128),
6
+ executable: z.string().startsWith('/').max(4096), sha256: z.string().regex(/^[a-f0-9]{64}$/),
7
+ socket: z.string().startsWith('/').max(103), token: z.string().regex(/^[a-f0-9]{64}$/) });
8
+ async function verifiedSocketPath(socket, checks) {
9
+ const realpath = checks.realpath ?? ((file) => fs.realpath(file)), canonical = await realpath(socket);
10
+ if (canonical === socket)
11
+ return true;
12
+ // Older macOS producers stored /tmp while the OS resolves its root-owned system alias.
13
+ // Exact prefix substitution rejects every additional symlink or path normalization.
14
+ if ((checks.platform ?? process.platform) !== 'darwin' || !socket.startsWith('/tmp/') || canonical !== '/private' + socket)
15
+ return false;
16
+ try {
17
+ const lstat = checks.systemLstat ?? ((file) => fs.lstat(file));
18
+ const link = await lstat('/tmp'), target = await lstat('/private/tmp');
19
+ const destination = await (checks.systemReadlink ?? ((file) => fs.readlink(file)))('/tmp');
20
+ return link.isSymbolicLink() && link.uid === 0 && target.isDirectory() && target.uid === 0 && !!(target.mode & 0o1000) &&
21
+ (destination === 'private/tmp' || destination === '/private/tmp') && await realpath('/tmp') === '/private/tmp' && await realpath('/private/tmp') === '/private/tmp';
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ export async function readDeviceHostDescriptor(fileName) {
28
+ const parent = await fs.lstat(path.dirname(fileName));
29
+ if (!parent.isDirectory() || parent.uid !== process.getuid?.() || parent.mode & 0o077)
30
+ throw new Error('unsafe_native_host');
31
+ const file = await fs.open(fileName, constants.O_RDONLY | constants.O_NOFOLLOW);
32
+ let grant;
33
+ try {
34
+ const stat = await file.stat();
35
+ if (!stat.isFile() || stat.uid !== process.getuid?.() || stat.mode & 0o077 || stat.size > 16384)
36
+ throw new Error('unsafe_native_host');
37
+ grant = deviceHostGrantSchema.parse(JSON.parse(await file.readFile('utf8')));
38
+ }
39
+ finally {
40
+ await file.close();
41
+ }
42
+ return grant;
43
+ }
44
+ export async function readDeviceHostGrant(fileName, allowRootExecutable = false, checks = {}) {
45
+ const grant = await readDeviceHostDescriptor(fileName);
46
+ const parent = await fs.lstat(path.dirname(grant.socket)), socket = await fs.lstat(grant.socket);
47
+ if (!parent.isDirectory() || parent.uid !== process.getuid?.() || parent.mode & 0o077 || !socket.isSocket() || socket.uid !== process.getuid?.() || socket.mode & 0o077 ||
48
+ !await verifiedSocketPath(grant.socket, checks) || await (checks.realpath ?? ((file) => fs.realpath(file)))(grant.executable) !== grant.executable)
49
+ throw new Error('unsafe_native_host');
50
+ const executable = await fs.open(grant.executable, constants.O_RDONLY | constants.O_NOFOLLOW);
51
+ try {
52
+ const stat = await executable.stat();
53
+ if (!stat.isFile() || stat.uid !== process.getuid?.() && !(allowRootExecutable && stat.uid === 0) || stat.mode & 0o022 || stat.size > 32 * 1024 * 1024)
54
+ throw new Error('unsafe_native_host');
55
+ const hash = createHash('sha256');
56
+ for await (const bytes of executable.createReadStream({ autoClose: false }))
57
+ hash.update(bytes);
58
+ if (hash.digest('hex') !== grant.sha256)
59
+ throw new Error('unsafe_native_host');
60
+ }
61
+ finally {
62
+ await executable.close();
63
+ }
64
+ return grant;
65
+ }
@@ -14,7 +14,8 @@ const resolutionSchema = z.discriminatedUnion('mode', [z.strictObject({ mode: z.
14
14
  export const lobbyRequestSchema = z.strictObject({ requestId: identifier, revision: safe, viewport: viewportSchema,
15
15
  resolution: resolutionSchema, dpi: z.number().int().min(64).max(384).nullable(),
16
16
  transports: z.array(z.enum(['quic', 'wss'])).min(1).max(2).refine(values => new Set(values).size === values.length),
17
- targetId: identifier.nullable().optional(), viewer: z.unknown().nullable().optional() });
17
+ targetId: identifier.nullable().optional(), viewer: z.unknown().nullable().optional(),
18
+ backgroundVersion: z.string().regex(/^[A-Za-z0-9_-]{1,128}$/).nullish() });
18
19
  const rangeSchema = z.strictObject({ minWidth: axis, minHeight: axis, maxWidth: axis, maxHeight: axis });
19
20
  export const lobbyPlanSchema = z.strictObject({ lobbyId: identifier, planId: identifier, revision: safe, targetId: identifier,
20
21
  desktop: sourceModeSchema, dpi: z.number().int().min(64).max(384).nullable(), resize: z.enum(['none', 'modes', 'dynamic']),
@@ -40,8 +41,9 @@ export const wallpaperAssetSchema = z.strictObject({ assetId: z.string().min(1).
40
41
  anchorX: z.number().finite().min(0).max(1).default(0.5), anchorY: z.number().finite().min(0).max(1).default(0.5),
41
42
  background: z.string().regex(/^#[a-fA-F0-9]{6}$/).default('#000000'),
42
43
  intrinsicScale: z.number().finite().min(0.125).max(16).default(1),
44
+ mimeType: z.enum(['image/jpeg', 'image/webp']).default('image/jpeg'),
43
45
  fidelity: z.enum(['exact', 'approximate']).default('exact'), colorSpace: z.literal('srgb').default('srgb')
44
- }).refine(asset => asset.width * asset.height <= 4_000_000 && (asset.coverage !== 'composited' || asset.compatibleDesktop != null));
46
+ }).refine(asset => asset.width * asset.height <= 8_388_608 && (asset.coverage !== 'composited' || asset.compatibleDesktop != null));
45
47
  export function composeWallpaper(value, selected) {
46
48
  const asset = wallpaperAssetSchema.parse(value);
47
49
  const desktop = sourceModeSchema.parse(selected);
@@ -1,20 +1,20 @@
1
- import { promises as fs, constants } from 'node:fs';
1
+ import { promises as fs } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { createHash } from 'node:crypto';
4
3
  import { execFile } from 'node:child_process';
5
4
  import { promisify } from 'node:util';
6
5
  import { createConnection } from 'node:net';
7
6
  import { z } from 'zod';
8
7
  import { NativeProvider } from './nativeProvider.js';
9
8
  import { HelperProcess, bounded, NativeCommandError } from './helperProcess.js';
9
+ import { readDeviceHostGrant } from './hostGrant.js';
10
10
  import { desktopCapabilitiesSchema } from './protocol.js';
11
- import { normalizeWorkstationServer } from '../control/serverIdentity.js';
11
+ import { NativeDeviceAdmission } from './nativeDeviceAdmission.js';
12
12
  import { captureTargetLabel } from './captureTargetLabel.js';
13
13
  import { decodeWallpaper } from './wallpaper.js';
14
14
  import { captureSamplingModes as macCaptureModes } from './captureModes.js';
15
15
  const execute = promisify(execFile);
16
- export const macDesktopGrantSchema = z.strictObject({ schemaVersion: z.literal(1),
17
- binding: z.strictObject({ server: z.string(), workstationId: z.string(), ownerUserId: z.string() }),
16
+ export const macDesktopGrantSchema = z.strictObject({ schemaVersion: z.literal(2),
17
+ hostGeneration: z.string().min(1).max(128),
18
18
  executable: z.string().startsWith('/').max(4096), sha256: z.string().regex(/^[a-f0-9]{64}$/),
19
19
  socket: z.string().startsWith('/').max(103), token: z.string().regex(/^[a-f0-9]{64}$/) });
20
20
  const target = z.object({ id: z.string().regex(/^[0-9a-f]{32}$/), kind: z.enum(['display', 'window']), label: z.string(), displayIdentity: z.string().min(1).max(256), capturePixelScale: z.number().min(1).max(16).optional(),
@@ -39,45 +39,36 @@ export async function checkMacHost(grant) {
39
39
  /** Discovery can report permissions but cannot prompt, capture, encode or inject input. */
40
40
  export class MacProvider {
41
41
  directory;
42
- binding;
42
+ sourceFile;
43
+ async prepareAutomation(binding) {
44
+ const provider = await this.liveProvider();
45
+ if (!provider)
46
+ throw new NativeCommandError('unavailable');
47
+ return provider.prepareAutomation(binding);
48
+ }
49
+ admission;
50
+ original;
51
+ get deviceAdmission() { return this.admission; }
52
+ static async connect(directory, sourceFile) {
53
+ const provider = new MacProvider(directory, undefined, sourceFile);
54
+ if (!await provider.liveProvider())
55
+ throw new NativeCommandError('unavailable');
56
+ return provider;
57
+ }
43
58
  provider;
44
59
  identity = '';
45
60
  revisions = new Map();
46
- constructor(directory, binding) {
61
+ constructor(directory, _binding, sourceFile) {
47
62
  this.directory = directory;
48
- this.binding = binding;
63
+ this.sourceFile = sourceFile;
49
64
  }
50
65
  async grant() {
51
- const file = await fs.open(path.join(this.directory, 'desktop-provider.json'), constants.O_RDONLY | constants.O_NOFOLLOW);
52
- let grant;
53
- try {
54
- const stat = await file.stat();
55
- if (!stat.isFile() || stat.uid !== process.getuid?.() || stat.mode & 0o077 || stat.size > 16384)
56
- throw new NativeCommandError('unavailable');
57
- grant = macDesktopGrantSchema.parse(JSON.parse(await file.readFile('utf8')));
58
- }
59
- finally {
60
- await file.close();
61
- }
62
- if (normalizeWorkstationServer(grant.binding.server) !== normalizeWorkstationServer(this.binding.server) || grant.binding.workstationId !== this.binding.workstationId || grant.binding.ownerUserId !== this.binding.ownerUserId)
66
+ const grant = await readDeviceHostGrant(this.sourceFile ?? path.join(this.directory, 'desktop-provider.json'));
67
+ if (this.original && JSON.stringify(grant) !== JSON.stringify(this.original))
63
68
  throw new NativeCommandError('unavailable');
64
69
  if (!grant.executable.endsWith('/Axom Work Computer Use.app/Contents/MacOS/Axom Work Computer Use'))
65
70
  throw new NativeCommandError('unavailable');
66
- const executable = await fs.open(grant.executable, constants.O_RDONLY | constants.O_NOFOLLOW);
67
- try {
68
- const stat = await executable.stat();
69
- if (!stat.isFile() || stat.size > 32 * 1024 * 1024 || stat.mode & 0o022)
70
- throw new NativeCommandError('unavailable');
71
- const hash = createHash('sha256');
72
- for await (const bytes of executable.createReadStream({ autoClose: false }))
73
- hash.update(bytes);
74
- if (hash.digest('hex') !== grant.sha256)
75
- throw new NativeCommandError('unavailable');
76
- }
77
- finally {
78
- await executable.close();
79
- }
80
- await execute('/usr/bin/codesign', ['--verify', '--strict', path.resolve(grant.executable, '../../..')], { timeout: 7000, maxBuffer: 65536 });
71
+ await execute('/usr/bin/codesign', ['--verify', '--strict', '-R', '=identifier "com.platless.axom.computer-use"', path.resolve(grant.executable, '../../..')], { timeout: 7000, maxBuffer: 65536 });
81
72
  await checkMacHost(grant);
82
73
  return grant;
83
74
  }
@@ -107,10 +98,20 @@ export class MacProvider {
107
98
  async liveProvider() {
108
99
  try {
109
100
  const grant = await this.grant(), identity = grant.sha256 + grant.token;
101
+ if (this.original && identity !== this.identity)
102
+ return null;
110
103
  if (identity !== this.identity) {
104
+ const admission = await NativeDeviceAdmission.connect(grant);
105
+ if (admission.hostLifetimeId !== grant.hostGeneration)
106
+ throw new NativeCommandError('unavailable');
107
+ this.original = grant;
108
+ this.admission = admission;
111
109
  this.identity = identity;
112
110
  this.provider = new NativeProvider({ helperPath: grant.executable, runtimeDir: this.directory, backend: 'videotoolbox',
113
- enumerate: () => this.capabilities(), sessionHelperFactory: async (binding) => { const current = await this.grant(); return HelperProcess.launch(current.executable, { ...process.env, AXOM_KEEPER_TARGET: binding.displayId, AXOM_KEEPER_SOCKET: current.socket, AXOM_KEEPER_TOKEN: current.token }, 'macos'); } });
111
+ deviceAdmission: admission, environment: { ...process.env, AXOM_KEEPER_SOCKET: grant.socket, AXOM_KEEPER_TOKEN: grant.token },
112
+ enumerate: () => this.capabilities(), sessionHelperFactory: async (binding) => HelperProcess.launch(grant.executable, { ...process.env, AXOM_KEEPER_TARGET: binding.displayId,
113
+ AXOM_KEEPER_SOCKET: grant.socket, AXOM_KEEPER_TOKEN: grant.token,
114
+ AXOM_DEVICE_CONTEXT: admission.context(binding.deviceGrant?.registrationId ?? '', binding.deviceGeneration, binding.sessionId, binding.deviceGrant) }, 'macos') });
114
115
  }
115
116
  return this.provider;
116
117
  }
@@ -131,19 +132,19 @@ export class MacProvider {
131
132
  }
132
133
  return this.capabilities();
133
134
  }
134
- async prepare(selection, binding) {
135
+ async prepare(selection, binding, current) {
135
136
  const provider = await this.liveProvider();
136
137
  if (!provider)
137
138
  throw new NativeCommandError('unavailable');
138
139
  // NativeProvider.prepare performs fresh permission/geometry admission itself.
139
140
  // Repeating full enumeration here added another WindowServer round trip per window.
140
- return provider.prepare(selection, binding);
141
+ return provider.prepare(selection, binding, current);
141
142
  }
142
- async wallpaper(targetId) {
143
+ async wallpaper(targetId, pixels = { width: 1920, height: 1080 }) {
143
144
  const grant = await this.grant();
144
145
  if (!/^[a-f0-9]{32}$/.test(targetId))
145
146
  throw new NativeCommandError('invalid_request');
146
- const result = await execute(grant.executable, ['--wallpaper', targetId], { timeout: 8000, maxBuffer: 100000,
147
+ const result = await execute(grant.executable, ['--wallpaper', targetId, String(pixels.width), String(pixels.height)], { timeout: 8000, maxBuffer: 3 * 1024 * 1024,
147
148
  env: { ...process.env, AXOM_KEEPER_SOCKET: grant.socket, AXOM_KEEPER_TOKEN: grant.token } });
148
149
  return decodeWallpaper(result.stdout);
149
150
  }
@@ -0,0 +1,207 @@
1
+ import { createConnection } from 'node:net';
2
+ import { z } from 'zod';
3
+ import { matchesDeviceGrant, nativeReceiptMatches } from './ownershipProtocol.js';
4
+ const identity = z.string().min(1).max(128);
5
+ const grantSchema = z.strictObject({ registrationId: identity, controllerId: identity, deviceEpoch: identity,
6
+ revision: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER), authorization: identity });
7
+ const receiptSchema = z.strictObject({ commandId: identity, hostLifetimeId: identity, action: z.enum(['admit', 'isolate']),
8
+ grant: grantSchema, admission: z.enum(['active', 'revoked']), physical: z.literal('completed') });
9
+ const registrationReceiptSchema = z.strictObject({ commandId: identity, registrationId: identity, deviceGeneration: identity,
10
+ hostLifetimeId: identity, physical: z.literal('completed'), admission: z.literal('revoked') });
11
+ const statusSchema = z.strictObject({ hostLifetimeId: identity, deviceEpoch: identity.nullable(), connected: z.boolean(), active: z.boolean(),
12
+ workers: z.number().int().min(0).max(1024), operations: z.number().int().min(0).max(64),
13
+ scopeCount: z.number().int().min(0).max(1024), scopeBudget: z.literal(1024), rotationRequired: z.boolean(), quiescent: z.boolean(),
14
+ }).refine(value => value.quiescent === (!value.active && value.workers === 0 && value.operations === 0) &&
15
+ value.rotationRequired === (value.scopeCount === value.scopeBudget || !value.connected));
16
+ const hostRetirementSchema = z.strictObject({ commandId: identity, hostLifetimeId: identity, action: z.literal('retireHost'),
17
+ admission: z.literal('revoked'), physical: z.literal('completed') });
18
+ export function nativeDeviceRequest(original) {
19
+ const endpoint = { ...original };
20
+ return value => new Promise((resolve, reject) => {
21
+ const socket = createConnection(endpoint.socket);
22
+ let authenticated = false, settled = false, bytes = Buffer.alloc(0);
23
+ const finish = (error, result) => {
24
+ if (settled)
25
+ return;
26
+ settled = true;
27
+ clearTimeout(timer);
28
+ socket.destroy();
29
+ if (error)
30
+ reject(error);
31
+ else
32
+ resolve(result);
33
+ };
34
+ const timer = setTimeout(() => finish(new Error('device_admission_unconfirmed')), 5000);
35
+ socket.once('connect', () => socket.write(endpoint.token));
36
+ socket.on('error', error => finish(error));
37
+ socket.on('end', () => finish(new Error('device_admission_unconfirmed')));
38
+ socket.on('data', chunk => {
39
+ bytes = Buffer.concat([bytes, chunk]);
40
+ if (!authenticated) {
41
+ if (bytes[0] !== 1) {
42
+ finish(new Error('device_admission_unconfirmed'));
43
+ return;
44
+ }
45
+ authenticated = true;
46
+ bytes = bytes.subarray(1);
47
+ socket.write(`device ${JSON.stringify(value)}\n`);
48
+ }
49
+ if (bytes.length > 8192) {
50
+ finish(new Error('device_admission_unconfirmed'));
51
+ return;
52
+ }
53
+ const newline = bytes.indexOf(10);
54
+ if (newline < 0)
55
+ return;
56
+ try {
57
+ finish(undefined, JSON.parse(bytes.subarray(0, newline).toString('utf8')));
58
+ }
59
+ catch {
60
+ finish(new Error('device_admission_unconfirmed'));
61
+ }
62
+ });
63
+ });
64
+ }
65
+ export class NativeDeviceAdmission {
66
+ hostLifetimeId;
67
+ request;
68
+ live = true;
69
+ nativeRetired = false;
70
+ retirementCommandId;
71
+ epoch;
72
+ retiredRevision = -1;
73
+ installed = new Map();
74
+ obligations = new Map();
75
+ revocations = new Map();
76
+ constructor(hostLifetimeId, request) {
77
+ this.hostLifetimeId = hostLifetimeId;
78
+ this.request = request;
79
+ identity.parse(hostLifetimeId);
80
+ }
81
+ static async connect(original) {
82
+ const request = nativeDeviceRequest(original);
83
+ const hello = z.strictObject({ hostLifetimeId: identity, deviceAdmissionProtocol: z.literal(1) }).parse(await request({ action: 'hello' }));
84
+ return new NativeDeviceAdmission(hello.hostLifetimeId, request);
85
+ }
86
+ current() { return this.live; }
87
+ async bindDeviceEpoch(commandId, deviceEpoch) {
88
+ identity.parse(deviceEpoch);
89
+ if (!this.live || this.epoch && this.epoch !== deviceEpoch)
90
+ throw Error('control_lost');
91
+ const schema = z.strictObject({ commandId: identity, hostLifetimeId: identity, action: z.literal('bindDeviceEpoch'), deviceEpoch: identity });
92
+ const receipt = schema.parse(await this.request({ action: 'bindDeviceEpoch', commandId, hostLifetimeId: this.hostLifetimeId, deviceEpoch }));
93
+ if (!this.live || receipt.commandId !== commandId || receipt.hostLifetimeId !== this.hostLifetimeId || receipt.deviceEpoch !== deviceEpoch)
94
+ throw Error('device_admission_unconfirmed');
95
+ this.epoch = deviceEpoch;
96
+ }
97
+ retire() { this.live = false; for (const grant of this.obligations.values())
98
+ this.revoke(grant); }
99
+ key(grant) { return JSON.stringify(grantSchema.parse(grant)); }
100
+ async admit(commandId, grant) {
101
+ const key = this.key(grant);
102
+ if (!this.live || grant.revision <= this.retiredRevision || (this.epoch && this.epoch !== grant.deviceEpoch))
103
+ throw new Error('control_lost');
104
+ const original = grantSchema.parse(grant);
105
+ this.epoch = original.deviceEpoch;
106
+ this.obligations.set(key, original);
107
+ try {
108
+ const receipt = receiptSchema.parse(await this.request({ action: 'admit', commandId, grant: original, hostLifetimeId: this.hostLifetimeId }));
109
+ if (!this.live || original.revision <= this.retiredRevision || !nativeReceiptMatches(this, commandId, original, 'admit', receipt))
110
+ throw new Error('device_admission_unconfirmed');
111
+ this.installed.set(key, original);
112
+ return receipt;
113
+ }
114
+ catch (error) {
115
+ if (this.nativeRetired)
116
+ this.obligations.delete(key);
117
+ else
118
+ this.revoke(original);
119
+ throw error;
120
+ }
121
+ }
122
+ revoke(grant) {
123
+ const key = this.key(grant);
124
+ if (this.nativeRetired)
125
+ return;
126
+ if (this.epoch && this.epoch !== grant.deviceEpoch)
127
+ throw new Error('control_lost');
128
+ this.epoch = grant.deviceEpoch;
129
+ this.retiredRevision = Math.max(this.retiredRevision, grant.revision);
130
+ this.installed.delete(key);
131
+ this.obligations.set(key, grantSchema.parse(grant));
132
+ if (this.revocations.has(key))
133
+ return;
134
+ const pending = this.request({ action: 'revoke', grant: grantSchema.parse(grant), hostLifetimeId: this.hostLifetimeId }).then(value => z.strictObject({}).parse(value));
135
+ this.revocations.set(key, pending);
136
+ void pending.catch(() => { if (this.revocations.get(key) === pending)
137
+ this.revocations.delete(key); });
138
+ }
139
+ async isolate(commandId, grant) {
140
+ this.revoke(grant);
141
+ await this.revocations.get(this.key(grant))?.catch(() => { });
142
+ const original = grantSchema.parse(grant);
143
+ const receipt = receiptSchema.parse(await this.request({ action: 'isolate', commandId, grant: original, hostLifetimeId: this.hostLifetimeId }));
144
+ if (!nativeReceiptMatches(this, commandId, original, 'isolate', receipt))
145
+ throw new Error('device_admission_unconfirmed');
146
+ const key = this.key(original);
147
+ this.installed.delete(key);
148
+ this.obligations.delete(key);
149
+ this.revocations.delete(key);
150
+ return receipt;
151
+ }
152
+ async isolateRegistration(commandId, registrationId, deviceGeneration) {
153
+ const receipt = registrationReceiptSchema.parse(await this.request({ action: 'isolateRegistration', commandId, registrationId,
154
+ deviceGeneration, hostLifetimeId: this.hostLifetimeId }));
155
+ if (receipt.commandId !== commandId || receipt.registrationId !== registrationId || receipt.deviceGeneration !== deviceGeneration ||
156
+ receipt.hostLifetimeId !== this.hostLifetimeId)
157
+ throw new Error('device_admission_unconfirmed');
158
+ return receipt;
159
+ }
160
+ async status() {
161
+ const status = statusSchema.parse(await this.request({ action: 'status', hostLifetimeId: this.hostLifetimeId }));
162
+ if (status.hostLifetimeId !== this.hostLifetimeId)
163
+ throw new Error('device_admission_unconfirmed');
164
+ return status;
165
+ }
166
+ async retireHost(commandId) {
167
+ const receipt = hostRetirementSchema.parse(await this.request({ action: 'retireHost', commandId, hostLifetimeId: this.hostLifetimeId }));
168
+ if (receipt.commandId !== commandId || receipt.hostLifetimeId !== this.hostLifetimeId)
169
+ throw new Error('device_admission_unconfirmed');
170
+ this.nativeRetired = true;
171
+ this.live = false;
172
+ this.installed.clear();
173
+ this.obligations.clear();
174
+ this.revocations.clear();
175
+ this.retirementCommandId = commandId;
176
+ return receipt;
177
+ }
178
+ async stopHost(commandId) {
179
+ const schema = z.strictObject({ commandId: identity, hostLifetimeId: identity, action: z.literal('stopHost'), admission: z.literal('revoked'), physical: z.literal('completed') });
180
+ const receipt = schema.parse(await this.request({ action: 'stopHost', commandId, hostLifetimeId: this.hostLifetimeId }));
181
+ if (receipt.commandId !== commandId || receipt.hostLifetimeId !== this.hostLifetimeId)
182
+ throw new Error('device_admission_unconfirmed');
183
+ this.live = false;
184
+ this.installed.clear();
185
+ return receipt;
186
+ }
187
+ async acknowledgeHostRetirement(commandId) {
188
+ if (!this.nativeRetired || !this.retirementCommandId)
189
+ throw new Error('device_admission_unconfirmed');
190
+ const schema = z.strictObject({ commandId: identity, hostLifetimeId: identity, action: z.literal('acknowledgeHostRetirement'), admission: z.literal('revoked'), physical: z.literal('completed') });
191
+ const receipt = schema.parse(await this.request({ action: 'acknowledgeHostRetirement', commandId, retirementCommandId: this.retirementCommandId, hostLifetimeId: this.hostLifetimeId }));
192
+ if (receipt.commandId !== commandId || receipt.hostLifetimeId !== this.hostLifetimeId)
193
+ throw new Error('device_admission_unconfirmed');
194
+ return receipt;
195
+ }
196
+ context(registrationId, deviceGeneration, consumerId, grant) {
197
+ identity.parse(registrationId);
198
+ identity.parse(deviceGeneration);
199
+ identity.parse(consumerId);
200
+ if (!this.live || !this.epoch)
201
+ throw new Error('control_lost');
202
+ if (grant && (grant.registrationId !== registrationId || grant.controllerId !== consumerId ||
203
+ ![...this.installed.values()].some(value => matchesDeviceGrant(value, grant))))
204
+ throw new Error('control_lost');
205
+ return JSON.stringify({ hostLifetimeId: this.hostLifetimeId, deviceEpoch: this.epoch, registrationId, deviceGeneration, consumerId, grant: grant ?? null });
206
+ }
207
+ }
@@ -10,6 +10,7 @@ import { DesktopMediaStream, decodeMedia, desktopMediaLane, mediaRequire } from
10
10
  import { canonicalDesktopJson, decodeCapabilityReport, decodeDesktopControl, supportsDesktopMode, captureRegion } from './protocol.js';
11
11
  import { captureSamplingModes } from './captureModes.js';
12
12
  import { captureTargetLabel } from './captureTargetLabel.js';
13
+ import { launchAutomationHelper } from './helperProcess.js';
13
14
  import { XfceWallpaperReader, decodeWallpaper } from './wallpaper.js';
14
15
  const execute = promisify(execFile);
15
16
  const totals = ['captured', 'encoded', 'videoPayloadBytes', 'headerBytes', 'rateReopens'];
@@ -51,6 +52,15 @@ class ViewerLifetime {
51
52
  /** No helper/session construction in discovery. Only the image's admitted software is offered. */
52
53
  export class NativeProvider {
53
54
  options;
55
+ async prepareAutomation(binding) {
56
+ await this.options.validateSession?.();
57
+ const admission = this.options.deviceAdmission;
58
+ if (binding.deviceGrant && !admission)
59
+ throw new NativeCommandError('control_lost');
60
+ const context = admission ? { AXOM_DEVICE_CONTEXT: admission.context(binding.registrationId ?? binding.deviceGrant?.registrationId ?? '', binding.route.deviceGeneration, binding.route.consumerId, binding.deviceGrant) } : {};
61
+ return launchAutomationHelper(this.executable, { ...this.environment, ...context }, binding);
62
+ }
63
+ get deviceAdmission() { return this.options.deviceAdmission; }
54
64
  lifetimes = new Map();
55
65
  revision = 1;
56
66
  geometry;
@@ -66,6 +76,7 @@ export class NativeProvider {
66
76
  this.displayId = '00000000000000000000000000000001';
67
77
  this.executable = options.helperPath;
68
78
  this.factory = options.helperFactory ?? (binding => HelperProcess.launch(this.executable, { ...this.environment,
79
+ ...(binding && options.deviceAdmission ? { AXOM_DEVICE_CONTEXT: options.deviceAdmission.context(binding.deviceGrant?.registrationId ?? '', binding.deviceGeneration, binding.sessionId, binding.deviceGrant) } : {}),
69
80
  ...(binding ? { AXOM_KEEPER_TARGET: binding.displayId } : {}) }));
70
81
  }
71
82
  get environment() {
@@ -98,7 +109,7 @@ export class NativeProvider {
98
109
  return next;
99
110
  }
100
111
  probe() { return this.withGeometry(() => this.probeCurrent()); }
101
- async wallpaper(targetId) {
112
+ async wallpaper(targetId, pixels = { width: 1920, height: 1080 }) {
102
113
  mediaRequire(/^[a-f0-9]{32}$/.test(targetId));
103
114
  const capabilities = await this.probe();
104
115
  mediaRequire(capabilities?.displays.some(display => display.id === targetId));
@@ -108,10 +119,10 @@ export class NativeProvider {
108
119
  return result.stdout;
109
120
  });
110
121
  const discovered = await reader.discover();
111
- const result = await execute(this.executable, ['--wallpaper-asset', discovered.file, discovered.background], { timeout: 4000, maxBuffer: 100000, env: this.environment });
122
+ const result = await execute(this.executable, ['--wallpaper-asset', discovered.file, discovered.background, String(pixels.width), String(pixels.height)], { timeout: 4000, maxBuffer: 3 * 1024 * 1024, env: this.environment });
112
123
  const wallpaper = decodeWallpaper(result.stdout);
113
124
  return { asset: { ...wallpaper.asset, placement: discovered.placement,
114
- background: discovered.background, fidelity: discovered.fidelity === 'approximate' ? 'approximate' : wallpaper.asset.fidelity }, jpeg: wallpaper.jpeg };
125
+ background: discovered.background, fidelity: discovered.fidelity === 'approximate' ? 'approximate' : wallpaper.asset.fidelity }, bytes: wallpaper.bytes };
115
126
  }
116
127
  async probeCurrent() {
117
128
  if (this.options.enumerate)
@@ -192,9 +203,16 @@ export class NativeProvider {
192
203
  return actual;
193
204
  });
194
205
  }
195
- async prepare(selection, binding) {
206
+ async prepare(selection, binding, current) {
207
+ const admitted = (beginEffect = false) => { if (current?.(beginEffect) === false)
208
+ throw new NativeCommandError('busy'); };
209
+ admitted();
210
+ if (binding.deviceGrant && !this.options.deviceAdmission)
211
+ throw new NativeCommandError('control_lost');
196
212
  await this.options.validateSession?.();
213
+ admitted();
197
214
  const caps = await this.probe();
215
+ admitted();
198
216
  mediaRequire(caps?.health === 'ready' && ['granted', 'not_applicable'].includes(caps.permission));
199
217
  if (selection.cursorVisible !== undefined)
200
218
  mediaRequire(caps.cursorVisibility === true);
@@ -225,14 +243,19 @@ export class NativeProvider {
225
243
  life = new ViewerLifetime();
226
244
  this.lifetimes.set(binding.sessionId, life);
227
245
  }
228
- if (life.session)
246
+ admitted();
247
+ if (life.session) {
248
+ admitted(true);
229
249
  await life.session.stop();
250
+ }
251
+ admitted();
230
252
  life.bind(binding);
231
253
  let revision = Math.max(this.revision, display.revision);
232
254
  if ((display.captureViewport !== true && (selection.width !== display.width || selection.height !== display.height)) || selection.dpi !== undefined) {
233
255
  const physicalWidth = display.captureViewport === true ? display.width : selection.width;
234
256
  const physicalHeight = display.captureViewport === true ? display.height : selection.height;
235
257
  const actual = await this.geometryTransaction(async (displayRevision) => {
258
+ admitted(true);
236
259
  const { stdout } = await execute(this.displayControl, ['--transaction', `${physicalWidth}x${physicalHeight}${selection.dpi === undefined ? '' : '@' + selection.dpi}`], { env: this.environment, timeout: 4500, maxBuffer: 4096 });
237
260
  const result = z.object({ outcome: z.literal('applied'), width: z.literal(physicalWidth), height: z.literal(physicalHeight),
238
261
  dpi: selection.dpi === undefined ? z.number().optional() : z.literal(selection.dpi) }).parse(JSON.parse(stdout));
@@ -240,12 +263,15 @@ export class NativeProvider {
240
263
  });
241
264
  revision = actual.displayRevision;
242
265
  }
266
+ admitted();
243
267
  const helper = await (this.options.sessionHelperFactory?.(binding) ?? this.factory(binding));
244
268
  const identity = { sessionId: binding.sessionId, deviceGeneration: binding.deviceGeneration,
245
269
  controlGeneration: binding.controlGeneration, displayRevision: revision };
246
270
  const reserved = life.reserve();
247
271
  try {
272
+ admitted();
248
273
  mediaRequire((await helper.request('verify_cleanup', identity)).result.verified === true);
274
+ admitted(true);
249
275
  const payload = { ...(selection.cursorVisible === undefined ? {} : { cursorVisible: selection.cursorVisible }), ...(effectiveRegion ? { captureRegion: effectiveRegion } : {}), width: selection.width, height: selection.height, fpsCeiling: life.prepared ? Math.min(life.appliedFps, selection.fpsCeiling) : selection.fpsCeiling,
250
276
  rateTargetBps: life.appliedTargetBps, quality: selection.quality, backend: this.options.backend ?? 'libx264', leaseMs: 30000, representation: 1,
251
277
  ...reserved, ...(life.prepared ? { continuation: { initialReservoirBits: life.initialReservoirBits, inputSequence: life.inputSequence,
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod';
2
+ const bounded = z.string().min(1).max(128).regex(/^[^\x00-\x1f\x7f]+$/);
3
+ const revision = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
4
+ export const deviceAcquisitionSchema = z.discriminatedUnion('kind', [
5
+ z.strictObject({ kind: z.literal('if_free') }),
6
+ z.strictObject({ kind: z.literal('takeover'), expectedDeviceEpoch: bounded, expectedRevision: revision }),
7
+ ]);
8
+ export const deviceOwnershipSnapshotSchema = z.strictObject({ deviceEpoch: bounded, revision,
9
+ state: z.enum(['free', 'draining', 'active']),
10
+ controller: z.strictObject({ registrationId: bounded, controllerId: bounded, kind: bounded }).nullable(),
11
+ });
12
+ export const deviceOwnershipCommandSchema = z.strictObject({ t: z.literal('desktop_ownership'), ownershipProtocol: z.literal(1),
13
+ commandId: z.string().regex(/^[0-9a-f]{32}$/), deviceGeneration: bounded, controllerId: bounded,
14
+ kind: z.enum(['viewer', 'automation']), action: z.enum(['acquire', 'commit', 'release', 'cancel']),
15
+ requestSequence: revision.refine(value => value > 0).optional(), acquireCommandId: z.string().regex(/^[0-9a-f]{32}$/).optional(),
16
+ acquisition: deviceAcquisitionSchema.optional(), deviceEpoch: bounded.optional(), revision: revision.optional(),
17
+ }).refine(value => value.action === 'acquire' ? value.acquisition != null && value.requestSequence != null :
18
+ value.action === 'cancel' ? value.acquireCommandId != null && value.requestSequence != null : value.deviceEpoch != null && value.revision != null);
19
+ const reservationCauseSchema = z.strictObject({ acquireCommandId: z.string().regex(/^[0-9a-f]{32}$/), contenderControllerId: bounded, deviceGeneration: bounded });
20
+ export const deviceOwnershipEventSchema = z.strictObject({ t: z.literal('desktop_ownership_result'), ownershipProtocol: z.literal(1),
21
+ commandId: z.string().regex(/^[0-9a-f]{32}$/), deviceGeneration: bounded, controllerId: bounded,
22
+ status: z.enum(['acquired', 'committed', 'released', 'cancelled', 'busy', 'conflict', 'unsupported', 'lost', 'snapshot']),
23
+ snapshot: deviceOwnershipSnapshotSchema, reservationCause: reservationCauseSchema.optional(),
24
+ }).refine(value => value.reservationCause == null || value.status === 'lost');
25
+ export const matchesDeviceGrant = (left, right) => left.deviceEpoch === right.deviceEpoch && left.revision === right.revision && left.registrationId === right.registrationId &&
26
+ left.controllerId === right.controllerId && left.authorization === right.authorization;
27
+ export function nativeReceiptMatches(handle, commandId, grant, action, receipt) {
28
+ return (action === 'isolate' || handle.current()) && receipt.commandId === commandId && receipt.hostLifetimeId === handle.hostLifetimeId &&
29
+ receipt.action === action && receipt.physical === 'completed' && receipt.admission === (action === 'admit' ? 'active' : 'revoked') &&
30
+ matchesDeviceGrant(grant, receipt.grant);
31
+ }