@myagentroam/node 0.13.11 → 0.13.12
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.
- package/dist/connection/direct-transfer-service.js +60 -0
- package/dist/connection/endpoints.js +16 -0
- package/dist/connector.js +298 -10
- package/dist/ctags-binary.js +5 -1
- package/dist/managed-binaries.js +8 -0
- package/dist/marctl/package.json +1 -1
- package/dist/remote-desktop/desktop-byte-stream.js +128 -0
- package/dist/remote-desktop/desktop-error.js +35 -0
- package/dist/remote-desktop/desktop-service-core.js +384 -0
- package/dist/remote-desktop/direct-desktop-service.js +46 -0
- package/dist/remote-desktop/linux-managed-binaries.js +46 -0
- package/dist/remote-desktop/macos-managed-binaries.js +19 -0
- package/dist/remote-desktop/macos-remote-desktop-runtime.js +121 -0
- package/dist/remote-desktop/macvnc-managed-process.js +196 -0
- package/dist/remote-desktop/pooled-desktop-attachment.js +62 -0
- package/dist/remote-desktop/relay-desktop-service.js +40 -0
- package/dist/remote-desktop/remote-desktop-attachment.js +87 -0
- package/dist/remote-desktop/rfb-backend-handshake.js +98 -0
- package/dist/remote-desktop/rfb-client-gate.js +318 -0
- package/dist/remote-desktop/rfb-frontend-handshake.js +38 -0
- package/dist/remote-desktop/rfb-handshake-io.js +91 -0
- package/dist/remote-desktop/rfb-input-release.js +77 -0
- package/dist/remote-desktop/standard-rfb-attachment.js +40 -0
- package/dist/remote-desktop/tigervnc-attachment.js +68 -0
- package/dist/remote-desktop/tigervnc-backend-pool.js +88 -0
- package/dist/remote-desktop/tigervnc-cleanup-channel.js +129 -0
- package/dist/remote-desktop/tigervnc-managed-process.js +274 -0
- package/dist/remote-desktop/tigervnc-rfb-session.js +190 -0
- package/dist/remote-desktop/ultravnc-config.js +35 -0
- package/dist/remote-desktop/ultravnc-managed-process.js +180 -0
- package/dist/remote-desktop/vnc-password.js +15 -0
- package/dist/remote-desktop/windows-managed-binaries.js +18 -0
- package/dist/remote-desktop/windows-remote-desktop-runtime.js +126 -0
- package/dist/remote-desktop/x11-desktop-topology.js +82 -0
- package/dist/remote-desktop/x11-remote-desktop-runtime.js +99 -0
- package/dist/remote-desktop/x11-topology-source.js +205 -0
- package/dist/remote-desktop/x11-topology.js +88 -0
- package/dist/ripgrep-binary.js +5 -1
- package/dist/service/node-control-channel.js +7 -1
- package/dist/service/node-control-message-service.js +11 -0
- package/package.json +6 -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
|
|
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
|
-
|
|
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: () =>
|
|
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
|
-
|
|
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.
|
|
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]);
|
package/dist/ctags-binary.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/dist/marctl/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"type":"module","version":"0.13.
|
|
1
|
+
{"type":"module","version":"0.13.12"}
|