@myagentroam/node 0.13.11 → 0.13.13

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 (41) hide show
  1. package/dist/connection/direct-transfer-service.js +60 -0
  2. package/dist/connection/endpoints.js +16 -0
  3. package/dist/connector.js +298 -10
  4. package/dist/ctags-binary.js +5 -1
  5. package/dist/managed-binaries.js +8 -0
  6. package/dist/marctl/package.json +1 -1
  7. package/dist/remote-desktop/desktop-byte-stream.js +128 -0
  8. package/dist/remote-desktop/desktop-error.js +35 -0
  9. package/dist/remote-desktop/desktop-service-core.js +384 -0
  10. package/dist/remote-desktop/direct-desktop-service.js +46 -0
  11. package/dist/remote-desktop/linux-managed-binaries.js +46 -0
  12. package/dist/remote-desktop/macos-managed-binaries.js +19 -0
  13. package/dist/remote-desktop/macos-remote-desktop-runtime.js +121 -0
  14. package/dist/remote-desktop/macvnc-managed-process.js +196 -0
  15. package/dist/remote-desktop/pooled-desktop-attachment.js +62 -0
  16. package/dist/remote-desktop/relay-desktop-service.js +40 -0
  17. package/dist/remote-desktop/remote-desktop-attachment.js +87 -0
  18. package/dist/remote-desktop/rfb-backend-handshake.js +98 -0
  19. package/dist/remote-desktop/rfb-client-gate.js +318 -0
  20. package/dist/remote-desktop/rfb-frontend-handshake.js +38 -0
  21. package/dist/remote-desktop/rfb-handshake-io.js +91 -0
  22. package/dist/remote-desktop/rfb-input-release.js +77 -0
  23. package/dist/remote-desktop/standard-rfb-attachment.js +40 -0
  24. package/dist/remote-desktop/tigervnc-attachment.js +68 -0
  25. package/dist/remote-desktop/tigervnc-backend-pool.js +88 -0
  26. package/dist/remote-desktop/tigervnc-cleanup-channel.js +129 -0
  27. package/dist/remote-desktop/tigervnc-managed-process.js +274 -0
  28. package/dist/remote-desktop/tigervnc-rfb-session.js +190 -0
  29. package/dist/remote-desktop/ultravnc-config.js +35 -0
  30. package/dist/remote-desktop/ultravnc-managed-process.js +180 -0
  31. package/dist/remote-desktop/vnc-password.js +15 -0
  32. package/dist/remote-desktop/windows-managed-binaries.js +18 -0
  33. package/dist/remote-desktop/windows-remote-desktop-runtime.js +151 -0
  34. package/dist/remote-desktop/x11-desktop-topology.js +82 -0
  35. package/dist/remote-desktop/x11-remote-desktop-runtime.js +99 -0
  36. package/dist/remote-desktop/x11-topology-source.js +205 -0
  37. package/dist/remote-desktop/x11-topology.js +88 -0
  38. package/dist/ripgrep-binary.js +5 -1
  39. package/dist/service/node-control-channel.js +7 -1
  40. package/dist/service/node-control-message-service.js +11 -0
  41. package/package.json +7 -6
@@ -2,6 +2,7 @@ import { createSocket } from 'node:dgram';
2
2
  import { isIP } from 'node:net';
3
3
  import { directDataControlFrameSchema, directNodeCommandSchema } from '@myagentroam/protocol';
4
4
  import { TerminalDirectService } from '../service/terminal-direct-service.js';
5
+ import { DirectDesktopService } from '../remote-desktop/direct-desktop-service.js';
5
6
  import { IceUdpMuxListener as NativeIceUdpMuxListener } from 'node-datachannel';
6
7
  import { RTCPeerConnection } from 'node-datachannel/polyfill';
7
8
  const MAX_HOST_CANDIDATES = 32;
@@ -23,6 +24,7 @@ export class DirectTransferService {
23
24
  leaseGraceMs;
24
25
  leaseCheckMs;
25
26
  terminalDirect;
27
+ desktopDirect;
26
28
  disposed = false;
27
29
  static async create(options) {
28
30
  const range = options.udpPortRange?.() ?? { begin: 49_152, end: 49_215 };
@@ -62,6 +64,26 @@ export class DirectTransferService {
62
64
  command.browserFingerprint === fingerprint(peer.remoteDescription?.sdp) &&
63
65
  command.nodeFingerprint === fingerprint(peer.localDescription?.sdp));
64
66
  }, this.now);
67
+ if (options.desktop) {
68
+ this.desktopDirect = new DirectDesktopService({
69
+ ...options.desktop,
70
+ nodeGeneration: options.nodeGeneration,
71
+ peer: (command) => {
72
+ const transport = this.transports.get(command.transportSessionId);
73
+ const peer = transport?.peer;
74
+ return peer &&
75
+ transport.command.workbenchConnectionId === command.workbenchConnectionId &&
76
+ transport.command.nodeGeneration === command.nodeGeneration &&
77
+ command.nodeGeneration === options.nodeGeneration() &&
78
+ peer.connectionState === 'connected' &&
79
+ command.browserFingerprint === fingerprint(peer.remoteDescription?.sdp) &&
80
+ command.nodeFingerprint === fingerprint(peer.localDescription?.sdp)
81
+ ? peer
82
+ : undefined;
83
+ },
84
+ ...(options.now ? { now: options.now } : {})
85
+ });
86
+ }
65
87
  }
66
88
  handle(value) {
67
89
  const parsed = directNodeCommandSchema.safeParse(value);
@@ -82,7 +104,24 @@ export class DirectTransferService {
82
104
  handleTerminal(value) {
83
105
  return this.terminalDirect?.handle(value, (id) => this.transports.get(id)?.peer) ?? false;
84
106
  }
107
+ handleDesktop(value) {
108
+ return this.desktopDirect?.handle(value) ?? false;
109
+ }
110
+ invalidateDesktop(code) {
111
+ this.desktopDirect?.invalidate(code);
112
+ }
113
+ /** Pause only Remote Desktop; the RTC transport remains owned by Direct Transfer. */
114
+ suspendDesktop(signal) {
115
+ return this.desktopDirect?.suspend(signal) ?? Promise.resolve();
116
+ }
117
+ canResumeDesktop() {
118
+ return this.desktopDirect?.canResume() ?? true;
119
+ }
120
+ resumeDesktop() {
121
+ return this.desktopDirect?.resume() ?? true;
122
+ }
85
123
  close(code = 'DIRECT_NODE_STOPPED') {
124
+ this.desktopDirect?.invalidate('DESKTOP_TRANSPORT_ERROR');
86
125
  this.terminalDirect?.close();
87
126
  for (const transfer of [...this.transfers.values()])
88
127
  void this.removeTransfer(transfer, code);
@@ -93,10 +132,17 @@ export class DirectTransferService {
93
132
  if (this.disposed)
94
133
  return;
95
134
  this.disposed = true;
135
+ // Preserve the shutdown reason before closing the shared RTC transport.
136
+ this.desktopDirect?.dispose();
96
137
  this.close();
97
138
  this.terminalDirect?.dispose();
98
139
  this.iceUdpMuxListener.stop();
99
140
  }
141
+ /** Stop transport admission immediately, then join Remote-owned native cleanup. */
142
+ shutdown(signal) {
143
+ this.dispose();
144
+ return this.desktopDirect?.shutdown(signal) ?? Promise.resolve();
145
+ }
100
146
  async dispatch(command) {
101
147
  if (command.type === 'direct.prepare') {
102
148
  if (command.purpose === 'TRANSPORT')
@@ -158,6 +204,7 @@ export class DirectTransferService {
158
204
  command,
159
205
  peer,
160
206
  sentCandidateKeys: new Set(),
207
+ pendingChannels: new Set(),
161
208
  lastLeaseAliveAt: this.now(),
162
209
  leaseExpiresAt: command.expiresAt,
163
210
  leaseTimer: undefined
@@ -283,12 +330,21 @@ export class DirectTransferService {
283
330
  });
284
331
  }
285
332
  attachPendingChannel(transport, channel) {
333
+ if (this.transports.get(transport.command.directSessionId) !== transport) {
334
+ channel.close();
335
+ return;
336
+ }
337
+ transport.pendingChannels.add(channel);
338
+ channel.addEventListener('close', () => transport.pendingChannels.delete(channel), {
339
+ once: true
340
+ });
286
341
  channel.binaryType = 'arraybuffer';
287
342
  let attached = false;
288
343
  channel.onmessage = (event) => {
289
344
  if (attached)
290
345
  return;
291
346
  attached = true;
347
+ transport.pendingChannels.delete(channel);
292
348
  if (channel.label.startsWith('mar-terminal-')) {
293
349
  this.terminalDirect?.open(transport.command.directSessionId, channel, event.data);
294
350
  if (this.terminalDirect === undefined)
@@ -480,9 +536,13 @@ export class DirectTransferService {
480
536
  return;
481
537
  clearInterval(transport.leaseTimer);
482
538
  this.terminalDirect?.removeTransport(transport.command.directSessionId);
539
+ this.desktopDirect?.removeTransport(transport.command.directSessionId);
483
540
  for (const transfer of [...this.transfers.values()])
484
541
  if (transfer.command.transportSessionId === transport.command.directSessionId)
485
542
  void this.removeTransfer(transfer, code ?? 'DIRECT_CONNECTION_FAILED');
543
+ for (const channel of transport.pendingChannels)
544
+ channel.close();
545
+ transport.pendingChannels.clear();
486
546
  transport.peer.close();
487
547
  if (code !== undefined)
488
548
  this.options.send('direct.error', {
@@ -30,3 +30,19 @@ export function nodeTerminalConnectEndpoint(serverUrl) {
30
30
  endpoint.hash = '';
31
31
  return endpoint.toString();
32
32
  }
33
+ export function nodeDesktopRelayEndpoint(serverUrl) {
34
+ const endpoint = new URL(serverUrl);
35
+ if (endpoint.username || endpoint.password)
36
+ throw new Error('SERVER_URL_INVALID');
37
+ if (endpoint.protocol === 'http:')
38
+ endpoint.protocol = 'ws:';
39
+ if (endpoint.protocol === 'https:')
40
+ endpoint.protocol = 'wss:';
41
+ if (endpoint.protocol !== 'ws:' && endpoint.protocol !== 'wss:') {
42
+ throw new Error('SERVER_URL_INVALID');
43
+ }
44
+ endpoint.pathname = `${endpoint.pathname.replace(/\/$/, '')}/api/node/desktop-relay`;
45
+ endpoint.search = '';
46
+ endpoint.hash = '';
47
+ return endpoint.toString();
48
+ }
package/dist/connector.js CHANGED
@@ -12,7 +12,8 @@ import { ModelUsageOutbox } from './service/model-usage-outbox.js';
12
12
  import { NodeModelUsageService } from './service/model-usage-service.js';
13
13
  import { dirname, resolve } from 'node:path';
14
14
  import { homedir } from 'node:os';
15
- import { createEnvelope, directNodeCommandSchema } from '@myagentroam/protocol';
15
+ import WebSocket from 'ws';
16
+ import { createEnvelope, directNodeCommandSchema, remoteDesktopDescriptionSchema } from '@myagentroam/protocol';
16
17
  import { loadNodeConfig, computerUseRuntimeExecutable, directTransferUdpPortRange, nodeDataDirectory, nodeDatabasePath } from './config.js';
17
18
  import { NodeMetrics, nodeLog } from './operational.js';
18
19
  import { assertLocalSqlitePath } from './storage.js';
@@ -44,6 +45,16 @@ import { TerminalService } from './service/terminal-service.js';
44
45
  import { GitExecutionService } from './service/git-execution-service.js';
45
46
  import { GitOperationService } from './service/git-operation-service.js';
46
47
  import { DirectTransferService } from './connection/direct-transfer-service.js';
48
+ import { nodeDesktopRelayEndpoint } from './connection/endpoints.js';
49
+ import { RelayDesktopService } from './remote-desktop/relay-desktop-service.js';
50
+ import { DesktopAttachmentRegistry } from './remote-desktop/desktop-service-core.js';
51
+ import { X11RemoteDesktopRuntime } from './remote-desktop/x11-remote-desktop-runtime.js';
52
+ import { WindowsRemoteDesktopRuntime } from './remote-desktop/windows-remote-desktop-runtime.js';
53
+ import { windowsManagedDesktopBinaries } from './remote-desktop/windows-managed-binaries.js';
54
+ import { MacosRemoteDesktopRuntime } from './remote-desktop/macos-remote-desktop-runtime.js';
55
+ import { macosManagedDesktopBinaries } from './remote-desktop/macos-managed-binaries.js';
56
+ import { linuxManagedDesktopBinaries } from './remote-desktop/linux-managed-binaries.js';
57
+ import { RemoteDesktopError } from './remote-desktop/desktop-error.js';
47
58
  import { WorkspaceChangeService } from './workspace/workspace-change-service.js';
48
59
  import { WorkbenchEventService } from './service/workbench-event-service.js';
49
60
  import { RecentRunProjectionService } from './service/recent-run-projection-service.js';
@@ -166,6 +177,7 @@ export class NodeConnector {
166
177
  usageOutbox;
167
178
  modelUsage = new NodeModelUsageService(() => this.usageOutbox, () => this.config?.nodeId);
168
179
  nodeGeneration = crypto.randomUUID().replace(/-/g, '');
180
+ nodeGenerationRetired = false;
169
181
  controlChannel;
170
182
  controlMessages;
171
183
  scheduledRunAuthorization;
@@ -247,6 +259,7 @@ export class NodeConnector {
247
259
  marAgentClient;
248
260
  marAgentRunController;
249
261
  computerUseProvider;
262
+ desktopAttachmentRegistry = new DesktopAttachmentRegistry();
250
263
  computerUseEnablement;
251
264
  configMutations = new NodeConfigMutations();
252
265
  managedComputerUseRuntime;
@@ -314,6 +327,12 @@ export class NodeConnector {
314
327
  workspaceUploadService;
315
328
  invalidateWorkspaceFiles;
316
329
  directTransferService;
330
+ relayDesktopService;
331
+ desktopRuntime;
332
+ desktopDiscovery;
333
+ desktopStarting;
334
+ desktopDiscoveryAbort;
335
+ desktopDiscoveryEpoch = 0;
317
336
  attachmentTemporaryRoot() {
318
337
  if (this.config === undefined)
319
338
  throw new Error('NODE_CONFIG_UNAVAILABLE');
@@ -410,10 +429,12 @@ export class NodeConnector {
410
429
  const detect = options.capabilityDetector ?? detectCapabilitiesAsync;
411
430
  this.capabilityDetector = async () => {
412
431
  const capabilities = await detect();
413
- return this.projectComputerUseCapability(await this.conversationHosting.project({
432
+ if (!this.stopped)
433
+ await this.initializeDesktopRuntime();
434
+ return this.projectRemoteDesktopCapability(this.projectComputerUseCapability(await this.conversationHosting.project({
414
435
  ...capabilities,
415
436
  metadata: upgradeMetadata(capabilities.metadata, this.upgradeCoordinator.snapshot(), options.supervisorUpgradeHandoff !== undefined)
416
- }));
437
+ })));
417
438
  };
418
439
  this.reconnect = options.reconnect ?? true;
419
440
  const composedRunners = composeRunners(options);
@@ -1244,6 +1265,7 @@ export class NodeConnector {
1244
1265
  this.promptCompletion?.disconnect();
1245
1266
  this.usageOutbox?.disconnected();
1246
1267
  this.directTransferService?.close('DIRECT_TRANSFER_CONTROL_UNAVAILABLE');
1268
+ this.relayDesktopService?.invalidate('DESKTOP_TRANSPORT_ERROR');
1247
1269
  this.runtimeCredentials.disconnected();
1248
1270
  this.chatgptLogins.close();
1249
1271
  this.scheduledRunAuthorization.disconnected();
@@ -1312,6 +1334,8 @@ export class NodeConnector {
1312
1334
  handleUpgradeCredentialResponse: (envelope) => this.upgradeCredentials.handle(envelope),
1313
1335
  handleDirectTransfer: (envelope) => this.handleDirectTransfer(envelope),
1314
1336
  handleTerminalDirect: (envelope) => this.handleTerminalDirect(envelope),
1337
+ handleDesktopDirect: (envelope) => this.handleDesktopDirect(envelope),
1338
+ handleDesktopRelay: (envelope) => this.handleDesktopRelay(envelope),
1315
1339
  runners: this.runners,
1316
1340
  deliverSessionChannelMessage: (envelope) => this.runnerChannelMessages.handle(envelope),
1317
1341
  completeUncontrolledCancellation: (runId) => {
@@ -1405,10 +1429,10 @@ export class NodeConnector {
1405
1429
  detectCapabilities: this.capabilityDetector,
1406
1430
  capabilitiesProvided: this.capabilitiesProvided,
1407
1431
  stopped: () => this.stopped,
1408
- setCapabilities: (capabilities) => (this.capabilities = {
1432
+ setCapabilities: (capabilities) => (this.capabilities = this.projectRemoteDesktopCapability({
1409
1433
  ...capabilities,
1410
1434
  metadata: upgradeMetadata(capabilities.metadata, this.upgradeCoordinator.snapshot(), options.supervisorUpgradeHandoff !== undefined)
1411
- }),
1435
+ })),
1412
1436
  control: this.controlChannel,
1413
1437
  terminal: this.terminalChannel,
1414
1438
  metrics: this.metrics,
@@ -1418,6 +1442,9 @@ export class NodeConnector {
1418
1442
  await this.conversationHosting.initialize();
1419
1443
  this.capabilities = await this.conversationHosting.project(this.capabilities);
1420
1444
  this.computerUseEnablement.initialize();
1445
+ if (!this.stopped)
1446
+ this.initializeDesktopRelay();
1447
+ this.capabilities = this.projectRemoteDesktopCapability(this.capabilities);
1421
1448
  },
1422
1449
  onConnected: () => {
1423
1450
  this.runtimeCredentials.connected();
@@ -1523,6 +1550,18 @@ export class NodeConnector {
1523
1550
  service: this.agentNetworkService,
1524
1551
  observations: this.agentNetworkObservations
1525
1552
  }));
1553
+ this.operationRouter.register('desktop.describe', async () => {
1554
+ const runtime = this.desktopRuntime;
1555
+ if (!runtime)
1556
+ throw new RemoteDesktopError('DESKTOP_UNAVAILABLE');
1557
+ if (runtime instanceof WindowsRemoteDesktopRuntime ||
1558
+ runtime instanceof MacosRemoteDesktopRuntime)
1559
+ await runtime.refresh(AbortSignal.timeout(10_000));
1560
+ const description = remoteDesktopDescriptionSchema.safeParse(runtime.describe());
1561
+ if (!description.success)
1562
+ throw new RemoteDesktopError('DESKTOP_TOPOLOGY_UNAVAILABLE');
1563
+ return description.data;
1564
+ });
1526
1565
  this.operationRouter.registerAll(this.promptCompletion.operations());
1527
1566
  for (const operation of CONVERSATION_TEMPLATE_OPERATIONS)
1528
1567
  this.operationRouter.register(operation, (data) => {
@@ -1642,15 +1681,62 @@ export class NodeConnector {
1642
1681
  });
1643
1682
  }
1644
1683
  projectComputerUseCapability(capabilities) {
1645
- return {
1684
+ return this.projectRemoteDesktopCapability({
1646
1685
  ...capabilities,
1647
1686
  computerUse: this.computerUseEnablement.capability()
1687
+ });
1688
+ }
1689
+ projectRemoteDesktopCapability(capabilities) {
1690
+ try {
1691
+ if (this.desktopRuntime && (this.directTransferService || this.relayDesktopService)) {
1692
+ this.desktopRuntime.describe();
1693
+ const transports = [];
1694
+ if (this.directTransferService)
1695
+ transports.push('DIRECT');
1696
+ if (this.relayDesktopService)
1697
+ transports.push('RELAY');
1698
+ return {
1699
+ ...capabilities,
1700
+ remoteDesktop: {
1701
+ apiVersion: 1,
1702
+ available: true,
1703
+ nodeGeneration: this.nodeGeneration,
1704
+ backend: this.desktopRuntime instanceof WindowsRemoteDesktopRuntime
1705
+ ? 'ULTRAVNC'
1706
+ : this.desktopRuntime instanceof MacosRemoteDesktopRuntime
1707
+ ? 'MACVNC'
1708
+ : 'TIGERVNC_X0',
1709
+ supportsView: true,
1710
+ supportsControl: true,
1711
+ supportsMultiDisplay: !(this.desktopRuntime instanceof MacosRemoteDesktopRuntime),
1712
+ supportsUnicodePaste: !(this.desktopRuntime instanceof MacosRemoteDesktopRuntime),
1713
+ transports
1714
+ }
1715
+ };
1716
+ }
1717
+ }
1718
+ catch {
1719
+ // A temporarily unavailable topology is not a usable desktop.
1720
+ }
1721
+ return {
1722
+ ...capabilities,
1723
+ remoteDesktop: {
1724
+ apiVersion: 1,
1725
+ available: false,
1726
+ nodeGeneration: this.nodeGeneration,
1727
+ reasonCode: 'DESKTOP_UNAVAILABLE'
1728
+ }
1648
1729
  };
1649
1730
  }
1650
1731
  start() {
1651
1732
  return this.lifecycle.start();
1652
1733
  }
1653
1734
  async startResources() {
1735
+ if (this.nodeGenerationRetired) {
1736
+ this.nodeGeneration = crypto.randomUUID().replace(/-/g, '');
1737
+ this.nodeGenerationRetired = false;
1738
+ }
1739
+ this.capabilities = this.projectRemoteDesktopCapability(this.capabilities);
1654
1740
  this.workspaceSessionService.resume();
1655
1741
  this.continuousRunCoordinator.resumeScheduling();
1656
1742
  this.sessionMessageService.setAccepting(true);
@@ -1676,16 +1762,132 @@ export class NodeConnector {
1676
1762
  await this.conversationHosting.initialize();
1677
1763
  this.capabilities = await this.conversationHosting.project(this.capabilities);
1678
1764
  this.computerUseEnablement.initialize();
1765
+ await this.initializeDesktopRuntime();
1679
1766
  this.directTransferService ??= await this.createDirectTransferService();
1767
+ this.initializeDesktopRelay();
1768
+ this.capabilities = this.projectRemoteDesktopCapability(this.capabilities);
1680
1769
  await this.terminalManager.warmup();
1681
1770
  this.connectionLifecycle.start();
1682
1771
  this.controlChannel.connect();
1683
1772
  }
1773
+ /** Discover the current process's existing GUI using only packaged native resources. */
1774
+ initializeDesktopRuntime() {
1775
+ if (this.desktopDiscovery)
1776
+ return this.desktopDiscovery;
1777
+ const epoch = this.desktopDiscoveryEpoch;
1778
+ const cancel = new AbortController();
1779
+ this.desktopDiscoveryAbort = cancel;
1780
+ const discovery = this.discoverDesktopRuntime(epoch, cancel.signal).finally(() => {
1781
+ if (this.desktopDiscovery === discovery)
1782
+ this.desktopDiscovery = undefined;
1783
+ if (this.desktopDiscoveryAbort === cancel)
1784
+ this.desktopDiscoveryAbort = undefined;
1785
+ });
1786
+ this.desktopDiscovery = discovery;
1787
+ return discovery;
1788
+ }
1789
+ async discoverDesktopRuntime(epoch, signal) {
1790
+ const display = process.env.DISPLAY;
1791
+ const cancelled = () => signal.aborted || this.stopped || epoch !== this.desktopDiscoveryEpoch;
1792
+ if (cancelled() ||
1793
+ (process.platform !== 'linux' &&
1794
+ process.platform !== 'win32' &&
1795
+ process.platform !== 'darwin'))
1796
+ return;
1797
+ if (process.platform === 'linux' && !display)
1798
+ return;
1799
+ try {
1800
+ const previous = this.desktopRuntime;
1801
+ if (previous) {
1802
+ try {
1803
+ if (previous instanceof WindowsRemoteDesktopRuntime ||
1804
+ previous instanceof MacosRemoteDesktopRuntime)
1805
+ await previous.refresh(signal);
1806
+ previous.describe();
1807
+ return;
1808
+ }
1809
+ catch {
1810
+ // Keep ownership on cleanup failure; a later refresh can retry disposal.
1811
+ await previous.dispose();
1812
+ if (this.desktopRuntime === previous)
1813
+ this.desktopRuntime = undefined;
1814
+ }
1815
+ }
1816
+ if (cancelled())
1817
+ return;
1818
+ const binaries = process.platform === 'win32'
1819
+ ? await windowsManagedDesktopBinaries()
1820
+ : process.platform === 'darwin'
1821
+ ? await macosManagedDesktopBinaries()
1822
+ : await linuxManagedDesktopBinaries();
1823
+ if (!binaries || cancelled())
1824
+ return;
1825
+ const starting = (async () => {
1826
+ let runtime;
1827
+ try {
1828
+ const fence = (code) => {
1829
+ this.directTransferService?.invalidateDesktop(code);
1830
+ this.relayDesktopService?.invalidate(code);
1831
+ };
1832
+ const deadline = AbortSignal.any([signal, AbortSignal.timeout(10_000)]);
1833
+ runtime =
1834
+ process.platform === 'win32'
1835
+ ? await WindowsRemoteDesktopRuntime.start({ binaryPath: binaries.binaryPath, maxPixels: 67_108_864, fence }, deadline)
1836
+ : process.platform === 'darwin'
1837
+ ? await MacosRemoteDesktopRuntime.start({
1838
+ binaryPath: binaries.binaryPath,
1839
+ topologyBinary: 'topologyBinary' in binaries && typeof binaries.topologyBinary === 'string'
1840
+ ? binaries.topologyBinary
1841
+ : '',
1842
+ maxPixels: 67_108_864,
1843
+ fence
1844
+ }, deadline)
1845
+ : await X11RemoteDesktopRuntime.start({
1846
+ ...binaries,
1847
+ topologyBinary: 'topologyBinary' in binaries && typeof binaries.topologyBinary === 'string'
1848
+ ? binaries.topologyBinary
1849
+ : '',
1850
+ display: display,
1851
+ xAuthority: resolve(process.env.XAUTHORITY || resolve(homedir(), '.Xauthority')),
1852
+ maxPixels: 67_108_864,
1853
+ fence
1854
+ }, deadline);
1855
+ }
1856
+ catch (error) {
1857
+ if (cancelled())
1858
+ return;
1859
+ throw error;
1860
+ }
1861
+ if (cancelled() || this.desktopRuntime)
1862
+ await runtime.dispose();
1863
+ else
1864
+ this.desktopRuntime = runtime;
1865
+ })();
1866
+ this.desktopStarting = starting;
1867
+ try {
1868
+ await starting;
1869
+ }
1870
+ finally {
1871
+ if (this.desktopStarting === starting)
1872
+ this.desktopStarting = undefined;
1873
+ }
1874
+ }
1875
+ catch {
1876
+ // No accessible existing GUI or managed backend: advertise unavailable.
1877
+ }
1878
+ }
1684
1879
  async createDirectTransferService() {
1880
+ const nodeGeneration = this.nodeGeneration;
1685
1881
  try {
1686
1882
  return await DirectTransferService.create({
1687
- nodeGeneration: () => this.nodeGeneration,
1883
+ nodeGeneration: () => nodeGeneration,
1688
1884
  send: (type, payload) => this.send(type, payload),
1885
+ desktop: {
1886
+ registry: this.desktopAttachmentRegistry,
1887
+ isCurrent: (command) => this.isCurrentDesktop(command, nodeGeneration),
1888
+ createAttachment: (command, stream, callbacks) => this.createDesktopAttachment(command, stream, callbacks, nodeGeneration),
1889
+ emit: (event) => this.emitDesktopEvent(event, nodeGeneration)
1890
+ },
1689
1891
  terminalManager: this.terminalManager,
1690
1892
  sendTerminalReady: (directSessionId, phase) => this.send('terminal.direct.ready', { directSessionId, ...(phase ? { phase } : {}) }),
1691
1893
  udpPortRange: () => directTransferUdpPortRange(this.config),
@@ -1727,6 +1929,57 @@ export class NodeConnector {
1727
1929
  return undefined;
1728
1930
  }
1729
1931
  }
1932
+ /** Owns both path adapters; a real runtime and capability must independently permit admission. */
1933
+ initializeDesktopRelay() {
1934
+ const config = this.config;
1935
+ if (this.relayDesktopService || !config?.nodeId || !config.credential || this.stopped)
1936
+ return;
1937
+ const nodeId = config.nodeId;
1938
+ const nodeGeneration = this.nodeGeneration;
1939
+ this.relayDesktopService = new RelayDesktopService({
1940
+ registry: this.desktopAttachmentRegistry,
1941
+ nodeId,
1942
+ nodeGeneration: () => nodeGeneration,
1943
+ connect: () => new WebSocket(nodeDesktopRelayEndpoint(config.serverUrl), {
1944
+ maxPayload: 16_384,
1945
+ perMessageDeflate: false
1946
+ }),
1947
+ credential: () => {
1948
+ if (this.config?.nodeId !== nodeId ||
1949
+ this.config.serverUrl !== config.serverUrl ||
1950
+ this.config.credential !== config.credential)
1951
+ throw new RemoteDesktopError('DESKTOP_UNAVAILABLE');
1952
+ return config.credential;
1953
+ },
1954
+ isCurrent: (command) => this.config?.nodeId === nodeId &&
1955
+ this.config.serverUrl === config.serverUrl &&
1956
+ this.config.credential === config.credential &&
1957
+ this.isCurrentDesktop(command, nodeGeneration),
1958
+ createAttachment: (command, stream, callbacks) => this.createDesktopAttachment(command, stream, callbacks, nodeGeneration),
1959
+ emit: (event) => this.emitDesktopEvent(event, nodeGeneration)
1960
+ });
1961
+ }
1962
+ isCurrentDesktop(command, nodeGeneration) {
1963
+ return (!this.stopped &&
1964
+ this.nodeGeneration === nodeGeneration &&
1965
+ command.nodeGeneration === nodeGeneration &&
1966
+ this.capabilities.remoteDesktop?.available === true &&
1967
+ this.capabilities.remoteDesktop.nodeGeneration === nodeGeneration &&
1968
+ this.controlChannel.connected() &&
1969
+ this.desktopRuntime?.isCurrent(command) === true);
1970
+ }
1971
+ createDesktopAttachment(command, stream, callbacks, nodeGeneration) {
1972
+ const runtime = this.desktopRuntime;
1973
+ if (!runtime || !this.isCurrentDesktop(command, nodeGeneration))
1974
+ throw new RemoteDesktopError('DESKTOP_UNAVAILABLE');
1975
+ return runtime.createAttachment(command, stream, callbacks);
1976
+ }
1977
+ emitDesktopEvent(event, nodeGeneration) {
1978
+ if (this.nodeGeneration !== nodeGeneration || !this.controlChannel.connected())
1979
+ return false;
1980
+ this.send(event.type, event);
1981
+ return true;
1982
+ }
1730
1983
  handleDirectTransfer(envelope) {
1731
1984
  const command = {
1732
1985
  type: envelope.type,
@@ -1752,10 +2005,32 @@ export class NodeConnector {
1752
2005
  };
1753
2006
  return this.directTransferService?.handleTerminal(command) ?? false;
1754
2007
  }
2008
+ handleDesktopDirect(envelope) {
2009
+ const command = {
2010
+ ...(typeof envelope.payload === 'object' && envelope.payload !== null
2011
+ ? envelope.payload
2012
+ : {}),
2013
+ type: envelope.type
2014
+ };
2015
+ return this.directTransferService?.handleDesktop(command) ?? false;
2016
+ }
2017
+ handleDesktopRelay(envelope) {
2018
+ const command = {
2019
+ ...(typeof envelope.payload === 'object' && envelope.payload !== null
2020
+ ? envelope.payload
2021
+ : {}),
2022
+ type: envelope.type
2023
+ };
2024
+ return this.relayDesktopService?.handle(command) ?? false;
2025
+ }
1755
2026
  stop(options = {}) {
2027
+ this.desktopDiscoveryEpoch++;
2028
+ this.desktopDiscoveryAbort?.abort();
1756
2029
  return this.lifecycle.stop(options.handoff === true);
1757
2030
  }
1758
2031
  disposeRuntime() {
2032
+ this.nodeGenerationRetired = true;
2033
+ this.controlChannel.muteInbound();
1759
2034
  // Earlier shutdown stages may fail; disposal must still join internal reads before DB release.
1760
2035
  const workspaceSessionDrain = this.workspaceSessionService.stop();
1761
2036
  this.recentRunProjectionService.dispose();
@@ -1797,9 +2072,21 @@ export class NodeConnector {
1797
2072
  this.upgradeCredentials.disconnect();
1798
2073
  const notificationMcpDrain = this.notificationMcpBridge.close();
1799
2074
  const webToolDrain = this.webToolService.close();
1800
- this.directTransferService?.dispose();
2075
+ const desktopShutdown = AbortSignal.timeout(10_000);
2076
+ const directDrain = this.directTransferService?.shutdown(desktopShutdown);
2077
+ const relayDrain = this.relayDesktopService?.shutdown(desktopShutdown);
2078
+ const desktopRuntime = this.desktopRuntime;
1801
2079
  this.directTransferService = undefined;
1802
- this.controlChannel.stop();
2080
+ this.relayDesktopService = undefined;
2081
+ this.desktopRuntime = undefined;
2082
+ const controlDrain = Promise.allSettled([directDrain, relayDrain, this.desktopStarting])
2083
+ .then(async (results) => {
2084
+ const runtime = await Promise.allSettled([desktopRuntime?.dispose()]);
2085
+ const failure = [...results, ...runtime].find((result) => result.status === 'rejected');
2086
+ if (failure)
2087
+ throw failure.reason;
2088
+ })
2089
+ .finally(() => this.controlChannel.stop());
1803
2090
  const database = this.database;
1804
2091
  const drains = Promise.allSettled([
1805
2092
  workspaceSessionDrain,
@@ -1811,7 +2098,8 @@ export class NodeConnector {
1811
2098
  goalMcpDrain,
1812
2099
  scheduleMcpDrain,
1813
2100
  notificationMcpDrain,
1814
- webToolDrain
2101
+ webToolDrain,
2102
+ controlDrain
1815
2103
  ]);
1816
2104
  return (async () => {
1817
2105
  const hosting = await Promise.allSettled([conversationHostingDrain]);
@@ -1,6 +1,7 @@
1
1
  import { chmod } from 'node:fs/promises';
2
2
  import { dirname, resolve } from 'node:path';
3
3
  import { createRequire } from 'node:module';
4
+ import { managedBinariesPlatformDirectory } from './managed-binaries.js';
4
5
  export const BUNDLED_CTAGS_VERSION = '6.2.0-ab95af1';
5
6
  const TARGETS = {
6
7
  'linux-x64': ['linux-x64', 'ctags'],
@@ -18,7 +19,10 @@ export function bundledCtagsPath(platform = process.platform, architecture = pro
18
19
  const target = TARGETS[`${platform}-${architecture}`];
19
20
  if (target === undefined)
20
21
  throw new Error('CTAGS_PLATFORM_UNSUPPORTED');
21
- return resolve(bundledCtagsDirectory(packageRoot), ...target);
22
+ const directory = packageRoot === undefined
23
+ ? resolve(managedBinariesPlatformDirectory(platform, architecture), 'vendor/ctags')
24
+ : bundledCtagsDirectory(packageRoot);
25
+ return resolve(directory, ...target);
22
26
  }
23
27
  /**
24
28
  * Restores the execute bit when an installation mechanism lost the bundled
@@ -0,0 +1,8 @@
1
+ import { createRequire } from 'node:module';
2
+ import { dirname } from 'node:path';
3
+ /** Resolve platform assets from the unified binaries package's own dependencies. */
4
+ export function managedBinariesPlatformDirectory(platform, architecture) {
5
+ const require = createRequire(import.meta.url);
6
+ const entry = require.resolve('@myagentroam/binaries/package.json');
7
+ return dirname(createRequire(entry).resolve(`@myagentroam/binaries-${platform}-${architecture}/package.json`));
8
+ }
@@ -1 +1 @@
1
- {"type":"module","version":"0.13.11"}
1
+ {"type":"module","version":"0.13.13"}