@livedesk/hub 0.1.59 → 0.1.64
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-tool-registry.js +1 -1
- package/src/agents/codex-agent-runtime.js +5 -4
- package/src/auth/workspace-access.js +2 -0
- package/src/control-presentation-borrow-contract.test.mjs +999 -0
- package/src/live-capture-transition-retry.mjs +25 -10
- package/src/live-stream-monitor-contract.js +195 -3
- package/src/plan-device-access.mjs +100 -0
- package/src/plan-device-access.test.mjs +97 -0
- package/src/pwa-runtime-diagnostic-store.mjs +365 -0
- package/src/remote-hub.js +1144 -463
- package/src/server.js +867 -248
- package/src/settings/settings-schema.js +25 -6
- package/src/settings/settings-store.js +9 -8
- package/src/transport/relay-hub-control.js +10 -8
- package/src/wall-source-restart-contract.test.mjs +48 -0
- package/src/wall-source-restart-runtime.test.mjs +146 -0
package/src/remote-hub.js
CHANGED
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
isSecureDirectHandshakeStart
|
|
10
10
|
} from '../../runtime-core/src/direct-secure-transport.js';
|
|
11
11
|
import { createHubRelayControl } from './transport/relay-hub-control.js';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
parseExactLiveStreamMonitorIndex,
|
|
14
|
+
resolveReadOnlyControlPresentationBorrow
|
|
15
|
+
} from './live-stream-monitor-contract.js';
|
|
13
16
|
import {
|
|
14
17
|
BoundedSegmentedBuffer,
|
|
15
18
|
createBoundedAgentBinaryIngressLane
|
|
@@ -53,9 +56,13 @@ const MAX_AGENT_SOCKET_ACCUMULATOR_BYTES = MAX_LINE_CHARS
|
|
|
53
56
|
const HTTP_HEADER_TERMINATOR = Buffer.from('\r\n\r\n', 'ascii');
|
|
54
57
|
const MAX_AGENT_TASK_CHARS = 4000;
|
|
55
58
|
const MAX_AGENT_TASK_RESULT_CHARS = 3000;
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
+
const MAX_AGENT_TASK_DATA_BYTES = 64 * 1024;
|
|
60
|
+
const MAX_AGENT_TASK_PREVIEW_BYTES = 16 * 1024;
|
|
61
|
+
const MAX_AGENT_TOOL_ARGUMENTS_BYTES = 24 * 1024;
|
|
62
|
+
const RECENT_TASK_LIMIT = 12;
|
|
63
|
+
const MAX_PENDING_AGENT_TASKS_PER_DEVICE = RECENT_TASK_LIMIT;
|
|
64
|
+
const RECENT_TASK_BATCH_LIMIT = 16;
|
|
65
|
+
const MAX_AGENT_SOCKET_WRITE_BUFFER_BYTES = MAX_LINE_CHARS + (256 * 1024);
|
|
59
66
|
const RECENT_FRAME_CACHE_TTL_MS = 4000;
|
|
60
67
|
const RECENT_THUMBNAIL_FRAME_CACHE_LIMIT = 2;
|
|
61
68
|
const RECENT_LIVE_FRAME_CACHE_LIMIT = 1;
|
|
@@ -63,9 +70,10 @@ const RECENT_THUMBNAIL_FRAME_CACHE_MAX_BYTES = 2 * 1024 * 1024;
|
|
|
63
70
|
const RECENT_LIVE_FRAME_CACHE_MAX_BYTES = 8 * 1024 * 1024;
|
|
64
71
|
const LIVE_STREAM_PENDING_REUSE_MS = 5000;
|
|
65
72
|
const LIVE_STREAM_REPLACEMENT_PENDING_MS = 7000;
|
|
66
|
-
const LIVE_STREAM_MIN_FRESH_MS = 3000;
|
|
67
|
-
const LIVE_STREAM_MAX_FRESH_MS = 12000;
|
|
68
|
-
const
|
|
73
|
+
const LIVE_STREAM_MIN_FRESH_MS = 3000;
|
|
74
|
+
const LIVE_STREAM_MAX_FRESH_MS = 12000;
|
|
75
|
+
const TASK_BATCH_OWNER = Symbol('remote-hub-task-batch-owner');
|
|
76
|
+
const DEFAULT_REMOTE_INPUT_ACK_TIMEOUT_MS = 5000;
|
|
69
77
|
// RemoteFast owns a shared 7s native stop deadline. Keep four seconds for a
|
|
70
78
|
// priority command.result to cross either Direct or encrypted Relay transport.
|
|
71
79
|
const DEFAULT_LIVE_CAPTURE_STOP_ACK_TIMEOUT_MS = 11000;
|
|
@@ -302,9 +310,17 @@ function normalizePort(value) {
|
|
|
302
310
|
return clampNumber(value, 0, 65535, DEFAULT_REMOTE_HUB_PORT);
|
|
303
311
|
}
|
|
304
312
|
|
|
305
|
-
function safeString(value, maxLength = 200) {
|
|
306
|
-
return String(value ?? '').replace(/[\r\n\t]/g, ' ').trim().slice(0, maxLength);
|
|
307
|
-
}
|
|
313
|
+
function safeString(value, maxLength = 200) {
|
|
314
|
+
return String(value ?? '').replace(/[\r\n\t]/g, ' ').trim().slice(0, maxLength);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function normalizeMediaBinaryKind(value) {
|
|
318
|
+
if (typeof value !== 'string' || value.length > 64) {
|
|
319
|
+
return '';
|
|
320
|
+
}
|
|
321
|
+
const normalized = value.trim().toLowerCase();
|
|
322
|
+
return normalized.length <= 40 ? normalized : '';
|
|
323
|
+
}
|
|
308
324
|
|
|
309
325
|
function remoteInputPayloadType(payload) {
|
|
310
326
|
return safeString(payload?.payload?.type ?? payload?.type, 48).toLowerCase();
|
|
@@ -1221,16 +1237,131 @@ function normalizeRemoteKeyboardText(value) {
|
|
|
1221
1237
|
return raw.slice(0, 256);
|
|
1222
1238
|
}
|
|
1223
1239
|
|
|
1224
|
-
function
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
}
|
|
1240
|
+
function utf8Prefix(value, maxBytes) {
|
|
1241
|
+
const text = String(value ?? '');
|
|
1242
|
+
const encoded = Buffer.from(text, 'utf8');
|
|
1243
|
+
if (encoded.length <= maxBytes) {
|
|
1244
|
+
return text;
|
|
1245
|
+
}
|
|
1246
|
+
let end = Math.max(0, Math.min(encoded.length, Math.floor(maxBytes)));
|
|
1247
|
+
while (end > 0 && end < encoded.length && (encoded[end] & 0xc0) === 0x80) {
|
|
1248
|
+
end -= 1;
|
|
1249
|
+
}
|
|
1250
|
+
return encoded.subarray(0, end).toString('utf8');
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function boundedSerializedTaskData(serialized) {
|
|
1254
|
+
const byteLength = Buffer.byteLength(serialized, 'utf8');
|
|
1255
|
+
if (byteLength <= MAX_AGENT_TASK_DATA_BYTES) {
|
|
1256
|
+
return JSON.parse(serialized);
|
|
1257
|
+
}
|
|
1258
|
+
return {
|
|
1259
|
+
truncated: true,
|
|
1260
|
+
byteLength,
|
|
1261
|
+
preview: utf8Prefix(serialized, MAX_AGENT_TASK_PREVIEW_BYTES)
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function safeTaskData(value) {
|
|
1266
|
+
if (value === undefined || value === null) return undefined;
|
|
1267
|
+
try {
|
|
1268
|
+
const serialized = JSON.stringify(value);
|
|
1269
|
+
return serialized === undefined ? undefined : boundedSerializedTaskData(serialized);
|
|
1270
|
+
} catch {
|
|
1271
|
+
return { truncated: true };
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function isPlainObject(value) {
|
|
1276
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1277
|
+
return false;
|
|
1278
|
+
}
|
|
1279
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1280
|
+
return prototype === Object.prototype || prototype === null;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
const DIAGNOSTIC_AGENT_OPERATIONS = new Set(['logs.collect', 'diagnostics.collect']);
|
|
1284
|
+
const DIAGNOSTIC_LOG_SOURCES = new Set(['vuvodesk', 'system']);
|
|
1285
|
+
const DIAGNOSTIC_SENSITIVE_KEY = /(?:token|secret|password|passwd|api[-_]?key|authorization|private[-_]?key|connection[-_]?string|access[-_]?key|credential|cookie|pair)/i;
|
|
1286
|
+
const DIAGNOSTIC_STANDALONE_SECRET_VALUE = /\b(?:sk-(?:proj-|ant-(?:api\d{2}-)?)?[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|(?:AKIA|ASIA)[A-Z0-9]{16}|AIza[A-Za-z0-9_-]{20,}|npm_[A-Za-z0-9]{20,}|xox[A-Za-z]-[A-Za-z0-9-]{10,}|[sr]k_(?:live|test)_[A-Za-z0-9]{10,})\b/g;
|
|
1287
|
+
|
|
1288
|
+
function redactDiagnosticText(value) {
|
|
1289
|
+
let text = String(value ?? '').replace(/\0/g, '');
|
|
1290
|
+
text = text.replace(
|
|
1291
|
+
/-----BEGIN(?: [A-Z0-9]+)* PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z0-9]+)* PRIVATE KEY-----/gi,
|
|
1292
|
+
'[redacted-private-key]');
|
|
1293
|
+
text = text.replace(/\b(?:Proxy-)?Authorization\s*[:=]\s*[^\r\n]*/gi, 'Authorization: [redacted]');
|
|
1294
|
+
text = text.replace(/\b(?:Set-)?Cookie\s*[:=]\s*[^\r\n]*/gi, 'Cookie: [redacted]');
|
|
1295
|
+
text = text.replace(/\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, '[redacted-authorization]');
|
|
1296
|
+
text = text.replace(DIAGNOSTIC_STANDALONE_SECRET_VALUE, '[redacted-secret]');
|
|
1297
|
+
text = text.replace(/\b(?:eyJ)?[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, '[redacted-jwt]');
|
|
1298
|
+
text = text.replace(
|
|
1299
|
+
/(^|[\s,;?&])([A-Za-z0-9_.-]*(?:token|secret|password|passwd|api[-_]?key|authorization|private[-_]?key|connection[-_]?string|access[-_]?key|credential|cookie|pair)[A-Za-z0-9_.-]*)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;&#]+)/gi,
|
|
1300
|
+
'$1$2=[redacted]');
|
|
1301
|
+
return text;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function diagnosticJsonReplacer(key, value) {
|
|
1305
|
+
if (key && DIAGNOSTIC_SENSITIVE_KEY.test(key)) {
|
|
1306
|
+
return '[redacted]';
|
|
1307
|
+
}
|
|
1308
|
+
return typeof value === 'string' ? redactDiagnosticText(value) : value;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function safeDiagnosticText(value, maxBytes) {
|
|
1312
|
+
return utf8Prefix(redactDiagnosticText(value), maxBytes).trim();
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function safeDiagnosticTaskData(value) {
|
|
1316
|
+
if (value === undefined || value === null) return undefined;
|
|
1317
|
+
try {
|
|
1318
|
+
const serialized = JSON.stringify(value, diagnosticJsonReplacer);
|
|
1319
|
+
return serialized === undefined ? undefined : boundedSerializedTaskData(serialized);
|
|
1320
|
+
} catch {
|
|
1321
|
+
return { truncated: true };
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function normalizeAgentToolArguments(operation, value) {
|
|
1326
|
+
const candidate = value === undefined || value === null ? {} : value;
|
|
1327
|
+
if (!isPlainObject(candidate)) {
|
|
1328
|
+
return { ok: false, error: 'invalid-tool-arguments' };
|
|
1329
|
+
}
|
|
1330
|
+
try {
|
|
1331
|
+
const serialized = JSON.stringify(candidate);
|
|
1332
|
+
if (serialized === undefined || Buffer.byteLength(serialized, 'utf8') > MAX_AGENT_TOOL_ARGUMENTS_BYTES) {
|
|
1333
|
+
return { ok: false, error: 'tool-arguments-too-large' };
|
|
1334
|
+
}
|
|
1335
|
+
const cloned = JSON.parse(serialized);
|
|
1336
|
+
if (!isPlainObject(cloned)) {
|
|
1337
|
+
return { ok: false, error: 'invalid-tool-arguments' };
|
|
1338
|
+
}
|
|
1339
|
+
if (DIAGNOSTIC_AGENT_OPERATIONS.has(operation)) {
|
|
1340
|
+
const requestedSource = safeString(cloned.source, 40).toLowerCase();
|
|
1341
|
+
return {
|
|
1342
|
+
ok: true,
|
|
1343
|
+
value: {
|
|
1344
|
+
source: DIAGNOSTIC_LOG_SOURCES.has(requestedSource) ? requestedSource : 'vuvodesk',
|
|
1345
|
+
maxLines: clampNumber(cloned.maxLines, 1, 500, 100)
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
return { ok: true, value: cloned };
|
|
1350
|
+
} catch {
|
|
1351
|
+
return { ok: false, error: 'invalid-tool-arguments' };
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function safeRetryToolArguments(operation, value) {
|
|
1356
|
+
const normalized = normalizeAgentToolArguments(operation, value);
|
|
1357
|
+
return normalized.ok ? normalized.value : {};
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function normalizeAgentPermissionMode(operation, value) {
|
|
1361
|
+
return DIAGNOSTIC_AGENT_OPERATIONS.has(operation)
|
|
1362
|
+
? 'safe-auto'
|
|
1363
|
+
: safeString(value, 40) || 'ask';
|
|
1364
|
+
}
|
|
1234
1365
|
|
|
1235
1366
|
function normalizeApprovalLevel(value) {
|
|
1236
1367
|
const level = safeString(value, 80).toLowerCase();
|
|
@@ -1279,10 +1410,20 @@ const SUPPORTED_AGENT_OPERATIONS = new Set([
|
|
|
1279
1410
|
'logs.collect'
|
|
1280
1411
|
]);
|
|
1281
1412
|
|
|
1282
|
-
function normalizeAgentOperation(value) {
|
|
1413
|
+
function normalizeAgentOperation(value) {
|
|
1283
1414
|
const operation = safeString(value, 80).toLowerCase();
|
|
1284
1415
|
return SUPPORTED_AGENT_OPERATIONS.has(operation) ? operation : '';
|
|
1285
|
-
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function deviceSupportsAgentOperation(device, operation) {
|
|
1419
|
+
const advertised = device?.capabilities?.agentTools;
|
|
1420
|
+
if (!Array.isArray(advertised)) {
|
|
1421
|
+
// Older Clients did not advertise the list. Keep their established
|
|
1422
|
+
// behavior, but fail closed whenever a Client provides an exact list.
|
|
1423
|
+
return true;
|
|
1424
|
+
}
|
|
1425
|
+
return advertised.some(value => safeString(value, 80) === operation);
|
|
1426
|
+
}
|
|
1286
1427
|
|
|
1287
1428
|
const REMOTE_INPUT_MONITOR_KEYS = Object.freeze([
|
|
1288
1429
|
'monitorIndex',
|
|
@@ -1658,7 +1799,23 @@ function findRecentFramePayload(device, frameKind, options = {}) {
|
|
|
1658
1799
|
return entry?.frame || null;
|
|
1659
1800
|
}
|
|
1660
1801
|
|
|
1661
|
-
function
|
|
1802
|
+
function serializeDeviceTask(task) {
|
|
1803
|
+
if (!task) {
|
|
1804
|
+
return null;
|
|
1805
|
+
}
|
|
1806
|
+
const publicTask = { ...task };
|
|
1807
|
+
const resultData = publicTask.resultData;
|
|
1808
|
+
delete publicTask.resultData;
|
|
1809
|
+
delete publicTask.toolArguments;
|
|
1810
|
+
delete publicTask.retryToolArguments;
|
|
1811
|
+
delete publicTask.retryPermissionMode;
|
|
1812
|
+
return {
|
|
1813
|
+
...publicTask,
|
|
1814
|
+
hasResultData: resultData !== undefined && resultData !== null
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
function serializeDevice(device, options = {}) {
|
|
1662
1819
|
if (!device) {
|
|
1663
1820
|
return null;
|
|
1664
1821
|
}
|
|
@@ -1733,17 +1890,17 @@ function serializeDevice(device, options = {}) {
|
|
|
1733
1890
|
latestAudioStatus: device.latestAudioStatus ? { ...device.latestAudioStatus } : null,
|
|
1734
1891
|
clientUpdateProof: device.clientUpdateProof ? { ...device.clientUpdateProof } : null,
|
|
1735
1892
|
udp: device.udp ? { ...device.udp } : { enabled: false, state: 'tcp-only', ready: false },
|
|
1736
|
-
latestTask: device.latestTask
|
|
1893
|
+
latestTask: serializeDeviceTask(device.latestTask),
|
|
1737
1894
|
synthetic: device.synthetic === true,
|
|
1738
1895
|
recentTasks: Array.isArray(device.recentTasks)
|
|
1739
|
-
? device.recentTasks.map(
|
|
1896
|
+
? device.recentTasks.map(serializeDeviceTask).filter(Boolean)
|
|
1740
1897
|
: [],
|
|
1741
1898
|
status: { ...device.status },
|
|
1742
1899
|
counters: { ...device.counters }
|
|
1743
1900
|
};
|
|
1744
1901
|
}
|
|
1745
1902
|
|
|
1746
|
-
function serializeTaskBatch(batch) {
|
|
1903
|
+
function serializeTaskBatch(batch, options = {}) {
|
|
1747
1904
|
if (!batch) {
|
|
1748
1905
|
return null;
|
|
1749
1906
|
}
|
|
@@ -1765,24 +1922,51 @@ function serializeTaskBatch(batch) {
|
|
|
1765
1922
|
failed: batch.failed,
|
|
1766
1923
|
cancelled: batch.cancelled,
|
|
1767
1924
|
timedOut: batch.timedOut,
|
|
1768
|
-
results: Array.isArray(batch.results)
|
|
1769
|
-
? batch.results.map(item =>
|
|
1770
|
-
|
|
1925
|
+
results: Array.isArray(batch.results)
|
|
1926
|
+
? batch.results.map(item => {
|
|
1927
|
+
const { data, ...publicItem } = item;
|
|
1928
|
+
return options.includeData === true
|
|
1929
|
+
? {
|
|
1930
|
+
...publicItem,
|
|
1931
|
+
hasResultData: data !== undefined && data !== null,
|
|
1932
|
+
data: safeTaskData(data)
|
|
1933
|
+
}
|
|
1934
|
+
: {
|
|
1935
|
+
...publicItem,
|
|
1936
|
+
hasResultData: data !== undefined && data !== null
|
|
1937
|
+
};
|
|
1938
|
+
})
|
|
1939
|
+
: []
|
|
1771
1940
|
};
|
|
1772
1941
|
}
|
|
1773
1942
|
|
|
1774
|
-
function writeJsonLine(socket, payload) {
|
|
1943
|
+
function writeJsonLine(socket, payload) {
|
|
1775
1944
|
if (!socket || socket.destroyed) {
|
|
1776
1945
|
return false;
|
|
1777
1946
|
}
|
|
1778
1947
|
|
|
1779
|
-
if (socket.__remoteHubWebSocket === true && typeof socket.sendJson === 'function') {
|
|
1780
|
-
return socket.sendJson(payload);
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1948
|
+
if (socket.__remoteHubWebSocket === true && typeof socket.sendJson === 'function') {
|
|
1949
|
+
return socket.sendJson(payload);
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
try {
|
|
1953
|
+
const line = Buffer.from(`${JSON.stringify(payload)}\n`, 'utf8');
|
|
1954
|
+
const bufferedBytes = Math.max(0, Number(socket.writableLength || 0));
|
|
1955
|
+
if (bufferedBytes + line.length > MAX_AGENT_SOCKET_WRITE_BUFFER_BYTES) {
|
|
1956
|
+
socket.destroy();
|
|
1957
|
+
return false;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// net.Socket.write(false) means the bytes were accepted into Node's
|
|
1961
|
+
// writable queue. Keep that accepted write successful, but never allow
|
|
1962
|
+
// later writes to grow the queue past the fixed byte cap above.
|
|
1963
|
+
socket.write(line);
|
|
1964
|
+
return true;
|
|
1965
|
+
} catch {
|
|
1966
|
+
socket.destroy();
|
|
1967
|
+
return false;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1786
1970
|
|
|
1787
1971
|
function writeJsonLineAndClose(socket, payload) {
|
|
1788
1972
|
if (!socket || socket.destroyed) {
|
|
@@ -2047,16 +2231,26 @@ export class RemoteHubWebSocketAgentSocket {
|
|
|
2047
2231
|
return this.sendFrame(0x1, Buffer.from(String(text || ''), 'utf8'));
|
|
2048
2232
|
}
|
|
2049
2233
|
|
|
2050
|
-
sendFrame(opcode, payload) {
|
|
2051
|
-
if (this.destroyed || this.socket.destroyed) {
|
|
2052
|
-
return false;
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
try {
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2234
|
+
sendFrame(opcode, payload) {
|
|
2235
|
+
if (this.destroyed || this.socket.destroyed) {
|
|
2236
|
+
return false;
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
try {
|
|
2240
|
+
const frame = buildRemoteHubWebSocketFrame(opcode, payload);
|
|
2241
|
+
const bufferedBytes = Math.max(0, Number(this.socket?.writableLength || 0));
|
|
2242
|
+
if (bufferedBytes + frame.length > MAX_AGENT_SOCKET_WRITE_BUFFER_BYTES) {
|
|
2243
|
+
this.destroy();
|
|
2244
|
+
return false;
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
// The raw socket owns a single bounded queue for every WebSocket
|
|
2248
|
+
// message. A false return still means this exact frame was queued.
|
|
2249
|
+
this.socket.write(frame);
|
|
2250
|
+
return true;
|
|
2251
|
+
} catch {
|
|
2252
|
+
this.destroy();
|
|
2253
|
+
return false;
|
|
2060
2254
|
}
|
|
2061
2255
|
}
|
|
2062
2256
|
|
|
@@ -2303,7 +2497,7 @@ export class RemoteHubWebSocketAgentSocket {
|
|
|
2303
2497
|
}
|
|
2304
2498
|
}
|
|
2305
2499
|
|
|
2306
|
-
export function createRemoteHub(options = {}) {
|
|
2500
|
+
export function createRemoteHub(options = {}) {
|
|
2307
2501
|
const env = options.env || process.env;
|
|
2308
2502
|
const transportDiagnosticCommandTimeoutMs = clampNumber(
|
|
2309
2503
|
options.transportDiagnosticCommandTimeoutMs,
|
|
@@ -2367,13 +2561,18 @@ export function createRemoteHub(options = {}) {
|
|
|
2367
2561
|
defaultDeviceConnectionStaleMs,
|
|
2368
2562
|
10 * 60 * 1000,
|
|
2369
2563
|
defaultDeviceConnectionStaleMs);
|
|
2370
|
-
const deviceConnectionSweepMs = Number.isFinite(Number(options.deviceConnectionSweepMs))
|
|
2371
|
-
? clampNumber(options.deviceConnectionSweepMs, 25, 60000, DEFAULT_DEVICE_CONNECTION_SWEEP_MS)
|
|
2372
|
-
: clampNumber(
|
|
2373
|
-
env.REMOTE_HUB_DEVICE_CONNECTION_SWEEP_MS,
|
|
2374
|
-
1000,
|
|
2375
|
-
60000,
|
|
2376
|
-
Math.min(DEFAULT_DEVICE_CONNECTION_SWEEP_MS, heartbeatMs));
|
|
2564
|
+
const deviceConnectionSweepMs = Number.isFinite(Number(options.deviceConnectionSweepMs))
|
|
2565
|
+
? clampNumber(options.deviceConnectionSweepMs, 25, 60000, DEFAULT_DEVICE_CONNECTION_SWEEP_MS)
|
|
2566
|
+
: clampNumber(
|
|
2567
|
+
env.REMOTE_HUB_DEVICE_CONNECTION_SWEEP_MS,
|
|
2568
|
+
1000,
|
|
2569
|
+
60000,
|
|
2570
|
+
Math.min(DEFAULT_DEVICE_CONNECTION_SWEEP_MS, heartbeatMs));
|
|
2571
|
+
const udpStartRetryInitialMs = clampNumber(
|
|
2572
|
+
options.udpStartRetryMs ?? env.LIVEDESK_UDP_START_RETRY_MS,
|
|
2573
|
+
250,
|
|
2574
|
+
60000,
|
|
2575
|
+
5000);
|
|
2377
2576
|
const taskTimeoutMs = clampNumber(
|
|
2378
2577
|
options.taskTimeoutMs ?? env.REMOTE_HUB_TASK_TIMEOUT_MS,
|
|
2379
2578
|
50,
|
|
@@ -2412,13 +2611,14 @@ export function createRemoteHub(options = {}) {
|
|
|
2412
2611
|
false);
|
|
2413
2612
|
const relayControl = options.relayControl && typeof options.relayControl === 'object'
|
|
2414
2613
|
? options.relayControl
|
|
2415
|
-
: createHubRelayControl({
|
|
2416
|
-
env,
|
|
2417
|
-
pairToken,
|
|
2418
|
-
logEvent,
|
|
2419
|
-
logWarn,
|
|
2420
|
-
|
|
2421
|
-
|
|
2614
|
+
: createHubRelayControl({
|
|
2615
|
+
env,
|
|
2616
|
+
pairToken,
|
|
2617
|
+
logEvent,
|
|
2618
|
+
logWarn,
|
|
2619
|
+
retiredPayloadRelayTestOnly: options.retiredPayloadRelayTestOnly === true,
|
|
2620
|
+
onPeerSocket: socket => handleTcpAgentSocket(socket)
|
|
2621
|
+
});
|
|
2422
2622
|
let pairingPin = /^\d{6}$/.test(String(options.pairingPin || env.LIVEDESK_PAIRING_PIN || '').trim())
|
|
2423
2623
|
? String(options.pairingPin || env.LIVEDESK_PAIRING_PIN).trim()
|
|
2424
2624
|
: generatePairingPin();
|
|
@@ -2447,11 +2647,15 @@ export function createRemoteHub(options = {}) {
|
|
|
2447
2647
|
const clipboardOperations = new Map();
|
|
2448
2648
|
const transportDiagnostics = new Map();
|
|
2449
2649
|
const transportDiagnosticStartingDevices = new Set();
|
|
2450
|
-
const duplicateDeviceLogAt = new Map();
|
|
2451
|
-
let server = null;
|
|
2452
|
-
let started = false;
|
|
2453
|
-
let
|
|
2454
|
-
let
|
|
2650
|
+
const duplicateDeviceLogAt = new Map();
|
|
2651
|
+
let server = null;
|
|
2652
|
+
let started = false;
|
|
2653
|
+
let closing = false;
|
|
2654
|
+
let deviceConnectionSweepTimer = null;
|
|
2655
|
+
let udpStartRetryTimer = null;
|
|
2656
|
+
let udpStartRetryDelayMs = udpStartRetryInitialMs;
|
|
2657
|
+
let udpStartPromise = null;
|
|
2658
|
+
let boundPort = requestedPort;
|
|
2455
2659
|
let lastError = '';
|
|
2456
2660
|
let hostTarget = null;
|
|
2457
2661
|
let publicIPv4Cache = {
|
|
@@ -2760,12 +2964,20 @@ export function createRemoteHub(options = {}) {
|
|
|
2760
2964
|
const activeHostTarget = serializeHostTarget();
|
|
2761
2965
|
const routeInfo = getAgentEndpointRouteInfo();
|
|
2762
2966
|
const diagnostics = sampleHubRuntimeDiagnostics();
|
|
2763
|
-
diagnostics.network = {
|
|
2764
|
-
lanDeviceCount: connectedRoutes.length - externalDeviceCount,
|
|
2765
|
-
externalDeviceCount,
|
|
2766
|
-
route: externalDeviceCount > 0 ? 'external' : connectedRoutes.length > 0 ? 'lan' : 'idle'
|
|
2767
|
-
};
|
|
2768
|
-
|
|
2967
|
+
diagnostics.network = {
|
|
2968
|
+
lanDeviceCount: connectedRoutes.length - externalDeviceCount,
|
|
2969
|
+
externalDeviceCount,
|
|
2970
|
+
route: externalDeviceCount > 0 ? 'external' : connectedRoutes.length > 0 ? 'lan' : 'idle'
|
|
2971
|
+
};
|
|
2972
|
+
const udpStatus = udpTransport?.getStatus?.() || {
|
|
2973
|
+
enabled: false,
|
|
2974
|
+
preferred: false,
|
|
2975
|
+
started: false,
|
|
2976
|
+
port: 0,
|
|
2977
|
+
sessionCount: 0,
|
|
2978
|
+
readySessionCount: 0
|
|
2979
|
+
};
|
|
2980
|
+
return {
|
|
2769
2981
|
enabled,
|
|
2770
2982
|
started,
|
|
2771
2983
|
host,
|
|
@@ -2809,14 +3021,14 @@ export function createRemoteHub(options = {}) {
|
|
|
2809
3021
|
publicIPv4Address: publicIPv4Cache.address,
|
|
2810
3022
|
publicIPv4UpdatedAt: publicIPv4Cache.updatedAt ? new Date(publicIPv4Cache.updatedAt).toISOString() : '',
|
|
2811
3023
|
publicIPv4Error: publicIPv4Cache.error,
|
|
2812
|
-
udp:
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
},
|
|
3024
|
+
udp: {
|
|
3025
|
+
...udpStatus,
|
|
3026
|
+
startRetryTimerActive: udpStartRetryTimer !== null,
|
|
3027
|
+
startInFlight: udpStartPromise !== null,
|
|
3028
|
+
startRetryDelayMs: udpStartRetryTimer !== null
|
|
3029
|
+
? udpStartRetryDelayMs
|
|
3030
|
+
: 0
|
|
3031
|
+
},
|
|
2820
3032
|
relay: relayControl?.getStatus?.() || {
|
|
2821
3033
|
enabled: false,
|
|
2822
3034
|
started: false,
|
|
@@ -2915,8 +3127,8 @@ export function createRemoteHub(options = {}) {
|
|
|
2915
3127
|
return listTaskBatches()[0] || null;
|
|
2916
3128
|
}
|
|
2917
3129
|
|
|
2918
|
-
function getTaskBatch(batchId) {
|
|
2919
|
-
return serializeTaskBatch(taskBatches.get(safeString(batchId, 128)) || null);
|
|
3130
|
+
function getTaskBatch(batchId) {
|
|
3131
|
+
return serializeTaskBatch(taskBatches.get(safeString(batchId, 128)) || null, { includeData: true });
|
|
2920
3132
|
}
|
|
2921
3133
|
|
|
2922
3134
|
function cancelTaskBatch(batchId) {
|
|
@@ -2971,12 +3183,14 @@ export function createRemoteHub(options = {}) {
|
|
|
2971
3183
|
if (targetIds.length === 0) {
|
|
2972
3184
|
return { ok: false, error: 'no-retry-targets' };
|
|
2973
3185
|
}
|
|
2974
|
-
return requestAgentTaskBatch(targetIds, {
|
|
2975
|
-
instruction: batch.instructionPreview,
|
|
2976
|
-
title: batch.title,
|
|
2977
|
-
operation: batch.operation,
|
|
2978
|
-
targetQuery: batch.targetQuery,
|
|
2979
|
-
approvalLevel: batch.approvalLevel
|
|
3186
|
+
return requestAgentTaskBatch(targetIds, {
|
|
3187
|
+
instruction: batch.instructionPreview,
|
|
3188
|
+
title: batch.title,
|
|
3189
|
+
operation: batch.operation,
|
|
3190
|
+
targetQuery: batch.targetQuery,
|
|
3191
|
+
approvalLevel: batch.approvalLevel,
|
|
3192
|
+
toolArguments: batch.retryToolArguments,
|
|
3193
|
+
permissionMode: batch.retryPermissionMode
|
|
2980
3194
|
});
|
|
2981
3195
|
}
|
|
2982
3196
|
|
|
@@ -3011,18 +3225,20 @@ export function createRemoteHub(options = {}) {
|
|
|
3011
3225
|
}
|
|
3012
3226
|
}
|
|
3013
3227
|
|
|
3014
|
-
function settlePendingCommandResult(device, message = {}) {
|
|
3015
|
-
|
|
3228
|
+
function settlePendingCommandResult(device, message = {}, channel = 'control') {
|
|
3229
|
+
channel = safeString(channel, 24) || 'control';
|
|
3230
|
+
const commandId = safeString(message.commandId, 128);
|
|
3016
3231
|
const waiterKey = buildPendingCommandResultWaiterKey(
|
|
3017
3232
|
device?.deviceId,
|
|
3018
3233
|
device?.sessionId,
|
|
3019
3234
|
commandId);
|
|
3020
3235
|
const waiter = pendingCommandResultWaiters.get(waiterKey);
|
|
3021
|
-
if (!waiter
|
|
3022
|
-
|| waiter.deviceId !== device?.deviceId
|
|
3023
|
-
|| waiter.sessionId !== device?.sessionId
|
|
3024
|
-
|
|
3025
|
-
|
|
3236
|
+
if (!waiter
|
|
3237
|
+
|| waiter.deviceId !== device?.deviceId
|
|
3238
|
+
|| waiter.sessionId !== device?.sessionId
|
|
3239
|
+
|| (safeString(waiter.channel, 24) || 'control') !== channel) {
|
|
3240
|
+
return false;
|
|
3241
|
+
}
|
|
3026
3242
|
|
|
3027
3243
|
pendingCommandResultWaiters.delete(waiterKey);
|
|
3028
3244
|
clearPendingCommandResultWaiterDeadline(waiter);
|
|
@@ -4818,7 +5034,107 @@ export function createRemoteHub(options = {}) {
|
|
|
4818
5034
|
};
|
|
4819
5035
|
}
|
|
4820
5036
|
|
|
4821
|
-
function
|
|
5037
|
+
function registerCurrentDeviceUdpSession(device) {
|
|
5038
|
+
const socket = device?.socket;
|
|
5039
|
+
if (!device?.connected
|
|
5040
|
+
|| !socket
|
|
5041
|
+
|| socket.destroyed
|
|
5042
|
+
|| devices.get(device.deviceId) !== device) {
|
|
5043
|
+
return null;
|
|
5044
|
+
}
|
|
5045
|
+
if (device.udp?.sessionId) {
|
|
5046
|
+
return device.udp;
|
|
5047
|
+
}
|
|
5048
|
+
const sessionId = device.sessionId;
|
|
5049
|
+
const udpSession = udpTransport?.registerDevice?.({
|
|
5050
|
+
deviceId: device.deviceId,
|
|
5051
|
+
sessionId,
|
|
5052
|
+
sendControl: payload => {
|
|
5053
|
+
if (devices.get(device.deviceId) === device
|
|
5054
|
+
&& device.sessionId === sessionId
|
|
5055
|
+
&& device.socket === socket
|
|
5056
|
+
&& !socket.destroyed) {
|
|
5057
|
+
writeJsonLine(socket, payload);
|
|
5058
|
+
}
|
|
5059
|
+
},
|
|
5060
|
+
capabilities: device.capabilities
|
|
5061
|
+
});
|
|
5062
|
+
if (devices.get(device.deviceId) !== device
|
|
5063
|
+
|| device.sessionId !== sessionId
|
|
5064
|
+
|| device.socket !== socket
|
|
5065
|
+
|| socket.destroyed) {
|
|
5066
|
+
udpTransport?.unregisterDevice?.(device.deviceId, sessionId);
|
|
5067
|
+
return null;
|
|
5068
|
+
}
|
|
5069
|
+
if (udpSession?.sessionId) {
|
|
5070
|
+
device.udp = {
|
|
5071
|
+
...device.udp,
|
|
5072
|
+
state: 'udp-session-created',
|
|
5073
|
+
sessionId: udpSession.sessionId
|
|
5074
|
+
};
|
|
5075
|
+
}
|
|
5076
|
+
return udpSession || null;
|
|
5077
|
+
}
|
|
5078
|
+
|
|
5079
|
+
function scheduleOptionalUdpStartRetry() {
|
|
5080
|
+
if (closing
|
|
5081
|
+
|| !started
|
|
5082
|
+
|| udpStartRetryTimer
|
|
5083
|
+
|| udpTransport?.getStatus?.().enabled === false) {
|
|
5084
|
+
return;
|
|
5085
|
+
}
|
|
5086
|
+
const delayMs = udpStartRetryDelayMs;
|
|
5087
|
+
udpStartRetryTimer = setTimeout(() => {
|
|
5088
|
+
udpStartRetryTimer = null;
|
|
5089
|
+
udpStartRetryDelayMs = Math.min(30000, delayMs * 2);
|
|
5090
|
+
void startOptionalUdpTransport('retry');
|
|
5091
|
+
}, delayMs);
|
|
5092
|
+
udpStartRetryTimer.unref?.();
|
|
5093
|
+
}
|
|
5094
|
+
|
|
5095
|
+
function startOptionalUdpTransport(reason) {
|
|
5096
|
+
if (!udpTransport?.start
|
|
5097
|
+
|| udpTransport?.getStatus?.().enabled === false
|
|
5098
|
+
|| closing
|
|
5099
|
+
|| !started) {
|
|
5100
|
+
return Promise.resolve(false);
|
|
5101
|
+
}
|
|
5102
|
+
if (udpStartPromise) {
|
|
5103
|
+
return udpStartPromise;
|
|
5104
|
+
}
|
|
5105
|
+
|
|
5106
|
+
const run = Promise.resolve().then(async () => {
|
|
5107
|
+
try {
|
|
5108
|
+
await udpTransport.start();
|
|
5109
|
+
if (closing || !started) {
|
|
5110
|
+
return false;
|
|
5111
|
+
}
|
|
5112
|
+
udpStartRetryDelayMs = udpStartRetryInitialMs;
|
|
5113
|
+
for (const device of devices.values()) {
|
|
5114
|
+
registerCurrentDeviceUdpSession(device);
|
|
5115
|
+
}
|
|
5116
|
+
return true;
|
|
5117
|
+
} catch (error) {
|
|
5118
|
+
if (!closing && started) {
|
|
5119
|
+
logWarn(
|
|
5120
|
+
'udp',
|
|
5121
|
+
`UDP P2P could not start (${reason}); Direct TCP remains available: ${error?.message || error}`);
|
|
5122
|
+
scheduleOptionalUdpStartRetry();
|
|
5123
|
+
}
|
|
5124
|
+
return false;
|
|
5125
|
+
}
|
|
5126
|
+
});
|
|
5127
|
+
let owner = null;
|
|
5128
|
+
owner = run.finally(() => {
|
|
5129
|
+
if (udpStartPromise === owner) {
|
|
5130
|
+
udpStartPromise = null;
|
|
5131
|
+
}
|
|
5132
|
+
});
|
|
5133
|
+
udpStartPromise = owner;
|
|
5134
|
+
return owner;
|
|
5135
|
+
}
|
|
5136
|
+
|
|
5137
|
+
function attachDevice(socket, hello) {
|
|
4822
5138
|
const nowMs = Date.now();
|
|
4823
5139
|
const now = new Date(nowMs).toISOString();
|
|
4824
5140
|
const sessionId = crypto.randomUUID();
|
|
@@ -4944,19 +5260,7 @@ export function createRemoteHub(options = {}) {
|
|
|
4944
5260
|
serverTime: now
|
|
4945
5261
|
});
|
|
4946
5262
|
|
|
4947
|
-
|
|
4948
|
-
deviceId,
|
|
4949
|
-
sessionId,
|
|
4950
|
-
sendControl: payload => writeJsonLine(socket, payload),
|
|
4951
|
-
capabilities
|
|
4952
|
-
});
|
|
4953
|
-
if (udpSession?.sessionId) {
|
|
4954
|
-
device.udp = {
|
|
4955
|
-
...device.udp,
|
|
4956
|
-
state: 'udp-session-created',
|
|
4957
|
-
sessionId: udpSession.sessionId
|
|
4958
|
-
};
|
|
4959
|
-
}
|
|
5263
|
+
registerCurrentDeviceUdpSession(device);
|
|
4960
5264
|
|
|
4961
5265
|
logEvent('remote', `device connected ${device.deviceName} (${deviceId})`, 'success');
|
|
4962
5266
|
emitRemoteEvent('RemoteDeviceConnected', device);
|
|
@@ -5046,16 +5350,26 @@ export function createRemoteHub(options = {}) {
|
|
|
5046
5350
|
return device;
|
|
5047
5351
|
}
|
|
5048
5352
|
|
|
5049
|
-
function attachFrameSocket(socket, hello) {
|
|
5050
|
-
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
5051
|
-
const device = devices.get(deviceId);
|
|
5052
|
-
if (!device || !device.connected || !device.socket || device.socket.destroyed) {
|
|
5053
|
-
writeJsonLine(socket, { type: 'error', error: 'device-not-connected' });
|
|
5054
|
-
socket.destroy();
|
|
5055
|
-
return null;
|
|
5056
|
-
}
|
|
5057
|
-
|
|
5058
|
-
|
|
5353
|
+
function attachFrameSocket(socket, hello) {
|
|
5354
|
+
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
5355
|
+
const device = devices.get(deviceId);
|
|
5356
|
+
if (!device || !device.connected || !device.socket || device.socket.destroyed) {
|
|
5357
|
+
writeJsonLine(socket, { type: 'error', error: 'device-not-connected' });
|
|
5358
|
+
socket.destroy();
|
|
5359
|
+
return null;
|
|
5360
|
+
}
|
|
5361
|
+
const parentSessionId = safeString(hello.parentSessionId, 160);
|
|
5362
|
+
const requiresSessionBoundMedia = device.capabilities?.sessionBoundMediaChannels === true;
|
|
5363
|
+
if ((parentSessionId && parentSessionId !== safeString(device.sessionId, 160))
|
|
5364
|
+
|| (!parentSessionId && requiresSessionBoundMedia)) {
|
|
5365
|
+
writeJsonLineAndClose(socket, { type: 'error', error: 'side-channel-parent-session-stale' });
|
|
5366
|
+
return null;
|
|
5367
|
+
}
|
|
5368
|
+
if (!parentSessionId) {
|
|
5369
|
+
emitRemoteEvent('RemoteLegacyMediaSideChannelAccepted', device, { channel: 'frame' });
|
|
5370
|
+
}
|
|
5371
|
+
|
|
5372
|
+
closeFrameSocket(device, 'replaced-by-new-frame-socket');
|
|
5059
5373
|
|
|
5060
5374
|
const now = new Date().toISOString();
|
|
5061
5375
|
device.frameSocket = socket;
|
|
@@ -5079,16 +5393,26 @@ export function createRemoteHub(options = {}) {
|
|
|
5079
5393
|
return device;
|
|
5080
5394
|
}
|
|
5081
5395
|
|
|
5082
|
-
function attachAudioSocket(socket, hello) {
|
|
5083
|
-
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
5084
|
-
const device = devices.get(deviceId);
|
|
5085
|
-
if (!device || !device.connected || !device.socket || device.socket.destroyed) {
|
|
5086
|
-
writeJsonLine(socket, { type: 'error', error: 'device-not-connected' });
|
|
5087
|
-
socket.destroy();
|
|
5088
|
-
return null;
|
|
5089
|
-
}
|
|
5090
|
-
|
|
5091
|
-
|
|
5396
|
+
function attachAudioSocket(socket, hello) {
|
|
5397
|
+
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
5398
|
+
const device = devices.get(deviceId);
|
|
5399
|
+
if (!device || !device.connected || !device.socket || device.socket.destroyed) {
|
|
5400
|
+
writeJsonLine(socket, { type: 'error', error: 'device-not-connected' });
|
|
5401
|
+
socket.destroy();
|
|
5402
|
+
return null;
|
|
5403
|
+
}
|
|
5404
|
+
const parentSessionId = safeString(hello.parentSessionId, 160);
|
|
5405
|
+
const requiresSessionBoundMedia = device.capabilities?.sessionBoundMediaChannels === true;
|
|
5406
|
+
if ((parentSessionId && parentSessionId !== safeString(device.sessionId, 160))
|
|
5407
|
+
|| (!parentSessionId && requiresSessionBoundMedia)) {
|
|
5408
|
+
writeJsonLineAndClose(socket, { type: 'error', error: 'side-channel-parent-session-stale' });
|
|
5409
|
+
return null;
|
|
5410
|
+
}
|
|
5411
|
+
if (!parentSessionId) {
|
|
5412
|
+
emitRemoteEvent('RemoteLegacyMediaSideChannelAccepted', device, { channel: 'audio' });
|
|
5413
|
+
}
|
|
5414
|
+
|
|
5415
|
+
closeAudioSocket(device, 'replaced-by-new-audio-socket');
|
|
5092
5416
|
const now = new Date().toISOString();
|
|
5093
5417
|
device.audioSocket = socket;
|
|
5094
5418
|
device.audioConnectedAt = now;
|
|
@@ -5155,8 +5479,10 @@ export function createRemoteHub(options = {}) {
|
|
|
5155
5479
|
device.socket = null;
|
|
5156
5480
|
abortTransportDiagnosticsForDevice(device, reason);
|
|
5157
5481
|
udpTransport?.unregisterDevice?.(deviceId, device.sessionId);
|
|
5158
|
-
closeInputSocket(device, reason);
|
|
5159
|
-
closeFrameSocket(device, reason);
|
|
5482
|
+
closeInputSocket(device, reason);
|
|
5483
|
+
closeFrameSocket(device, reason);
|
|
5484
|
+
closeAudioSocket(device, reason);
|
|
5485
|
+
closeFileSocket(device, reason);
|
|
5160
5486
|
device.disconnectedAt = new Date().toISOString();
|
|
5161
5487
|
device.lastDisconnectReason = reason;
|
|
5162
5488
|
deactivateDeviceLiveStreams(device, reason, device.disconnectedAt);
|
|
@@ -5168,10 +5494,10 @@ export function createRemoteHub(options = {}) {
|
|
|
5168
5494
|
emitRemoteEvent('RemoteDeviceDisconnected', device, { reason });
|
|
5169
5495
|
}
|
|
5170
5496
|
|
|
5171
|
-
function rememberDeviceTask(device, task) {
|
|
5172
|
-
if (!device || !task) {
|
|
5173
|
-
return null;
|
|
5174
|
-
}
|
|
5497
|
+
function rememberDeviceTask(device, task) {
|
|
5498
|
+
if (!device || !task) {
|
|
5499
|
+
return null;
|
|
5500
|
+
}
|
|
5175
5501
|
|
|
5176
5502
|
const existingIndex = device.recentTasks.findIndex(item =>
|
|
5177
5503
|
item.taskId === task.taskId || item.commandId === task.commandId);
|
|
@@ -5184,13 +5510,85 @@ export function createRemoteHub(options = {}) {
|
|
|
5184
5510
|
device.recentTasks.length = RECENT_TASK_LIMIT;
|
|
5185
5511
|
}
|
|
5186
5512
|
|
|
5187
|
-
device.latestTask = task;
|
|
5188
|
-
if (task.commandId) {
|
|
5189
|
-
device.pendingTaskCommands.set(task.commandId, task);
|
|
5190
|
-
}
|
|
5513
|
+
device.latestTask = task;
|
|
5514
|
+
if (task.commandId) {
|
|
5515
|
+
device.pendingTaskCommands.set(task.commandId, task);
|
|
5516
|
+
}
|
|
5191
5517
|
|
|
5192
|
-
return task;
|
|
5193
|
-
}
|
|
5518
|
+
return task;
|
|
5519
|
+
}
|
|
5520
|
+
|
|
5521
|
+
function markOwnedTaskBatch(batch) {
|
|
5522
|
+
Object.defineProperty(batch, TASK_BATCH_OWNER, {
|
|
5523
|
+
value: true,
|
|
5524
|
+
enumerable: false,
|
|
5525
|
+
configurable: false,
|
|
5526
|
+
writable: false
|
|
5527
|
+
});
|
|
5528
|
+
return batch;
|
|
5529
|
+
}
|
|
5530
|
+
|
|
5531
|
+
function getOwnedTaskBatch(options = {}) {
|
|
5532
|
+
const batch = options.taskBatch;
|
|
5533
|
+
if (!batch
|
|
5534
|
+
|| batch[TASK_BATCH_OWNER] !== true
|
|
5535
|
+
|| taskBatches.get(batch.batchId) !== batch) {
|
|
5536
|
+
return null;
|
|
5537
|
+
}
|
|
5538
|
+
return batch;
|
|
5539
|
+
}
|
|
5540
|
+
|
|
5541
|
+
function createAgentTaskOwner(device, taskId, commandId, operation, batchId = '') {
|
|
5542
|
+
return Object.freeze({
|
|
5543
|
+
deviceId: safeString(device?.deviceId, 160),
|
|
5544
|
+
sessionId: safeString(device?.sessionId, 160),
|
|
5545
|
+
taskId: safeString(taskId, 128),
|
|
5546
|
+
commandId: safeString(commandId, 128),
|
|
5547
|
+
operation: safeString(operation, 80),
|
|
5548
|
+
batchId: safeString(batchId, 128)
|
|
5549
|
+
});
|
|
5550
|
+
}
|
|
5551
|
+
|
|
5552
|
+
function isPendingAgentTaskOwner(device, task, commandId) {
|
|
5553
|
+
const owner = task?.owner;
|
|
5554
|
+
const expectedCommandId = safeString(commandId, 128);
|
|
5555
|
+
return !!owner
|
|
5556
|
+
&& device?.pendingTaskCommands?.get(expectedCommandId) === task
|
|
5557
|
+
&& owner.deviceId === safeString(device?.deviceId, 160)
|
|
5558
|
+
&& owner.sessionId === safeString(device?.sessionId, 160)
|
|
5559
|
+
&& owner.commandId === expectedCommandId
|
|
5560
|
+
&& owner.taskId === safeString(task?.taskId, 128)
|
|
5561
|
+
&& owner.operation === safeString(task?.operation, 80)
|
|
5562
|
+
&& owner.batchId === safeString(task?.batchId, 128);
|
|
5563
|
+
}
|
|
5564
|
+
|
|
5565
|
+
function taskResultMatchesOwner(device, task, commandId, result, message, channel) {
|
|
5566
|
+
if (channel !== 'control' || !isPendingAgentTaskOwner(device, task, commandId)) {
|
|
5567
|
+
return false;
|
|
5568
|
+
}
|
|
5569
|
+
const owner = task.owner;
|
|
5570
|
+
const resultTaskId = safeString(message?.taskId || result?.taskId, 128);
|
|
5571
|
+
const resultOperation = safeString(message?.operation || result?.operation, 80);
|
|
5572
|
+
const resultDeviceId = safeString(message?.deviceId || result?.deviceId, 160);
|
|
5573
|
+
const resultSessionId = safeString(message?.sessionId || result?.sessionId, 160);
|
|
5574
|
+
return (!resultTaskId || resultTaskId === owner.taskId)
|
|
5575
|
+
&& (!resultOperation || resultOperation === owner.operation)
|
|
5576
|
+
&& (!resultDeviceId || resultDeviceId === owner.deviceId)
|
|
5577
|
+
&& (!resultSessionId || resultSessionId === owner.sessionId);
|
|
5578
|
+
}
|
|
5579
|
+
|
|
5580
|
+
function canQueueAgentTask(device) {
|
|
5581
|
+
if (!(device?.pendingTaskCommands instanceof Map)
|
|
5582
|
+
|| !(device?.pendingTaskTimers instanceof Map)) {
|
|
5583
|
+
return false;
|
|
5584
|
+
}
|
|
5585
|
+
// Each pending task has one exact timer. Any mismatch is unsafe state,
|
|
5586
|
+
// so reject new work instead of making the retained owner unbounded.
|
|
5587
|
+
if (device.pendingTaskCommands.size !== device.pendingTaskTimers.size) {
|
|
5588
|
+
return false;
|
|
5589
|
+
}
|
|
5590
|
+
return device.pendingTaskCommands.size < MAX_PENDING_AGENT_TASKS_PER_DEVICE;
|
|
5591
|
+
}
|
|
5194
5592
|
|
|
5195
5593
|
function trimTaskBatches() {
|
|
5196
5594
|
const ordered = [...taskBatches.values()]
|
|
@@ -5209,17 +5607,19 @@ export function createRemoteHub(options = {}) {
|
|
|
5209
5607
|
const instruction = safeText(options.instruction, MAX_AGENT_TASK_CHARS);
|
|
5210
5608
|
const operation = normalizeAgentOperation(options.operation);
|
|
5211
5609
|
const targetQuery = safeText(options.targetQuery, 160);
|
|
5212
|
-
const batchId =
|
|
5610
|
+
const batchId = crypto.randomUUID();
|
|
5213
5611
|
const title = safeString(options.title, 120)
|
|
5214
5612
|
|| safeString(instruction.split(/\r?\n/)[0], 120)
|
|
5215
5613
|
|| 'Remote task batch';
|
|
5216
|
-
const batch = {
|
|
5614
|
+
const batch = {
|
|
5217
5615
|
batchId,
|
|
5218
5616
|
title,
|
|
5219
5617
|
instructionPreview: safeText(instruction, 320),
|
|
5220
5618
|
operation,
|
|
5221
5619
|
targetQuery,
|
|
5222
|
-
approvalLevel: normalizeApprovalLevel(options.approvalLevel),
|
|
5620
|
+
approvalLevel: normalizeApprovalLevel(options.approvalLevel),
|
|
5621
|
+
retryToolArguments: safeRetryToolArguments(operation, options.toolArguments),
|
|
5622
|
+
retryPermissionMode: normalizeAgentPermissionMode(operation, options.permissionMode),
|
|
5223
5623
|
status: targets.length > 0 ? 'running' : 'failed',
|
|
5224
5624
|
requestedAt: now,
|
|
5225
5625
|
updatedAt: now,
|
|
@@ -5246,7 +5646,7 @@ export function createRemoteHub(options = {}) {
|
|
|
5246
5646
|
}))
|
|
5247
5647
|
};
|
|
5248
5648
|
|
|
5249
|
-
taskBatches.set(batchId, batch);
|
|
5649
|
+
taskBatches.set(batchId, markOwnedTaskBatch(batch));
|
|
5250
5650
|
trimTaskBatches();
|
|
5251
5651
|
return batch;
|
|
5252
5652
|
}
|
|
@@ -5391,11 +5791,11 @@ export function createRemoteHub(options = {}) {
|
|
|
5391
5791
|
return null;
|
|
5392
5792
|
}
|
|
5393
5793
|
|
|
5394
|
-
const task = device.pendingTaskCommands.get(key);
|
|
5395
|
-
if (!task) {
|
|
5396
|
-
clearTaskTimeout(device, key);
|
|
5397
|
-
return null;
|
|
5398
|
-
}
|
|
5794
|
+
const task = device.pendingTaskCommands.get(key);
|
|
5795
|
+
if (!task || !isPendingAgentTaskOwner(device, task, key)) {
|
|
5796
|
+
clearTaskTimeout(device, key);
|
|
5797
|
+
return null;
|
|
5798
|
+
}
|
|
5399
5799
|
|
|
5400
5800
|
const now = new Date().toISOString();
|
|
5401
5801
|
clearTaskTimeout(device, key);
|
|
@@ -5912,62 +6312,66 @@ export function createRemoteHub(options = {}) {
|
|
|
5912
6312
|
return;
|
|
5913
6313
|
}
|
|
5914
6314
|
|
|
5915
|
-
clearTaskTimeout(device, commandId);
|
|
5916
|
-
const timer = setTimeout(() => {
|
|
5917
|
-
|
|
5918
|
-
|
|
6315
|
+
clearTaskTimeout(device, commandId);
|
|
6316
|
+
const timer = setTimeout(() => {
|
|
6317
|
+
if (isPendingAgentTaskOwner(device, task, commandId)) {
|
|
6318
|
+
failPendingTask(device, commandId, 'task-timeout');
|
|
6319
|
+
}
|
|
6320
|
+
}, taskTimeoutMs);
|
|
5919
6321
|
timer.unref?.();
|
|
5920
6322
|
device.pendingTaskTimers.set(commandId, timer);
|
|
5921
6323
|
}
|
|
5922
6324
|
|
|
5923
|
-
function summarizeTaskResult(result) {
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|| result.
|
|
5929
|
-
|| result.
|
|
5930
|
-
||
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
6325
|
+
function summarizeTaskResult(result, diagnostic = false) {
|
|
6326
|
+
let summary;
|
|
6327
|
+
if (result && typeof result === 'object') {
|
|
6328
|
+
try {
|
|
6329
|
+
summary = result.summary
|
|
6330
|
+
|| result.message
|
|
6331
|
+
|| result.output
|
|
6332
|
+
|| result.result
|
|
6333
|
+
|| JSON.stringify(result);
|
|
6334
|
+
} catch {
|
|
6335
|
+
summary = '';
|
|
6336
|
+
}
|
|
6337
|
+
} else {
|
|
6338
|
+
summary = result ?? '';
|
|
6339
|
+
}
|
|
6340
|
+
return diagnostic
|
|
6341
|
+
? safeDiagnosticText(summary, MAX_AGENT_TASK_RESULT_CHARS)
|
|
6342
|
+
: safeText(summary, MAX_AGENT_TASK_RESULT_CHARS);
|
|
5935
6343
|
}
|
|
5936
6344
|
|
|
5937
|
-
function applyTaskResult(device, commandId, result, error) {
|
|
5938
|
-
if (!device || !commandId) {
|
|
5939
|
-
return null;
|
|
5940
|
-
}
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
const task = resultTaskId
|
|
5946
|
-
? device.recentTasks.find(item => item.taskId === resultTaskId)
|
|
5947
|
-
: device.pendingTaskCommands.get(commandId);
|
|
5948
|
-
if (!task) {
|
|
5949
|
-
return null;
|
|
5950
|
-
}
|
|
5951
|
-
|
|
5952
|
-
if (device.pendingTaskCommands.get(commandId) !== task) {
|
|
5953
|
-
return null;
|
|
5954
|
-
}
|
|
6345
|
+
function applyTaskResult(device, commandId, result, error, message = {}, channel = 'control') {
|
|
6346
|
+
if (!device || !commandId) {
|
|
6347
|
+
return null;
|
|
6348
|
+
}
|
|
6349
|
+
const task = device.pendingTaskCommands.get(commandId);
|
|
6350
|
+
if (!task || !taskResultMatchesOwner(device, task, commandId, result, message, channel)) {
|
|
6351
|
+
return null;
|
|
6352
|
+
}
|
|
5955
6353
|
|
|
5956
6354
|
const now = device.lastSeenAt || new Date().toISOString();
|
|
5957
|
-
const resultFailed = Boolean(error)
|
|
5958
|
-
|| result?.ok === false
|
|
5959
|
-
|| safeString(result?.status, 40) === 'failed';
|
|
5960
|
-
const status = resultFailed ? 'failed' : safeString(result?.status, 40) || 'completed';
|
|
5961
|
-
const
|
|
5962
|
-
|
|
5963
|
-
|
|
6355
|
+
const resultFailed = Boolean(error)
|
|
6356
|
+
|| result?.ok === false
|
|
6357
|
+
|| safeString(result?.status, 40) === 'failed';
|
|
6358
|
+
const status = resultFailed ? 'failed' : safeString(result?.status, 40) || 'completed';
|
|
6359
|
+
const diagnostic = DIAGNOSTIC_AGENT_OPERATIONS.has(task.operation);
|
|
6360
|
+
const safeResultError = value => diagnostic
|
|
6361
|
+
? safeDiagnosticText(value, 500)
|
|
6362
|
+
: safeString(value, 500);
|
|
6363
|
+
const resultError = safeResultError(error)
|
|
6364
|
+
|| safeResultError(result?.error)
|
|
6365
|
+
|| (resultFailed ? safeResultError(result?.summary) || 'agent-task-failed' : '');
|
|
5964
6366
|
clearTaskTimeout(device, commandId);
|
|
5965
6367
|
task.status = status;
|
|
5966
6368
|
task.updatedAt = now;
|
|
5967
6369
|
task.completedAt = safeString(result?.completedAt, 80) || now;
|
|
5968
6370
|
task.error = resultError;
|
|
5969
|
-
task.resultSummary = resultError || summarizeTaskResult(result);
|
|
5970
|
-
task.resultData =
|
|
6371
|
+
task.resultSummary = resultError || summarizeTaskResult(result, diagnostic);
|
|
6372
|
+
task.resultData = diagnostic
|
|
6373
|
+
? safeDiagnosticTaskData(result?.data)
|
|
6374
|
+
: safeTaskData(result?.data);
|
|
5971
6375
|
task.resultKind = safeString(result?.kind || result?.mode || 'agent-task', 80);
|
|
5972
6376
|
device.latestTask = task;
|
|
5973
6377
|
device.pendingTaskCommands.delete(commandId);
|
|
@@ -7686,7 +8090,7 @@ export function createRemoteHub(options = {}) {
|
|
|
7686
8090
|
};
|
|
7687
8091
|
}
|
|
7688
8092
|
|
|
7689
|
-
function writeFrameTransportProofResponse(socket, result) {
|
|
8093
|
+
function writeFrameTransportProofResponse(socket, result) {
|
|
7690
8094
|
const proof = result?.proof;
|
|
7691
8095
|
if (!proof?.proofId) return;
|
|
7692
8096
|
const accepted = result.proofAccepted === true;
|
|
@@ -7702,24 +8106,98 @@ export function createRemoteHub(options = {}) {
|
|
|
7702
8106
|
streamPurpose: proof.streamPurpose,
|
|
7703
8107
|
monitorIndex: proof.monitorIndex,
|
|
7704
8108
|
...(accepted ? {} : { error: safeString(result.error || 'frame-proof-rejected', 160) })
|
|
7705
|
-
});
|
|
7706
|
-
}
|
|
7707
|
-
|
|
7708
|
-
function
|
|
8109
|
+
});
|
|
8110
|
+
}
|
|
8111
|
+
|
|
8112
|
+
function isCurrentAgentSocketOwner(socket, state, device) {
|
|
8113
|
+
if (!device?.connected
|
|
8114
|
+
|| devices.get(device.deviceId) !== device
|
|
8115
|
+
// Both values are the same Hub-created immutable session owner
|
|
8116
|
+
// captured during hello. Exact comparison preserves the boundary
|
|
8117
|
+
// without running general-purpose text cleanup on every frame.
|
|
8118
|
+
|| state?.sessionId !== device.sessionId) {
|
|
8119
|
+
return false;
|
|
8120
|
+
}
|
|
8121
|
+
if (state.inputOnly) return device.inputSocket === socket;
|
|
8122
|
+
if (state.frameOnly) return device.frameSocket === socket;
|
|
8123
|
+
if (state.audioOnly) return device.audioSocket === socket;
|
|
8124
|
+
if (state.fileOnly) return device.fileSocket === socket;
|
|
8125
|
+
return device.socket === socket;
|
|
8126
|
+
}
|
|
8127
|
+
|
|
8128
|
+
function getAgentMessageChannel(state) {
|
|
8129
|
+
if (state?.inputOnly) return 'input';
|
|
8130
|
+
if (state?.frameOnly) return 'frame';
|
|
8131
|
+
if (state?.audioOnly) return 'audio';
|
|
8132
|
+
if (state?.fileOnly) return 'file';
|
|
8133
|
+
return 'control';
|
|
8134
|
+
}
|
|
8135
|
+
|
|
8136
|
+
function failCloseMediaSideChannel(socket, state, reason, error = 'media-side-channel-message-not-supported') {
|
|
8137
|
+
const device = state?.device;
|
|
8138
|
+
if (device) {
|
|
8139
|
+
emitRemoteEvent('RemoteAgentMessageIgnored', device, {
|
|
8140
|
+
channel: getAgentMessageChannel(state),
|
|
8141
|
+
reason
|
|
8142
|
+
});
|
|
8143
|
+
}
|
|
8144
|
+
// Drain only this side-channel ingress. The main control owner and all
|
|
8145
|
+
// other side-channel owners remain untouched.
|
|
8146
|
+
closeAgentBinaryIngress(state, reason);
|
|
8147
|
+
writeJsonLineAndClose(socket, { type: 'error', error });
|
|
8148
|
+
}
|
|
8149
|
+
|
|
8150
|
+
function isAllowedMediaBinaryKind(state, frameKind) {
|
|
8151
|
+
if (state?.frameOnly === true) {
|
|
8152
|
+
return frameKind === 'thumbnail' || frameKind === 'stream' || frameKind === 'live';
|
|
8153
|
+
}
|
|
8154
|
+
if (state?.audioOnly === true) {
|
|
8155
|
+
return frameKind === 'audio' || frameKind === 'audio-status';
|
|
8156
|
+
}
|
|
8157
|
+
return true;
|
|
8158
|
+
}
|
|
8159
|
+
|
|
8160
|
+
function getBinaryFrameMaxBytes(frameKind) {
|
|
8161
|
+
if (frameKind === 'thumbnail') return MAX_THUMBNAIL_BINARY_BYTES;
|
|
8162
|
+
if (frameKind === 'audio' || frameKind === 'audio-status') return MAX_AUDIO_BINARY_BYTES;
|
|
8163
|
+
return MAX_STREAM_BINARY_BYTES;
|
|
8164
|
+
}
|
|
8165
|
+
|
|
8166
|
+
function handleAgentBinaryFrame(socket, state, header, framePayload) {
|
|
7709
8167
|
if (!state.authenticated || !state.device) {
|
|
7710
8168
|
writeJsonLine(socket, { type: 'error', error: 'hello-required' });
|
|
7711
8169
|
socket.destroy();
|
|
7712
8170
|
return;
|
|
7713
8171
|
}
|
|
7714
|
-
|
|
7715
|
-
const device = state.device;
|
|
7716
|
-
if (state
|
|
7717
|
-
|
|
7718
|
-
socket.destroy();
|
|
7719
|
-
return;
|
|
7720
|
-
}
|
|
7721
|
-
|
|
7722
|
-
|
|
8172
|
+
|
|
8173
|
+
const device = state.device;
|
|
8174
|
+
if (!isCurrentAgentSocketOwner(socket, state, device)) {
|
|
8175
|
+
closeAgentBinaryIngress(state, 'stale-agent-socket-owner');
|
|
8176
|
+
socket.destroy();
|
|
8177
|
+
return;
|
|
8178
|
+
}
|
|
8179
|
+
if (state.inputOnly || state.fileOnly) {
|
|
8180
|
+
writeJsonLine(socket, { type: 'error', error: 'input-channel-binary-not-supported' });
|
|
8181
|
+
socket.destroy();
|
|
8182
|
+
return;
|
|
8183
|
+
}
|
|
8184
|
+
|
|
8185
|
+
const hasDeclaredFrameKind = header.frameKind !== undefined
|
|
8186
|
+
&& header.frameKind !== null
|
|
8187
|
+
&& header.frameKind !== '';
|
|
8188
|
+
const declaredFrameKind = hasDeclaredFrameKind
|
|
8189
|
+
? normalizeMediaBinaryKind(header.frameKind)
|
|
8190
|
+
: '';
|
|
8191
|
+
const frameKind = hasDeclaredFrameKind
|
|
8192
|
+
? declaredFrameKind
|
|
8193
|
+
: normalizeMediaBinaryKind(header.kind || header.frameType || '');
|
|
8194
|
+
if ((state.frameOnly || state.audioOnly)
|
|
8195
|
+
&& (!frameKind || !isAllowedMediaBinaryKind(state, frameKind))) {
|
|
8196
|
+
failCloseMediaSideChannel(socket, state, 'media-side-channel-binary-kind-not-supported');
|
|
8197
|
+
return;
|
|
8198
|
+
}
|
|
8199
|
+
|
|
8200
|
+
device.counters.messagesReceived += 1;
|
|
7723
8201
|
device.lastSeenAt = new Date().toISOString();
|
|
7724
8202
|
if (state.frameOnly) {
|
|
7725
8203
|
device.frameLastSeenAt = device.lastSeenAt;
|
|
@@ -7728,8 +8206,7 @@ export function createRemoteHub(options = {}) {
|
|
|
7728
8206
|
device.audioLastSeenAt = device.lastSeenAt;
|
|
7729
8207
|
}
|
|
7730
8208
|
|
|
7731
|
-
const
|
|
7732
|
-
const binaryTransport = socket.__liveDeskRelayControl === true
|
|
8209
|
+
const binaryTransport = socket.__liveDeskRelayControl === true
|
|
7733
8210
|
? 'relay-binary'
|
|
7734
8211
|
: state.binaryTransport === 'ws-binary'
|
|
7735
8212
|
? 'ws-binary'
|
|
@@ -7956,9 +8433,12 @@ export function createRemoteHub(options = {}) {
|
|
|
7956
8433
|
return state.binaryIngress.enqueue(header, payload);
|
|
7957
8434
|
}
|
|
7958
8435
|
|
|
7959
|
-
function handleAgentMessage(socket, state, message) {
|
|
7960
|
-
if (!message || typeof message !== 'object') {
|
|
7961
|
-
|
|
8436
|
+
function handleAgentMessage(socket, state, message) {
|
|
8437
|
+
if (!message || typeof message !== 'object') {
|
|
8438
|
+
if (state?.authenticated && (state.frameOnly || state.audioOnly)) {
|
|
8439
|
+
failCloseMediaSideChannel(socket, state, 'media-side-channel-json-value-not-supported', 'invalid-json');
|
|
8440
|
+
}
|
|
8441
|
+
return;
|
|
7962
8442
|
}
|
|
7963
8443
|
|
|
7964
8444
|
if (!state.authenticated) {
|
|
@@ -8019,9 +8499,10 @@ export function createRemoteHub(options = {}) {
|
|
|
8019
8499
|
return;
|
|
8020
8500
|
}
|
|
8021
8501
|
|
|
8022
|
-
state.authenticated = true;
|
|
8023
|
-
state.device = device;
|
|
8024
|
-
state.
|
|
8502
|
+
state.authenticated = true;
|
|
8503
|
+
state.device = device;
|
|
8504
|
+
state.sessionId = device.sessionId;
|
|
8505
|
+
state.inputOnly = true;
|
|
8025
8506
|
return;
|
|
8026
8507
|
}
|
|
8027
8508
|
if (channel === 'frame') {
|
|
@@ -8035,9 +8516,10 @@ export function createRemoteHub(options = {}) {
|
|
|
8035
8516
|
return;
|
|
8036
8517
|
}
|
|
8037
8518
|
|
|
8038
|
-
state.authenticated = true;
|
|
8039
|
-
state.device = device;
|
|
8040
|
-
state.
|
|
8519
|
+
state.authenticated = true;
|
|
8520
|
+
state.device = device;
|
|
8521
|
+
state.sessionId = device.sessionId;
|
|
8522
|
+
state.frameOnly = true;
|
|
8041
8523
|
return;
|
|
8042
8524
|
}
|
|
8043
8525
|
if (channel === 'audio') {
|
|
@@ -8046,9 +8528,10 @@ export function createRemoteHub(options = {}) {
|
|
|
8046
8528
|
return;
|
|
8047
8529
|
}
|
|
8048
8530
|
|
|
8049
|
-
state.authenticated = true;
|
|
8050
|
-
state.device = device;
|
|
8051
|
-
state.
|
|
8531
|
+
state.authenticated = true;
|
|
8532
|
+
state.device = device;
|
|
8533
|
+
state.sessionId = device.sessionId;
|
|
8534
|
+
state.audioOnly = true;
|
|
8052
8535
|
return;
|
|
8053
8536
|
}
|
|
8054
8537
|
if (channel === 'file') {
|
|
@@ -8057,9 +8540,10 @@ export function createRemoteHub(options = {}) {
|
|
|
8057
8540
|
return;
|
|
8058
8541
|
}
|
|
8059
8542
|
|
|
8060
|
-
state.authenticated = true;
|
|
8061
|
-
state.device = device;
|
|
8062
|
-
state.
|
|
8543
|
+
state.authenticated = true;
|
|
8544
|
+
state.device = device;
|
|
8545
|
+
state.sessionId = device.sessionId;
|
|
8546
|
+
state.fileOnly = true;
|
|
8063
8547
|
return;
|
|
8064
8548
|
}
|
|
8065
8549
|
|
|
@@ -8068,21 +8552,23 @@ export function createRemoteHub(options = {}) {
|
|
|
8068
8552
|
return;
|
|
8069
8553
|
}
|
|
8070
8554
|
|
|
8071
|
-
state.authenticated = true;
|
|
8072
|
-
state.device = device;
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
const device = state.device;
|
|
8077
|
-
if (!device) {
|
|
8078
|
-
socket.destroy();
|
|
8079
|
-
return;
|
|
8555
|
+
state.authenticated = true;
|
|
8556
|
+
state.device = device;
|
|
8557
|
+
state.sessionId = device.sessionId;
|
|
8558
|
+
return;
|
|
8080
8559
|
}
|
|
8081
8560
|
|
|
8561
|
+
const device = state.device;
|
|
8562
|
+
if (!device) {
|
|
8563
|
+
socket.destroy();
|
|
8564
|
+
return;
|
|
8565
|
+
}
|
|
8566
|
+
if (!isCurrentAgentSocketOwner(socket, state, device)) {
|
|
8567
|
+
socket.destroy();
|
|
8568
|
+
return;
|
|
8569
|
+
}
|
|
8570
|
+
|
|
8082
8571
|
if (state.inputOnly) {
|
|
8083
|
-
if (device.inputSocket !== socket) {
|
|
8084
|
-
return;
|
|
8085
|
-
}
|
|
8086
8572
|
device.inputLastSeenAt = new Date().toISOString();
|
|
8087
8573
|
if (message.type === 'input.applied' || message.type === 'input.error') {
|
|
8088
8574
|
acknowledgePendingDedicatedInput(device, message);
|
|
@@ -8092,21 +8578,50 @@ export function createRemoteHub(options = {}) {
|
|
|
8092
8578
|
}, 'input');
|
|
8093
8579
|
}
|
|
8094
8580
|
return;
|
|
8095
|
-
}
|
|
8096
|
-
if (state.frameOnly) {
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8581
|
+
}
|
|
8582
|
+
if (state.frameOnly) {
|
|
8583
|
+
switch (message.type) {
|
|
8584
|
+
case 'thumbnail.frame':
|
|
8585
|
+
case 'stream.open':
|
|
8586
|
+
case 'stream.frame': {
|
|
8587
|
+
const receivedAt = new Date().toISOString();
|
|
8588
|
+
device.counters.messagesReceived += 1;
|
|
8589
|
+
device.frameLastSeenAt = receivedAt;
|
|
8590
|
+
device.lastSeenAt = receivedAt;
|
|
8591
|
+
if (message.type === 'thumbnail.frame') {
|
|
8592
|
+
applyThumbnailFrame(device, message, message.data, 'json-base64-frame-channel');
|
|
8593
|
+
} else if (message.type === 'stream.open') {
|
|
8594
|
+
applyLiveStreamOpen(device, message, 'json-frame-channel');
|
|
8595
|
+
} else {
|
|
8596
|
+
applyLiveFrame(device, message, message.data, 'json-base64-frame-channel');
|
|
8597
|
+
}
|
|
8598
|
+
break;
|
|
8599
|
+
}
|
|
8600
|
+
default:
|
|
8601
|
+
failCloseMediaSideChannel(socket, state, 'frame-channel-message-not-supported', 'frame-channel-message-not-supported');
|
|
8602
|
+
break;
|
|
8603
|
+
}
|
|
8604
|
+
return;
|
|
8605
|
+
}
|
|
8606
|
+
if (state.audioOnly) {
|
|
8607
|
+
if (message.type !== 'audio.frame') {
|
|
8608
|
+
failCloseMediaSideChannel(socket, state, 'audio-channel-message-not-supported', 'audio-channel-message-not-supported');
|
|
8609
|
+
return;
|
|
8610
|
+
}
|
|
8611
|
+
const receivedAt = new Date().toISOString();
|
|
8612
|
+
device.counters.messagesReceived += 1;
|
|
8613
|
+
device.audioLastSeenAt = receivedAt;
|
|
8614
|
+
device.lastSeenAt = receivedAt;
|
|
8615
|
+
applyAudioFrame(device, message, message.data, 'json-base64-audio-channel');
|
|
8616
|
+
return;
|
|
8617
|
+
}
|
|
8104
8618
|
if (state.fileOnly) {
|
|
8105
8619
|
device.fileLastSeenAt = new Date().toISOString();
|
|
8106
8620
|
}
|
|
8107
8621
|
|
|
8108
|
-
device.counters.messagesReceived += 1;
|
|
8109
|
-
device.lastSeenAt = new Date().toISOString();
|
|
8622
|
+
device.counters.messagesReceived += 1;
|
|
8623
|
+
device.lastSeenAt = new Date().toISOString();
|
|
8624
|
+
const channel = getAgentMessageChannel(state);
|
|
8110
8625
|
|
|
8111
8626
|
switch (message.type) {
|
|
8112
8627
|
case 'heartbeat':
|
|
@@ -8134,12 +8649,14 @@ export function createRemoteHub(options = {}) {
|
|
|
8134
8649
|
message.error || (message.type === 'command.error' ? 'command-failed' : ''),
|
|
8135
8650
|
500);
|
|
8136
8651
|
const result = message.result ?? null;
|
|
8137
|
-
settlePendingCommandResult(device, message);
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
|
|
8652
|
+
settlePendingCommandResult(device, message, channel);
|
|
8653
|
+
if (channel === 'control') {
|
|
8654
|
+
handleInputFallbackCommandResult(device, message);
|
|
8655
|
+
if (error || result?.ok === false || result?.status === 'failed') {
|
|
8656
|
+
failPendingLiveStream(device, commandId, error || result?.error || 'stream-start-failed');
|
|
8657
|
+
}
|
|
8658
|
+
}
|
|
8659
|
+
const task = applyTaskResult(device, commandId, result, error, message, channel);
|
|
8143
8660
|
if (task) {
|
|
8144
8661
|
emitRemoteEvent('RemoteTaskResult', device, {
|
|
8145
8662
|
commandId,
|
|
@@ -8155,8 +8672,16 @@ export function createRemoteHub(options = {}) {
|
|
|
8155
8672
|
error: safeString(message.error, 500)
|
|
8156
8673
|
});
|
|
8157
8674
|
break;
|
|
8158
|
-
case 'client.update.verified': {
|
|
8159
|
-
|
|
8675
|
+
case 'client.update.verified': {
|
|
8676
|
+
if (channel !== 'control') {
|
|
8677
|
+
emitRemoteEvent('RemoteAgentMessageIgnored', device, {
|
|
8678
|
+
channel,
|
|
8679
|
+
messageType: 'client.update.verified',
|
|
8680
|
+
reason: 'client-update-proof-control-channel-required'
|
|
8681
|
+
});
|
|
8682
|
+
break;
|
|
8683
|
+
}
|
|
8684
|
+
const receivedAt = new Date().toISOString();
|
|
8160
8685
|
const proof = normalizeClientUpdateVerifiedProof(device, message, receivedAt);
|
|
8161
8686
|
if (!proof) {
|
|
8162
8687
|
logWarn(
|
|
@@ -8202,10 +8727,11 @@ export function createRemoteHub(options = {}) {
|
|
|
8202
8727
|
socket.setNoDelay(true);
|
|
8203
8728
|
socket.setKeepAlive(true, heartbeatMs);
|
|
8204
8729
|
|
|
8205
|
-
const state = {
|
|
8206
|
-
authenticated: false,
|
|
8207
|
-
device: null,
|
|
8208
|
-
|
|
8730
|
+
const state = {
|
|
8731
|
+
authenticated: false,
|
|
8732
|
+
device: null,
|
|
8733
|
+
sessionId: '',
|
|
8734
|
+
binaryTransport: 'ws-binary',
|
|
8209
8735
|
inputOnly: false,
|
|
8210
8736
|
frameOnly: false,
|
|
8211
8737
|
audioOnly: false,
|
|
@@ -8225,33 +8751,56 @@ export function createRemoteHub(options = {}) {
|
|
|
8225
8751
|
}
|
|
8226
8752
|
}, 10000);
|
|
8227
8753
|
|
|
8228
|
-
socket.onTextMessage = text => {
|
|
8229
|
-
try {
|
|
8230
|
-
handleAgentMessage(socket, state, parseJsonLine(String(text || '')));
|
|
8231
|
-
} catch (err) {
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
|
|
8754
|
+
socket.onTextMessage = text => {
|
|
8755
|
+
try {
|
|
8756
|
+
handleAgentMessage(socket, state, parseJsonLine(String(text || '')));
|
|
8757
|
+
} catch (err) {
|
|
8758
|
+
if (state.authenticated && (state.frameOnly || state.audioOnly)) {
|
|
8759
|
+
failCloseMediaSideChannel(socket, state, 'media-side-channel-malformed-json', 'invalid-json');
|
|
8760
|
+
return;
|
|
8761
|
+
}
|
|
8762
|
+
writeJsonLine(socket, { type: 'error', error: 'invalid-json' });
|
|
8763
|
+
logWarn('remote', `invalid websocket agent message: ${err?.message || err}`);
|
|
8764
|
+
}
|
|
8235
8765
|
};
|
|
8236
8766
|
|
|
8237
8767
|
socket.onBinaryMessage = payload => {
|
|
8238
8768
|
try {
|
|
8239
8769
|
const packet = parseRemoteHubWebSocketBinaryFrame(payload);
|
|
8240
|
-
if (!packet?.header || !Buffer.isBuffer(packet.payload)) {
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
const
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8770
|
+
if (!packet?.header || !Buffer.isBuffer(packet.payload)) {
|
|
8771
|
+
if (state.authenticated && (state.frameOnly || state.audioOnly)) {
|
|
8772
|
+
failCloseMediaSideChannel(socket, state, 'media-side-channel-malformed-binary', 'invalid-websocket-binary-frame');
|
|
8773
|
+
return;
|
|
8774
|
+
}
|
|
8775
|
+
writeJsonLine(socket, { type: 'error', error: 'invalid-websocket-binary-frame' });
|
|
8776
|
+
return;
|
|
8777
|
+
}
|
|
8778
|
+
|
|
8779
|
+
const hasDeclaredFrameKind = packet.header.frameKind !== undefined
|
|
8780
|
+
&& packet.header.frameKind !== null
|
|
8781
|
+
&& packet.header.frameKind !== '';
|
|
8782
|
+
const declaredFrameKind = hasDeclaredFrameKind
|
|
8783
|
+
? normalizeMediaBinaryKind(packet.header.frameKind)
|
|
8784
|
+
: '';
|
|
8785
|
+
const frameKind = hasDeclaredFrameKind
|
|
8786
|
+
? declaredFrameKind
|
|
8787
|
+
: normalizeMediaBinaryKind(packet.header.kind || packet.header.frameType);
|
|
8788
|
+
if ((state.frameOnly || state.audioOnly)
|
|
8789
|
+
&& (!frameKind || !isAllowedMediaBinaryKind(state, frameKind))) {
|
|
8790
|
+
failCloseMediaSideChannel(socket, state, 'media-side-channel-binary-kind-not-supported');
|
|
8791
|
+
return;
|
|
8792
|
+
}
|
|
8793
|
+
const maxBytes = getBinaryFrameMaxBytes(frameKind);
|
|
8794
|
+
const byteLength = Number(packet.header.byteLength ?? packet.header.payloadBytes ?? packet.payload.length);
|
|
8795
|
+
if (!Number.isFinite(byteLength)
|
|
8796
|
+
|| byteLength < 1
|
|
8797
|
+
|| byteLength > maxBytes
|
|
8798
|
+
|| byteLength !== packet.payload.length) {
|
|
8799
|
+
if (state.authenticated && (state.frameOnly || state.audioOnly)) {
|
|
8800
|
+
failCloseMediaSideChannel(socket, state, 'media-side-channel-invalid-binary-length', 'invalid-binary-frame');
|
|
8801
|
+
return;
|
|
8802
|
+
}
|
|
8803
|
+
writeJsonLine(socket, { type: 'error', error: 'invalid-binary-frame' });
|
|
8255
8804
|
logWarn('remote', 'invalid websocket binary frame from agent.');
|
|
8256
8805
|
return;
|
|
8257
8806
|
}
|
|
@@ -8379,10 +8928,11 @@ export function createRemoteHub(options = {}) {
|
|
|
8379
8928
|
socket.setNoDelay(true);
|
|
8380
8929
|
socket.setKeepAlive(true, heartbeatMs);
|
|
8381
8930
|
|
|
8382
|
-
const state = {
|
|
8383
|
-
authenticated: false,
|
|
8384
|
-
device: null,
|
|
8385
|
-
|
|
8931
|
+
const state = {
|
|
8932
|
+
authenticated: false,
|
|
8933
|
+
device: null,
|
|
8934
|
+
sessionId: '',
|
|
8935
|
+
binaryTransport: 'binary',
|
|
8386
8936
|
inputOnly: false,
|
|
8387
8937
|
frameOnly: false,
|
|
8388
8938
|
audioOnly: false,
|
|
@@ -8448,24 +8998,14 @@ export function createRemoteHub(options = {}) {
|
|
|
8448
8998
|
}
|
|
8449
8999
|
|
|
8450
9000
|
if (startsWithTcpBinaryFrameMagic(state.buffer)) {
|
|
8451
|
-
const packet = parseTcpBinaryFramePacket(state.buffer);
|
|
8452
|
-
if (packet?.incomplete) {
|
|
8453
|
-
return;
|
|
8454
|
-
}
|
|
8455
|
-
if (!packet || packet.invalid) {
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
state.lineScanOffset = 0;
|
|
8460
|
-
continue;
|
|
8461
|
-
}
|
|
8462
|
-
|
|
8463
|
-
const syncTail = keepTcpFrameOnlySyncTail(state.buffer);
|
|
8464
|
-
state.buffer.clear();
|
|
8465
|
-
state.buffer.append(syncTail);
|
|
8466
|
-
state.lineScanOffset = 0;
|
|
8467
|
-
return;
|
|
8468
|
-
}
|
|
9001
|
+
const packet = parseTcpBinaryFramePacket(state.buffer);
|
|
9002
|
+
if (packet?.incomplete) {
|
|
9003
|
+
return;
|
|
9004
|
+
}
|
|
9005
|
+
if (!packet || packet.invalid) {
|
|
9006
|
+
failCloseMediaSideChannel(socket, state, 'media-side-channel-malformed-binary', 'invalid-binary-frame');
|
|
9007
|
+
return;
|
|
9008
|
+
}
|
|
8469
9009
|
|
|
8470
9010
|
state.buffer.discard(packet.packetLength);
|
|
8471
9011
|
state.lineScanOffset = 0;
|
|
@@ -8473,20 +9013,10 @@ export function createRemoteHub(options = {}) {
|
|
|
8473
9013
|
continue;
|
|
8474
9014
|
}
|
|
8475
9015
|
|
|
8476
|
-
if (!isJsonObjectStartByte(state.buffer.byteAt(0))) {
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
state.lineScanOffset = 0;
|
|
8481
|
-
continue;
|
|
8482
|
-
}
|
|
8483
|
-
|
|
8484
|
-
const syncTail = keepTcpFrameOnlySyncTail(state.buffer);
|
|
8485
|
-
state.buffer.clear();
|
|
8486
|
-
state.buffer.append(syncTail);
|
|
8487
|
-
state.lineScanOffset = 0;
|
|
8488
|
-
return;
|
|
8489
|
-
}
|
|
9016
|
+
if (!isJsonObjectStartByte(state.buffer.byteAt(0))) {
|
|
9017
|
+
failCloseMediaSideChannel(socket, state, 'media-side-channel-malformed-binary', 'invalid-binary-frame');
|
|
9018
|
+
return;
|
|
9019
|
+
}
|
|
8490
9020
|
}
|
|
8491
9021
|
|
|
8492
9022
|
const newlineIndex = state.buffer.indexOfByte(0x0a, state.lineScanOffset);
|
|
@@ -8508,18 +9038,33 @@ export function createRemoteHub(options = {}) {
|
|
|
8508
9038
|
return;
|
|
8509
9039
|
}
|
|
8510
9040
|
|
|
8511
|
-
try {
|
|
8512
|
-
const message = parseJsonLine(lineBuffer.toString('utf8'));
|
|
8513
|
-
if (message?.type === 'frame.binary') {
|
|
8514
|
-
const
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
9041
|
+
try {
|
|
9042
|
+
const message = parseJsonLine(lineBuffer.toString('utf8'));
|
|
9043
|
+
if (message?.type === 'frame.binary') {
|
|
9044
|
+
const hasDeclaredFrameKind = message.frameKind !== undefined
|
|
9045
|
+
&& message.frameKind !== null
|
|
9046
|
+
&& message.frameKind !== '';
|
|
9047
|
+
const declaredFrameKind = hasDeclaredFrameKind
|
|
9048
|
+
? normalizeMediaBinaryKind(message.frameKind)
|
|
9049
|
+
: '';
|
|
9050
|
+
const frameKind = hasDeclaredFrameKind
|
|
9051
|
+
? declaredFrameKind
|
|
9052
|
+
: normalizeMediaBinaryKind(message.kind || message.frameType);
|
|
9053
|
+
if ((state.frameOnly || state.audioOnly)
|
|
9054
|
+
&& (!frameKind || !isAllowedMediaBinaryKind(state, frameKind))) {
|
|
9055
|
+
failCloseMediaSideChannel(socket, state, 'media-side-channel-binary-kind-not-supported');
|
|
9056
|
+
return;
|
|
9057
|
+
}
|
|
9058
|
+
const maxBytes = getBinaryFrameMaxBytes(frameKind);
|
|
9059
|
+
const byteLength = Number(message.byteLength ?? message.payloadBytes ?? message.dataLength);
|
|
9060
|
+
if (!Number.isFinite(byteLength) || byteLength < 1 || byteLength > maxBytes) {
|
|
9061
|
+
if (state.authenticated && (state.frameOnly || state.audioOnly)) {
|
|
9062
|
+
failCloseMediaSideChannel(socket, state, 'media-side-channel-invalid-binary-length', 'invalid-binary-frame');
|
|
9063
|
+
return;
|
|
9064
|
+
}
|
|
9065
|
+
writeJsonLine(socket, { type: 'error', error: 'invalid-binary-frame' });
|
|
9066
|
+
logWarn('remote', 'invalid binary frame header from agent.');
|
|
9067
|
+
continue;
|
|
8523
9068
|
}
|
|
8524
9069
|
|
|
8525
9070
|
state.pendingBinaryFrame = {
|
|
@@ -8529,11 +9074,12 @@ export function createRemoteHub(options = {}) {
|
|
|
8529
9074
|
continue;
|
|
8530
9075
|
}
|
|
8531
9076
|
|
|
8532
|
-
handleAgentMessage(socket, state, message);
|
|
8533
|
-
} catch (err) {
|
|
8534
|
-
if (state.authenticated && (state.frameOnly || state.audioOnly)) {
|
|
8535
|
-
|
|
8536
|
-
|
|
9077
|
+
handleAgentMessage(socket, state, message);
|
|
9078
|
+
} catch (err) {
|
|
9079
|
+
if (state.authenticated && (state.frameOnly || state.audioOnly)) {
|
|
9080
|
+
failCloseMediaSideChannel(socket, state, 'media-side-channel-malformed-json', 'invalid-json');
|
|
9081
|
+
return;
|
|
9082
|
+
}
|
|
8537
9083
|
writeJsonLine(socket, { type: 'error', error: 'invalid-json' });
|
|
8538
9084
|
logWarn('remote', `invalid agent message: ${err?.message || err}`);
|
|
8539
9085
|
}
|
|
@@ -8647,20 +9193,36 @@ export function createRemoteHub(options = {}) {
|
|
|
8647
9193
|
});
|
|
8648
9194
|
}
|
|
8649
9195
|
|
|
8650
|
-
async function start() {
|
|
8651
|
-
if (!enabled || started) {
|
|
8652
|
-
return getStatus({ includeSecrets: false });
|
|
8653
|
-
}
|
|
8654
|
-
|
|
8655
|
-
try {
|
|
8656
|
-
|
|
8657
|
-
await listenOnPort(requestedPort);
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
9196
|
+
async function start() {
|
|
9197
|
+
if (!enabled || started) {
|
|
9198
|
+
return getStatus({ includeSecrets: false });
|
|
9199
|
+
}
|
|
9200
|
+
|
|
9201
|
+
try {
|
|
9202
|
+
closing = false;
|
|
9203
|
+
await listenOnPort(requestedPort);
|
|
9204
|
+
// Direct TCP is the authoritative Client control path. UDP is an
|
|
9205
|
+
// optional frame optimization, so a temporary socket/DNS failure
|
|
9206
|
+
// must not keep the Hub offline after its Direct listener is ready.
|
|
9207
|
+
// UDP is optional and may be delayed by a sleeping adapter or a
|
|
9208
|
+
// transient bind failure. Its exact promise remains owned for
|
|
9209
|
+
// retry and shutdown, but it must never hold the Hub HTTP/PWA
|
|
9210
|
+
// startup path behind the already-listening Direct TCP socket.
|
|
9211
|
+
void startOptionalUdpTransport('startup');
|
|
9212
|
+
// Product relay status is permanently disabled. Do not even call
|
|
9213
|
+
// its dormant start owner; only explicitly injected retired-protocol
|
|
9214
|
+
// tests can expose enabled=true.
|
|
9215
|
+
if (relayControl?.getStatus?.().enabled === true) {
|
|
9216
|
+
try {
|
|
9217
|
+
await relayControl?.start?.();
|
|
9218
|
+
} catch {
|
|
9219
|
+
logWarn('relay', 'Encrypted relay control could not start; direct TCP remains available.');
|
|
9220
|
+
}
|
|
9221
|
+
}
|
|
9222
|
+
if (closing || !started) {
|
|
9223
|
+
return getStatus({ includeSecrets: false });
|
|
9224
|
+
}
|
|
9225
|
+
scheduleDeviceConnectionSweep();
|
|
8664
9226
|
} catch (error) {
|
|
8665
9227
|
if (error?.code === 'EADDRINUSE') {
|
|
8666
9228
|
const conflict = new Error(
|
|
@@ -8677,8 +9239,14 @@ export function createRemoteHub(options = {}) {
|
|
|
8677
9239
|
return getStatus({ includeSecrets: false });
|
|
8678
9240
|
}
|
|
8679
9241
|
|
|
8680
|
-
async function close() {
|
|
8681
|
-
|
|
9242
|
+
async function close() {
|
|
9243
|
+
closing = true;
|
|
9244
|
+
if (udpStartRetryTimer) {
|
|
9245
|
+
clearTimeout(udpStartRetryTimer);
|
|
9246
|
+
udpStartRetryTimer = null;
|
|
9247
|
+
}
|
|
9248
|
+
const pendingUdpStart = udpStartPromise;
|
|
9249
|
+
if (deviceConnectionSweepTimer) {
|
|
8682
9250
|
clearTimeout(deviceConnectionSweepTimer);
|
|
8683
9251
|
deviceConnectionSweepTimer = null;
|
|
8684
9252
|
}
|
|
@@ -8690,9 +9258,12 @@ export function createRemoteHub(options = {}) {
|
|
|
8690
9258
|
abortTransportDiagnosticsForDevice(device, 'hub-shutdown');
|
|
8691
9259
|
failAllPendingTasks(device, 'hub-shutdown');
|
|
8692
9260
|
failAllPendingInputFallbacks(device, 'hub-shutdown');
|
|
8693
|
-
failPendingCommandResultWaiters(device, 'hub-shutdown');
|
|
8694
|
-
closeInputSocket(device, 'hub-shutdown');
|
|
8695
|
-
|
|
9261
|
+
failPendingCommandResultWaiters(device, 'hub-shutdown');
|
|
9262
|
+
closeInputSocket(device, 'hub-shutdown');
|
|
9263
|
+
closeFrameSocket(device, 'hub-shutdown');
|
|
9264
|
+
closeAudioSocket(device, 'hub-shutdown');
|
|
9265
|
+
closeFileSocket(device, 'hub-shutdown');
|
|
9266
|
+
if (device.socket && !device.socket.destroyed) {
|
|
8696
9267
|
writeJsonLine(device.socket, { type: 'disconnect', reason: 'hub-shutdown' });
|
|
8697
9268
|
device.socket.destroy();
|
|
8698
9269
|
}
|
|
@@ -8703,10 +9274,13 @@ export function createRemoteHub(options = {}) {
|
|
|
8703
9274
|
socket.destroy();
|
|
8704
9275
|
}
|
|
8705
9276
|
}
|
|
8706
|
-
|
|
8707
|
-
sockets.clear();
|
|
8708
|
-
await relayControl?.close?.();
|
|
8709
|
-
|
|
9277
|
+
|
|
9278
|
+
sockets.clear();
|
|
9279
|
+
await relayControl?.close?.();
|
|
9280
|
+
if (pendingUdpStart) {
|
|
9281
|
+
await pendingUdpStart;
|
|
9282
|
+
}
|
|
9283
|
+
await udpTransport?.close?.();
|
|
8710
9284
|
if (!server) {
|
|
8711
9285
|
started = false;
|
|
8712
9286
|
return;
|
|
@@ -8840,7 +9414,7 @@ export function createRemoteHub(options = {}) {
|
|
|
8840
9414
|
: '';
|
|
8841
9415
|
const denied = policyError(device, requiredPermission, commandName);
|
|
8842
9416
|
if (denied) return { ok: false, error: denied };
|
|
8843
|
-
if (device?.synthetic === true && device.connected) {
|
|
9417
|
+
if (device?.synthetic === true && device.connected) {
|
|
8844
9418
|
const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
|
|
8845
9419
|
device.counters.commandsSent += 1;
|
|
8846
9420
|
device.lastSeenAt = new Date().toISOString();
|
|
@@ -8885,12 +9459,17 @@ export function createRemoteHub(options = {}) {
|
|
|
8885
9459
|
&& !device.fileSocket.destroyed
|
|
8886
9460
|
? device.fileSocket
|
|
8887
9461
|
: null;
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
9462
|
+
let sent = false;
|
|
9463
|
+
try {
|
|
9464
|
+
sent = writeJsonLine(dedicatedFileSocket || device.socket, payload);
|
|
9465
|
+
} catch {
|
|
9466
|
+
retireStaleDeviceConnection(device, 'command-channel-write-failed');
|
|
9467
|
+
return { ok: false, error: 'device-not-connected' };
|
|
9468
|
+
}
|
|
9469
|
+
if (!sent) {
|
|
9470
|
+
retireStaleDeviceConnection(device, 'command-channel-write-failed');
|
|
9471
|
+
return { ok: false, error: 'device-not-connected' };
|
|
9472
|
+
}
|
|
8894
9473
|
device.counters.commandsSent += 1;
|
|
8895
9474
|
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
8896
9475
|
commandId,
|
|
@@ -9731,14 +10310,23 @@ export function createRemoteHub(options = {}) {
|
|
|
9731
10310
|
}
|
|
9732
10311
|
const device = devices.get(String(deviceId || ''));
|
|
9733
10312
|
const requestedOperation = safeString(options.operation, 80);
|
|
9734
|
-
const operation = normalizeAgentOperation(requestedOperation);
|
|
9735
|
-
if (requestedOperation && !operation) {
|
|
9736
|
-
return { ok: false, error: 'unsupported-agent-operation' };
|
|
9737
|
-
}
|
|
9738
|
-
|
|
9739
|
-
|
|
9740
|
-
|
|
9741
|
-
|
|
10313
|
+
const operation = normalizeAgentOperation(requestedOperation);
|
|
10314
|
+
if (requestedOperation && !operation) {
|
|
10315
|
+
return { ok: false, error: 'unsupported-agent-operation' };
|
|
10316
|
+
}
|
|
10317
|
+
const normalizedToolArguments = normalizeAgentToolArguments(operation, options.toolArguments);
|
|
10318
|
+
if (!normalizedToolArguments.ok) {
|
|
10319
|
+
return { ok: false, error: normalizedToolArguments.error };
|
|
10320
|
+
}
|
|
10321
|
+
const toolArguments = normalizedToolArguments.value;
|
|
10322
|
+
const permissionMode = normalizeAgentPermissionMode(operation, options.permissionMode);
|
|
10323
|
+
if (!device) {
|
|
10324
|
+
return { ok: false, error: 'device-not-found' };
|
|
10325
|
+
}
|
|
10326
|
+
if (operation && !deviceSupportsAgentOperation(device, operation)) {
|
|
10327
|
+
return { ok: false, error: 'agent-operation-not-advertised' };
|
|
10328
|
+
}
|
|
10329
|
+
const denied = policyError(device, 'allowAgent');
|
|
9742
10330
|
if (denied) return { ok: false, error: denied };
|
|
9743
10331
|
|
|
9744
10332
|
if (device?.synthetic === true && device.connected) {
|
|
@@ -9749,21 +10337,21 @@ export function createRemoteHub(options = {}) {
|
|
|
9749
10337
|
|
|
9750
10338
|
const now = new Date().toISOString();
|
|
9751
10339
|
const approvalLevel = normalizeApprovalLevel(options.approvalLevel);
|
|
9752
|
-
const commandId =
|
|
9753
|
-
const taskId =
|
|
10340
|
+
const commandId = crypto.randomUUID();
|
|
10341
|
+
const taskId = crypto.randomUUID();
|
|
10342
|
+
const batch = getOwnedTaskBatch(options);
|
|
9754
10343
|
const title = safeString(options.title, 120)
|
|
9755
10344
|
|| safeString(instruction.split(/\r?\n/)[0], 120)
|
|
9756
10345
|
|| 'Remote task';
|
|
9757
10346
|
const task = {
|
|
9758
|
-
batchId:
|
|
10347
|
+
batchId: batch?.batchId || '',
|
|
9759
10348
|
taskId,
|
|
9760
10349
|
commandId,
|
|
9761
10350
|
title,
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
|
|
9766
|
-
instructionPreview: safeText(instruction, 320),
|
|
10351
|
+
operation,
|
|
10352
|
+
targetQuery: safeText(options.targetQuery, 160),
|
|
10353
|
+
permissionMode,
|
|
10354
|
+
instructionPreview: safeText(instruction, 320),
|
|
9767
10355
|
status: 'completed',
|
|
9768
10356
|
stage: 'Completed',
|
|
9769
10357
|
approvalLevel,
|
|
@@ -9776,8 +10364,14 @@ export function createRemoteHub(options = {}) {
|
|
|
9776
10364
|
resultSummary: operation === 'process.list'
|
|
9777
10365
|
? 'Process list collected.'
|
|
9778
10366
|
: `Synthetic task accepted by ${device.deviceName}.`,
|
|
9779
|
-
resultData: undefined
|
|
9780
|
-
};
|
|
10367
|
+
resultData: undefined
|
|
10368
|
+
};
|
|
10369
|
+
Object.defineProperty(task, 'owner', {
|
|
10370
|
+
value: createAgentTaskOwner(device, taskId, commandId, operation, task.batchId),
|
|
10371
|
+
enumerable: false,
|
|
10372
|
+
configurable: false,
|
|
10373
|
+
writable: false
|
|
10374
|
+
});
|
|
9781
10375
|
|
|
9782
10376
|
device.counters.commandsSent += 1;
|
|
9783
10377
|
device.counters.commandResultsReceived += 1;
|
|
@@ -9808,9 +10402,12 @@ export function createRemoteHub(options = {}) {
|
|
|
9808
10402
|
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
9809
10403
|
return { ok: false, error: 'device-not-connected' };
|
|
9810
10404
|
}
|
|
9811
|
-
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
9812
|
-
return { ok: false, error: 'device-not-connected' };
|
|
9813
|
-
}
|
|
10405
|
+
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
10406
|
+
return { ok: false, error: 'device-not-connected' };
|
|
10407
|
+
}
|
|
10408
|
+
if (!canQueueAgentTask(device)) {
|
|
10409
|
+
return { ok: false, error: 'agent-task-capacity-reached' };
|
|
10410
|
+
}
|
|
9814
10411
|
|
|
9815
10412
|
const instruction = safeText(options.instruction, MAX_AGENT_TASK_CHARS);
|
|
9816
10413
|
if (!instruction) {
|
|
@@ -9819,21 +10416,21 @@ export function createRemoteHub(options = {}) {
|
|
|
9819
10416
|
|
|
9820
10417
|
const now = new Date().toISOString();
|
|
9821
10418
|
const approvalLevel = normalizeApprovalLevel(options.approvalLevel);
|
|
9822
|
-
const commandId =
|
|
9823
|
-
const taskId =
|
|
10419
|
+
const commandId = crypto.randomUUID();
|
|
10420
|
+
const taskId = crypto.randomUUID();
|
|
10421
|
+
const batch = getOwnedTaskBatch(options);
|
|
9824
10422
|
const title = safeString(options.title, 120)
|
|
9825
10423
|
|| safeString(instruction.split(/\r?\n/)[0], 120)
|
|
9826
10424
|
|| 'Remote task';
|
|
9827
10425
|
const task = {
|
|
9828
|
-
batchId:
|
|
10426
|
+
batchId: batch?.batchId || '',
|
|
9829
10427
|
taskId,
|
|
9830
10428
|
commandId,
|
|
9831
10429
|
title,
|
|
9832
|
-
|
|
9833
|
-
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
instructionPreview: safeText(instruction, 320),
|
|
10430
|
+
operation,
|
|
10431
|
+
targetQuery: safeText(options.targetQuery, 160),
|
|
10432
|
+
permissionMode,
|
|
10433
|
+
instructionPreview: safeText(instruction, 320),
|
|
9837
10434
|
status: 'queued',
|
|
9838
10435
|
stage: operation === 'process.list' ? 'Checking processes' : 'Queued',
|
|
9839
10436
|
approvalLevel,
|
|
@@ -9844,8 +10441,14 @@ export function createRemoteHub(options = {}) {
|
|
|
9844
10441
|
error: '',
|
|
9845
10442
|
resultKind: '',
|
|
9846
10443
|
resultSummary: '',
|
|
9847
|
-
resultData: undefined
|
|
9848
|
-
};
|
|
10444
|
+
resultData: undefined
|
|
10445
|
+
};
|
|
10446
|
+
Object.defineProperty(task, 'owner', {
|
|
10447
|
+
value: createAgentTaskOwner(device, taskId, commandId, operation, task.batchId),
|
|
10448
|
+
enumerable: false,
|
|
10449
|
+
configurable: false,
|
|
10450
|
+
writable: false
|
|
10451
|
+
});
|
|
9849
10452
|
|
|
9850
10453
|
const commandName = operation || 'agent.task';
|
|
9851
10454
|
const sent = writeJsonLine(device.socket, {
|
|
@@ -9856,11 +10459,11 @@ export function createRemoteHub(options = {}) {
|
|
|
9856
10459
|
taskId,
|
|
9857
10460
|
title,
|
|
9858
10461
|
instruction,
|
|
9859
|
-
|
|
9860
|
-
|
|
9861
|
-
|
|
9862
|
-
|
|
9863
|
-
|
|
10462
|
+
operation,
|
|
10463
|
+
targetQuery: safeText(options.targetQuery, 160),
|
|
10464
|
+
toolArguments,
|
|
10465
|
+
permissionMode,
|
|
10466
|
+
approvalLevel,
|
|
9864
10467
|
requestedAt: now
|
|
9865
10468
|
},
|
|
9866
10469
|
issuedAt: now
|
|
@@ -9902,23 +10505,52 @@ export function createRemoteHub(options = {}) {
|
|
|
9902
10505
|
};
|
|
9903
10506
|
}
|
|
9904
10507
|
|
|
9905
|
-
if (targets.length === 0) {
|
|
10508
|
+
if (targets.length === 0) {
|
|
9906
10509
|
return {
|
|
9907
10510
|
ok: false,
|
|
9908
10511
|
error: 'no-target-devices',
|
|
9909
10512
|
total: 0,
|
|
9910
10513
|
queued: 0,
|
|
9911
10514
|
approvalLevel: normalizeApprovalLevel(options.approvalLevel),
|
|
9912
|
-
results: []
|
|
9913
|
-
};
|
|
9914
|
-
}
|
|
9915
|
-
|
|
9916
|
-
const
|
|
9917
|
-
const
|
|
9918
|
-
|
|
9919
|
-
|
|
9920
|
-
|
|
9921
|
-
|
|
10515
|
+
results: []
|
|
10516
|
+
};
|
|
10517
|
+
}
|
|
10518
|
+
|
|
10519
|
+
const requestedOperation = safeString(options.operation, 80);
|
|
10520
|
+
const operation = normalizeAgentOperation(requestedOperation);
|
|
10521
|
+
if (requestedOperation && !operation) {
|
|
10522
|
+
return {
|
|
10523
|
+
ok: false,
|
|
10524
|
+
error: 'unsupported-agent-operation',
|
|
10525
|
+
total: targets.length,
|
|
10526
|
+
queued: 0,
|
|
10527
|
+
approvalLevel: normalizeApprovalLevel(options.approvalLevel),
|
|
10528
|
+
results: []
|
|
10529
|
+
};
|
|
10530
|
+
}
|
|
10531
|
+
const normalizedToolArguments = normalizeAgentToolArguments(operation, options.toolArguments);
|
|
10532
|
+
if (!normalizedToolArguments.ok) {
|
|
10533
|
+
return {
|
|
10534
|
+
ok: false,
|
|
10535
|
+
error: normalizedToolArguments.error,
|
|
10536
|
+
total: targets.length,
|
|
10537
|
+
queued: 0,
|
|
10538
|
+
approvalLevel: normalizeApprovalLevel(options.approvalLevel),
|
|
10539
|
+
results: []
|
|
10540
|
+
};
|
|
10541
|
+
}
|
|
10542
|
+
const normalizedOptions = {
|
|
10543
|
+
...options,
|
|
10544
|
+
operation,
|
|
10545
|
+
toolArguments: normalizedToolArguments.value,
|
|
10546
|
+
permissionMode: normalizeAgentPermissionMode(operation, options.permissionMode)
|
|
10547
|
+
};
|
|
10548
|
+
const batch = beginTaskBatch(targets, normalizedOptions);
|
|
10549
|
+
const results = targets.map(deviceId => {
|
|
10550
|
+
const result = requestAgentTask(deviceId, {
|
|
10551
|
+
...normalizedOptions,
|
|
10552
|
+
taskBatch: batch
|
|
10553
|
+
});
|
|
9922
10554
|
|
|
9923
10555
|
if (result.ok !== true) {
|
|
9924
10556
|
syncTaskBatchDispatchFailure(batch.batchId, deviceId, result.error, result);
|
|
@@ -10638,7 +11270,7 @@ export function createRemoteHub(options = {}) {
|
|
|
10638
11270
|
=== normalized.streamPurpose;
|
|
10639
11271
|
}
|
|
10640
11272
|
|
|
10641
|
-
function sharedLiveProfileResult(device, activeLiveStream, normalized, extra = {}) {
|
|
11273
|
+
function sharedLiveProfileResult(device, activeLiveStream, normalized, extra = {}) {
|
|
10642
11274
|
return {
|
|
10643
11275
|
ok: true,
|
|
10644
11276
|
commandId: activeLiveStream.commandId,
|
|
@@ -10667,9 +11299,40 @@ export function createRemoteHub(options = {}) {
|
|
|
10667
11299
|
},
|
|
10668
11300
|
...extra
|
|
10669
11301
|
};
|
|
10670
|
-
}
|
|
10671
|
-
|
|
10672
|
-
function
|
|
11302
|
+
}
|
|
11303
|
+
|
|
11304
|
+
function readOnlyControlPresentationBorrowResult(device, normalized, options = {}) {
|
|
11305
|
+
const borrowed = resolveReadOnlyControlPresentationBorrow({
|
|
11306
|
+
device,
|
|
11307
|
+
liveOptions: {
|
|
11308
|
+
allowReadOnlyControlBorrow: options.allowReadOnlyControlBorrow === true,
|
|
11309
|
+
streamPurpose: normalized.streamPurpose,
|
|
11310
|
+
mode: normalized.transfer.mode,
|
|
11311
|
+
frameMode: normalized.transfer.frameMode,
|
|
11312
|
+
fps: normalized.fps,
|
|
11313
|
+
maxWidth: normalized.maxWidth,
|
|
11314
|
+
maxHeight: normalized.maxHeight,
|
|
11315
|
+
quality: normalized.quality
|
|
11316
|
+
},
|
|
11317
|
+
monitorIndex: normalized.monitorIndex
|
|
11318
|
+
});
|
|
11319
|
+
if (!borrowed) {
|
|
11320
|
+
return null;
|
|
11321
|
+
}
|
|
11322
|
+
emitRemoteEvent('RemoteLiveStreamControlPresentationBorrowed', device, {
|
|
11323
|
+
streamId: borrowed.streamId,
|
|
11324
|
+
commandId: borrowed.commandId,
|
|
11325
|
+
captureGeneration: borrowed.captureGeneration,
|
|
11326
|
+
streamPurpose: borrowed.streamPurpose,
|
|
11327
|
+
presentationPurpose: borrowed.presentationPurpose,
|
|
11328
|
+
monitorIndex: borrowed.monitorIndex,
|
|
11329
|
+
requestedProfile: borrowed.requestedProfile,
|
|
11330
|
+
effectiveProfile: borrowed.effectiveProfile
|
|
11331
|
+
});
|
|
11332
|
+
return borrowed;
|
|
11333
|
+
}
|
|
11334
|
+
|
|
11335
|
+
function startLiveStream(deviceId, options = {}) {
|
|
10673
11336
|
const device = devices.get(String(deviceId || ''));
|
|
10674
11337
|
const denied = policyError(device);
|
|
10675
11338
|
if (denied) return { ok: false, error: denied };
|
|
@@ -10693,11 +11356,13 @@ export function createRemoteHub(options = {}) {
|
|
|
10693
11356
|
}
|
|
10694
11357
|
|
|
10695
11358
|
const now = new Date().toISOString();
|
|
10696
|
-
const normalized = normalizeLiveStreamStartOptions({ fps: 2, ...options, platform: device.platform });
|
|
10697
|
-
const modePolicyError = liveStreamModePolicyError(normalized);
|
|
10698
|
-
if (modePolicyError) return { ok: false, error: modePolicyError };
|
|
10699
|
-
const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
|
|
10700
|
-
const
|
|
11359
|
+
const normalized = normalizeLiveStreamStartOptions({ fps: 2, ...options, platform: device.platform });
|
|
11360
|
+
const modePolicyError = liveStreamModePolicyError(normalized);
|
|
11361
|
+
if (modePolicyError) return { ok: false, error: modePolicyError };
|
|
11362
|
+
const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
|
|
11363
|
+
const borrowedControl = readOnlyControlPresentationBorrowResult(device, normalized, options);
|
|
11364
|
+
if (borrowedControl) return { ...borrowedControl, synthetic: true };
|
|
11365
|
+
const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
|
|
10701
11366
|
if (!reserveDeviceLiveStreamDescriptor(device, streamId)) {
|
|
10702
11367
|
return {
|
|
10703
11368
|
ok: false,
|
|
@@ -10776,12 +11441,13 @@ export function createRemoteHub(options = {}) {
|
|
|
10776
11441
|
monitorIndex,
|
|
10777
11442
|
fps
|
|
10778
11443
|
});
|
|
10779
|
-
emitRemoteEvent('RemoteLiveStreamStarted', device, {
|
|
10780
|
-
streamId,
|
|
10781
|
-
commandId,
|
|
10782
|
-
|
|
10783
|
-
|
|
10784
|
-
|
|
11444
|
+
emitRemoteEvent('RemoteLiveStreamStarted', device, {
|
|
11445
|
+
streamId,
|
|
11446
|
+
commandId,
|
|
11447
|
+
streamPurpose,
|
|
11448
|
+
fps,
|
|
11449
|
+
mode: transfer.mode,
|
|
11450
|
+
synthetic: true
|
|
10785
11451
|
});
|
|
10786
11452
|
return {
|
|
10787
11453
|
ok: true,
|
|
@@ -10811,14 +11477,16 @@ export function createRemoteHub(options = {}) {
|
|
|
10811
11477
|
const modePolicyError = liveStreamModePolicyError(normalized);
|
|
10812
11478
|
if (modePolicyError) return { ok: false, error: modePolicyError };
|
|
10813
11479
|
const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
|
|
10814
|
-
if (deviceExplicitlyRejectsFrameMode(device, transfer.frameMode)) {
|
|
10815
|
-
return {
|
|
10816
|
-
ok: false,
|
|
10817
|
-
error: 'device-frame-mode-unavailable',
|
|
10818
|
-
frameMode: transfer.frameMode
|
|
10819
|
-
};
|
|
10820
|
-
}
|
|
10821
|
-
const
|
|
11480
|
+
if (deviceExplicitlyRejectsFrameMode(device, transfer.frameMode)) {
|
|
11481
|
+
return {
|
|
11482
|
+
ok: false,
|
|
11483
|
+
error: 'device-frame-mode-unavailable',
|
|
11484
|
+
frameMode: transfer.frameMode
|
|
11485
|
+
};
|
|
11486
|
+
}
|
|
11487
|
+
const borrowedControl = readOnlyControlPresentationBorrowResult(device, normalized, options);
|
|
11488
|
+
if (borrowedControl) return borrowedControl;
|
|
11489
|
+
const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
|
|
10822
11490
|
if (!reserveDeviceLiveStreamDescriptor(device, streamId)) {
|
|
10823
11491
|
return {
|
|
10824
11492
|
ok: false,
|
|
@@ -10875,9 +11543,9 @@ export function createRemoteHub(options = {}) {
|
|
|
10875
11543
|
}
|
|
10876
11544
|
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
10877
11545
|
const restartToken = safeString(options.restartToken, 128);
|
|
10878
|
-
if (activeLiveStream
|
|
10879
|
-
&& options.forceRestart === true
|
|
10880
|
-
&& restartToken
|
|
11546
|
+
if (activeLiveStream
|
|
11547
|
+
&& options.forceRestart === true
|
|
11548
|
+
&& restartToken
|
|
10881
11549
|
&& safeString(activeLiveStream.restartToken, 128) === restartToken
|
|
10882
11550
|
&& liveStreamMatchesOptions(activeLiveStream, normalized)
|
|
10883
11551
|
&& liveStreamIsReusable(activeLiveStream)) {
|
|
@@ -10900,11 +11568,11 @@ export function createRemoteHub(options = {}) {
|
|
|
10900
11568
|
captureGeneration: Number(activeLiveStream.captureGeneration || 0),
|
|
10901
11569
|
ready: liveStreamHasCurrentFrame(activeLiveStream),
|
|
10902
11570
|
pending: activeLiveStream.open !== true,
|
|
10903
|
-
reused: true
|
|
10904
|
-
};
|
|
10905
|
-
}
|
|
10906
|
-
if (activeLiveStream
|
|
10907
|
-
&& options.forceRestart !== true
|
|
11571
|
+
reused: true
|
|
11572
|
+
};
|
|
11573
|
+
}
|
|
11574
|
+
if (activeLiveStream
|
|
11575
|
+
&& options.forceRestart !== true
|
|
10908
11576
|
&& options.reuseExisting === true
|
|
10909
11577
|
&& liveStreamMatchesOptions(activeLiveStream, normalized)
|
|
10910
11578
|
&& liveStreamIsReusable(activeLiveStream)) {
|
|
@@ -11084,11 +11752,12 @@ export function createRemoteHub(options = {}) {
|
|
|
11084
11752
|
setDeviceLiveStream(device, nextStreamState);
|
|
11085
11753
|
}
|
|
11086
11754
|
device.counters.liveStreamsStarted += 1;
|
|
11087
|
-
emitRemoteEvent('RemoteLiveStreamStarted', device, {
|
|
11088
|
-
streamId,
|
|
11089
|
-
commandId,
|
|
11090
|
-
|
|
11091
|
-
|
|
11755
|
+
emitRemoteEvent('RemoteLiveStreamStarted', device, {
|
|
11756
|
+
streamId,
|
|
11757
|
+
commandId,
|
|
11758
|
+
streamPurpose,
|
|
11759
|
+
previousCommandId,
|
|
11760
|
+
pending: replacingActiveStream,
|
|
11092
11761
|
fps,
|
|
11093
11762
|
mode: transfer.mode,
|
|
11094
11763
|
frameMode: transfer.frameMode,
|
|
@@ -11277,11 +11946,13 @@ export function createRemoteHub(options = {}) {
|
|
|
11277
11946
|
captureLiveStreamStopOwner(device, streamState, commandId));
|
|
11278
11947
|
}
|
|
11279
11948
|
device.counters.liveStreamsStopped += 1;
|
|
11280
|
-
emitRemoteEvent('RemoteLiveStreamStopped', device, {
|
|
11281
|
-
streamId,
|
|
11282
|
-
commandId,
|
|
11283
|
-
|
|
11284
|
-
|
|
11949
|
+
emitRemoteEvent('RemoteLiveStreamStopped', device, {
|
|
11950
|
+
streamId,
|
|
11951
|
+
commandId,
|
|
11952
|
+
streamPurpose: safeString(streamState?.streamPurpose || streamPurpose, 24).toLowerCase() || 'wall',
|
|
11953
|
+
captureStopConfirmed: true,
|
|
11954
|
+
synthetic: true
|
|
11955
|
+
});
|
|
11285
11956
|
return { ok: true, commandId, streamId, synthetic: true };
|
|
11286
11957
|
}
|
|
11287
11958
|
|
|
@@ -11316,7 +11987,7 @@ export function createRemoteHub(options = {}) {
|
|
|
11316
11987
|
: (streamState ? [streamState] : []);
|
|
11317
11988
|
const stopRequestedAt = new Date().toISOString();
|
|
11318
11989
|
const targetOwners = [];
|
|
11319
|
-
for (const targetStream of targetStreams) {
|
|
11990
|
+
for (const targetStream of targetStreams) {
|
|
11320
11991
|
const pendingTarget = getPendingLiveStreamDescriptor(targetStream);
|
|
11321
11992
|
const targetDescriptor = pendingTarget || targetStream;
|
|
11322
11993
|
const targetOwnerKind = pendingTarget ? 'pending' : 'active';
|
|
@@ -11340,10 +12011,19 @@ export function createRemoteHub(options = {}) {
|
|
|
11340
12011
|
targetOwners.push({
|
|
11341
12012
|
owner: targetOwner,
|
|
11342
12013
|
ownerKind: targetOwnerKind
|
|
11343
|
-
});
|
|
11344
|
-
}
|
|
11345
|
-
}
|
|
11346
|
-
const
|
|
12014
|
+
});
|
|
12015
|
+
}
|
|
12016
|
+
}
|
|
12017
|
+
const stoppedStreamPurposes = new Set(
|
|
12018
|
+
targetOwners
|
|
12019
|
+
.map(targetRecord => safeString(targetRecord.owner?.streamPurpose, 24).toLowerCase())
|
|
12020
|
+
.filter(Boolean));
|
|
12021
|
+
const stoppedStreamPurpose = stoppedStreamPurposes.size === 1
|
|
12022
|
+
? [...stoppedStreamPurposes][0]
|
|
12023
|
+
: targetOwners.length === 0
|
|
12024
|
+
? safeString(streamState?.streamPurpose || streamPurpose, 24).toLowerCase()
|
|
12025
|
+
: '';
|
|
12026
|
+
const stopPromise = acknowledgementPromise.then(acknowledgement => {
|
|
11347
12027
|
const currentDevice = devices.get(String(deviceId || ''));
|
|
11348
12028
|
const stopReported = acknowledgement.result?.stream === true
|
|
11349
12029
|
&& acknowledgement.result?.stopped === true;
|
|
@@ -11411,11 +12091,12 @@ export function createRemoteHub(options = {}) {
|
|
|
11411
12091
|
if (captureStopConfirmed
|
|
11412
12092
|
&& (targetOwners.length === 0 || appliedOwnerCount > 0)) {
|
|
11413
12093
|
currentDevice.counters.liveStreamsStopped += 1;
|
|
11414
|
-
emitRemoteEvent('RemoteLiveStreamStopped', currentDevice, {
|
|
11415
|
-
streamId,
|
|
11416
|
-
commandId,
|
|
11417
|
-
|
|
11418
|
-
|
|
12094
|
+
emitRemoteEvent('RemoteLiveStreamStopped', currentDevice, {
|
|
12095
|
+
streamId,
|
|
12096
|
+
commandId,
|
|
12097
|
+
streamPurpose: stoppedStreamPurpose,
|
|
12098
|
+
appliedOwnerCount,
|
|
12099
|
+
captureStopConfirmed: true
|
|
11419
12100
|
});
|
|
11420
12101
|
} else if (!captureStopConfirmed && appliedOwnerCount > 0) {
|
|
11421
12102
|
emitRemoteEvent('RemoteLiveStreamStopUnconfirmed', currentDevice, {
|