livedesk 0.1.227 → 0.1.228
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 +128 -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,9 @@ 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 { buildEffectiveDevicePolicy, effectiveDevicePolicy } from './settings/settings-schema.js';
|
|
25
27
|
|
|
26
28
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
29
|
const webDistCandidates = [
|
|
@@ -140,9 +142,25 @@ function activeDeviceLimit() {
|
|
|
140
142
|
return plan === 'pro' ? Number.POSITIVE_INFINITY : plan === 'ltd' ? PLUS_DEVICE_LIMIT : FREE_DEVICE_LIMIT;
|
|
141
143
|
}
|
|
142
144
|
|
|
143
|
-
function hasHubFeatureAccess() {
|
|
144
|
-
return connectedDeviceCount <= activeDeviceLimit();
|
|
145
|
-
}
|
|
145
|
+
function hasHubFeatureAccess() {
|
|
146
|
+
return connectedDeviceCount <= activeDeviceLimit();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function hasHubFeatureAccessForRequest(req) {
|
|
150
|
+
const limit = activeDeviceLimit();
|
|
151
|
+
if (!Number.isFinite(limit)) return true;
|
|
152
|
+
const ids = new Set();
|
|
153
|
+
const parameterId = String(req?.params?.deviceId || '').trim();
|
|
154
|
+
if (parameterId) ids.add(parameterId);
|
|
155
|
+
const bodyIds = Array.isArray(req?.body?.deviceIds) ? req.body.deviceIds : [];
|
|
156
|
+
for (const value of bodyIds) {
|
|
157
|
+
const id = String(value || '').trim();
|
|
158
|
+
if (id) ids.add(id);
|
|
159
|
+
}
|
|
160
|
+
// A user may operate on any selected device, but a single request may not
|
|
161
|
+
// fan out to more devices than the active plan includes.
|
|
162
|
+
return ids.size <= limit;
|
|
163
|
+
}
|
|
146
164
|
|
|
147
165
|
function licenseSnapshot() {
|
|
148
166
|
const plan = activeLicensePlan();
|
|
@@ -201,15 +219,26 @@ async function syncVerifiedLicense(accessToken) {
|
|
|
201
219
|
return licenseSnapshot();
|
|
202
220
|
}
|
|
203
221
|
|
|
204
|
-
function requireHubFeatureAccess(_req, res, next) {
|
|
205
|
-
if (
|
|
206
|
-
|
|
222
|
+
function requireHubFeatureAccess(_req, res, next) {
|
|
223
|
+
if (hasHubFeatureAccessForRequest(_req)) {
|
|
224
|
+
const policy = effectiveDevicePolicy(liveDeskSettingsStore.getCached());
|
|
225
|
+
if (policy.accessMode === 'block-remote-access') {
|
|
226
|
+
res.status(403).json({ ok: false, error: 'remote-access-blocked-by-settings' });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
next();
|
|
207
230
|
return;
|
|
208
231
|
}
|
|
209
232
|
res.status(402).json({ ok: false, error: 'livedesk-plan-device-limit', license: licenseSnapshot() });
|
|
210
233
|
}
|
|
211
234
|
|
|
212
|
-
const
|
|
235
|
+
const agentDataDir = process.env.LIVEDESK_DATA_DIR || undefined;
|
|
236
|
+
const liveDeskSettingsStore = new LiveDeskSettingsStore({ dataDir: agentDataDir });
|
|
237
|
+
void liveDeskSettingsStore.getRecord().catch(error => {
|
|
238
|
+
console.warn(`[LiveDesk Hub] settings load failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const remoteHub = createRemoteHub({
|
|
213
242
|
managerPackage: '@livedesk/hub',
|
|
214
243
|
managerVersion: packageInfo.version,
|
|
215
244
|
env: {
|
|
@@ -218,12 +247,12 @@ const remoteHub = createRemoteHub({
|
|
|
218
247
|
MINDEXEC_MANAGER_VERSION: packageInfo.version
|
|
219
248
|
},
|
|
220
249
|
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
|
-
|
|
250
|
+
logWarn: (_scope, message) => console.warn(`[LiveDesk Hub] ${message}`),
|
|
251
|
+
emitEvent: handleRemoteHubEvent,
|
|
252
|
+
emitFrame: broadcastRemoteBinaryFrame,
|
|
253
|
+
emitAudio: broadcastRemoteBinaryAudio,
|
|
254
|
+
getEffectiveDevicePolicy: ({ deviceId, capabilities }) => buildEffectiveDevicePolicy(liveDeskSettingsStore.getCached(), { deviceId, capabilities })
|
|
255
|
+
});
|
|
227
256
|
const agentMcpSessions = new Map();
|
|
228
257
|
const agentMcpRunBatches = new Map();
|
|
229
258
|
const agentMcpRunCleanupTimers = new Map();
|
|
@@ -1218,12 +1247,8 @@ function updateFrameSubscription(ws, payload = {}) {
|
|
|
1218
1247
|
}
|
|
1219
1248
|
}
|
|
1220
1249
|
|
|
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 || [])];
|
|
1250
|
+
function startFrameSubscriptionLive(ws, reason = 'subscribe', onlyDeviceId = '', overrideLiveOptions = null) {
|
|
1251
|
+
const subscribedIds = [...(ws.liveDeskDeviceIds || [])];
|
|
1227
1252
|
if (!ws.liveDeskAutoStart || subscribedIds.length === 0) {
|
|
1228
1253
|
return;
|
|
1229
1254
|
}
|
|
@@ -1439,13 +1464,10 @@ function buildRemoteAudioBinaryPacket(audioEvent) {
|
|
|
1439
1464
|
return Buffer.concat([header, metaBuffer, payload], 4 + metaBuffer.length + payload.length);
|
|
1440
1465
|
}
|
|
1441
1466
|
|
|
1442
|
-
function broadcastRemoteBinaryFrame(frameEvent) {
|
|
1467
|
+
function broadcastRemoteBinaryFrame(frameEvent) {
|
|
1443
1468
|
for (const session of atlasClients.values()) {
|
|
1444
1469
|
session.ingest(frameEvent);
|
|
1445
1470
|
}
|
|
1446
|
-
if (!hasHubFeatureAccess()) {
|
|
1447
|
-
return;
|
|
1448
|
-
}
|
|
1449
1471
|
const deviceId = String(frameEvent?.deviceId || frameEvent?.frame?.deviceId || '').trim();
|
|
1450
1472
|
if (!deviceId || frameClients.size === 0) {
|
|
1451
1473
|
return;
|
|
@@ -1490,10 +1512,7 @@ function broadcastRemoteBinaryFrame(frameEvent) {
|
|
|
1490
1512
|
}
|
|
1491
1513
|
}
|
|
1492
1514
|
|
|
1493
|
-
function broadcastRemoteBinaryAudio(audioEvent) {
|
|
1494
|
-
if (!hasHubFeatureAccess()) {
|
|
1495
|
-
return;
|
|
1496
|
-
}
|
|
1515
|
+
function broadcastRemoteBinaryAudio(audioEvent) {
|
|
1497
1516
|
const deviceId = String(audioEvent?.deviceId || audioEvent?.frame?.deviceId || '').trim();
|
|
1498
1517
|
if (!deviceId || audioClients.size === 0) {
|
|
1499
1518
|
return;
|
|
@@ -1517,8 +1536,8 @@ function broadcastRemoteBinaryAudio(audioEvent) {
|
|
|
1517
1536
|
}
|
|
1518
1537
|
}
|
|
1519
1538
|
|
|
1520
|
-
function sendMode4AtlasFrame(ws, output) {
|
|
1521
|
-
if (
|
|
1539
|
+
function sendMode4AtlasFrame(ws, output) {
|
|
1540
|
+
if (ws.readyState !== ws.OPEN || !output?.payload?.length) return;
|
|
1522
1541
|
const metadata = output.metadata || {};
|
|
1523
1542
|
const frameSeq = Number(metadata.frameSeq || 0) || 0;
|
|
1524
1543
|
const isKeyFrame = metadata.isKeyFrame === true;
|
|
@@ -1697,12 +1716,83 @@ function isCapabilityEnabled(device, key) {
|
|
|
1697
1716
|
return /^(1|true|yes|on)$/i.test(String(value || '').trim());
|
|
1698
1717
|
}
|
|
1699
1718
|
|
|
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
|
|
1719
|
+
app.get('/api/health', (_req, res) => {
|
|
1720
|
+
noStore(res);
|
|
1721
|
+
res.json({ ok: true, product: 'LiveDesk', timestamp: new Date().toISOString() });
|
|
1722
|
+
});
|
|
1723
|
+
|
|
1724
|
+
app.get('/api/settings', async (_req, res) => {
|
|
1725
|
+
noStore(res);
|
|
1726
|
+
try {
|
|
1727
|
+
const record = await liveDeskSettingsStore.getRecord();
|
|
1728
|
+
res.json({ ok: true, revision: record.revision, updatedAt: record.updatedAt, settings: record.settings });
|
|
1729
|
+
} catch (error) {
|
|
1730
|
+
res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
1731
|
+
}
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1734
|
+
app.patch('/api/settings', async (req, res) => {
|
|
1735
|
+
noStore(res);
|
|
1736
|
+
try {
|
|
1737
|
+
const body = req.body && typeof req.body === 'object' && !Array.isArray(req.body) ? req.body : {};
|
|
1738
|
+
const revision = body.revision === undefined ? undefined : Number(body.revision);
|
|
1739
|
+
const patch = { ...body };
|
|
1740
|
+
delete patch.revision;
|
|
1741
|
+
const record = await liveDeskSettingsStore.update(patch, revision);
|
|
1742
|
+
res.json({ ok: true, revision: record.revision, updatedAt: record.updatedAt, settings: record.settings });
|
|
1743
|
+
} catch (error) {
|
|
1744
|
+
if (error instanceof SettingsConflictError) {
|
|
1745
|
+
res.status(409).json({ ok: false, error: error.code, revision: error.settings.revision, updatedAt: error.settings.updatedAt, settings: error.settings.settings });
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
res.status(error?.status || 400).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
1749
|
+
}
|
|
1750
|
+
});
|
|
1751
|
+
|
|
1752
|
+
app.get('/api/settings/capabilities', async (_req, res) => {
|
|
1753
|
+
noStore(res);
|
|
1754
|
+
const settings = await liveDeskSettingsStore.get();
|
|
1755
|
+
res.json({
|
|
1756
|
+
ok: true,
|
|
1757
|
+
settingsSchemaVersion: settings.settingsSchemaVersion,
|
|
1758
|
+
sections: ['connection', 'security', 'control', 'wall', 'filesAudio', 'agent', 'advanced'],
|
|
1759
|
+
policy: effectiveDevicePolicy(settings),
|
|
1760
|
+
externalTransport: 'encrypted-only'
|
|
1761
|
+
});
|
|
1762
|
+
});
|
|
1763
|
+
|
|
1764
|
+
app.get('/api/security/trusted-devices', (_req, res) => {
|
|
1765
|
+
noStore(res);
|
|
1766
|
+
const devices = remoteHub.listDevices({ includeDataUrl: false }).map(device => ({
|
|
1767
|
+
deviceId: device.deviceId,
|
|
1768
|
+
deviceName: device.deviceName,
|
|
1769
|
+
platform: device.platform,
|
|
1770
|
+
connected: device.connected === true,
|
|
1771
|
+
trusted: device.connected === true,
|
|
1772
|
+
firstConnectedAt: device.connectedAt || device.lastSeenAt || '',
|
|
1773
|
+
lastSeenAt: device.lastSeenAt || '',
|
|
1774
|
+
ipRange: String(device.remoteAddress || '').replace(/^::ffff:/, '').split(':')[0]
|
|
1775
|
+
}));
|
|
1776
|
+
res.json({ ok: true, devices });
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1779
|
+
app.delete('/api/security/trusted-devices/:deviceId', (req, res) => {
|
|
1780
|
+
noStore(res);
|
|
1781
|
+
const result = remoteHub.disconnectDevice(req.params.deviceId, 'trusted-device-revoked');
|
|
1782
|
+
res.json({ ok: result === true, revoked: result === true, deviceId: req.params.deviceId });
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
app.post('/api/security/revoke-all', (_req, res) => {
|
|
1786
|
+
noStore(res);
|
|
1787
|
+
const devices = remoteHub.listDevices({ includeDataUrl: false });
|
|
1788
|
+
let revoked = 0;
|
|
1789
|
+
for (const device of devices) {
|
|
1790
|
+
if (device.connected && remoteHub.disconnectDevice(device.deviceId, 'trusted-devices-revoked')) revoked += 1;
|
|
1791
|
+
}
|
|
1792
|
+
res.json({ ok: true, revoked });
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
app.get('/api/settings/agent', async (_req, res) => {
|
|
1706
1796
|
noStore(res);
|
|
1707
1797
|
try {
|
|
1708
1798
|
res.json({ ok: true, settings: await agentManager.getSettings() });
|
|
@@ -1798,10 +1888,6 @@ app.post('/api/internal/agent-mcp/tool', async (req, res) => {
|
|
|
1798
1888
|
res.status(401).json({ ok: false, error: 'agent-mcp-session-invalid' });
|
|
1799
1889
|
return;
|
|
1800
1890
|
}
|
|
1801
|
-
if (!hasHubFeatureAccess()) {
|
|
1802
|
-
res.status(402).json({ ok: false, error: 'livedesk-plan-device-limit' });
|
|
1803
|
-
return;
|
|
1804
|
-
}
|
|
1805
1891
|
try {
|
|
1806
1892
|
const name = String(req.body?.name || '').slice(0, 120);
|
|
1807
1893
|
const result = await dispatchAgentMcpTool(session, name, req.body?.arguments || {});
|
|
@@ -2605,12 +2691,8 @@ audioWss.on('connection', (ws, req) => {
|
|
|
2605
2691
|
inputWss.on('connection', ws => {
|
|
2606
2692
|
inputClients.add(ws);
|
|
2607
2693
|
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;
|
|
2694
|
+
ws.on('message', data => {
|
|
2695
|
+
let payload;
|
|
2614
2696
|
try {
|
|
2615
2697
|
payload = JSON.parse(Buffer.isBuffer(data) ? data.toString('utf8') : String(data || ''));
|
|
2616
2698
|
} 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
|
+
}
|