livedesk 0.1.227 → 0.1.229
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/hub/src/remote-hub.js +46 -2
- package/hub/src/server.js +129 -46
- package/hub/src/settings/effective-device-policy.js +16 -0
- package/hub/src/settings/settings-schema.js +236 -0
- package/hub/src/settings/settings-store.js +77 -0
- package/package.json +2 -2
- package/web/dist/assets/icons-D0OP8EcD.js +1 -0
- package/web/dist/assets/index-BR3_7nUB.js +15 -0
- package/web/dist/assets/{react-BFjiD-l6.js → react-DeWZ_kOG.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/dist/assets/icons-eiORJXFO.js +0 -1
- package/web/dist/assets/index-BeXhnYq1.js +0 -15
package/hub/src/remote-hub.js
CHANGED
|
@@ -1388,6 +1388,29 @@ export function createRemoteHub(options = {}) {
|
|
|
1388
1388
|
const emitEvent = options.emitEvent || (() => {});
|
|
1389
1389
|
const emitFrame = typeof options.emitFrame === 'function' ? options.emitFrame : (() => {});
|
|
1390
1390
|
const emitAudio = typeof options.emitAudio === 'function' ? options.emitAudio : (() => {});
|
|
1391
|
+
const getEffectiveDevicePolicy = typeof options.getEffectiveDevicePolicy === 'function'
|
|
1392
|
+
? options.getEffectiveDevicePolicy
|
|
1393
|
+
: (() => ({}));
|
|
1394
|
+
|
|
1395
|
+
function getDevicePolicy(device) {
|
|
1396
|
+
try {
|
|
1397
|
+
return getEffectiveDevicePolicy({
|
|
1398
|
+
deviceId: device?.deviceId || '',
|
|
1399
|
+
capabilities: device?.capabilities || {}
|
|
1400
|
+
}) || {};
|
|
1401
|
+
} catch {
|
|
1402
|
+
return {};
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function policyError(device, permission = '') {
|
|
1407
|
+
const policy = getDevicePolicy(device);
|
|
1408
|
+
if (policy.accessMode === 'block-remote-access') return 'remote-access-blocked-by-settings';
|
|
1409
|
+
if (permission && policy[permission] === false) {
|
|
1410
|
+
return `${permission}-blocked-by-settings`;
|
|
1411
|
+
}
|
|
1412
|
+
return '';
|
|
1413
|
+
}
|
|
1391
1414
|
|
|
1392
1415
|
const enabled = isEnabledValue(env.LIVEDESK_REMOTE_HUB ?? env.MINDEXEC_REMOTE_HUB ?? env.REMOTE_HUB_ENABLED, true)
|
|
1393
1416
|
&& !isDisabledValue(env.REMOTE_HUB_DISABLED);
|
|
@@ -2644,6 +2667,7 @@ export function createRemoteHub(options = {}) {
|
|
|
2644
2667
|
heartbeatMs,
|
|
2645
2668
|
frameProtocol: buildRemoteFrameProtocolDescriptor(),
|
|
2646
2669
|
frameModes: getSupportedRemoteFrameModeProfiles(),
|
|
2670
|
+
effectivePolicy: getEffectiveDevicePolicy({ deviceId, capabilities }),
|
|
2647
2671
|
serverTime: now
|
|
2648
2672
|
});
|
|
2649
2673
|
|
|
@@ -4425,6 +4449,18 @@ export function createRemoteHub(options = {}) {
|
|
|
4425
4449
|
|
|
4426
4450
|
function sendCommand(deviceId, command) {
|
|
4427
4451
|
const device = devices.get(String(deviceId || ''));
|
|
4452
|
+
const commandName = safeString(command?.command || 'ping', 80);
|
|
4453
|
+
const requiredPermission = commandName === 'input.control'
|
|
4454
|
+
? 'allowControl'
|
|
4455
|
+
: commandName.startsWith('file.transfer')
|
|
4456
|
+
? 'allowFileTransfer'
|
|
4457
|
+
: commandName === 'audio.start'
|
|
4458
|
+
? 'allowRemoteAudio'
|
|
4459
|
+
: commandName.startsWith('agent.')
|
|
4460
|
+
? 'allowAgent'
|
|
4461
|
+
: '';
|
|
4462
|
+
const denied = policyError(device, requiredPermission);
|
|
4463
|
+
if (denied) return { ok: false, error: denied };
|
|
4428
4464
|
if (device?.synthetic === true && device.connected) {
|
|
4429
4465
|
const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
|
|
4430
4466
|
device.counters.commandsSent += 1;
|
|
@@ -4437,7 +4473,7 @@ export function createRemoteHub(options = {}) {
|
|
|
4437
4473
|
}
|
|
4438
4474
|
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
4439
4475
|
commandId,
|
|
4440
|
-
command:
|
|
4476
|
+
command: commandName,
|
|
4441
4477
|
synthetic: true
|
|
4442
4478
|
});
|
|
4443
4479
|
emitRemoteEvent('RemoteCommandResult', device, {
|
|
@@ -4453,7 +4489,6 @@ export function createRemoteHub(options = {}) {
|
|
|
4453
4489
|
}
|
|
4454
4490
|
|
|
4455
4491
|
const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
|
|
4456
|
-
const commandName = safeString(command?.command || 'ping', 80);
|
|
4457
4492
|
const payload = {
|
|
4458
4493
|
type: 'command',
|
|
4459
4494
|
commandId,
|
|
@@ -4487,6 +4522,9 @@ export function createRemoteHub(options = {}) {
|
|
|
4487
4522
|
return { ok: false, error: 'device-not-connected' };
|
|
4488
4523
|
}
|
|
4489
4524
|
|
|
4525
|
+
const denied = policyError(device, 'allowControl');
|
|
4526
|
+
if (denied) return { ok: false, error: denied };
|
|
4527
|
+
|
|
4490
4528
|
if (!readCapabilityFlag(device.capabilities, 'control')) {
|
|
4491
4529
|
return { ok: false, error: 'device-input-control-unavailable' };
|
|
4492
4530
|
}
|
|
@@ -4527,6 +4565,8 @@ export function createRemoteHub(options = {}) {
|
|
|
4527
4565
|
|
|
4528
4566
|
function requestAgentTask(deviceId, options = {}) {
|
|
4529
4567
|
const device = devices.get(String(deviceId || ''));
|
|
4568
|
+
const denied = policyError(device, 'allowAgent');
|
|
4569
|
+
if (denied) return { ok: false, error: denied };
|
|
4530
4570
|
const requestedOperation = safeString(options.operation, 80);
|
|
4531
4571
|
const operation = normalizeAgentOperation(requestedOperation);
|
|
4532
4572
|
if (requestedOperation && !operation) {
|
|
@@ -4890,6 +4930,8 @@ export function createRemoteHub(options = {}) {
|
|
|
4890
4930
|
|
|
4891
4931
|
function startLiveStream(deviceId, options = {}) {
|
|
4892
4932
|
const device = devices.get(String(deviceId || ''));
|
|
4933
|
+
const denied = policyError(device);
|
|
4934
|
+
if (denied) return { ok: false, error: denied };
|
|
4893
4935
|
if (device?.synthetic === true && device.connected) {
|
|
4894
4936
|
if (!readCapabilityFlag(device.capabilities, 'liveStream')) {
|
|
4895
4937
|
return { ok: false, error: 'device-live-stream-unavailable' };
|
|
@@ -5186,6 +5228,8 @@ export function createRemoteHub(options = {}) {
|
|
|
5186
5228
|
if (!device.connected) {
|
|
5187
5229
|
return { ok: false, error: 'device-not-connected' };
|
|
5188
5230
|
}
|
|
5231
|
+
const denied = policyError(device, 'allowRemoteAudio');
|
|
5232
|
+
if (denied) return { ok: false, error: denied };
|
|
5189
5233
|
if (!readCapabilityFlag(device.capabilities, 'audio') && !readCapabilityFlag(device.capabilities, 'remoteAudio')) {
|
|
5190
5234
|
return { ok: false, error: 'device-audio-unavailable' };
|
|
5191
5235
|
}
|
package/hub/src/server.js
CHANGED
|
@@ -21,7 +21,10 @@ import { AgentProviderError } from './agents/provider-errors.js';
|
|
|
21
21
|
import { AGENT_PERMISSION_MODES, createAgentPermissionPolicy, evaluateAgentToolPermission, hashAgentPermissionPolicy } from './agents/agent-permissions.js';
|
|
22
22
|
import { createAgentPermissionStore } from './agents/agent-permission-store.js';
|
|
23
23
|
import { createAgentAuditStore } from './agents/agent-audit-store.js';
|
|
24
|
-
import { getAgentToolDefinition } from './agents/agent-tool-registry.js';
|
|
24
|
+
import { getAgentToolDefinition } from './agents/agent-tool-registry.js';
|
|
25
|
+
import { LiveDeskSettingsStore, SettingsConflictError } from './settings/settings-store.js';
|
|
26
|
+
import { effectiveDevicePolicy } from './settings/settings-schema.js';
|
|
27
|
+
import { buildEffectiveDevicePolicy } from './settings/effective-device-policy.js';
|
|
25
28
|
|
|
26
29
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
30
|
const webDistCandidates = [
|
|
@@ -140,9 +143,25 @@ function activeDeviceLimit() {
|
|
|
140
143
|
return plan === 'pro' ? Number.POSITIVE_INFINITY : plan === 'ltd' ? PLUS_DEVICE_LIMIT : FREE_DEVICE_LIMIT;
|
|
141
144
|
}
|
|
142
145
|
|
|
143
|
-
function hasHubFeatureAccess() {
|
|
144
|
-
return connectedDeviceCount <= activeDeviceLimit();
|
|
145
|
-
}
|
|
146
|
+
function hasHubFeatureAccess() {
|
|
147
|
+
return connectedDeviceCount <= activeDeviceLimit();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function hasHubFeatureAccessForRequest(req) {
|
|
151
|
+
const limit = activeDeviceLimit();
|
|
152
|
+
if (!Number.isFinite(limit)) return true;
|
|
153
|
+
const ids = new Set();
|
|
154
|
+
const parameterId = String(req?.params?.deviceId || '').trim();
|
|
155
|
+
if (parameterId) ids.add(parameterId);
|
|
156
|
+
const bodyIds = Array.isArray(req?.body?.deviceIds) ? req.body.deviceIds : [];
|
|
157
|
+
for (const value of bodyIds) {
|
|
158
|
+
const id = String(value || '').trim();
|
|
159
|
+
if (id) ids.add(id);
|
|
160
|
+
}
|
|
161
|
+
// A user may operate on any selected device, but a single request may not
|
|
162
|
+
// fan out to more devices than the active plan includes.
|
|
163
|
+
return ids.size <= limit;
|
|
164
|
+
}
|
|
146
165
|
|
|
147
166
|
function licenseSnapshot() {
|
|
148
167
|
const plan = activeLicensePlan();
|
|
@@ -201,15 +220,26 @@ async function syncVerifiedLicense(accessToken) {
|
|
|
201
220
|
return licenseSnapshot();
|
|
202
221
|
}
|
|
203
222
|
|
|
204
|
-
function requireHubFeatureAccess(_req, res, next) {
|
|
205
|
-
if (
|
|
206
|
-
|
|
223
|
+
function requireHubFeatureAccess(_req, res, next) {
|
|
224
|
+
if (hasHubFeatureAccessForRequest(_req)) {
|
|
225
|
+
const policy = effectiveDevicePolicy(liveDeskSettingsStore.getCached());
|
|
226
|
+
if (policy.accessMode === 'block-remote-access') {
|
|
227
|
+
res.status(403).json({ ok: false, error: 'remote-access-blocked-by-settings' });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
next();
|
|
207
231
|
return;
|
|
208
232
|
}
|
|
209
233
|
res.status(402).json({ ok: false, error: 'livedesk-plan-device-limit', license: licenseSnapshot() });
|
|
210
234
|
}
|
|
211
235
|
|
|
212
|
-
const
|
|
236
|
+
const agentDataDir = process.env.LIVEDESK_DATA_DIR || undefined;
|
|
237
|
+
const liveDeskSettingsStore = new LiveDeskSettingsStore({ dataDir: agentDataDir });
|
|
238
|
+
void liveDeskSettingsStore.getRecord().catch(error => {
|
|
239
|
+
console.warn(`[LiveDesk Hub] settings load failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const remoteHub = createRemoteHub({
|
|
213
243
|
managerPackage: '@livedesk/hub',
|
|
214
244
|
managerVersion: packageInfo.version,
|
|
215
245
|
env: {
|
|
@@ -218,12 +248,12 @@ const remoteHub = createRemoteHub({
|
|
|
218
248
|
MINDEXEC_MANAGER_VERSION: packageInfo.version
|
|
219
249
|
},
|
|
220
250
|
logEvent: (_scope, message) => console.log(`[LiveDesk Hub] ${message}`),
|
|
221
|
-
logWarn: (_scope, message) => console.warn(`[LiveDesk Hub] ${message}`),
|
|
222
|
-
emitEvent: handleRemoteHubEvent,
|
|
223
|
-
emitFrame: broadcastRemoteBinaryFrame,
|
|
224
|
-
emitAudio: broadcastRemoteBinaryAudio
|
|
225
|
-
})
|
|
226
|
-
|
|
251
|
+
logWarn: (_scope, message) => console.warn(`[LiveDesk Hub] ${message}`),
|
|
252
|
+
emitEvent: handleRemoteHubEvent,
|
|
253
|
+
emitFrame: broadcastRemoteBinaryFrame,
|
|
254
|
+
emitAudio: broadcastRemoteBinaryAudio,
|
|
255
|
+
getEffectiveDevicePolicy: ({ deviceId, capabilities }) => buildEffectiveDevicePolicy(liveDeskSettingsStore.getCached(), { deviceId, capabilities })
|
|
256
|
+
});
|
|
227
257
|
const agentMcpSessions = new Map();
|
|
228
258
|
const agentMcpRunBatches = new Map();
|
|
229
259
|
const agentMcpRunCleanupTimers = new Map();
|
|
@@ -1218,12 +1248,8 @@ function updateFrameSubscription(ws, payload = {}) {
|
|
|
1218
1248
|
}
|
|
1219
1249
|
}
|
|
1220
1250
|
|
|
1221
|
-
function startFrameSubscriptionLive(ws, reason = 'subscribe', onlyDeviceId = '', overrideLiveOptions = null) {
|
|
1222
|
-
|
|
1223
|
-
sendJson(ws, { type: 'RemoteFrameSubscriptionError', error: 'livedesk-plan-device-limit', license: licenseSnapshot() });
|
|
1224
|
-
return;
|
|
1225
|
-
}
|
|
1226
|
-
const subscribedIds = [...(ws.liveDeskDeviceIds || [])];
|
|
1251
|
+
function startFrameSubscriptionLive(ws, reason = 'subscribe', onlyDeviceId = '', overrideLiveOptions = null) {
|
|
1252
|
+
const subscribedIds = [...(ws.liveDeskDeviceIds || [])];
|
|
1227
1253
|
if (!ws.liveDeskAutoStart || subscribedIds.length === 0) {
|
|
1228
1254
|
return;
|
|
1229
1255
|
}
|
|
@@ -1439,13 +1465,10 @@ function buildRemoteAudioBinaryPacket(audioEvent) {
|
|
|
1439
1465
|
return Buffer.concat([header, metaBuffer, payload], 4 + metaBuffer.length + payload.length);
|
|
1440
1466
|
}
|
|
1441
1467
|
|
|
1442
|
-
function broadcastRemoteBinaryFrame(frameEvent) {
|
|
1468
|
+
function broadcastRemoteBinaryFrame(frameEvent) {
|
|
1443
1469
|
for (const session of atlasClients.values()) {
|
|
1444
1470
|
session.ingest(frameEvent);
|
|
1445
1471
|
}
|
|
1446
|
-
if (!hasHubFeatureAccess()) {
|
|
1447
|
-
return;
|
|
1448
|
-
}
|
|
1449
1472
|
const deviceId = String(frameEvent?.deviceId || frameEvent?.frame?.deviceId || '').trim();
|
|
1450
1473
|
if (!deviceId || frameClients.size === 0) {
|
|
1451
1474
|
return;
|
|
@@ -1490,10 +1513,7 @@ function broadcastRemoteBinaryFrame(frameEvent) {
|
|
|
1490
1513
|
}
|
|
1491
1514
|
}
|
|
1492
1515
|
|
|
1493
|
-
function broadcastRemoteBinaryAudio(audioEvent) {
|
|
1494
|
-
if (!hasHubFeatureAccess()) {
|
|
1495
|
-
return;
|
|
1496
|
-
}
|
|
1516
|
+
function broadcastRemoteBinaryAudio(audioEvent) {
|
|
1497
1517
|
const deviceId = String(audioEvent?.deviceId || audioEvent?.frame?.deviceId || '').trim();
|
|
1498
1518
|
if (!deviceId || audioClients.size === 0) {
|
|
1499
1519
|
return;
|
|
@@ -1517,8 +1537,8 @@ function broadcastRemoteBinaryAudio(audioEvent) {
|
|
|
1517
1537
|
}
|
|
1518
1538
|
}
|
|
1519
1539
|
|
|
1520
|
-
function sendMode4AtlasFrame(ws, output) {
|
|
1521
|
-
if (
|
|
1540
|
+
function sendMode4AtlasFrame(ws, output) {
|
|
1541
|
+
if (ws.readyState !== ws.OPEN || !output?.payload?.length) return;
|
|
1522
1542
|
const metadata = output.metadata || {};
|
|
1523
1543
|
const frameSeq = Number(metadata.frameSeq || 0) || 0;
|
|
1524
1544
|
const isKeyFrame = metadata.isKeyFrame === true;
|
|
@@ -1697,12 +1717,83 @@ function isCapabilityEnabled(device, key) {
|
|
|
1697
1717
|
return /^(1|true|yes|on)$/i.test(String(value || '').trim());
|
|
1698
1718
|
}
|
|
1699
1719
|
|
|
1700
|
-
app.get('/api/health', (_req, res) => {
|
|
1701
|
-
noStore(res);
|
|
1702
|
-
res.json({ ok: true, product: 'LiveDesk', timestamp: new Date().toISOString() });
|
|
1703
|
-
});
|
|
1704
|
-
|
|
1705
|
-
app.get('/api/settings
|
|
1720
|
+
app.get('/api/health', (_req, res) => {
|
|
1721
|
+
noStore(res);
|
|
1722
|
+
res.json({ ok: true, product: 'LiveDesk', timestamp: new Date().toISOString() });
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
app.get('/api/settings', async (_req, res) => {
|
|
1726
|
+
noStore(res);
|
|
1727
|
+
try {
|
|
1728
|
+
const record = await liveDeskSettingsStore.getRecord();
|
|
1729
|
+
res.json({ ok: true, revision: record.revision, updatedAt: record.updatedAt, settings: record.settings });
|
|
1730
|
+
} catch (error) {
|
|
1731
|
+
res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
1732
|
+
}
|
|
1733
|
+
});
|
|
1734
|
+
|
|
1735
|
+
app.patch('/api/settings', async (req, res) => {
|
|
1736
|
+
noStore(res);
|
|
1737
|
+
try {
|
|
1738
|
+
const body = req.body && typeof req.body === 'object' && !Array.isArray(req.body) ? req.body : {};
|
|
1739
|
+
const revision = body.revision === undefined ? undefined : Number(body.revision);
|
|
1740
|
+
const patch = { ...body };
|
|
1741
|
+
delete patch.revision;
|
|
1742
|
+
const record = await liveDeskSettingsStore.update(patch, revision);
|
|
1743
|
+
res.json({ ok: true, revision: record.revision, updatedAt: record.updatedAt, settings: record.settings });
|
|
1744
|
+
} catch (error) {
|
|
1745
|
+
if (error instanceof SettingsConflictError) {
|
|
1746
|
+
res.status(409).json({ ok: false, error: error.code, revision: error.settings.revision, updatedAt: error.settings.updatedAt, settings: error.settings.settings });
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
res.status(error?.status || 400).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
|
|
1753
|
+
app.get('/api/settings/capabilities', async (_req, res) => {
|
|
1754
|
+
noStore(res);
|
|
1755
|
+
const settings = await liveDeskSettingsStore.get();
|
|
1756
|
+
res.json({
|
|
1757
|
+
ok: true,
|
|
1758
|
+
settingsSchemaVersion: settings.settingsSchemaVersion,
|
|
1759
|
+
sections: ['connection', 'security', 'control', 'wall', 'filesAudio', 'agent', 'advanced'],
|
|
1760
|
+
policy: effectiveDevicePolicy(settings),
|
|
1761
|
+
externalTransport: 'encrypted-only'
|
|
1762
|
+
});
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
app.get('/api/security/trusted-devices', (_req, res) => {
|
|
1766
|
+
noStore(res);
|
|
1767
|
+
const devices = remoteHub.listDevices({ includeDataUrl: false }).map(device => ({
|
|
1768
|
+
deviceId: device.deviceId,
|
|
1769
|
+
deviceName: device.deviceName,
|
|
1770
|
+
platform: device.platform,
|
|
1771
|
+
connected: device.connected === true,
|
|
1772
|
+
trusted: device.connected === true,
|
|
1773
|
+
firstConnectedAt: device.connectedAt || device.lastSeenAt || '',
|
|
1774
|
+
lastSeenAt: device.lastSeenAt || '',
|
|
1775
|
+
ipRange: String(device.remoteAddress || '').replace(/^::ffff:/, '').split(':')[0]
|
|
1776
|
+
}));
|
|
1777
|
+
res.json({ ok: true, devices });
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
app.delete('/api/security/trusted-devices/:deviceId', (req, res) => {
|
|
1781
|
+
noStore(res);
|
|
1782
|
+
const result = remoteHub.disconnectDevice(req.params.deviceId, 'trusted-device-revoked');
|
|
1783
|
+
res.json({ ok: result === true, revoked: result === true, deviceId: req.params.deviceId });
|
|
1784
|
+
});
|
|
1785
|
+
|
|
1786
|
+
app.post('/api/security/revoke-all', (_req, res) => {
|
|
1787
|
+
noStore(res);
|
|
1788
|
+
const devices = remoteHub.listDevices({ includeDataUrl: false });
|
|
1789
|
+
let revoked = 0;
|
|
1790
|
+
for (const device of devices) {
|
|
1791
|
+
if (device.connected && remoteHub.disconnectDevice(device.deviceId, 'trusted-devices-revoked')) revoked += 1;
|
|
1792
|
+
}
|
|
1793
|
+
res.json({ ok: true, revoked });
|
|
1794
|
+
});
|
|
1795
|
+
|
|
1796
|
+
app.get('/api/settings/agent', async (_req, res) => {
|
|
1706
1797
|
noStore(res);
|
|
1707
1798
|
try {
|
|
1708
1799
|
res.json({ ok: true, settings: await agentManager.getSettings() });
|
|
@@ -1798,10 +1889,6 @@ app.post('/api/internal/agent-mcp/tool', async (req, res) => {
|
|
|
1798
1889
|
res.status(401).json({ ok: false, error: 'agent-mcp-session-invalid' });
|
|
1799
1890
|
return;
|
|
1800
1891
|
}
|
|
1801
|
-
if (!hasHubFeatureAccess()) {
|
|
1802
|
-
res.status(402).json({ ok: false, error: 'livedesk-plan-device-limit' });
|
|
1803
|
-
return;
|
|
1804
|
-
}
|
|
1805
1892
|
try {
|
|
1806
1893
|
const name = String(req.body?.name || '').slice(0, 120);
|
|
1807
1894
|
const result = await dispatchAgentMcpTool(session, name, req.body?.arguments || {});
|
|
@@ -2605,12 +2692,8 @@ audioWss.on('connection', (ws, req) => {
|
|
|
2605
2692
|
inputWss.on('connection', ws => {
|
|
2606
2693
|
inputClients.add(ws);
|
|
2607
2694
|
ws.liveDeskInputClientId = `riws-${++inputClientSeq}`;
|
|
2608
|
-
ws.on('message', data => {
|
|
2609
|
-
|
|
2610
|
-
sendJson(ws, { type: 'RemoteInputError', error: 'livedesk-plan-device-limit', license: licenseSnapshot() });
|
|
2611
|
-
return;
|
|
2612
|
-
}
|
|
2613
|
-
let payload;
|
|
2695
|
+
ws.on('message', data => {
|
|
2696
|
+
let payload;
|
|
2614
2697
|
try {
|
|
2615
2698
|
payload = JSON.parse(Buffer.isBuffer(data) ? data.toString('utf8') : String(data || ''));
|
|
2616
2699
|
} catch {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { effectiveDevicePolicy } from './settings-schema.js';
|
|
2
|
+
|
|
3
|
+
export function buildEffectiveDevicePolicy(settings, { deviceId = '', capabilities = {} } = {}) {
|
|
4
|
+
const policy = effectiveDevicePolicy(settings);
|
|
5
|
+
return {
|
|
6
|
+
...policy,
|
|
7
|
+
deviceId: String(deviceId || ''),
|
|
8
|
+
supported: {
|
|
9
|
+
control: capabilities.control !== false,
|
|
10
|
+
clipboardText: capabilities.clipboardText !== false,
|
|
11
|
+
fileTransfer: capabilities.fileTransfer !== false,
|
|
12
|
+
remoteAudio: capabilities.audio === true || capabilities.remoteAudio === true,
|
|
13
|
+
agent: Array.isArray(capabilities.agentTools) && capabilities.agentTools.length > 0
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const SETTINGS_SCHEMA_VERSION = 1;
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_LIVEDESK_SETTINGS = Object.freeze({
|
|
7
|
+
settingsSchemaVersion: SETTINGS_SCHEMA_VERSION,
|
|
8
|
+
connection: {
|
|
9
|
+
usePinToAddDevices: true,
|
|
10
|
+
pinValidityMinutes: 30,
|
|
11
|
+
rotatePinAfterPairing: true,
|
|
12
|
+
allowNewDevices: true,
|
|
13
|
+
approveNewDevices: true,
|
|
14
|
+
startWithComputer: true,
|
|
15
|
+
keepRunningInTray: true,
|
|
16
|
+
showConnectionNotifications: true
|
|
17
|
+
},
|
|
18
|
+
security: {
|
|
19
|
+
accessMode: 'trusted-only',
|
|
20
|
+
requireEncryptedConnections: true,
|
|
21
|
+
allowInternetConnections: false,
|
|
22
|
+
allowLanConnections: true,
|
|
23
|
+
allowUnencryptedLanFallback: false,
|
|
24
|
+
requireNewDeviceApproval: true,
|
|
25
|
+
requireAdministratorForSecurityChanges: true,
|
|
26
|
+
visibleControlIndicator: true,
|
|
27
|
+
lockOnControlEnd: true,
|
|
28
|
+
disconnectIdleControlSessions: true,
|
|
29
|
+
idleControlMinutes: 30,
|
|
30
|
+
keepSessionHistory: true,
|
|
31
|
+
sessionHistoryDays: 30
|
|
32
|
+
},
|
|
33
|
+
control: {
|
|
34
|
+
allowKeyboardMouse: true,
|
|
35
|
+
allowSystemShortcuts: true,
|
|
36
|
+
allowClipboardText: true,
|
|
37
|
+
allowRemoteRestart: false,
|
|
38
|
+
reconnectAfterRemoteRestart: true,
|
|
39
|
+
allowSwitchingMonitors: true,
|
|
40
|
+
showConnectionToolbar: true,
|
|
41
|
+
showRemoteCursor: true,
|
|
42
|
+
openControlOnDoubleClick: true,
|
|
43
|
+
startRemoteAudioWithControl: false,
|
|
44
|
+
fitRemoteScreen: true,
|
|
45
|
+
rememberLastMonitor: true,
|
|
46
|
+
keepControlReadyBetweenPages: true
|
|
47
|
+
},
|
|
48
|
+
wall: {
|
|
49
|
+
performanceMode: 'auto',
|
|
50
|
+
autoStart: true,
|
|
51
|
+
connectedOnly: false,
|
|
52
|
+
keepEmptySlots: true,
|
|
53
|
+
showDeviceStatus: true,
|
|
54
|
+
showPerformanceDetails: false,
|
|
55
|
+
pauseHiddenTiles: true,
|
|
56
|
+
reduceWhenHidden: true,
|
|
57
|
+
autoAdjustTileQuality: true,
|
|
58
|
+
rememberDevicePositions: true
|
|
59
|
+
},
|
|
60
|
+
filesAudio: {
|
|
61
|
+
allowFileTransfer: true,
|
|
62
|
+
allowFolderSync: false,
|
|
63
|
+
askBeforeReceivingFiles: true,
|
|
64
|
+
openReceivedFolder: false,
|
|
65
|
+
notifyTransferComplete: true,
|
|
66
|
+
allowOverwrite: false,
|
|
67
|
+
defaultReceiveFolder: 'Desktop/LiveDeskFiles',
|
|
68
|
+
maxFileSizeBytes: 1024 * 1024 * 1024,
|
|
69
|
+
allowRemoteAudio: true,
|
|
70
|
+
startAudioMuted: false,
|
|
71
|
+
rememberVolume: true,
|
|
72
|
+
automaticallyRecoverAudio: true,
|
|
73
|
+
showAudioTroubleshooting: false
|
|
74
|
+
},
|
|
75
|
+
agent: {
|
|
76
|
+
enabled: false,
|
|
77
|
+
defaultPermissionMode: 'safe-auto',
|
|
78
|
+
askBeforeDestructive: true,
|
|
79
|
+
allowProcessManagement: true,
|
|
80
|
+
allowServiceManagement: true,
|
|
81
|
+
allowFileChanges: true,
|
|
82
|
+
allowCommandExecution: false,
|
|
83
|
+
allowSoftwareInstallation: false,
|
|
84
|
+
allowPowerActions: false,
|
|
85
|
+
keepAuditHistory: true,
|
|
86
|
+
timeoutMinutes: 10
|
|
87
|
+
},
|
|
88
|
+
advanced: {
|
|
89
|
+
wallFrameMode: 'auto',
|
|
90
|
+
wallFps: 20,
|
|
91
|
+
wallMaxWidth: 960,
|
|
92
|
+
wallMaxHeight: 540,
|
|
93
|
+
wallQuality: 55,
|
|
94
|
+
controlFrameMode: 'mode3-h264-hw',
|
|
95
|
+
controlFps: 30,
|
|
96
|
+
controlMaxWidth: 1920,
|
|
97
|
+
controlMaxHeight: 1080,
|
|
98
|
+
controlQuality: 60,
|
|
99
|
+
transport: 'auto',
|
|
100
|
+
allowPlainLanFallback: false,
|
|
101
|
+
verboseLogs: false,
|
|
102
|
+
frameStatistics: false
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const ENUMS = {
|
|
107
|
+
accessMode: new Set(['trusted-only', 'ask-every-time', 'view-only', 'block-remote-access']),
|
|
108
|
+
performanceMode: new Set(['auto', 'responsive', 'quality', 'custom']),
|
|
109
|
+
permissionMode: new Set(['ask', 'safe-auto', 'full-access', 'custom']),
|
|
110
|
+
wallFrameMode: new Set(['auto', 'mode2-lzo', 'mode3-h264-hw', 'mode4-h264-atlas']),
|
|
111
|
+
controlFrameMode: new Set(['mode3-h264-hw', 'mode5-lzo-delta']),
|
|
112
|
+
transport: new Set(['auto', 'encrypted', 'plain-lan'])
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
function booleanValue(value, fallback) {
|
|
116
|
+
return typeof value === 'boolean' ? value : fallback;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function numberValue(value, min, max, fallback) {
|
|
120
|
+
const number = Number(value);
|
|
121
|
+
return Number.isFinite(number) ? Math.max(min, Math.min(max, Math.round(number))) : fallback;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function enumValue(value, allowed, fallback) {
|
|
125
|
+
const normalized = String(value ?? fallback).trim().toLowerCase();
|
|
126
|
+
return allowed.has(normalized) ? normalized : fallback;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function stringValue(value, fallback, maxLength = 600) {
|
|
130
|
+
return typeof value === 'string' ? value.replace(/[\0\r\n]/g, ' ').trim().slice(0, maxLength) : fallback;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function normalizeSection(source, defaults, rules = {}) {
|
|
134
|
+
const input = source && typeof source === 'object' && !Array.isArray(source) ? source : {};
|
|
135
|
+
const result = {};
|
|
136
|
+
for (const [key, fallback] of Object.entries(defaults)) {
|
|
137
|
+
const rule = rules[key];
|
|
138
|
+
if (rule?.type === 'boolean') result[key] = booleanValue(input[key], fallback);
|
|
139
|
+
else if (rule?.type === 'number') result[key] = numberValue(input[key], rule.min, rule.max, fallback);
|
|
140
|
+
else if (rule?.type === 'enum') result[key] = enumValue(input[key], rule.values, fallback);
|
|
141
|
+
else if (rule?.type === 'string') result[key] = stringValue(input[key], fallback, rule.maxLength);
|
|
142
|
+
else result[key] = input[key] === undefined ? fallback : fallback;
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const bools = keys => Object.fromEntries(keys.map(key => [key, { type: 'boolean' }]));
|
|
148
|
+
const numbers = (entries) => Object.fromEntries(entries.map(([key, min, max]) => [key, { type: 'number', min, max }]));
|
|
149
|
+
|
|
150
|
+
const RULES = {
|
|
151
|
+
connection: {
|
|
152
|
+
...bools(['usePinToAddDevices', 'rotatePinAfterPairing', 'allowNewDevices', 'approveNewDevices', 'startWithComputer', 'keepRunningInTray', 'showConnectionNotifications']),
|
|
153
|
+
...numbers([['pinValidityMinutes', 10, 1440]])
|
|
154
|
+
},
|
|
155
|
+
security: {
|
|
156
|
+
accessMode: { type: 'enum', values: ENUMS.accessMode },
|
|
157
|
+
...bools(['requireEncryptedConnections', 'allowInternetConnections', 'allowLanConnections', 'allowUnencryptedLanFallback', 'requireNewDeviceApproval', 'requireAdministratorForSecurityChanges', 'visibleControlIndicator', 'lockOnControlEnd', 'disconnectIdleControlSessions', 'keepSessionHistory']),
|
|
158
|
+
...numbers([['idleControlMinutes', 5, 1440], ['sessionHistoryDays', 7, 365]])
|
|
159
|
+
},
|
|
160
|
+
control: {
|
|
161
|
+
...bools(['allowKeyboardMouse', 'allowSystemShortcuts', 'allowClipboardText', 'allowRemoteRestart', 'reconnectAfterRemoteRestart', 'allowSwitchingMonitors', 'showConnectionToolbar', 'showRemoteCursor', 'openControlOnDoubleClick', 'startRemoteAudioWithControl', 'fitRemoteScreen', 'rememberLastMonitor', 'keepControlReadyBetweenPages'])
|
|
162
|
+
},
|
|
163
|
+
wall: {
|
|
164
|
+
performanceMode: { type: 'enum', values: ENUMS.performanceMode },
|
|
165
|
+
...bools(['autoStart', 'connectedOnly', 'keepEmptySlots', 'showDeviceStatus', 'showPerformanceDetails', 'pauseHiddenTiles', 'reduceWhenHidden', 'autoAdjustTileQuality', 'rememberDevicePositions'])
|
|
166
|
+
},
|
|
167
|
+
filesAudio: {
|
|
168
|
+
...bools(['allowFileTransfer', 'allowFolderSync', 'askBeforeReceivingFiles', 'openReceivedFolder', 'notifyTransferComplete', 'allowOverwrite', 'allowRemoteAudio', 'startAudioMuted', 'rememberVolume', 'automaticallyRecoverAudio', 'showAudioTroubleshooting']),
|
|
169
|
+
defaultReceiveFolder: { type: 'string', maxLength: 600 },
|
|
170
|
+
maxFileSizeBytes: { type: 'number', min: 1, max: Number.MAX_SAFE_INTEGER }
|
|
171
|
+
},
|
|
172
|
+
agent: {
|
|
173
|
+
defaultPermissionMode: { type: 'enum', values: ENUMS.permissionMode },
|
|
174
|
+
...bools(['enabled', 'askBeforeDestructive', 'allowProcessManagement', 'allowServiceManagement', 'allowFileChanges', 'allowCommandExecution', 'allowSoftwareInstallation', 'allowPowerActions', 'keepAuditHistory']),
|
|
175
|
+
timeoutMinutes: { type: 'number', min: 1, max: 120 }
|
|
176
|
+
},
|
|
177
|
+
advanced: {
|
|
178
|
+
wallFrameMode: { type: 'enum', values: ENUMS.wallFrameMode },
|
|
179
|
+
controlFrameMode: { type: 'enum', values: ENUMS.controlFrameMode },
|
|
180
|
+
transport: { type: 'enum', values: ENUMS.transport },
|
|
181
|
+
...bools(['allowPlainLanFallback', 'verboseLogs', 'frameStatistics']),
|
|
182
|
+
...numbers([['wallFps', 1, 60], ['wallMaxWidth', 320, 3840], ['wallMaxHeight', 180, 2160], ['wallQuality', 20, 95], ['controlFps', 20, 60], ['controlMaxWidth', 640, 3840], ['controlMaxHeight', 360, 2160], ['controlQuality', 20, 95]])
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export function normalizeLiveDeskSettings(value = {}) {
|
|
187
|
+
const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
188
|
+
const settings = {
|
|
189
|
+
settingsSchemaVersion: SETTINGS_SCHEMA_VERSION,
|
|
190
|
+
connection: normalizeSection(source.connection, DEFAULT_LIVEDESK_SETTINGS.connection, RULES.connection),
|
|
191
|
+
security: normalizeSection(source.security, DEFAULT_LIVEDESK_SETTINGS.security, RULES.security),
|
|
192
|
+
control: normalizeSection(source.control, DEFAULT_LIVEDESK_SETTINGS.control, RULES.control),
|
|
193
|
+
wall: normalizeSection(source.wall, DEFAULT_LIVEDESK_SETTINGS.wall, RULES.wall),
|
|
194
|
+
filesAudio: normalizeSection(source.filesAudio, DEFAULT_LIVEDESK_SETTINGS.filesAudio, RULES.filesAudio),
|
|
195
|
+
agent: normalizeSection(source.agent, DEFAULT_LIVEDESK_SETTINGS.agent, RULES.agent),
|
|
196
|
+
advanced: normalizeSection(source.advanced, DEFAULT_LIVEDESK_SETTINGS.advanced, RULES.advanced)
|
|
197
|
+
};
|
|
198
|
+
// Security invariants are enforced at the authority boundary, not just in
|
|
199
|
+
// the browser. Internet access can never use an unencrypted transport.
|
|
200
|
+
if (settings.security.requireEncryptedConnections) {
|
|
201
|
+
settings.security.allowUnencryptedLanFallback = false;
|
|
202
|
+
}
|
|
203
|
+
if (!settings.security.allowInternetConnections) {
|
|
204
|
+
settings.advanced.transport = settings.advanced.transport === 'encrypted' ? 'encrypted' : 'auto';
|
|
205
|
+
}
|
|
206
|
+
if (settings.agent.defaultPermissionMode === 'safe-auto') {
|
|
207
|
+
settings.agent.askBeforeDestructive = true;
|
|
208
|
+
}
|
|
209
|
+
return settings;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function defaultSettingsPath(dataDir = undefined) {
|
|
213
|
+
return path.join(dataDir || path.join(os.homedir(), '.livedesk'), 'settings.json');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function publicSettings(settings) {
|
|
217
|
+
return normalizeLiveDeskSettings(settings);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function effectiveDevicePolicy(settings = DEFAULT_LIVEDESK_SETTINGS) {
|
|
221
|
+
const normalized = normalizeLiveDeskSettings(settings);
|
|
222
|
+
return {
|
|
223
|
+
settingsSchemaVersion: normalized.settingsSchemaVersion,
|
|
224
|
+
policyRevision: Number(settings?.revision || 0),
|
|
225
|
+
accessMode: normalized.security.accessMode,
|
|
226
|
+
allowInternetConnections: normalized.security.allowInternetConnections,
|
|
227
|
+
allowLanConnections: normalized.security.allowLanConnections,
|
|
228
|
+
requireEncryptedConnections: normalized.security.requireEncryptedConnections,
|
|
229
|
+
allowUnencryptedLanFallback: normalized.security.allowUnencryptedLanFallback,
|
|
230
|
+
allowControl: normalized.security.accessMode !== 'block-remote-access' && normalized.control.allowKeyboardMouse,
|
|
231
|
+
allowClipboardText: normalized.security.accessMode !== 'block-remote-access' && normalized.control.allowClipboardText,
|
|
232
|
+
allowFileTransfer: normalized.security.accessMode !== 'block-remote-access' && normalized.filesAudio.allowFileTransfer,
|
|
233
|
+
allowRemoteAudio: normalized.security.accessMode !== 'block-remote-access' && normalized.filesAudio.allowRemoteAudio,
|
|
234
|
+
allowAgent: normalized.security.accessMode !== 'block-remote-access' && normalized.agent.enabled
|
|
235
|
+
};
|
|
236
|
+
}
|