@livedesk/hub 0.1.49 → 0.1.50
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/README.md +1 -1
- package/package.json +4 -4
- package/src/agents/agent-manager.js +2 -2
- package/src/agents/agent-permissions.js +2 -2
- package/src/agents/agent-tool-registry.js +36 -36
- package/src/agents/codex-agent-runtime.js +16 -16
- package/src/agents/codex-mcp-server.js +5 -5
- package/src/filesystem/roots.js +20 -20
- package/src/filesystem/shared-folders.js +2 -2
- package/src/live-desk-update.js +9 -9
- package/src/remote-clipboard-contract.mjs +482 -482
- package/src/remote-hub.js +1025 -1025
- package/src/server.js +146 -146
- package/src/settings/effective-device-policy.js +31 -31
- package/src/settings/settings-schema.js +2 -2
package/src/server.js
CHANGED
|
@@ -8,12 +8,12 @@ import { dirname, resolve } from 'node:path';
|
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import os from 'node:os';
|
|
10
10
|
import { WebSocketServer } from 'ws';
|
|
11
|
-
import { createRemoteHub } from './remote-hub.js';
|
|
12
|
-
import {
|
|
13
|
-
REMOTE_CLIPBOARD_INPUT_ACTIONS,
|
|
14
|
-
RemoteClipboardContractError,
|
|
15
|
-
normalizeRemoteClipboardRequest
|
|
16
|
-
} from './remote-clipboard-contract.mjs';
|
|
11
|
+
import { createRemoteHub } from './remote-hub.js';
|
|
12
|
+
import {
|
|
13
|
+
REMOTE_CLIPBOARD_INPUT_ACTIONS,
|
|
14
|
+
RemoteClipboardContractError,
|
|
15
|
+
normalizeRemoteClipboardRequest
|
|
16
|
+
} from './remote-clipboard-contract.mjs';
|
|
17
17
|
import { createHubConsoleRelay } from './console-relay.js';
|
|
18
18
|
import {
|
|
19
19
|
buildImmutableRemoteFramePacket,
|
|
@@ -151,7 +151,7 @@ const atlasPool = new Mode4AtlasPool({
|
|
|
151
151
|
onStatus: status => {
|
|
152
152
|
if (status?.type !== 'log') return;
|
|
153
153
|
const logger = status.level === 'warn' ? console.warn : console.log;
|
|
154
|
-
logger(`[
|
|
154
|
+
logger(`[VuvoDesk Hub] ${status.message}`);
|
|
155
155
|
}
|
|
156
156
|
});
|
|
157
157
|
const inputClients = new Set();
|
|
@@ -266,7 +266,7 @@ function handleRemoteHubEvent(type, event) {
|
|
|
266
266
|
hubTransferJobs?.handleRemoteEvent(type, event);
|
|
267
267
|
if (traceRemoteTestEventsEnabled
|
|
268
268
|
&& (type === 'RemoteFrameDropped' || type === 'RemoteFrameTransportProofAccepted')) {
|
|
269
|
-
console.warn(`[
|
|
269
|
+
console.warn(`[VuvoDesk Hub Test Event] ${type} ${JSON.stringify({
|
|
270
270
|
reason: event?.reason || '',
|
|
271
271
|
deviceId: event?.deviceId || event?.device?.deviceId || '',
|
|
272
272
|
proofOnly: event?.proofOnly === true,
|
|
@@ -572,7 +572,7 @@ async function getRuntimeAccessToken() {
|
|
|
572
572
|
expires_at: Math.floor(runtimeAccessTokenExpiresAt / 1000)
|
|
573
573
|
});
|
|
574
574
|
} catch (error) {
|
|
575
|
-
console.warn(`[
|
|
575
|
+
console.warn(`[VuvoDesk Hub] Refreshed session persistence failed: ${error?.message || error}`);
|
|
576
576
|
}
|
|
577
577
|
return runtimeAccessToken;
|
|
578
578
|
}
|
|
@@ -658,7 +658,7 @@ async function watchAuthoritativeRuntimeRole() {
|
|
|
658
658
|
try {
|
|
659
659
|
const role = await queryAuthoritativeRuntimeRole();
|
|
660
660
|
if (role === 'client') {
|
|
661
|
-
console.warn('[
|
|
661
|
+
console.warn('[VuvoDesk Hub] Supabase selected another Sync Server. Transitioning this runtime to Client.');
|
|
662
662
|
void clearHubHostTarget('role-demoted').finally(() => {
|
|
663
663
|
setTimeout(() => process.exit(ROLE_TRANSITION_EXIT_CODE), 150);
|
|
664
664
|
});
|
|
@@ -692,16 +692,16 @@ const agentDataDir = process.env.LIVEDESK_DATA_DIR || undefined;
|
|
|
692
692
|
const liveDeskSettingsStore = new LiveDeskSettingsStore({ dataDir: agentDataDir });
|
|
693
693
|
const captureStore = new CaptureStore({ dataDir: agentDataDir });
|
|
694
694
|
void captureStore.initialize().catch(error => {
|
|
695
|
-
console.warn(`[
|
|
695
|
+
console.warn(`[VuvoDesk Hub] capture store initialization failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
696
696
|
});
|
|
697
697
|
void liveDeskSettingsStore.getRecord().catch(error => {
|
|
698
|
-
console.warn(`[
|
|
698
|
+
console.warn(`[VuvoDesk Hub] settings load failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
699
699
|
});
|
|
700
700
|
|
|
701
701
|
const udpTransport = createHubUdpTransport({
|
|
702
702
|
env: process.env,
|
|
703
|
-
logEvent: (_scope, message) => console.log(`[
|
|
704
|
-
logWarn: (_scope, message) => console.warn(`[
|
|
703
|
+
logEvent: (_scope, message) => console.log(`[VuvoDesk Hub] ${message}`),
|
|
704
|
+
logWarn: (_scope, message) => console.warn(`[VuvoDesk Hub] ${message}`)
|
|
705
705
|
});
|
|
706
706
|
|
|
707
707
|
const remoteHub = createRemoteHub({
|
|
@@ -713,8 +713,8 @@ const remoteHub = createRemoteHub({
|
|
|
713
713
|
MINDEXEC_MANAGER_VERSION: packageInfo.version
|
|
714
714
|
},
|
|
715
715
|
udpTransport,
|
|
716
|
-
logEvent: (_scope, message) => console.log(`[
|
|
717
|
-
logWarn: (_scope, message) => console.warn(`[
|
|
716
|
+
logEvent: (_scope, message) => console.log(`[VuvoDesk Hub] ${message}`),
|
|
717
|
+
logWarn: (_scope, message) => console.warn(`[VuvoDesk Hub] ${message}`),
|
|
718
718
|
emitEvent: handleRemoteHubEvent,
|
|
719
719
|
emitFrame: broadcastRemoteBinaryFrame,
|
|
720
720
|
emitAudio: broadcastRemoteBinaryAudio,
|
|
@@ -870,7 +870,7 @@ if (process.env.LIVEDESK_UPDATE_CONTINUE === '1') {
|
|
|
870
870
|
clearInterval(continueUpdateTimer);
|
|
871
871
|
}
|
|
872
872
|
} catch (error) {
|
|
873
|
-
console.warn(`[
|
|
873
|
+
console.warn(`[VuvoDesk Hub] automatic client update continuation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
874
874
|
clearInterval(continueUpdateTimer);
|
|
875
875
|
} finally {
|
|
876
876
|
continueUpdateInFlight = false;
|
|
@@ -1167,11 +1167,11 @@ async function dispatchAgentMcpTool(session, name, args = {}) {
|
|
|
1167
1167
|
'livedesk.close_application': 'application.close',
|
|
1168
1168
|
'livedesk.read_file': 'file.read',
|
|
1169
1169
|
'livedesk.write_file': 'file.write',
|
|
1170
|
-
'livedesk.delete_file': 'file.delete',
|
|
1171
|
-
'livedesk.list_directory': 'file.list',
|
|
1172
|
-
'livedesk.search_files': 'file.search',
|
|
1173
|
-
'livedesk.create_directory': 'directory.create',
|
|
1174
|
-
'livedesk.run_command': 'command.run',
|
|
1170
|
+
'livedesk.delete_file': 'file.delete',
|
|
1171
|
+
'livedesk.list_directory': 'file.list',
|
|
1172
|
+
'livedesk.search_files': 'file.search',
|
|
1173
|
+
'livedesk.create_directory': 'directory.create',
|
|
1174
|
+
'livedesk.run_command': 'command.run',
|
|
1175
1175
|
'livedesk.run_script': 'script.run',
|
|
1176
1176
|
'livedesk.install_software': 'software.install',
|
|
1177
1177
|
'livedesk.get_network_status': 'network.status',
|
|
@@ -1255,8 +1255,8 @@ async function dispatchAgentMcpTool(session, name, args = {}) {
|
|
|
1255
1255
|
? String(args.serviceName || '').replace(/[\0\r\n]/g, ' ').trim().slice(0, 120)
|
|
1256
1256
|
: '';
|
|
1257
1257
|
const result = remoteHub.requestAgentTaskBatch(targetIds, {
|
|
1258
|
-
instruction: `${tool.readOnly ? `
|
|
1259
|
-
title: `
|
|
1258
|
+
instruction: `${tool.readOnly ? `VuvoDesk read-only ${operation}` : `VuvoDesk ${name}`}${targetQuery ? ` for ${targetQuery}` : ''}. Return only the collected result.`,
|
|
1259
|
+
title: `VuvoDesk ${operation}`,
|
|
1260
1260
|
operation,
|
|
1261
1261
|
targetQuery,
|
|
1262
1262
|
approvalLevel: tool.readOnly ? 'read-only' : 'task-only',
|
|
@@ -1330,7 +1330,7 @@ const codexRuntime = createCodexAgentRuntime({
|
|
|
1330
1330
|
const detail = ['failed', 'recovering'].includes(progress.stage) && progress.message
|
|
1331
1331
|
? ` detail=${JSON.stringify(String(progress.message).slice(0, 800))}`
|
|
1332
1332
|
: '';
|
|
1333
|
-
console.log(`[
|
|
1333
|
+
console.log(`[VuvoDesk Hub] Agent ${progress.stage} run=${progress.runId}${tool} clients=${delivery.sent}/${delivery.total}${detail}`);
|
|
1334
1334
|
}
|
|
1335
1335
|
});
|
|
1336
1336
|
const agentManager = createAgentManager({
|
|
@@ -1761,40 +1761,40 @@ function normalizeLiveOptions(payload = {}) {
|
|
|
1761
1761
|
};
|
|
1762
1762
|
}
|
|
1763
1763
|
|
|
1764
|
-
function sendJson(ws, payload) {
|
|
1764
|
+
function sendJson(ws, payload) {
|
|
1765
1765
|
if (!ws || ws.readyState !== 1) {
|
|
1766
1766
|
return false;
|
|
1767
1767
|
}
|
|
1768
1768
|
return safeWebSocketSend(ws, JSON.stringify(payload));
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
function findLiveRemoteInputClient(hubConnectionId) {
|
|
1772
|
-
const requestedId = String(hubConnectionId || '').trim();
|
|
1773
|
-
if (!requestedId) {
|
|
1774
|
-
return null;
|
|
1775
|
-
}
|
|
1776
|
-
for (const ws of inputClients) {
|
|
1777
|
-
if (ws.readyState === 1 && ws.liveDeskInputClientId === requestedId) {
|
|
1778
|
-
return ws;
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
return null;
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
function clipboardHttpStatus(error) {
|
|
1785
|
-
const code = String(error || '');
|
|
1786
|
-
if (code.includes('blocked-by-settings') || code === 'remote-access-blocked-by-settings') return 403;
|
|
1787
|
-
if (code.includes('too-large')) return 413;
|
|
1788
|
-
if (code.includes('unavailable') || code === 'device-not-connected') return 503;
|
|
1789
|
-
if (code.startsWith('STALE_')
|
|
1790
|
-
|| code.includes('owner')
|
|
1791
|
-
|| code.includes('not-ready')
|
|
1792
|
-
|| code.includes('not-found')
|
|
1793
|
-
|| code.includes('already-exists')
|
|
1794
|
-
|| code.includes('busy')
|
|
1795
|
-
|| code.includes('capacity')) return 409;
|
|
1796
|
-
return 400;
|
|
1797
|
-
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
function findLiveRemoteInputClient(hubConnectionId) {
|
|
1772
|
+
const requestedId = String(hubConnectionId || '').trim();
|
|
1773
|
+
if (!requestedId) {
|
|
1774
|
+
return null;
|
|
1775
|
+
}
|
|
1776
|
+
for (const ws of inputClients) {
|
|
1777
|
+
if (ws.readyState === 1 && ws.liveDeskInputClientId === requestedId) {
|
|
1778
|
+
return ws;
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return null;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
function clipboardHttpStatus(error) {
|
|
1785
|
+
const code = String(error || '');
|
|
1786
|
+
if (code.includes('blocked-by-settings') || code === 'remote-access-blocked-by-settings') return 403;
|
|
1787
|
+
if (code.includes('too-large')) return 413;
|
|
1788
|
+
if (code.includes('unavailable') || code === 'device-not-connected') return 503;
|
|
1789
|
+
if (code.startsWith('STALE_')
|
|
1790
|
+
|| code.includes('owner')
|
|
1791
|
+
|| code.includes('not-ready')
|
|
1792
|
+
|| code.includes('not-found')
|
|
1793
|
+
|| code.includes('already-exists')
|
|
1794
|
+
|| code.includes('busy')
|
|
1795
|
+
|| code.includes('capacity')) return 409;
|
|
1796
|
+
return 400;
|
|
1797
|
+
}
|
|
1798
1798
|
|
|
1799
1799
|
function forgetWebSocketClient(ws) {
|
|
1800
1800
|
unregisterFrameClient(ws);
|
|
@@ -1931,12 +1931,12 @@ function scheduleFrameStreamStop(deviceId, streamId, streamPurpose) {
|
|
|
1931
1931
|
if (result?.stopPromise) {
|
|
1932
1932
|
void result.stopPromise.then(confirmation => {
|
|
1933
1933
|
if (confirmation?.captureStopConfirmed === true) {
|
|
1934
|
-
console.log(`[
|
|
1934
|
+
console.log(`[VuvoDesk Hub] confirmed ${streamPurpose} capture stopped after frame client closed device=${deviceId} stream=${streamId}`);
|
|
1935
1935
|
return;
|
|
1936
1936
|
}
|
|
1937
1937
|
if (!hasOtherFrameStreamOwner(null, deviceId, streamId, streamPurpose)) {
|
|
1938
1938
|
console.warn(
|
|
1939
|
-
`[
|
|
1939
|
+
`[VuvoDesk Hub] ${streamPurpose} capture stop was not confirmed device=${deviceId} `
|
|
1940
1940
|
+ `stream=${streamId}: ${confirmation?.error || 'capture-stop-unconfirmed'}. `
|
|
1941
1941
|
+ 'Disconnecting the Agent so its final capture drain runs before reconnect.'
|
|
1942
1942
|
);
|
|
@@ -1944,12 +1944,12 @@ function scheduleFrameStreamStop(deviceId, streamId, streamPurpose) {
|
|
|
1944
1944
|
}
|
|
1945
1945
|
}).catch(error => {
|
|
1946
1946
|
if (!hasOtherFrameStreamOwner(null, deviceId, streamId, streamPurpose)) {
|
|
1947
|
-
console.warn(`[
|
|
1947
|
+
console.warn(`[VuvoDesk Hub] ${streamPurpose} capture stop confirmation failed device=${deviceId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1948
1948
|
remoteHub.disconnectDevice(deviceId, 'frame-capture-stop-confirmation-failed');
|
|
1949
1949
|
}
|
|
1950
1950
|
});
|
|
1951
1951
|
} else if (result?.ok && !result.alreadyStopped) {
|
|
1952
|
-
console.log(`[
|
|
1952
|
+
console.log(`[VuvoDesk Hub] queued ${streamPurpose} capture stop after frame client closed device=${deviceId} stream=${streamId}`);
|
|
1953
1953
|
}
|
|
1954
1954
|
}, frameStreamStopGraceMs);
|
|
1955
1955
|
pendingFrameStreamStops.set(key, { timer });
|
|
@@ -3748,11 +3748,11 @@ function stopUnsubscribedAudioOwners(owners, reason) {
|
|
|
3748
3748
|
disconnect: (targetDeviceId, disconnectReason) => (
|
|
3749
3749
|
remoteHub.disconnectDevice(targetDeviceId, disconnectReason)
|
|
3750
3750
|
),
|
|
3751
|
-
warn: message => console.warn(`[
|
|
3751
|
+
warn: message => console.warn(`[VuvoDesk Hub] ${message}. Disconnecting the Agent for a final drain.`)
|
|
3752
3752
|
});
|
|
3753
3753
|
if (result?.ok === false
|
|
3754
3754
|
&& !['device-not-found', 'device-not-connected', 'device-audio-unavailable'].includes(String(result.error || ''))) {
|
|
3755
|
-
console.warn(`[
|
|
3755
|
+
console.warn(`[VuvoDesk Hub] Could not stop unused remote audio capture device=${deviceId} reason=${reason}: ${result.error || 'unknown-error'}`);
|
|
3756
3756
|
}
|
|
3757
3757
|
}
|
|
3758
3758
|
}
|
|
@@ -3860,7 +3860,7 @@ function buildHubHealthPayload({
|
|
|
3860
3860
|
};
|
|
3861
3861
|
return {
|
|
3862
3862
|
ok: true,
|
|
3863
|
-
product: 'LiveDesk',
|
|
3863
|
+
product: 'LiveDesk',
|
|
3864
3864
|
timestamp: new Date().toISOString(),
|
|
3865
3865
|
persistentSessionGc,
|
|
3866
3866
|
memoryBeforeGc,
|
|
@@ -4323,7 +4323,7 @@ app.get('/api/remote/status', (_req, res) => {
|
|
|
4323
4323
|
res.json({
|
|
4324
4324
|
...remoteHub.getStatus({ includeSecrets: false }),
|
|
4325
4325
|
pairingPin: secretStatus.pairingPin,
|
|
4326
|
-
product: 'LiveDesk',
|
|
4326
|
+
product: 'LiveDesk',
|
|
4327
4327
|
runtimeRole,
|
|
4328
4328
|
deviceId: runtimeDeviceId,
|
|
4329
4329
|
deviceName: runtimeDeviceName,
|
|
@@ -4653,7 +4653,7 @@ async function notifyHubOnline({ accessToken, target, remoteStatus, endpointCand
|
|
|
4653
4653
|
lastEventId: eventId,
|
|
4654
4654
|
nextAttemptAt: ''
|
|
4655
4655
|
};
|
|
4656
|
-
console.log(`[
|
|
4656
|
+
console.log(`[VuvoDesk Hub] Hub-online wake signal sent event=${eventId.slice(0, 8)} delivered=${Number(data?.delivered || 0)}.`);
|
|
4657
4657
|
return { ok: true, eventId, delivered: Number(data?.delivered || 0) };
|
|
4658
4658
|
} catch (error) {
|
|
4659
4659
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -4664,7 +4664,7 @@ async function notifyHubOnline({ accessToken, target, remoteStatus, endpointCand
|
|
|
4664
4664
|
lastError: message,
|
|
4665
4665
|
nextAttemptAt: new Date(hubWakeNextAttemptAtMs).toISOString()
|
|
4666
4666
|
};
|
|
4667
|
-
console.warn(`[
|
|
4667
|
+
console.warn(`[VuvoDesk Hub] Hub-online wake signal failed; Supabase target remains authoritative: ${message}`);
|
|
4668
4668
|
return { ok: false, error: message, eventId };
|
|
4669
4669
|
}
|
|
4670
4670
|
}
|
|
@@ -4714,7 +4714,7 @@ async function promoteCurrentRuntimeToHub() {
|
|
|
4714
4714
|
p_assigned_hub_id: null,
|
|
4715
4715
|
p_expected_role_version: null
|
|
4716
4716
|
}, accessToken);
|
|
4717
|
-
console.log(`[
|
|
4717
|
+
console.log(`[VuvoDesk Hub] Sync Server role confirmed for ${runtimeDeviceId}.`);
|
|
4718
4718
|
return result;
|
|
4719
4719
|
}
|
|
4720
4720
|
|
|
@@ -4784,12 +4784,12 @@ async function publishHubHostTarget({ takeover = false, reason = 'renewal' } = {
|
|
|
4784
4784
|
lastPublishedEndpoint: target.endpoint || remoteStatus.agentEndpoint || '',
|
|
4785
4785
|
lastExpiresAt: target.expiresAt || ''
|
|
4786
4786
|
});
|
|
4787
|
-
console.log(`[
|
|
4787
|
+
console.log(`[VuvoDesk Hub] Host target ${reason} published endpoint=${target.endpoint || remoteStatus.agentEndpoint} expires=${target.expiresAt || ''}.`);
|
|
4788
4788
|
return { ok: true, active: true, hostTarget: target, lease: getHubHostTargetLeaseStatus() };
|
|
4789
4789
|
} catch (error) {
|
|
4790
4790
|
const message = error instanceof Error ? error.message : String(error);
|
|
4791
4791
|
updateHubHostTargetLeaseState({ state: 'error', lastError: message });
|
|
4792
|
-
console.error(`[
|
|
4792
|
+
console.error(`[VuvoDesk Hub] Host target ${reason} publish failed: ${message}`);
|
|
4793
4793
|
return { ok: false, active: false, error: message, lease: getHubHostTargetLeaseStatus() };
|
|
4794
4794
|
} finally {
|
|
4795
4795
|
hubHostTargetRenewInFlight = false;
|
|
@@ -4833,7 +4833,7 @@ async function clearHubHostTarget(reason = 'shutdown') {
|
|
|
4833
4833
|
}
|
|
4834
4834
|
} catch (cause) {
|
|
4835
4835
|
error = cause instanceof Error ? cause.message : String(cause);
|
|
4836
|
-
console.error(`[
|
|
4836
|
+
console.error(`[VuvoDesk Hub] Host target ${reason} clear failed: ${error}`);
|
|
4837
4837
|
}
|
|
4838
4838
|
try {
|
|
4839
4839
|
await remoteHub.setHostTarget({ enabled: false, nodeId: targetNodeId });
|
|
@@ -4848,7 +4848,7 @@ async function clearHubHostTarget(reason = 'shutdown') {
|
|
|
4848
4848
|
lastExpiresAt: ''
|
|
4849
4849
|
});
|
|
4850
4850
|
if (!error) {
|
|
4851
|
-
console.log(`[
|
|
4851
|
+
console.log(`[VuvoDesk Hub] Host target cleared (${reason}).`);
|
|
4852
4852
|
}
|
|
4853
4853
|
return { ok: !error, error, lease: getHubHostTargetLeaseStatus() };
|
|
4854
4854
|
}
|
|
@@ -4869,7 +4869,7 @@ function scheduleAuthenticatedHubHostTargetPublication(reason = 'session-receive
|
|
|
4869
4869
|
void publishHubHostTargetWithPendingRoleTakeover(reason).catch(error => {
|
|
4870
4870
|
const message = error instanceof Error ? error.message : String(error);
|
|
4871
4871
|
updateHubHostTargetLeaseState({ state: 'error', lastError: message });
|
|
4872
|
-
console.error(`[
|
|
4872
|
+
console.error(`[VuvoDesk Hub] Host target ${reason} background publish failed: ${message}`);
|
|
4873
4873
|
});
|
|
4874
4874
|
});
|
|
4875
4875
|
}
|
|
@@ -4923,7 +4923,7 @@ app.post('/api/hub/sync', async (req, res) => {
|
|
|
4923
4923
|
} catch (error) {
|
|
4924
4924
|
const message = error instanceof Error ? error.message : String(error);
|
|
4925
4925
|
updateHubHostTargetLeaseState({ state: 'error', lastError: message, lastReason: 'sync-server' });
|
|
4926
|
-
console.error(`[
|
|
4926
|
+
console.error(`[VuvoDesk Hub] Sync Server failed: ${message}`);
|
|
4927
4927
|
res.status(409).json({ ok: false, error: message, lease: getHubHostTargetLeaseStatus() });
|
|
4928
4928
|
}
|
|
4929
4929
|
});
|
|
@@ -5184,64 +5184,64 @@ app.post('/api/remote/devices/:deviceId/status-diagnostic', (req, res) => {
|
|
|
5184
5184
|
}));
|
|
5185
5185
|
});
|
|
5186
5186
|
|
|
5187
|
-
app.post('/api/remote/devices/:deviceId/input', requireHubFeatureAccess, (req, res) => {
|
|
5188
|
-
noStore(res);
|
|
5189
|
-
res.json(remoteHub.sendInputControl(req.params.deviceId, req.body || {}));
|
|
5190
|
-
});
|
|
5191
|
-
|
|
5192
|
-
app.post('/api/remote/devices/:deviceId/clipboard', requireHubFeatureAccess, async (req, res) => {
|
|
5193
|
-
noStore(res);
|
|
5194
|
-
try {
|
|
5195
|
-
const request = normalizeRemoteClipboardRequest(req.body || {});
|
|
5196
|
-
const inputClient = findLiveRemoteInputClient(request.hubConnectionId);
|
|
5197
|
-
if (!inputClient) {
|
|
5198
|
-
res.status(409).json({ ok: false, error: 'clipboard-input-owner-not-live' });
|
|
5199
|
-
return;
|
|
5200
|
-
}
|
|
5201
|
-
const deviceId = String(req.params.deviceId || '').trim();
|
|
5202
|
-
inputClient.liveDeskInputDeviceIds?.add?.(deviceId);
|
|
5203
|
-
|
|
5204
|
-
if (REMOTE_CLIPBOARD_INPUT_ACTIONS.has(request.action)) {
|
|
5205
|
-
const result = remoteHub.sendInputControl(deviceId, {
|
|
5206
|
-
type: request.action,
|
|
5207
|
-
clipboardOperationId: request.operationId,
|
|
5208
|
-
operationId: request.operationId,
|
|
5209
|
-
clipboardDirection: request.clipboardDirection,
|
|
5210
|
-
manifest: request.payload.manifest || null,
|
|
5211
|
-
contentKind: request.payload.contentKind || request.payload.manifest?.contentKind || '',
|
|
5212
|
-
controlSessionId: request.binding.controlSessionId,
|
|
5213
|
-
controlCommandId: request.binding.controlCommandId,
|
|
5214
|
-
captureGeneration: request.binding.captureGeneration,
|
|
5215
|
-
monitorIndex: request.binding.monitorIndex,
|
|
5216
|
-
hubConnectionId: request.hubConnectionId,
|
|
5217
|
-
requestAck: true
|
|
5218
|
-
});
|
|
5219
|
-
res.status(result.ok ? 200 : clipboardHttpStatus(result.error)).json({
|
|
5220
|
-
ok: result.ok === true,
|
|
5221
|
-
action: request.action,
|
|
5222
|
-
operationId: request.operationId,
|
|
5223
|
-
input: result,
|
|
5224
|
-
...(result.ok ? {} : { error: result.error || 'clipboard-input-failed' })
|
|
5225
|
-
});
|
|
5226
|
-
return;
|
|
5227
|
-
}
|
|
5228
|
-
|
|
5229
|
-
const result = await remoteHub.sendClipboardCommand(deviceId, request);
|
|
5230
|
-
res.status(result.ok ? 200 : clipboardHttpStatus(result.error)).json(result);
|
|
5231
|
-
} catch (error) {
|
|
5232
|
-
const status = error instanceof RemoteClipboardContractError
|
|
5233
|
-
? error.status
|
|
5234
|
-
: 500;
|
|
5235
|
-
res.status(status).json({
|
|
5236
|
-
ok: false,
|
|
5237
|
-
error: error instanceof RemoteClipboardContractError
|
|
5238
|
-
? error.code
|
|
5239
|
-
: 'clipboard-request-failed'
|
|
5240
|
-
});
|
|
5241
|
-
}
|
|
5242
|
-
});
|
|
5243
|
-
|
|
5244
|
-
app.get('/api/remote/filesystem/roots', requireHubFeatureAccess, async (req, res) => {
|
|
5187
|
+
app.post('/api/remote/devices/:deviceId/input', requireHubFeatureAccess, (req, res) => {
|
|
5188
|
+
noStore(res);
|
|
5189
|
+
res.json(remoteHub.sendInputControl(req.params.deviceId, req.body || {}));
|
|
5190
|
+
});
|
|
5191
|
+
|
|
5192
|
+
app.post('/api/remote/devices/:deviceId/clipboard', requireHubFeatureAccess, async (req, res) => {
|
|
5193
|
+
noStore(res);
|
|
5194
|
+
try {
|
|
5195
|
+
const request = normalizeRemoteClipboardRequest(req.body || {});
|
|
5196
|
+
const inputClient = findLiveRemoteInputClient(request.hubConnectionId);
|
|
5197
|
+
if (!inputClient) {
|
|
5198
|
+
res.status(409).json({ ok: false, error: 'clipboard-input-owner-not-live' });
|
|
5199
|
+
return;
|
|
5200
|
+
}
|
|
5201
|
+
const deviceId = String(req.params.deviceId || '').trim();
|
|
5202
|
+
inputClient.liveDeskInputDeviceIds?.add?.(deviceId);
|
|
5203
|
+
|
|
5204
|
+
if (REMOTE_CLIPBOARD_INPUT_ACTIONS.has(request.action)) {
|
|
5205
|
+
const result = remoteHub.sendInputControl(deviceId, {
|
|
5206
|
+
type: request.action,
|
|
5207
|
+
clipboardOperationId: request.operationId,
|
|
5208
|
+
operationId: request.operationId,
|
|
5209
|
+
clipboardDirection: request.clipboardDirection,
|
|
5210
|
+
manifest: request.payload.manifest || null,
|
|
5211
|
+
contentKind: request.payload.contentKind || request.payload.manifest?.contentKind || '',
|
|
5212
|
+
controlSessionId: request.binding.controlSessionId,
|
|
5213
|
+
controlCommandId: request.binding.controlCommandId,
|
|
5214
|
+
captureGeneration: request.binding.captureGeneration,
|
|
5215
|
+
monitorIndex: request.binding.monitorIndex,
|
|
5216
|
+
hubConnectionId: request.hubConnectionId,
|
|
5217
|
+
requestAck: true
|
|
5218
|
+
});
|
|
5219
|
+
res.status(result.ok ? 200 : clipboardHttpStatus(result.error)).json({
|
|
5220
|
+
ok: result.ok === true,
|
|
5221
|
+
action: request.action,
|
|
5222
|
+
operationId: request.operationId,
|
|
5223
|
+
input: result,
|
|
5224
|
+
...(result.ok ? {} : { error: result.error || 'clipboard-input-failed' })
|
|
5225
|
+
});
|
|
5226
|
+
return;
|
|
5227
|
+
}
|
|
5228
|
+
|
|
5229
|
+
const result = await remoteHub.sendClipboardCommand(deviceId, request);
|
|
5230
|
+
res.status(result.ok ? 200 : clipboardHttpStatus(result.error)).json(result);
|
|
5231
|
+
} catch (error) {
|
|
5232
|
+
const status = error instanceof RemoteClipboardContractError
|
|
5233
|
+
? error.status
|
|
5234
|
+
: 500;
|
|
5235
|
+
res.status(status).json({
|
|
5236
|
+
ok: false,
|
|
5237
|
+
error: error instanceof RemoteClipboardContractError
|
|
5238
|
+
? error.code
|
|
5239
|
+
: 'clipboard-request-failed'
|
|
5240
|
+
});
|
|
5241
|
+
}
|
|
5242
|
+
});
|
|
5243
|
+
|
|
5244
|
+
app.get('/api/remote/filesystem/roots', requireHubFeatureAccess, async (req, res) => {
|
|
5245
5245
|
noStore(res);
|
|
5246
5246
|
try {
|
|
5247
5247
|
res.json(await hubFilesystem.getRoots({ refresh: /^(1|true|yes|on)$/i.test(String(req.query?.refresh || '')) }));
|
|
@@ -5955,14 +5955,14 @@ await remoteHub.start();
|
|
|
5955
5955
|
connectedDeviceCount = Number(remoteHub.getStatus({ includeSecrets: false }).connectedDeviceCount || 0);
|
|
5956
5956
|
hubSharedFolders.startAutoSync(
|
|
5957
5957
|
() => remoteHub.listDevices({ includeDataUrl: false }).filter(device => device.connected).map(device => device.deviceId),
|
|
5958
|
-
() => process.env.LIVEDESK_REMOTE_DIRECTORY || 'Desktop/
|
|
5958
|
+
() => process.env.LIVEDESK_REMOTE_DIRECTORY || 'Desktop/VuvoDeskFiles'
|
|
5959
5959
|
);
|
|
5960
5960
|
httpServer.listen(httpPort, httpHost, () => {
|
|
5961
5961
|
const status = remoteHub.getStatus({ includeSecrets: true });
|
|
5962
5962
|
const managerVersion = String(process.env.LIVEDESK_MANAGER_VERSION || packageInfo.version || 'dev');
|
|
5963
|
-
console.log(`[
|
|
5964
|
-
console.log(`[
|
|
5965
|
-
console.log(`[
|
|
5963
|
+
console.log(`[VuvoDesk Hub] Version ${managerVersion}`);
|
|
5964
|
+
console.log(`[VuvoDesk Hub] HTTP API http://${httpHost}:${httpPort}`);
|
|
5965
|
+
console.log(`[VuvoDesk Hub] Client endpoint ${status.agentEndpoint} pair=${status.pairTokenPreview}`);
|
|
5966
5966
|
hubConsoleRelay?.start();
|
|
5967
5967
|
});
|
|
5968
5968
|
|
|
@@ -5988,9 +5988,9 @@ async function boundedShutdownStep(label, action, timeoutMs) {
|
|
|
5988
5988
|
timer.unref?.();
|
|
5989
5989
|
})
|
|
5990
5990
|
]);
|
|
5991
|
-
console.log(`[
|
|
5991
|
+
console.log(`[VuvoDesk Hub] Shutdown stage completed: ${label}.`);
|
|
5992
5992
|
} catch (error) {
|
|
5993
|
-
console.warn(`[
|
|
5993
|
+
console.warn(`[VuvoDesk Hub] Shutdown stage warning: ${error instanceof Error ? error.message : String(error)}`);
|
|
5994
5994
|
} finally {
|
|
5995
5995
|
if (timer) clearTimeout(timer);
|
|
5996
5996
|
}
|
|
@@ -6000,7 +6000,7 @@ function runSynchronousShutdownStep(label, action) {
|
|
|
6000
6000
|
try {
|
|
6001
6001
|
action();
|
|
6002
6002
|
} catch (error) {
|
|
6003
|
-
console.warn(`[
|
|
6003
|
+
console.warn(`[VuvoDesk Hub] Shutdown stage warning: ${label} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
6004
6004
|
}
|
|
6005
6005
|
}
|
|
6006
6006
|
|
|
@@ -6010,13 +6010,13 @@ function closeBrowserWebSockets() {
|
|
|
6010
6010
|
for (const ws of wss.clients) {
|
|
6011
6011
|
closeRequested += 1;
|
|
6012
6012
|
try {
|
|
6013
|
-
ws.close(1012, '
|
|
6013
|
+
ws.close(1012, 'VuvoDesk Hub restarting');
|
|
6014
6014
|
} catch {
|
|
6015
6015
|
try { ws.terminate(); } catch { /* socket already closed */ }
|
|
6016
6016
|
}
|
|
6017
6017
|
}
|
|
6018
6018
|
}
|
|
6019
|
-
console.log(`[
|
|
6019
|
+
console.log(`[VuvoDesk Hub] Shutdown stage: requested ${closeRequested} browser WebSocket connection(s) to close.`);
|
|
6020
6020
|
}
|
|
6021
6021
|
|
|
6022
6022
|
function terminateOpenConnections() {
|
|
@@ -6035,7 +6035,7 @@ function terminateOpenConnections() {
|
|
|
6035
6035
|
try { socket.destroy(); } catch { /* socket already closed */ }
|
|
6036
6036
|
}
|
|
6037
6037
|
try { httpServer.closeAllConnections?.(); } catch { /* older Node runtime */ }
|
|
6038
|
-
console.log(`[
|
|
6038
|
+
console.log(`[VuvoDesk Hub] Shutdown stage: force-closed ${terminatedWebSockets} WebSocket and ${destroyedHttpConnections} HTTP connection(s).`);
|
|
6039
6039
|
}
|
|
6040
6040
|
|
|
6041
6041
|
let hubShutdownPromise = null;
|
|
@@ -6043,7 +6043,7 @@ function shutdownHub(signal) {
|
|
|
6043
6043
|
if (hubShutdownPromise) return hubShutdownPromise;
|
|
6044
6044
|
hubShutdownPromise = (async () => {
|
|
6045
6045
|
const startedAt = Date.now();
|
|
6046
|
-
console.log(`[
|
|
6046
|
+
console.log(`[VuvoDesk Hub] Shutdown started signal=${signal} grace=${hubShutdownGraceMs}ms timeout=${hubShutdownTimeoutMs}ms.`);
|
|
6047
6047
|
if (roleWatchTimer) clearInterval(roleWatchTimer);
|
|
6048
6048
|
clearInterval(browserWebSocketHeartbeatTimer);
|
|
6049
6049
|
runSynchronousShutdownStep('update manager close', () => liveDeskUpdateManager?.close());
|
|
@@ -6055,15 +6055,15 @@ function shutdownHub(signal) {
|
|
|
6055
6055
|
try {
|
|
6056
6056
|
httpServer.close(error => {
|
|
6057
6057
|
if (error) {
|
|
6058
|
-
console.warn(`[
|
|
6058
|
+
console.warn(`[VuvoDesk Hub] HTTP listener close warning: ${error.message}`);
|
|
6059
6059
|
} else {
|
|
6060
|
-
console.log('[
|
|
6060
|
+
console.log('[VuvoDesk Hub] Shutdown stage completed: HTTP listener closed.');
|
|
6061
6061
|
}
|
|
6062
6062
|
resolveClose();
|
|
6063
6063
|
});
|
|
6064
6064
|
httpServer.closeIdleConnections?.();
|
|
6065
6065
|
} catch (error) {
|
|
6066
|
-
console.warn(`[
|
|
6066
|
+
console.warn(`[VuvoDesk Hub] HTTP listener close warning: ${error instanceof Error ? error.message : String(error)}`);
|
|
6067
6067
|
resolveClose();
|
|
6068
6068
|
}
|
|
6069
6069
|
});
|
|
@@ -6083,7 +6083,7 @@ function shutdownHub(signal) {
|
|
|
6083
6083
|
await Promise.race([httpClosed, waitForShutdownDelay(remainingMs)]);
|
|
6084
6084
|
clearTimeout(forceCloseTimer);
|
|
6085
6085
|
terminateOpenConnections();
|
|
6086
|
-
console.log(`[
|
|
6086
|
+
console.log(`[VuvoDesk Hub] Shutdown complete in ${Date.now() - startedAt}ms.`);
|
|
6087
6087
|
})();
|
|
6088
6088
|
return hubShutdownPromise;
|
|
6089
6089
|
}
|
|
@@ -6093,7 +6093,7 @@ for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
|
6093
6093
|
void shutdownHub(signal)
|
|
6094
6094
|
.then(() => process.exit(0))
|
|
6095
6095
|
.catch(error => {
|
|
6096
|
-
console.error(`[
|
|
6096
|
+
console.error(`[VuvoDesk Hub] Shutdown failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
6097
6097
|
terminateOpenConnections();
|
|
6098
6098
|
process.exit(1);
|
|
6099
6099
|
});
|
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import { effectiveDevicePolicy } from './settings-schema.js';
|
|
2
|
-
import { REMOTE_CLIPBOARD_LIMITS } from '../remote-clipboard-contract.mjs';
|
|
1
|
+
import { effectiveDevicePolicy } from './settings-schema.js';
|
|
2
|
+
import { REMOTE_CLIPBOARD_LIMITS } from '../remote-clipboard-contract.mjs';
|
|
3
3
|
|
|
4
|
-
export function buildEffectiveDevicePolicy(settings, { deviceId = '', capabilities = {} } = {}) {
|
|
5
|
-
const policy = effectiveDevicePolicy(settings);
|
|
6
|
-
const clipboardV1 = capabilities.clipboardProtocol === 'livedesk.clipboard.v1';
|
|
7
|
-
const clipboardCommonLimits = clipboardV1
|
|
8
|
-
&& capabilities.sessionBoundSideChannels === true
|
|
9
|
-
&& Number.isSafeInteger(Number(capabilities.clipboardMaxTextBytes))
|
|
10
|
-
&& Number(capabilities.clipboardMaxTextBytes) > 0
|
|
11
|
-
&& Number.isSafeInteger(Number(capabilities.clipboardMaxTotalBytes))
|
|
12
|
-
&& Number(capabilities.clipboardMaxTotalBytes) > 0
|
|
13
|
-
&& Number.isSafeInteger(Number(capabilities.clipboardMaxChunkBytes))
|
|
14
|
-
&& Number(capabilities.clipboardMaxChunkBytes) >= REMOTE_CLIPBOARD_LIMITS.maxChunkBytes;
|
|
15
|
-
const clipboardMaxImageBytes = Number(
|
|
16
|
-
capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes);
|
|
17
|
-
return {
|
|
4
|
+
export function buildEffectiveDevicePolicy(settings, { deviceId = '', capabilities = {} } = {}) {
|
|
5
|
+
const policy = effectiveDevicePolicy(settings);
|
|
6
|
+
const clipboardV1 = capabilities.clipboardProtocol === 'livedesk.clipboard.v1';
|
|
7
|
+
const clipboardCommonLimits = clipboardV1
|
|
8
|
+
&& capabilities.sessionBoundSideChannels === true
|
|
9
|
+
&& Number.isSafeInteger(Number(capabilities.clipboardMaxTextBytes))
|
|
10
|
+
&& Number(capabilities.clipboardMaxTextBytes) > 0
|
|
11
|
+
&& Number.isSafeInteger(Number(capabilities.clipboardMaxTotalBytes))
|
|
12
|
+
&& Number(capabilities.clipboardMaxTotalBytes) > 0
|
|
13
|
+
&& Number.isSafeInteger(Number(capabilities.clipboardMaxChunkBytes))
|
|
14
|
+
&& Number(capabilities.clipboardMaxChunkBytes) >= REMOTE_CLIPBOARD_LIMITS.maxChunkBytes;
|
|
15
|
+
const clipboardMaxImageBytes = Number(
|
|
16
|
+
capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes);
|
|
17
|
+
return {
|
|
18
18
|
...policy,
|
|
19
|
-
deviceId: String(deviceId || ''),
|
|
20
|
-
supported: {
|
|
21
|
-
control: capabilities.control !== false,
|
|
22
|
-
clipboardText: clipboardCommonLimits && capabilities.clipboardText === true,
|
|
23
|
-
clipboardImage: clipboardCommonLimits
|
|
24
|
-
&& capabilities.clipboardImage === true
|
|
25
|
-
&& Number.isSafeInteger(clipboardMaxImageBytes)
|
|
26
|
-
&& clipboardMaxImageBytes > 0,
|
|
27
|
-
clipboardFiles: clipboardCommonLimits
|
|
28
|
-
&& capabilities.clipboardFiles === true
|
|
29
|
-
&& Number.isSafeInteger(Number(capabilities.clipboardMaxFiles))
|
|
30
|
-
&& Number(capabilities.clipboardMaxFiles) > 0,
|
|
31
|
-
clipboardCopy: clipboardCommonLimits && capabilities.clipboardCopy === true,
|
|
32
|
-
clipboardPaste: clipboardCommonLimits && capabilities.clipboardPaste === true,
|
|
33
|
-
fileTransfer: capabilities.fileTransfer !== false,
|
|
19
|
+
deviceId: String(deviceId || ''),
|
|
20
|
+
supported: {
|
|
21
|
+
control: capabilities.control !== false,
|
|
22
|
+
clipboardText: clipboardCommonLimits && capabilities.clipboardText === true,
|
|
23
|
+
clipboardImage: clipboardCommonLimits
|
|
24
|
+
&& capabilities.clipboardImage === true
|
|
25
|
+
&& Number.isSafeInteger(clipboardMaxImageBytes)
|
|
26
|
+
&& clipboardMaxImageBytes > 0,
|
|
27
|
+
clipboardFiles: clipboardCommonLimits
|
|
28
|
+
&& capabilities.clipboardFiles === true
|
|
29
|
+
&& Number.isSafeInteger(Number(capabilities.clipboardMaxFiles))
|
|
30
|
+
&& Number(capabilities.clipboardMaxFiles) > 0,
|
|
31
|
+
clipboardCopy: clipboardCommonLimits && capabilities.clipboardCopy === true,
|
|
32
|
+
clipboardPaste: clipboardCommonLimits && capabilities.clipboardPaste === true,
|
|
33
|
+
fileTransfer: capabilities.fileTransfer !== false,
|
|
34
34
|
remoteAudio: capabilities.audio === true || capabilities.remoteAudio === true,
|
|
35
35
|
agent: Array.isArray(capabilities.agentTools) && capabilities.agentTools.length > 0
|
|
36
36
|
}
|
|
@@ -65,7 +65,7 @@ export const DEFAULT_LIVEDESK_SETTINGS = Object.freeze({
|
|
|
65
65
|
openReceivedFolder: false,
|
|
66
66
|
notifyTransferComplete: true,
|
|
67
67
|
allowOverwrite: false,
|
|
68
|
-
defaultReceiveFolder: 'Desktop/
|
|
68
|
+
defaultReceiveFolder: 'Desktop/VuvoDeskFiles',
|
|
69
69
|
maxFileSizeBytes: 1024 * 1024 * 1024,
|
|
70
70
|
allowRemoteAudio: true,
|
|
71
71
|
startAudioMuted: false,
|
|
@@ -76,7 +76,7 @@ export const DEFAULT_LIVEDESK_SETTINGS = Object.freeze({
|
|
|
76
76
|
recordingQuality: 'standard',
|
|
77
77
|
timelapseIntervalSeconds: 10,
|
|
78
78
|
includeRemoteCursor: true,
|
|
79
|
-
captureSaveLocation: '
|
|
79
|
+
captureSaveLocation: 'VuvoDesk Captures',
|
|
80
80
|
captureAutoDelete: 'never'
|
|
81
81
|
},
|
|
82
82
|
agent: {
|