@livedesk/hub 0.1.14 → 0.1.16
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/package.json +1 -1
- package/src/agents/agent-manager.js +132 -175
- package/src/agents/agent-settings.js +7 -68
- package/src/agents/agent-tool-registry.js +1 -1
- package/src/agents/codex-agent-runtime.js +2 -2
- package/src/remote-hub.js +92 -124
- package/src/server.js +50 -38
- package/src/transport/relay-hub-control.js +48 -7
- package/src/agents/opencode-go-provider.js +0 -260
- package/src/agents/secret-store.js +0 -165
package/src/remote-hub.js
CHANGED
|
@@ -644,15 +644,27 @@ function safeTaskData(value) {
|
|
|
644
644
|
}
|
|
645
645
|
}
|
|
646
646
|
|
|
647
|
-
function normalizeApprovalLevel(value) {
|
|
648
|
-
const level = safeString(value, 80).toLowerCase();
|
|
649
|
-
if (level === 'read-only') {
|
|
650
|
-
return 'read-only';
|
|
651
|
-
}
|
|
652
|
-
return
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
const
|
|
647
|
+
function normalizeApprovalLevel(value) {
|
|
648
|
+
const level = safeString(value, 80).toLowerCase();
|
|
649
|
+
if (level === 'read-only') {
|
|
650
|
+
return 'read-only';
|
|
651
|
+
}
|
|
652
|
+
return 'task-only';
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const RETIRED_AGENT_TASK_ERROR = 'agent-ai-assist-retired';
|
|
656
|
+
|
|
657
|
+
function getRetiredAgentTaskError(options = {}) {
|
|
658
|
+
const approvalLevel = safeString(options?.approvalLevel, 80).toLowerCase();
|
|
659
|
+
const hasModelSelection = options
|
|
660
|
+
&& typeof options === 'object'
|
|
661
|
+
&& Object.prototype.hasOwnProperty.call(options, 'model');
|
|
662
|
+
return approvalLevel === 'ai-assist' || hasModelSelection
|
|
663
|
+
? RETIRED_AGENT_TASK_ERROR
|
|
664
|
+
: '';
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const SUPPORTED_AGENT_OPERATIONS = new Set([
|
|
656
668
|
'system.health',
|
|
657
669
|
'gpu.status',
|
|
658
670
|
'disk.status',
|
|
@@ -2586,13 +2598,12 @@ export function createRemoteHub(options = {}) {
|
|
|
2586
2598
|
const count = clampNumber(options.count, 1, MAX_SYNTHETIC_DEVICES, 250);
|
|
2587
2599
|
const connectedRatio = Math.max(0, Math.min(1, Number(options.connectedRatio ?? 0.88)));
|
|
2588
2600
|
const thumbnailRatio = Math.max(0, Math.min(1, Number(options.thumbnailRatio ?? 0.7)));
|
|
2589
|
-
const
|
|
2590
|
-
const liveCount = clampNumber(options.liveCount, 0, Math.min(count, 24), Math.min(6, count));
|
|
2601
|
+
const liveCount = clampNumber(options.liveCount, 0, Math.min(count, 24), Math.min(6, count));
|
|
2591
2602
|
const replace = options.replace !== false;
|
|
2592
2603
|
const now = new Date();
|
|
2593
2604
|
const platforms = ['win32', 'linux', 'darwin'];
|
|
2594
2605
|
const machineKinds = ['Mac mini', 'Mini PC', 'Client', 'Render node', 'Dev box'];
|
|
2595
|
-
const workloadRoles = ['
|
|
2606
|
+
const workloadRoles = ['Office computer', 'Media workstation', 'Development machine', 'Idle machine', 'Build machine'];
|
|
2596
2607
|
const gpuNames = ['Apple M-series GPU', 'NVIDIA RTX local', 'Radeon Pro', 'Intel Arc', 'Integrated GPU'];
|
|
2597
2608
|
const npuNames = ['Apple Neural Engine', 'Ryzen AI NPU', 'Intel AI Boost', 'Qualcomm Hexagon', 'NPU not present'];
|
|
2598
2609
|
|
|
@@ -2601,15 +2612,13 @@ export function createRemoteHub(options = {}) {
|
|
|
2601
2612
|
}
|
|
2602
2613
|
|
|
2603
2614
|
let connected = 0;
|
|
2604
|
-
let
|
|
2605
|
-
let live = 0;
|
|
2615
|
+
let live = 0;
|
|
2606
2616
|
for (let index = 0; index < count; index += 1) {
|
|
2607
2617
|
const ordinal = index + 1;
|
|
2608
2618
|
const deviceId = `synthetic-${String(ordinal).padStart(4, '0')}`;
|
|
2609
2619
|
const isConnected = index / count < connectedRatio;
|
|
2610
2620
|
const hasThumbnail = index / count < thumbnailRatio;
|
|
2611
|
-
const
|
|
2612
|
-
const isLive = isConnected && index < liveCount;
|
|
2621
|
+
const isLive = isConnected && index < liveCount;
|
|
2613
2622
|
const seenAt = new Date(now.getTime() - index * 1350).toISOString();
|
|
2614
2623
|
const connectedAt = new Date(now.getTime() - (index + 8) * 60000).toISOString();
|
|
2615
2624
|
const platform = platforms[index % platforms.length];
|
|
@@ -2631,9 +2640,7 @@ export function createRemoteHub(options = {}) {
|
|
|
2631
2640
|
const npuTops = index % 5 === 4 ? 0 : platform === 'darwin' ? 38 : index % 2 === 0 ? 45 : 16;
|
|
2632
2641
|
const machineKind = machineKinds[index % machineKinds.length];
|
|
2633
2642
|
const workloadRole = workloadRoles[index % workloadRoles.length];
|
|
2634
|
-
const
|
|
2635
|
-
const toolOnline = index % 4 !== 3;
|
|
2636
|
-
const taskStatus = index % 17 === 0
|
|
2643
|
+
const taskStatus = index % 17 === 0
|
|
2637
2644
|
? 'failed'
|
|
2638
2645
|
: index % 5 === 0
|
|
2639
2646
|
? 'completed'
|
|
@@ -2647,18 +2654,16 @@ export function createRemoteHub(options = {}) {
|
|
|
2647
2654
|
title: taskStatus === 'failed' ? 'Synthetic issue check' : 'Synthetic status task',
|
|
2648
2655
|
instructionPreview: 'Synthetic fleet scale validation task.',
|
|
2649
2656
|
status: taskStatus,
|
|
2650
|
-
approvalLevel:
|
|
2657
|
+
approvalLevel: 'task-only',
|
|
2651
2658
|
requestedAt: seenAt,
|
|
2652
2659
|
sentAt: seenAt,
|
|
2653
2660
|
updatedAt: seenAt,
|
|
2654
|
-
completedAt: taskStatus === 'completed' || taskStatus === 'failed' ? seenAt : '',
|
|
2655
|
-
error: taskStatus === 'failed' ? 'Synthetic task failure sample.' : '',
|
|
2656
|
-
resultKind: 'synthetic-agent-task',
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
? 'Synthetic task reported a sample issue.'
|
|
2661
|
-
: 'Synthetic task completed for scale validation.'
|
|
2661
|
+
completedAt: taskStatus === 'completed' || taskStatus === 'failed' ? seenAt : '',
|
|
2662
|
+
error: taskStatus === 'failed' ? 'Synthetic task failure sample.' : '',
|
|
2663
|
+
resultKind: 'synthetic-agent-task',
|
|
2664
|
+
resultSummary: taskStatus === 'failed'
|
|
2665
|
+
? 'Synthetic task reported a sample issue.'
|
|
2666
|
+
: 'Synthetic task completed for scale validation.'
|
|
2662
2667
|
}
|
|
2663
2668
|
: null;
|
|
2664
2669
|
const device = {
|
|
@@ -2677,11 +2682,8 @@ export function createRemoteHub(options = {}) {
|
|
|
2677
2682
|
thumbnail: hasThumbnail,
|
|
2678
2683
|
control: false,
|
|
2679
2684
|
liveStream: true,
|
|
2680
|
-
computerAgent: true,
|
|
2681
|
-
taskDispatch: true
|
|
2682
|
-
aiAssist: hasAiAssist,
|
|
2683
|
-
aiModel: hasAiAssist ? 'synthetic-ai' : '',
|
|
2684
|
-
aiProvider: hasAiAssist ? 'synthetic' : ''
|
|
2685
|
+
computerAgent: true,
|
|
2686
|
+
taskDispatch: true
|
|
2685
2687
|
},
|
|
2686
2688
|
connected: isConnected,
|
|
2687
2689
|
connectedAt: isConnected ? connectedAt : '',
|
|
@@ -2740,31 +2742,12 @@ export function createRemoteHub(options = {}) {
|
|
|
2740
2742
|
txMbps: 12 + ((index * 17) % 220),
|
|
2741
2743
|
usageRatio: Number(Math.min(0.94, ((index * 9) % 81) / 100).toFixed(2))
|
|
2742
2744
|
},
|
|
2743
|
-
workload: {
|
|
2744
|
-
role: workloadRole,
|
|
2745
|
-
status: taskStatus ||
|
|
2746
|
-
title: taskStatus ? 'Fleet validation task' :
|
|
2747
|
-
summary: taskStatus ? 'Synthetic status for fleet-scale validation.' : ''
|
|
2748
|
-
}
|
|
2749
|
-
tools: {
|
|
2750
|
-
ollama: {
|
|
2751
|
-
status: toolOnline ? (toolBusy ? 'busy' : 'online') : 'offline',
|
|
2752
|
-
port: 11434,
|
|
2753
|
-
version: toolOnline ? 'local' : ''
|
|
2754
|
-
},
|
|
2755
|
-
lmStudio: {
|
|
2756
|
-
status: index % 5 === 0 ? 'online' : 'offline',
|
|
2757
|
-
port: 1234
|
|
2758
|
-
},
|
|
2759
|
-
comfyui: {
|
|
2760
|
-
status: index % 3 === 0 ? (toolBusy ? 'busy' : 'online') : 'offline',
|
|
2761
|
-
port: 8188
|
|
2762
|
-
},
|
|
2763
|
-
stableDiffusion: {
|
|
2764
|
-
status: index % 7 === 0 ? 'online' : 'offline',
|
|
2765
|
-
port: 7860
|
|
2766
|
-
}
|
|
2767
|
-
}
|
|
2745
|
+
workload: {
|
|
2746
|
+
role: workloadRole,
|
|
2747
|
+
status: taskStatus || 'idle',
|
|
2748
|
+
title: taskStatus ? 'Fleet validation task' : 'idle',
|
|
2749
|
+
summary: taskStatus ? 'Synthetic status for fleet-scale validation.' : ''
|
|
2750
|
+
}
|
|
2768
2751
|
},
|
|
2769
2752
|
latestThumbnail: null,
|
|
2770
2753
|
latestLiveFrame: null,
|
|
@@ -2839,25 +2822,20 @@ export function createRemoteHub(options = {}) {
|
|
|
2839
2822
|
if (isConnected) {
|
|
2840
2823
|
connected += 1;
|
|
2841
2824
|
}
|
|
2842
|
-
|
|
2843
|
-
aiAssist += 1;
|
|
2844
|
-
}
|
|
2845
|
-
devices.set(deviceId, device);
|
|
2825
|
+
devices.set(deviceId, device);
|
|
2846
2826
|
}
|
|
2847
2827
|
|
|
2848
2828
|
emitRemoteEvent('RemoteSyntheticFleetSeeded', null, {
|
|
2849
|
-
count,
|
|
2850
|
-
connected,
|
|
2851
|
-
|
|
2852
|
-
live
|
|
2829
|
+
count,
|
|
2830
|
+
connected,
|
|
2831
|
+
live
|
|
2853
2832
|
});
|
|
2854
2833
|
return {
|
|
2855
2834
|
ok: true,
|
|
2856
2835
|
synthetic: true,
|
|
2857
|
-
seeded: count,
|
|
2858
|
-
connected,
|
|
2859
|
-
|
|
2860
|
-
live,
|
|
2836
|
+
seeded: count,
|
|
2837
|
+
connected,
|
|
2838
|
+
live,
|
|
2861
2839
|
total: devices.size,
|
|
2862
2840
|
max: MAX_SYNTHETIC_DEVICES
|
|
2863
2841
|
};
|
|
@@ -3754,12 +3732,10 @@ export function createRemoteHub(options = {}) {
|
|
|
3754
3732
|
task.updatedAt = now;
|
|
3755
3733
|
task.completedAt = safeString(result?.completedAt, 80) || now;
|
|
3756
3734
|
task.error = resultError;
|
|
3757
|
-
task.resultSummary = resultError || summarizeTaskResult(result);
|
|
3758
|
-
task.resultData = safeTaskData(result?.data);
|
|
3759
|
-
task.resultKind = safeString(result?.kind || result?.mode || 'agent-task', 80);
|
|
3760
|
-
|
|
3761
|
-
task.resultResponseId = safeString(result?.responseId || result?.id || '', 160);
|
|
3762
|
-
device.latestTask = task;
|
|
3735
|
+
task.resultSummary = resultError || summarizeTaskResult(result);
|
|
3736
|
+
task.resultData = safeTaskData(result?.data);
|
|
3737
|
+
task.resultKind = safeString(result?.kind || result?.mode || 'agent-task', 80);
|
|
3738
|
+
device.latestTask = task;
|
|
3763
3739
|
device.pendingTaskCommands.delete(commandId);
|
|
3764
3740
|
device.counters.taskResultsReceived += 1;
|
|
3765
3741
|
if (status === 'failed') {
|
|
@@ -5862,9 +5838,13 @@ export function createRemoteHub(options = {}) {
|
|
|
5862
5838
|
}
|
|
5863
5839
|
|
|
5864
5840
|
function requestAgentTask(deviceId, options = {}) {
|
|
5865
|
-
const
|
|
5866
|
-
|
|
5867
|
-
|
|
5841
|
+
const retiredRequestError = getRetiredAgentTaskError(options);
|
|
5842
|
+
if (retiredRequestError) {
|
|
5843
|
+
return { ok: false, error: retiredRequestError };
|
|
5844
|
+
}
|
|
5845
|
+
const device = devices.get(String(deviceId || ''));
|
|
5846
|
+
const requestedOperation = safeString(options.operation, 80);
|
|
5847
|
+
const operation = normalizeAgentOperation(requestedOperation);
|
|
5868
5848
|
if (requestedOperation && !operation) {
|
|
5869
5849
|
return { ok: false, error: 'unsupported-agent-operation' };
|
|
5870
5850
|
}
|
|
@@ -5882,10 +5862,6 @@ export function createRemoteHub(options = {}) {
|
|
|
5882
5862
|
|
|
5883
5863
|
const now = new Date().toISOString();
|
|
5884
5864
|
const approvalLevel = normalizeApprovalLevel(options.approvalLevel);
|
|
5885
|
-
if (approvalLevel === 'ai-assist' && !readCapabilityFlag(device.capabilities, 'aiAssist')) {
|
|
5886
|
-
return { ok: false, error: 'device-ai-assist-unavailable' };
|
|
5887
|
-
}
|
|
5888
|
-
|
|
5889
5865
|
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
5890
5866
|
const taskId = safeString(options.taskId, 128) || crypto.randomUUID();
|
|
5891
5867
|
const title = safeString(options.title, 120)
|
|
@@ -5907,24 +5883,12 @@ export function createRemoteHub(options = {}) {
|
|
|
5907
5883
|
requestedAt: now,
|
|
5908
5884
|
sentAt: now,
|
|
5909
5885
|
updatedAt: now,
|
|
5910
|
-
completedAt: now,
|
|
5911
|
-
error: '',
|
|
5912
|
-
resultKind:
|
|
5913
|
-
|
|
5914
|
-
?
|
|
5915
|
-
:
|
|
5916
|
-
resultResponseId: approvalLevel === 'ai-assist'
|
|
5917
|
-
? `synthetic-response-${taskId}`
|
|
5918
|
-
: '',
|
|
5919
|
-
resultSummary: operation === 'process.list'
|
|
5920
|
-
? (safeString(options.targetQuery, 160).toLowerCase() === 'comfyui'
|
|
5921
|
-
? (device.status?.tools?.comfyui?.status === 'online'
|
|
5922
|
-
? 'ComfyUI is running.'
|
|
5923
|
-
: 'ComfyUI is not running.')
|
|
5924
|
-
: 'Process list collected.')
|
|
5925
|
-
: approvalLevel === 'ai-assist'
|
|
5926
|
-
? `Synthetic AI assist completed for ${device.deviceName}.`
|
|
5927
|
-
: `Synthetic task accepted by ${device.deviceName}.`,
|
|
5886
|
+
completedAt: now,
|
|
5887
|
+
error: '',
|
|
5888
|
+
resultKind: 'synthetic-agent-task',
|
|
5889
|
+
resultSummary: operation === 'process.list'
|
|
5890
|
+
? 'Process list collected.'
|
|
5891
|
+
: `Synthetic task accepted by ${device.deviceName}.`,
|
|
5928
5892
|
resultData: undefined
|
|
5929
5893
|
};
|
|
5930
5894
|
|
|
@@ -5964,10 +5928,6 @@ export function createRemoteHub(options = {}) {
|
|
|
5964
5928
|
|
|
5965
5929
|
const now = new Date().toISOString();
|
|
5966
5930
|
const approvalLevel = normalizeApprovalLevel(options.approvalLevel);
|
|
5967
|
-
if (approvalLevel === 'ai-assist' && !readCapabilityFlag(device.capabilities, 'aiAssist')) {
|
|
5968
|
-
return { ok: false, error: 'device-ai-assist-unavailable' };
|
|
5969
|
-
}
|
|
5970
|
-
|
|
5971
5931
|
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
5972
5932
|
const taskId = safeString(options.taskId, 128) || crypto.randomUUID();
|
|
5973
5933
|
const title = safeString(options.title, 120)
|
|
@@ -5989,14 +5949,12 @@ export function createRemoteHub(options = {}) {
|
|
|
5989
5949
|
requestedAt: now,
|
|
5990
5950
|
sentAt: now,
|
|
5991
5951
|
updatedAt: now,
|
|
5992
|
-
completedAt: '',
|
|
5993
|
-
error: '',
|
|
5994
|
-
resultKind: '',
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
resultData: undefined
|
|
5999
|
-
};
|
|
5952
|
+
completedAt: '',
|
|
5953
|
+
error: '',
|
|
5954
|
+
resultKind: '',
|
|
5955
|
+
resultSummary: '',
|
|
5956
|
+
resultData: undefined
|
|
5957
|
+
};
|
|
6000
5958
|
|
|
6001
5959
|
const commandName = operation || 'agent.task';
|
|
6002
5960
|
const sent = writeJsonLine(device.socket, {
|
|
@@ -6012,8 +5970,7 @@ export function createRemoteHub(options = {}) {
|
|
|
6012
5970
|
toolArguments: options.toolArguments && typeof options.toolArguments === 'object' ? options.toolArguments : {},
|
|
6013
5971
|
permissionMode: safeString(options.permissionMode, 40) || 'ask',
|
|
6014
5972
|
approvalLevel,
|
|
6015
|
-
|
|
6016
|
-
requestedAt: now
|
|
5973
|
+
requestedAt: now
|
|
6017
5974
|
},
|
|
6018
5975
|
issuedAt: now
|
|
6019
5976
|
});
|
|
@@ -6035,15 +5992,26 @@ export function createRemoteHub(options = {}) {
|
|
|
6035
5992
|
approvalLevel
|
|
6036
5993
|
});
|
|
6037
5994
|
return { ok: true, commandId, taskId, approvalLevel };
|
|
6038
|
-
}
|
|
6039
|
-
|
|
6040
|
-
function requestAgentTaskBatch(deviceIds, options = {}) {
|
|
6041
|
-
const targets = [...new Set((deviceIds || [])
|
|
6042
|
-
.map(deviceId => safeString(deviceId, 128))
|
|
6043
|
-
.filter(Boolean))]
|
|
6044
|
-
.slice(0, 500);
|
|
6045
|
-
|
|
6046
|
-
if (
|
|
5995
|
+
}
|
|
5996
|
+
|
|
5997
|
+
function requestAgentTaskBatch(deviceIds, options = {}) {
|
|
5998
|
+
const targets = [...new Set((deviceIds || [])
|
|
5999
|
+
.map(deviceId => safeString(deviceId, 128))
|
|
6000
|
+
.filter(Boolean))]
|
|
6001
|
+
.slice(0, 500);
|
|
6002
|
+
const retiredRequestError = getRetiredAgentTaskError(options);
|
|
6003
|
+
if (retiredRequestError) {
|
|
6004
|
+
return {
|
|
6005
|
+
ok: false,
|
|
6006
|
+
error: retiredRequestError,
|
|
6007
|
+
total: targets.length,
|
|
6008
|
+
queued: 0,
|
|
6009
|
+
approvalLevel: normalizeApprovalLevel(options.approvalLevel),
|
|
6010
|
+
results: []
|
|
6011
|
+
};
|
|
6012
|
+
}
|
|
6013
|
+
|
|
6014
|
+
if (targets.length === 0) {
|
|
6047
6015
|
return {
|
|
6048
6016
|
ok: false,
|
|
6049
6017
|
error: 'no-target-devices',
|
package/src/server.js
CHANGED
|
@@ -1427,10 +1427,10 @@ function normalizeMonitorSelections(value) {
|
|
|
1427
1427
|
return result;
|
|
1428
1428
|
}
|
|
1429
1429
|
|
|
1430
|
-
function normalizeDeviceIds(value) {
|
|
1431
|
-
const raw = Array.isArray(value)
|
|
1432
|
-
? value
|
|
1433
|
-
: String(value || '').split(',');
|
|
1430
|
+
function normalizeDeviceIds(value) {
|
|
1431
|
+
const raw = Array.isArray(value)
|
|
1432
|
+
? value
|
|
1433
|
+
: String(value || '').split(',');
|
|
1434
1434
|
const ids = [];
|
|
1435
1435
|
const seen = new Set();
|
|
1436
1436
|
for (const entry of raw) {
|
|
@@ -1444,10 +1444,24 @@ function normalizeDeviceIds(value) {
|
|
|
1444
1444
|
break;
|
|
1445
1445
|
}
|
|
1446
1446
|
}
|
|
1447
|
-
return ids;
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
function
|
|
1447
|
+
return ids;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
function rejectRetiredAgentTaskRequest(req, res) {
|
|
1451
|
+
const body = req.body && typeof req.body === 'object' ? req.body : {};
|
|
1452
|
+
const approvalLevel = String(body.approvalLevel || '').trim().toLowerCase();
|
|
1453
|
+
const hasModelSelection = Object.prototype.hasOwnProperty.call(body, 'model');
|
|
1454
|
+
if (approvalLevel !== 'ai-assist' && !hasModelSelection) {
|
|
1455
|
+
return false;
|
|
1456
|
+
}
|
|
1457
|
+
res.status(400).json({
|
|
1458
|
+
ok: false,
|
|
1459
|
+
error: 'agent-ai-assist-retired'
|
|
1460
|
+
});
|
|
1461
|
+
return true;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
function normalizeTransferFiles(value) {
|
|
1451
1465
|
const rawFiles = Array.isArray(value) ? value : [];
|
|
1452
1466
|
const files = [];
|
|
1453
1467
|
let totalBytes = 0;
|
|
@@ -2985,7 +2999,7 @@ app.post('/api/settings/agent/run', async (req, res) => {
|
|
|
2985
2999
|
noStore(res);
|
|
2986
3000
|
try {
|
|
2987
3001
|
if (!(await synchronizeAgentEnablement())) {
|
|
2988
|
-
throw new AgentProviderError('agent-ai-disabled', 'Enable
|
|
3002
|
+
throw new AgentProviderError('agent-ai-disabled', 'Enable Codex Agent in Settings before running commands.', { status: 409 });
|
|
2989
3003
|
}
|
|
2990
3004
|
const instruction = typeof req.body?.instruction === 'string' ? req.body.instruction.slice(0, 4000) : '';
|
|
2991
3005
|
const deviceIds = normalizeDeviceIds(req.body?.deviceIds).slice(0, 500);
|
|
@@ -3775,12 +3789,11 @@ app.delete('/api/remote/host-target', async (req, res) => {
|
|
|
3775
3789
|
|
|
3776
3790
|
app.post('/api/remote/synthetic/seed', (req, res) => {
|
|
3777
3791
|
noStore(res);
|
|
3778
|
-
res.json(remoteHub.seedSyntheticFleet({
|
|
3779
|
-
count: req.body?.count,
|
|
3780
|
-
connectedRatio: req.body?.connectedRatio,
|
|
3781
|
-
thumbnailRatio: req.body?.thumbnailRatio,
|
|
3782
|
-
|
|
3783
|
-
liveCount: req.body?.liveCount,
|
|
3792
|
+
res.json(remoteHub.seedSyntheticFleet({
|
|
3793
|
+
count: req.body?.count,
|
|
3794
|
+
connectedRatio: req.body?.connectedRatio,
|
|
3795
|
+
thumbnailRatio: req.body?.thumbnailRatio,
|
|
3796
|
+
liveCount: req.body?.liveCount,
|
|
3784
3797
|
replace: req.body?.replace
|
|
3785
3798
|
}));
|
|
3786
3799
|
});
|
|
@@ -4075,42 +4088,41 @@ app.post('/api/remote/devices/:deviceId/quick-actions', requireHubFeatureAccess,
|
|
|
4075
4088
|
});
|
|
4076
4089
|
|
|
4077
4090
|
app.post('/api/remote/devices/:deviceId/tasks', requireHubFeatureAccess, (req, res) => {
|
|
4078
|
-
noStore(res);
|
|
4079
|
-
|
|
4091
|
+
noStore(res);
|
|
4092
|
+
if (rejectRetiredAgentTaskRequest(req, res)) {
|
|
4093
|
+
return;
|
|
4094
|
+
}
|
|
4095
|
+
res.json(remoteHub.requestAgentTask(req.params.deviceId, {
|
|
4080
4096
|
instruction: req.body?.instruction,
|
|
4081
4097
|
title: req.body?.title,
|
|
4082
4098
|
taskId: req.body?.taskId,
|
|
4083
4099
|
commandId: req.body?.commandId,
|
|
4084
|
-
approvalLevel: req.body?.approvalLevel,
|
|
4085
|
-
operation: req.body?.operation,
|
|
4086
|
-
targetQuery: req.body?.targetQuery
|
|
4087
|
-
model: req.body?.model
|
|
4100
|
+
approvalLevel: req.body?.approvalLevel,
|
|
4101
|
+
operation: req.body?.operation,
|
|
4102
|
+
targetQuery: req.body?.targetQuery
|
|
4088
4103
|
}));
|
|
4089
4104
|
});
|
|
4090
4105
|
|
|
4091
|
-
app.post('/api/remote/tasks', requireHubFeatureAccess, (req, res) => {
|
|
4092
|
-
noStore(res);
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
: 'task-only';
|
|
4106
|
+
app.post('/api/remote/tasks', requireHubFeatureAccess, (req, res) => {
|
|
4107
|
+
noStore(res);
|
|
4108
|
+
if (rejectRetiredAgentTaskRequest(req, res)) {
|
|
4109
|
+
return;
|
|
4110
|
+
}
|
|
4111
|
+
const requestedIds = normalizeDeviceIds(req.body?.deviceIds);
|
|
4112
|
+
const approvalLevel = req.body?.approvalLevel === 'read-only' ? 'read-only' : 'task-only';
|
|
4099
4113
|
const devices = remoteHub.listDevices({ includeDataUrl: false });
|
|
4100
4114
|
const targetIds = requestedIds.length > 0
|
|
4101
4115
|
? requestedIds
|
|
4102
|
-
: devices
|
|
4103
|
-
.filter(device => device.connected)
|
|
4104
|
-
.
|
|
4105
|
-
.map(device => device.deviceId);
|
|
4116
|
+
: devices
|
|
4117
|
+
.filter(device => device.connected)
|
|
4118
|
+
.map(device => device.deviceId);
|
|
4106
4119
|
const result = remoteHub.requestAgentTaskBatch([...new Set(targetIds)].slice(0, 500), {
|
|
4107
4120
|
instruction: req.body?.instruction,
|
|
4108
4121
|
title: req.body?.title,
|
|
4109
|
-
approvalLevel,
|
|
4110
|
-
operation: req.body?.operation,
|
|
4111
|
-
targetQuery: req.body?.targetQuery,
|
|
4112
|
-
|
|
4113
|
-
batchId: req.body?.batchId
|
|
4122
|
+
approvalLevel,
|
|
4123
|
+
operation: req.body?.operation,
|
|
4124
|
+
targetQuery: req.body?.targetQuery,
|
|
4125
|
+
batchId: req.body?.batchId
|
|
4114
4126
|
});
|
|
4115
4127
|
res.json({
|
|
4116
4128
|
...result,
|
|
@@ -21,6 +21,7 @@ const DEFAULT_MAX_PEER_QUEUE_BYTES = 1024 * 1024;
|
|
|
21
21
|
const DEFAULT_MAX_PEER_QUEUE_MESSAGES = 64;
|
|
22
22
|
const DEFAULT_RECONNECT_MIN_MS = 1_000;
|
|
23
23
|
const DEFAULT_RECONNECT_MAX_MS = 30_000;
|
|
24
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 5_000;
|
|
24
25
|
|
|
25
26
|
function isEnabled(value, fallback = false) {
|
|
26
27
|
if (value === undefined || value === null || value === '') return fallback;
|
|
@@ -429,6 +430,11 @@ export function createHubRelayControl({
|
|
|
429
430
|
reconnectMinMs,
|
|
430
431
|
5 * 60_000,
|
|
431
432
|
DEFAULT_RECONNECT_MAX_MS);
|
|
433
|
+
const connectTimeoutMs = boundedInteger(
|
|
434
|
+
env.LIVEDESK_RELAY_CONNECT_TIMEOUT_MS,
|
|
435
|
+
250,
|
|
436
|
+
30_000,
|
|
437
|
+
DEFAULT_CONNECT_TIMEOUT_MS);
|
|
432
438
|
const peers = new Map();
|
|
433
439
|
const retiredPeerIds = new Map();
|
|
434
440
|
const sendQueue = [];
|
|
@@ -449,6 +455,7 @@ export function createHubRelayControl({
|
|
|
449
455
|
let connection = null;
|
|
450
456
|
let connectionGeneration = 0;
|
|
451
457
|
let reconnectTimer = null;
|
|
458
|
+
let connectAttemptTimer = null;
|
|
452
459
|
let reconnectAttempt = 0;
|
|
453
460
|
let receiveBuffer = Buffer.alloc(0);
|
|
454
461
|
let queuedBytes = 0;
|
|
@@ -477,7 +484,8 @@ export function createHubRelayControl({
|
|
|
477
484
|
function releaseQueuedItem(item) {
|
|
478
485
|
queuedBytes = Math.max(0, queuedBytes - item.buffer.length);
|
|
479
486
|
if (!item.peerId) return;
|
|
480
|
-
const stats =
|
|
487
|
+
const stats = peerQueueStats.get(item.peerId);
|
|
488
|
+
if (!stats) return;
|
|
481
489
|
stats.messages = Math.max(0, stats.messages - 1);
|
|
482
490
|
stats.bytes = Math.max(0, stats.bytes - item.buffer.length);
|
|
483
491
|
}
|
|
@@ -566,7 +574,7 @@ export function createHubRelayControl({
|
|
|
566
574
|
roomId,
|
|
567
575
|
peerId,
|
|
568
576
|
reason: safeText(reason, 80) || 'relay-peer-closed'
|
|
569
|
-
}
|
|
577
|
+
});
|
|
570
578
|
}
|
|
571
579
|
|
|
572
580
|
function closePeer(peerId, reason = 'relay-peer-closed', {
|
|
@@ -786,6 +794,24 @@ export function createHubRelayControl({
|
|
|
786
794
|
|
|
787
795
|
function handleRelayEnvelope(message) {
|
|
788
796
|
if (!message || typeof message !== 'object' || Array.isArray(message)) return;
|
|
797
|
+
if (message.type === 'relay.registered') {
|
|
798
|
+
if (message.protocol !== RELAY_CONTROL_PROTOCOL
|
|
799
|
+
|| message.roomId !== roomId
|
|
800
|
+
|| message.role !== 'hub'
|
|
801
|
+
|| safeText(message.peerId, 64) !== ''
|
|
802
|
+
|| state !== 'registering') {
|
|
803
|
+
lastError = 'relay-registration-invalid';
|
|
804
|
+
connection?.destroy();
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
clearConnectAttemptTimer();
|
|
808
|
+
reconnectAttempt = 0;
|
|
809
|
+
lastConnectedAt = new Date().toISOString();
|
|
810
|
+
lastError = '';
|
|
811
|
+
transition('registered');
|
|
812
|
+
logEvent('relay', `Encrypted relay control registered at ${host}:${port}`);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
789
815
|
const peerId = safeText(message.peerId, 64);
|
|
790
816
|
if (message.protocol !== RELAY_CONTROL_PROTOCOL || message.roomId !== roomId) {
|
|
791
817
|
if (peerId) rejectPeer(peerId, 'relay-routing-mismatch');
|
|
@@ -859,8 +885,14 @@ export function createHubRelayControl({
|
|
|
859
885
|
reconnectTimer.unref?.();
|
|
860
886
|
}
|
|
861
887
|
|
|
888
|
+
function clearConnectAttemptTimer() {
|
|
889
|
+
if (connectAttemptTimer) clearTimeout(connectAttemptTimer);
|
|
890
|
+
connectAttemptTimer = null;
|
|
891
|
+
}
|
|
892
|
+
|
|
862
893
|
function handleConnectionClosed(socket, generation) {
|
|
863
894
|
if (connection !== socket || generation !== connectionGeneration) return;
|
|
895
|
+
clearConnectAttemptTimer();
|
|
864
896
|
connection = null;
|
|
865
897
|
receiveBuffer = Buffer.alloc(0);
|
|
866
898
|
clearQueue();
|
|
@@ -887,20 +919,17 @@ export function createHubRelayControl({
|
|
|
887
919
|
socket.once('connect', () => {
|
|
888
920
|
if (connection !== socket || generation !== connectionGeneration || !desired) return;
|
|
889
921
|
counters.connections += 1;
|
|
890
|
-
|
|
891
|
-
lastConnectedAt = new Date().toISOString();
|
|
892
|
-
lastError = '';
|
|
893
|
-
transition('registered');
|
|
922
|
+
transition('registering');
|
|
894
923
|
const registered = enqueueEnvelope({
|
|
895
924
|
type: 'relay.register',
|
|
896
925
|
protocol: RELAY_CONTROL_PROTOCOL,
|
|
897
926
|
roomId,
|
|
898
927
|
role: 'hub',
|
|
899
928
|
peerId: '',
|
|
929
|
+
ack: true,
|
|
900
930
|
token: registrationToken
|
|
901
931
|
});
|
|
902
932
|
if (!registered) socket.destroy();
|
|
903
|
-
else logEvent('relay', `Encrypted relay control registered at ${host}:${port}`);
|
|
904
933
|
});
|
|
905
934
|
socket.on('data', onConnectionData);
|
|
906
935
|
socket.on('drain', () => {
|
|
@@ -914,6 +943,16 @@ export function createHubRelayControl({
|
|
|
914
943
|
logWarn('relay', `Encrypted relay control connection failed code=${lastError}`);
|
|
915
944
|
});
|
|
916
945
|
socket.once('close', () => handleConnectionClosed(socket, generation));
|
|
946
|
+
connectAttemptTimer = setTimeout(() => {
|
|
947
|
+
if (connection !== socket
|
|
948
|
+
|| generation !== connectionGeneration
|
|
949
|
+
|| socket.destroyed
|
|
950
|
+
|| (state !== 'connecting' && state !== 'registering')) return;
|
|
951
|
+
lastError = 'relay-connect-timeout';
|
|
952
|
+
logWarn('relay', `Encrypted relay control connection failed code=${lastError}`);
|
|
953
|
+
socket.destroy();
|
|
954
|
+
}, connectTimeoutMs);
|
|
955
|
+
connectAttemptTimer.unref?.();
|
|
917
956
|
}
|
|
918
957
|
|
|
919
958
|
async function start() {
|
|
@@ -931,6 +970,7 @@ export function createHubRelayControl({
|
|
|
931
970
|
closing = true;
|
|
932
971
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
933
972
|
reconnectTimer = null;
|
|
973
|
+
clearConnectAttemptTimer();
|
|
934
974
|
closeAllPeers('hub-shutdown');
|
|
935
975
|
clearQueue();
|
|
936
976
|
const socket = connection;
|
|
@@ -975,6 +1015,7 @@ export function createHubRelayControl({
|
|
|
975
1015
|
host,
|
|
976
1016
|
port,
|
|
977
1017
|
peerCount: peers.size,
|
|
1018
|
+
peerQueueCount: peerQueueStats.size,
|
|
978
1019
|
queuedMessages: sendQueue.length,
|
|
979
1020
|
queuedBytes,
|
|
980
1021
|
maxPeers,
|