@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,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
+ }
@@ -27,7 +27,7 @@ export class DeviceSockets {
27
27
  const url = desktopSocketUrl(this.server, 'media', this.allowPrivateWs);
28
28
  url.protocol = url.protocol === 'wss:' ? 'https:' : 'http:';
29
29
  url.pathname = url.pathname.replace(/\/ws\/desktop\/media$/, '/desktop/background');
30
- const headers = { 'X-Axom-Background-Grant': grant, 'Content-Type': 'image/jpeg' };
30
+ const headers = { 'X-Axom-Background-Grant': grant, 'Content-Type': 'image/webp' };
31
31
  if (wallpaper) {
32
32
  headers['X-Axom-Background-Asset'] = Buffer.from(JSON.stringify(wallpaper.asset)).toString('base64url');
33
33
  headers['X-Axom-Background-Unchanged'] = unchanged ? '1' : '0';
@@ -35,7 +35,7 @@ export class DeviceSockets {
35
35
  else
36
36
  headers['X-Axom-Background-Error'] = 'unavailable';
37
37
  const response = await fetch(url, { method: 'POST', redirect: 'error', signal: AbortSignal.timeout(10_000), headers,
38
- body: wallpaper && !unchanged ? new Uint8Array(wallpaper.jpeg) : undefined });
38
+ body: wallpaper && !unchanged ? new Uint8Array(wallpaper.bytes) : undefined });
39
39
  if (!response.ok)
40
40
  throw new NativeCommandError('transport_failed');
41
41
  await response.body?.cancel();
@@ -5,6 +5,11 @@ import { z } from 'zod';
5
5
  import { DesktopProtocolError, parseDesktopJson } from './protocol.js';
6
6
  import { decodeMedia, mediaRequire } from './media.js';
7
7
  import { decodeAudio } from './audio.js';
8
+ import { randomBytes as automationRandomBytes } from 'node:crypto';
9
+ import { parseAutomationJson } from './automationJson.js';
10
+ import { AutomationTransport } from './automationTransport.js';
11
+ import { AutomationSession } from './automationSession.js';
12
+ import { decodeAutomationFailureDetail } from './automationProtocol.js';
8
13
  const execute = promisify(execFile);
9
14
  const commands = ['focus', 'cache_dependency', 'transport_cost', 'prepare_motion_cut', 'commit_motion_cut', 'prepare', 'start', 'decoder_ready', 'first_presented', 'acknowledge', 'credit', 'renew', 'input', 'clipboard', 'overview', 'reserve_overview', 'resize',
10
15
  'held_state', 'text_result', 'recover_display', 'set_rate', 'refresh', 'stop', 'verify_cleanup', 'probe', 'status'];
@@ -85,10 +90,12 @@ const replySchema = z.union([
85
90
  export class NativeCommandError extends Error {
86
91
  code;
87
92
  counters;
88
- constructor(code, counters) {
93
+ detail;
94
+ constructor(code, counters, detail = null) {
89
95
  super(code);
90
96
  this.code = code;
91
97
  this.counters = counters;
98
+ this.detail = detail;
92
99
  }
93
100
  }
94
101
  /** Local queue retirement, created only before allocating an ID or writing bytes. */
@@ -323,6 +330,248 @@ export class HelperProcess {
323
330
  function write(stream, bytes) {
324
331
  return new Promise((resolve, reject) => stream.write(bytes, error => error ? reject(error) : resolve()));
325
332
  }
333
+ export async function launchAutomationHelper(executable, env, binding) {
334
+ if (!binding.native || !binding.preparation)
335
+ throw new NativeCommandError('trusted_preparation_unavailable');
336
+ const token = automationRandomBytes(32).toString('hex');
337
+ const child = spawn(executable, ['--automation-fd', '3'], { stdio: ['pipe', 'pipe', 'ignore', 'pipe'],
338
+ env: { ...env, AXOM_AUTOMATION_TOKEN: token, AXOM_AUTOMATION_FEATURES: 'capture_wait_v1' } });
339
+ child.stdin.on('error', () => { });
340
+ const control = new NativeByteReader(child.stdout, 49152), binary = new NativeByteReader(child.stdio[3], 8388608 + 49156);
341
+ let ended = false, controlId = 0, writeCount = 0, writeTail = Promise.resolve();
342
+ const renewals = new Map();
343
+ const preparations = new Map();
344
+ const statuses = new Map();
345
+ const readControls = new Map();
346
+ const send = (text) => {
347
+ if (ended || writeCount >= 64)
348
+ return Promise.reject(new NativeCommandError('resource_limit'));
349
+ const body = Buffer.from(text);
350
+ if (!body.length || body.length > 49152)
351
+ return Promise.reject(new NativeCommandError('invalid_argument'));
352
+ const prefix = Buffer.alloc(4);
353
+ prefix.writeUInt32BE(body.length);
354
+ writeCount++;
355
+ const pending = writeTail.then(() => { if (ended)
356
+ throw new NativeCommandError('closed'); return bounded(write(child.stdin, Buffer.concat([prefix, body])), 1000); });
357
+ writeTail = pending.catch(() => { }).finally(() => { writeCount--; });
358
+ return pending;
359
+ };
360
+ const transport = new AutomationTransport(send, requestId => { void send(JSON.stringify({ command: 'cancel', requestId })).catch(() => { }); });
361
+ const retire = () => {
362
+ if (ended)
363
+ return;
364
+ ended = true;
365
+ transport.close('transport_failed');
366
+ for (const renewal of renewals.values()) {
367
+ clearTimeout(renewal.timer);
368
+ renewal.reject(new NativeCommandError('closed'));
369
+ }
370
+ renewals.clear();
371
+ for (const pending of preparations.values()) {
372
+ clearTimeout(pending.timer);
373
+ pending.detach();
374
+ pending.reject(new NativeCommandError('closed'));
375
+ }
376
+ preparations.clear();
377
+ for (const pending of statuses.values()) {
378
+ clearTimeout(pending.timer);
379
+ pending.reject(new NativeCommandError('closed'));
380
+ }
381
+ statuses.clear();
382
+ for (const pending of readControls.values()) {
383
+ clearTimeout(pending.timer);
384
+ pending.reject(new NativeCommandError('closed'));
385
+ }
386
+ readControls.clear();
387
+ };
388
+ const closed = new Promise(resolve => { child.once('exit', () => { retire(); resolve(); }); child.once('error', () => { retire(); resolve(); }); });
389
+ const readControl = async () => {
390
+ const size = (await control.exact(4)).readUInt32BE();
391
+ return parseAutomationJson(await control.exact(size), 49152).value;
392
+ };
393
+ const stop = async () => {
394
+ try {
395
+ if (!ended)
396
+ await send(JSON.stringify({ command: 'shutdown' }));
397
+ }
398
+ catch { }
399
+ retire();
400
+ child.kill('SIGTERM');
401
+ try {
402
+ await bounded(closed, 1000);
403
+ }
404
+ catch {
405
+ child.kill('SIGKILL');
406
+ await bounded(closed, 2000);
407
+ }
408
+ child.stdin?.destroy();
409
+ child.stdout?.destroy();
410
+ child.stdio[3].destroy();
411
+ };
412
+ try {
413
+ const handshake = await bounded(readControl(), 3000, 'unsupported_automation_protocol');
414
+ if (handshake.automationProtocol !== 1 || !Array.isArray(handshake.operations) || !handshake.operations.includes('capture'))
415
+ throw new NativeCommandError('unsupported_automation_protocol');
416
+ await send(JSON.stringify({ command: 'bind', token, context: { ...binding.native, route: binding.route, executionId: binding.executionId,
417
+ preparation: { ...binding.preparation, selector: null, mode: null } } }));
418
+ const attached = await bounded(readControl(), 3000, 'unsupported_automation_protocol');
419
+ if (attached.control !== 'bound' || !Array.isArray(attached.operations))
420
+ throw new NativeCommandError('unsupported_automation_protocol');
421
+ void (async () => {
422
+ while (!ended) {
423
+ const message = await readControl();
424
+ const readPending = readControls.get(message.id);
425
+ if (readPending && ['status_only', 'isolate', 'prepare_failed'].includes(message.control)) {
426
+ readControls.delete(message.id);
427
+ clearTimeout(readPending.timer);
428
+ if (message.control === 'prepare_failed')
429
+ readPending.reject(new NativeCommandError(message.code));
430
+ else if (message.control !== readPending.command || message.result?.physical !== 'completed' || message.result?.revoked !== true)
431
+ readPending.reject(new NativeCommandError('isolation_unconfirmed'));
432
+ else
433
+ readPending.resolve(message.result);
434
+ }
435
+ else if (message.control === 'lease_admitted') {
436
+ const renewal = renewals.get(message.id);
437
+ if (!renewal)
438
+ continue;
439
+ renewals.delete(message.id);
440
+ clearTimeout(renewal.timer);
441
+ renewal.resolve({ admitted: true, physical: message.physical });
442
+ }
443
+ else if (message.control === 'prepared' || message.control === 'prepare_failed') {
444
+ const pending = preparations.get(message.id);
445
+ if (!pending)
446
+ continue;
447
+ preparations.delete(message.id);
448
+ clearTimeout(pending.timer);
449
+ pending.detach();
450
+ if (message.control === 'prepared')
451
+ pending.resolve(message.attachment);
452
+ else
453
+ pending.reject(new NativeCommandError(message.code, undefined, decodeAutomationFailureDetail(message.detail)));
454
+ }
455
+ else if (message.control === 'control_status') {
456
+ const pending = statuses.get(message.id);
457
+ if (!pending)
458
+ continue;
459
+ statuses.delete(message.id);
460
+ clearTimeout(pending.timer);
461
+ pending.resolve({ physical: message.physical, revoked: message.revoked });
462
+ }
463
+ else if (message.control !== 'close_admitted') {
464
+ const accepted = transport.acceptResponse(JSON.stringify(message));
465
+ if (!accepted.ok && accepted.code !== 'unknown_request')
466
+ throw new NativeCommandError('invalid_response');
467
+ }
468
+ }
469
+ })().catch(() => { retire(); child.kill('SIGTERM'); });
470
+ void (async () => {
471
+ while (!ended) {
472
+ const prefix = await binary.exact(4), length = prefix.readUInt32BE();
473
+ if (length < 1 || length > 49152)
474
+ throw new NativeCommandError('invalid_binary');
475
+ const header = await binary.exact(length), value = parseAutomationJson(header, 49152).value;
476
+ if (!Number.isInteger(value.bytesLength) || value.bytesLength < 1 || value.bytesLength > 8388608)
477
+ throw new NativeCommandError('invalid_binary');
478
+ transport.acceptBinary(Buffer.concat([prefix, header, await binary.exact(value.bytesLength)]));
479
+ }
480
+ })().catch(() => { retire(); child.kill('SIGTERM'); });
481
+ return new AutomationSession(transport, binding, { operations: attached.operations.filter((operation) => handshake.operations.includes(operation)), close: stop,
482
+ readControl: async (command, fields) => {
483
+ if (readControls.size >= 8 || ended)
484
+ throw new NativeCommandError('resource_limit');
485
+ const requestId = `read-control-${++controlId}`;
486
+ const pending = new Promise((resolve, reject) => {
487
+ const timer = setTimeout(() => { readControls.delete(requestId); reject(new NativeCommandError('timed_out')); }, 5000);
488
+ readControls.set(requestId, { command, resolve, reject, timer });
489
+ });
490
+ void send(JSON.stringify({ command, requestId, ...fields })).catch(error => {
491
+ const entry = readControls.get(requestId);
492
+ if (entry) {
493
+ readControls.delete(requestId);
494
+ clearTimeout(entry.timer);
495
+ entry.reject(error);
496
+ }
497
+ });
498
+ return pending;
499
+ },
500
+ prepare: async (preparation, options = {}) => {
501
+ if (preparations.size + renewals.size + statuses.size >= 8 || ended)
502
+ throw new NativeCommandError('resource_limit');
503
+ if (options.signal?.aborted)
504
+ throw new NativeCommandError('cancelled');
505
+ const requestId = `attachment-${++controlId}`;
506
+ const pending = new Promise((resolve, reject) => {
507
+ const abort = () => {
508
+ const entry = preparations.get(requestId);
509
+ if (!entry)
510
+ return;
511
+ preparations.delete(requestId);
512
+ clearTimeout(entry.timer);
513
+ entry.detach();
514
+ reject(new NativeCommandError('cancelled'));
515
+ void send(JSON.stringify({ command: 'cancel', requestId })).catch(() => { });
516
+ };
517
+ const timer = setTimeout(() => { abort(); }, options.timeoutMs ?? 65000);
518
+ preparations.set(requestId, { resolve, reject, timer, detach: () => options.signal?.removeEventListener('abort', abort) });
519
+ options.signal?.addEventListener('abort', abort, { once: true });
520
+ });
521
+ void send(JSON.stringify({ command: 'prepare', requestId, preparation })).catch(error => {
522
+ const entry = preparations.get(requestId);
523
+ if (entry) {
524
+ preparations.delete(requestId);
525
+ clearTimeout(entry.timer);
526
+ entry.detach();
527
+ entry.reject(error);
528
+ }
529
+ });
530
+ return pending;
531
+ },
532
+ status: async () => {
533
+ if (preparations.size + renewals.size + statuses.size >= 8 || ended)
534
+ throw new NativeCommandError('resource_limit');
535
+ const id = ++controlId;
536
+ const pending = new Promise((resolve, reject) => {
537
+ const timer = setTimeout(() => { statuses.delete(id); reject(new NativeCommandError('timed_out')); }, 1000);
538
+ statuses.set(id, { resolve, reject, timer });
539
+ });
540
+ void send(JSON.stringify({ command: 'control_status', id })).catch(error => { const entry = statuses.get(id); if (entry) {
541
+ clearTimeout(entry.timer);
542
+ statuses.delete(id);
543
+ entry.reject(error);
544
+ } });
545
+ return pending;
546
+ },
547
+ renew: async (leaseMs) => {
548
+ if (preparations.size + renewals.size + statuses.size >= 8 || ended)
549
+ throw new NativeCommandError('resource_limit');
550
+ const id = ++controlId;
551
+ const pending = new Promise((resolve, reject) => {
552
+ const timer = setTimeout(() => { renewals.delete(id); reject(new NativeCommandError('timed_out')); }, 1000);
553
+ renewals.set(id, { resolve, reject, timer });
554
+ });
555
+ try {
556
+ await send(JSON.stringify({ command: 'renew', id, leaseMs }));
557
+ return await pending;
558
+ }
559
+ catch (error) {
560
+ const renewal = renewals.get(id);
561
+ if (renewal) {
562
+ clearTimeout(renewal.timer);
563
+ renewals.delete(id);
564
+ renewal.resolve({ admitted: true, physical: 'failed' });
565
+ }
566
+ throw error;
567
+ }
568
+ } });
569
+ }
570
+ catch (error) {
571
+ await stop();
572
+ throw error instanceof NativeCommandError ? error : new NativeCommandError('unsupported_automation_protocol');
573
+ }
574
+ }
326
575
  /** A single bounded session/event consumer; overflow retires the session. */
327
576
  export class DesktopQueue {
328
577
  limit;