@livedesk/hub 0.1.61 → 0.1.65
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/live-capture-transition-retry.mjs +25 -10
- package/src/live-desk-update.js +32 -17
- 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 +1036 -407
- package/src/server.js +684 -180
- package/src/transport/relay-hub-control.js +10 -8
package/src/remote-hub.js
CHANGED
|
@@ -56,9 +56,13 @@ const MAX_AGENT_SOCKET_ACCUMULATOR_BYTES = MAX_LINE_CHARS
|
|
|
56
56
|
const HTTP_HEADER_TERMINATOR = Buffer.from('\r\n\r\n', 'ascii');
|
|
57
57
|
const MAX_AGENT_TASK_CHARS = 4000;
|
|
58
58
|
const MAX_AGENT_TASK_RESULT_CHARS = 3000;
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
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);
|
|
62
66
|
const RECENT_FRAME_CACHE_TTL_MS = 4000;
|
|
63
67
|
const RECENT_THUMBNAIL_FRAME_CACHE_LIMIT = 2;
|
|
64
68
|
const RECENT_LIVE_FRAME_CACHE_LIMIT = 1;
|
|
@@ -68,6 +72,7 @@ const LIVE_STREAM_PENDING_REUSE_MS = 5000;
|
|
|
68
72
|
const LIVE_STREAM_REPLACEMENT_PENDING_MS = 7000;
|
|
69
73
|
const LIVE_STREAM_MIN_FRESH_MS = 3000;
|
|
70
74
|
const LIVE_STREAM_MAX_FRESH_MS = 12000;
|
|
75
|
+
const TASK_BATCH_OWNER = Symbol('remote-hub-task-batch-owner');
|
|
71
76
|
const DEFAULT_REMOTE_INPUT_ACK_TIMEOUT_MS = 5000;
|
|
72
77
|
// RemoteFast owns a shared 7s native stop deadline. Keep four seconds for a
|
|
73
78
|
// priority command.result to cross either Direct or encrypted Relay transport.
|
|
@@ -305,9 +310,17 @@ function normalizePort(value) {
|
|
|
305
310
|
return clampNumber(value, 0, 65535, DEFAULT_REMOTE_HUB_PORT);
|
|
306
311
|
}
|
|
307
312
|
|
|
308
|
-
function safeString(value, maxLength = 200) {
|
|
309
|
-
return String(value ?? '').replace(/[\r\n\t]/g, ' ').trim().slice(0, maxLength);
|
|
310
|
-
}
|
|
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
|
+
}
|
|
311
324
|
|
|
312
325
|
function remoteInputPayloadType(payload) {
|
|
313
326
|
return safeString(payload?.payload?.type ?? payload?.type, 48).toLowerCase();
|
|
@@ -1224,16 +1237,131 @@ function normalizeRemoteKeyboardText(value) {
|
|
|
1224
1237
|
return raw.slice(0, 256);
|
|
1225
1238
|
}
|
|
1226
1239
|
|
|
1227
|
-
function
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
}
|
|
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
|
+
}
|
|
1237
1365
|
|
|
1238
1366
|
function normalizeApprovalLevel(value) {
|
|
1239
1367
|
const level = safeString(value, 80).toLowerCase();
|
|
@@ -1282,10 +1410,20 @@ const SUPPORTED_AGENT_OPERATIONS = new Set([
|
|
|
1282
1410
|
'logs.collect'
|
|
1283
1411
|
]);
|
|
1284
1412
|
|
|
1285
|
-
function normalizeAgentOperation(value) {
|
|
1413
|
+
function normalizeAgentOperation(value) {
|
|
1286
1414
|
const operation = safeString(value, 80).toLowerCase();
|
|
1287
1415
|
return SUPPORTED_AGENT_OPERATIONS.has(operation) ? operation : '';
|
|
1288
|
-
}
|
|
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
|
+
}
|
|
1289
1427
|
|
|
1290
1428
|
const REMOTE_INPUT_MONITOR_KEYS = Object.freeze([
|
|
1291
1429
|
'monitorIndex',
|
|
@@ -1661,7 +1799,23 @@ function findRecentFramePayload(device, frameKind, options = {}) {
|
|
|
1661
1799
|
return entry?.frame || null;
|
|
1662
1800
|
}
|
|
1663
1801
|
|
|
1664
|
-
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 = {}) {
|
|
1665
1819
|
if (!device) {
|
|
1666
1820
|
return null;
|
|
1667
1821
|
}
|
|
@@ -1736,17 +1890,17 @@ function serializeDevice(device, options = {}) {
|
|
|
1736
1890
|
latestAudioStatus: device.latestAudioStatus ? { ...device.latestAudioStatus } : null,
|
|
1737
1891
|
clientUpdateProof: device.clientUpdateProof ? { ...device.clientUpdateProof } : null,
|
|
1738
1892
|
udp: device.udp ? { ...device.udp } : { enabled: false, state: 'tcp-only', ready: false },
|
|
1739
|
-
latestTask: device.latestTask
|
|
1893
|
+
latestTask: serializeDeviceTask(device.latestTask),
|
|
1740
1894
|
synthetic: device.synthetic === true,
|
|
1741
1895
|
recentTasks: Array.isArray(device.recentTasks)
|
|
1742
|
-
? device.recentTasks.map(
|
|
1896
|
+
? device.recentTasks.map(serializeDeviceTask).filter(Boolean)
|
|
1743
1897
|
: [],
|
|
1744
1898
|
status: { ...device.status },
|
|
1745
1899
|
counters: { ...device.counters }
|
|
1746
1900
|
};
|
|
1747
1901
|
}
|
|
1748
1902
|
|
|
1749
|
-
function serializeTaskBatch(batch) {
|
|
1903
|
+
function serializeTaskBatch(batch, options = {}) {
|
|
1750
1904
|
if (!batch) {
|
|
1751
1905
|
return null;
|
|
1752
1906
|
}
|
|
@@ -1768,24 +1922,51 @@ function serializeTaskBatch(batch) {
|
|
|
1768
1922
|
failed: batch.failed,
|
|
1769
1923
|
cancelled: batch.cancelled,
|
|
1770
1924
|
timedOut: batch.timedOut,
|
|
1771
|
-
results: Array.isArray(batch.results)
|
|
1772
|
-
? batch.results.map(item =>
|
|
1773
|
-
|
|
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
|
+
: []
|
|
1774
1940
|
};
|
|
1775
1941
|
}
|
|
1776
1942
|
|
|
1777
|
-
function writeJsonLine(socket, payload) {
|
|
1943
|
+
function writeJsonLine(socket, payload) {
|
|
1778
1944
|
if (!socket || socket.destroyed) {
|
|
1779
1945
|
return false;
|
|
1780
1946
|
}
|
|
1781
1947
|
|
|
1782
|
-
if (socket.__remoteHubWebSocket === true && typeof socket.sendJson === 'function') {
|
|
1783
|
-
return socket.sendJson(payload);
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
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
|
+
}
|
|
1789
1970
|
|
|
1790
1971
|
function writeJsonLineAndClose(socket, payload) {
|
|
1791
1972
|
if (!socket || socket.destroyed) {
|
|
@@ -2050,16 +2231,26 @@ export class RemoteHubWebSocketAgentSocket {
|
|
|
2050
2231
|
return this.sendFrame(0x1, Buffer.from(String(text || ''), 'utf8'));
|
|
2051
2232
|
}
|
|
2052
2233
|
|
|
2053
|
-
sendFrame(opcode, payload) {
|
|
2054
|
-
if (this.destroyed || this.socket.destroyed) {
|
|
2055
|
-
return false;
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2058
|
-
try {
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
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;
|
|
2063
2254
|
}
|
|
2064
2255
|
}
|
|
2065
2256
|
|
|
@@ -2370,13 +2561,18 @@ export function createRemoteHub(options = {}) {
|
|
|
2370
2561
|
defaultDeviceConnectionStaleMs,
|
|
2371
2562
|
10 * 60 * 1000,
|
|
2372
2563
|
defaultDeviceConnectionStaleMs);
|
|
2373
|
-
const deviceConnectionSweepMs = Number.isFinite(Number(options.deviceConnectionSweepMs))
|
|
2374
|
-
? clampNumber(options.deviceConnectionSweepMs, 25, 60000, DEFAULT_DEVICE_CONNECTION_SWEEP_MS)
|
|
2375
|
-
: clampNumber(
|
|
2376
|
-
env.REMOTE_HUB_DEVICE_CONNECTION_SWEEP_MS,
|
|
2377
|
-
1000,
|
|
2378
|
-
60000,
|
|
2379
|
-
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);
|
|
2380
2576
|
const taskTimeoutMs = clampNumber(
|
|
2381
2577
|
options.taskTimeoutMs ?? env.REMOTE_HUB_TASK_TIMEOUT_MS,
|
|
2382
2578
|
50,
|
|
@@ -2415,13 +2611,14 @@ export function createRemoteHub(options = {}) {
|
|
|
2415
2611
|
false);
|
|
2416
2612
|
const relayControl = options.relayControl && typeof options.relayControl === 'object'
|
|
2417
2613
|
? options.relayControl
|
|
2418
|
-
: createHubRelayControl({
|
|
2419
|
-
env,
|
|
2420
|
-
pairToken,
|
|
2421
|
-
logEvent,
|
|
2422
|
-
logWarn,
|
|
2423
|
-
|
|
2424
|
-
|
|
2614
|
+
: createHubRelayControl({
|
|
2615
|
+
env,
|
|
2616
|
+
pairToken,
|
|
2617
|
+
logEvent,
|
|
2618
|
+
logWarn,
|
|
2619
|
+
retiredPayloadRelayTestOnly: options.retiredPayloadRelayTestOnly === true,
|
|
2620
|
+
onPeerSocket: socket => handleTcpAgentSocket(socket)
|
|
2621
|
+
});
|
|
2425
2622
|
let pairingPin = /^\d{6}$/.test(String(options.pairingPin || env.LIVEDESK_PAIRING_PIN || '').trim())
|
|
2426
2623
|
? String(options.pairingPin || env.LIVEDESK_PAIRING_PIN).trim()
|
|
2427
2624
|
: generatePairingPin();
|
|
@@ -2450,11 +2647,15 @@ export function createRemoteHub(options = {}) {
|
|
|
2450
2647
|
const clipboardOperations = new Map();
|
|
2451
2648
|
const transportDiagnostics = new Map();
|
|
2452
2649
|
const transportDiagnosticStartingDevices = new Set();
|
|
2453
|
-
const duplicateDeviceLogAt = new Map();
|
|
2454
|
-
let server = null;
|
|
2455
|
-
let started = false;
|
|
2456
|
-
let
|
|
2457
|
-
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;
|
|
2458
2659
|
let lastError = '';
|
|
2459
2660
|
let hostTarget = null;
|
|
2460
2661
|
let publicIPv4Cache = {
|
|
@@ -2763,12 +2964,20 @@ export function createRemoteHub(options = {}) {
|
|
|
2763
2964
|
const activeHostTarget = serializeHostTarget();
|
|
2764
2965
|
const routeInfo = getAgentEndpointRouteInfo();
|
|
2765
2966
|
const diagnostics = sampleHubRuntimeDiagnostics();
|
|
2766
|
-
diagnostics.network = {
|
|
2767
|
-
lanDeviceCount: connectedRoutes.length - externalDeviceCount,
|
|
2768
|
-
externalDeviceCount,
|
|
2769
|
-
route: externalDeviceCount > 0 ? 'external' : connectedRoutes.length > 0 ? 'lan' : 'idle'
|
|
2770
|
-
};
|
|
2771
|
-
|
|
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 {
|
|
2772
2981
|
enabled,
|
|
2773
2982
|
started,
|
|
2774
2983
|
host,
|
|
@@ -2812,14 +3021,14 @@ export function createRemoteHub(options = {}) {
|
|
|
2812
3021
|
publicIPv4Address: publicIPv4Cache.address,
|
|
2813
3022
|
publicIPv4UpdatedAt: publicIPv4Cache.updatedAt ? new Date(publicIPv4Cache.updatedAt).toISOString() : '',
|
|
2814
3023
|
publicIPv4Error: publicIPv4Cache.error,
|
|
2815
|
-
udp:
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
},
|
|
3024
|
+
udp: {
|
|
3025
|
+
...udpStatus,
|
|
3026
|
+
startRetryTimerActive: udpStartRetryTimer !== null,
|
|
3027
|
+
startInFlight: udpStartPromise !== null,
|
|
3028
|
+
startRetryDelayMs: udpStartRetryTimer !== null
|
|
3029
|
+
? udpStartRetryDelayMs
|
|
3030
|
+
: 0
|
|
3031
|
+
},
|
|
2823
3032
|
relay: relayControl?.getStatus?.() || {
|
|
2824
3033
|
enabled: false,
|
|
2825
3034
|
started: false,
|
|
@@ -2918,8 +3127,8 @@ export function createRemoteHub(options = {}) {
|
|
|
2918
3127
|
return listTaskBatches()[0] || null;
|
|
2919
3128
|
}
|
|
2920
3129
|
|
|
2921
|
-
function getTaskBatch(batchId) {
|
|
2922
|
-
return serializeTaskBatch(taskBatches.get(safeString(batchId, 128)) || null);
|
|
3130
|
+
function getTaskBatch(batchId) {
|
|
3131
|
+
return serializeTaskBatch(taskBatches.get(safeString(batchId, 128)) || null, { includeData: true });
|
|
2923
3132
|
}
|
|
2924
3133
|
|
|
2925
3134
|
function cancelTaskBatch(batchId) {
|
|
@@ -2974,12 +3183,14 @@ export function createRemoteHub(options = {}) {
|
|
|
2974
3183
|
if (targetIds.length === 0) {
|
|
2975
3184
|
return { ok: false, error: 'no-retry-targets' };
|
|
2976
3185
|
}
|
|
2977
|
-
return requestAgentTaskBatch(targetIds, {
|
|
2978
|
-
instruction: batch.instructionPreview,
|
|
2979
|
-
title: batch.title,
|
|
2980
|
-
operation: batch.operation,
|
|
2981
|
-
targetQuery: batch.targetQuery,
|
|
2982
|
-
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
|
|
2983
3194
|
});
|
|
2984
3195
|
}
|
|
2985
3196
|
|
|
@@ -3014,18 +3225,20 @@ export function createRemoteHub(options = {}) {
|
|
|
3014
3225
|
}
|
|
3015
3226
|
}
|
|
3016
3227
|
|
|
3017
|
-
function settlePendingCommandResult(device, message = {}) {
|
|
3018
|
-
|
|
3228
|
+
function settlePendingCommandResult(device, message = {}, channel = 'control') {
|
|
3229
|
+
channel = safeString(channel, 24) || 'control';
|
|
3230
|
+
const commandId = safeString(message.commandId, 128);
|
|
3019
3231
|
const waiterKey = buildPendingCommandResultWaiterKey(
|
|
3020
3232
|
device?.deviceId,
|
|
3021
3233
|
device?.sessionId,
|
|
3022
3234
|
commandId);
|
|
3023
3235
|
const waiter = pendingCommandResultWaiters.get(waiterKey);
|
|
3024
|
-
if (!waiter
|
|
3025
|
-
|| waiter.deviceId !== device?.deviceId
|
|
3026
|
-
|| waiter.sessionId !== device?.sessionId
|
|
3027
|
-
|
|
3028
|
-
|
|
3236
|
+
if (!waiter
|
|
3237
|
+
|| waiter.deviceId !== device?.deviceId
|
|
3238
|
+
|| waiter.sessionId !== device?.sessionId
|
|
3239
|
+
|| (safeString(waiter.channel, 24) || 'control') !== channel) {
|
|
3240
|
+
return false;
|
|
3241
|
+
}
|
|
3029
3242
|
|
|
3030
3243
|
pendingCommandResultWaiters.delete(waiterKey);
|
|
3031
3244
|
clearPendingCommandResultWaiterDeadline(waiter);
|
|
@@ -4821,7 +5034,107 @@ export function createRemoteHub(options = {}) {
|
|
|
4821
5034
|
};
|
|
4822
5035
|
}
|
|
4823
5036
|
|
|
4824
|
-
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) {
|
|
4825
5138
|
const nowMs = Date.now();
|
|
4826
5139
|
const now = new Date(nowMs).toISOString();
|
|
4827
5140
|
const sessionId = crypto.randomUUID();
|
|
@@ -4947,19 +5260,7 @@ export function createRemoteHub(options = {}) {
|
|
|
4947
5260
|
serverTime: now
|
|
4948
5261
|
});
|
|
4949
5262
|
|
|
4950
|
-
|
|
4951
|
-
deviceId,
|
|
4952
|
-
sessionId,
|
|
4953
|
-
sendControl: payload => writeJsonLine(socket, payload),
|
|
4954
|
-
capabilities
|
|
4955
|
-
});
|
|
4956
|
-
if (udpSession?.sessionId) {
|
|
4957
|
-
device.udp = {
|
|
4958
|
-
...device.udp,
|
|
4959
|
-
state: 'udp-session-created',
|
|
4960
|
-
sessionId: udpSession.sessionId
|
|
4961
|
-
};
|
|
4962
|
-
}
|
|
5263
|
+
registerCurrentDeviceUdpSession(device);
|
|
4963
5264
|
|
|
4964
5265
|
logEvent('remote', `device connected ${device.deviceName} (${deviceId})`, 'success');
|
|
4965
5266
|
emitRemoteEvent('RemoteDeviceConnected', device);
|
|
@@ -5049,16 +5350,26 @@ export function createRemoteHub(options = {}) {
|
|
|
5049
5350
|
return device;
|
|
5050
5351
|
}
|
|
5051
5352
|
|
|
5052
|
-
function attachFrameSocket(socket, hello) {
|
|
5053
|
-
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
5054
|
-
const device = devices.get(deviceId);
|
|
5055
|
-
if (!device || !device.connected || !device.socket || device.socket.destroyed) {
|
|
5056
|
-
writeJsonLine(socket, { type: 'error', error: 'device-not-connected' });
|
|
5057
|
-
socket.destroy();
|
|
5058
|
-
return null;
|
|
5059
|
-
}
|
|
5060
|
-
|
|
5061
|
-
|
|
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');
|
|
5062
5373
|
|
|
5063
5374
|
const now = new Date().toISOString();
|
|
5064
5375
|
device.frameSocket = socket;
|
|
@@ -5082,16 +5393,26 @@ export function createRemoteHub(options = {}) {
|
|
|
5082
5393
|
return device;
|
|
5083
5394
|
}
|
|
5084
5395
|
|
|
5085
|
-
function attachAudioSocket(socket, hello) {
|
|
5086
|
-
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
5087
|
-
const device = devices.get(deviceId);
|
|
5088
|
-
if (!device || !device.connected || !device.socket || device.socket.destroyed) {
|
|
5089
|
-
writeJsonLine(socket, { type: 'error', error: 'device-not-connected' });
|
|
5090
|
-
socket.destroy();
|
|
5091
|
-
return null;
|
|
5092
|
-
}
|
|
5093
|
-
|
|
5094
|
-
|
|
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');
|
|
5095
5416
|
const now = new Date().toISOString();
|
|
5096
5417
|
device.audioSocket = socket;
|
|
5097
5418
|
device.audioConnectedAt = now;
|
|
@@ -5158,8 +5479,10 @@ export function createRemoteHub(options = {}) {
|
|
|
5158
5479
|
device.socket = null;
|
|
5159
5480
|
abortTransportDiagnosticsForDevice(device, reason);
|
|
5160
5481
|
udpTransport?.unregisterDevice?.(deviceId, device.sessionId);
|
|
5161
|
-
closeInputSocket(device, reason);
|
|
5162
|
-
closeFrameSocket(device, reason);
|
|
5482
|
+
closeInputSocket(device, reason);
|
|
5483
|
+
closeFrameSocket(device, reason);
|
|
5484
|
+
closeAudioSocket(device, reason);
|
|
5485
|
+
closeFileSocket(device, reason);
|
|
5163
5486
|
device.disconnectedAt = new Date().toISOString();
|
|
5164
5487
|
device.lastDisconnectReason = reason;
|
|
5165
5488
|
deactivateDeviceLiveStreams(device, reason, device.disconnectedAt);
|
|
@@ -5171,10 +5494,10 @@ export function createRemoteHub(options = {}) {
|
|
|
5171
5494
|
emitRemoteEvent('RemoteDeviceDisconnected', device, { reason });
|
|
5172
5495
|
}
|
|
5173
5496
|
|
|
5174
|
-
function rememberDeviceTask(device, task) {
|
|
5175
|
-
if (!device || !task) {
|
|
5176
|
-
return null;
|
|
5177
|
-
}
|
|
5497
|
+
function rememberDeviceTask(device, task) {
|
|
5498
|
+
if (!device || !task) {
|
|
5499
|
+
return null;
|
|
5500
|
+
}
|
|
5178
5501
|
|
|
5179
5502
|
const existingIndex = device.recentTasks.findIndex(item =>
|
|
5180
5503
|
item.taskId === task.taskId || item.commandId === task.commandId);
|
|
@@ -5187,13 +5510,85 @@ export function createRemoteHub(options = {}) {
|
|
|
5187
5510
|
device.recentTasks.length = RECENT_TASK_LIMIT;
|
|
5188
5511
|
}
|
|
5189
5512
|
|
|
5190
|
-
device.latestTask = task;
|
|
5191
|
-
if (task.commandId) {
|
|
5192
|
-
device.pendingTaskCommands.set(task.commandId, task);
|
|
5193
|
-
}
|
|
5513
|
+
device.latestTask = task;
|
|
5514
|
+
if (task.commandId) {
|
|
5515
|
+
device.pendingTaskCommands.set(task.commandId, task);
|
|
5516
|
+
}
|
|
5194
5517
|
|
|
5195
|
-
return task;
|
|
5196
|
-
}
|
|
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
|
+
}
|
|
5197
5592
|
|
|
5198
5593
|
function trimTaskBatches() {
|
|
5199
5594
|
const ordered = [...taskBatches.values()]
|
|
@@ -5212,17 +5607,19 @@ export function createRemoteHub(options = {}) {
|
|
|
5212
5607
|
const instruction = safeText(options.instruction, MAX_AGENT_TASK_CHARS);
|
|
5213
5608
|
const operation = normalizeAgentOperation(options.operation);
|
|
5214
5609
|
const targetQuery = safeText(options.targetQuery, 160);
|
|
5215
|
-
const batchId =
|
|
5610
|
+
const batchId = crypto.randomUUID();
|
|
5216
5611
|
const title = safeString(options.title, 120)
|
|
5217
5612
|
|| safeString(instruction.split(/\r?\n/)[0], 120)
|
|
5218
5613
|
|| 'Remote task batch';
|
|
5219
|
-
const batch = {
|
|
5614
|
+
const batch = {
|
|
5220
5615
|
batchId,
|
|
5221
5616
|
title,
|
|
5222
5617
|
instructionPreview: safeText(instruction, 320),
|
|
5223
5618
|
operation,
|
|
5224
5619
|
targetQuery,
|
|
5225
|
-
approvalLevel: normalizeApprovalLevel(options.approvalLevel),
|
|
5620
|
+
approvalLevel: normalizeApprovalLevel(options.approvalLevel),
|
|
5621
|
+
retryToolArguments: safeRetryToolArguments(operation, options.toolArguments),
|
|
5622
|
+
retryPermissionMode: normalizeAgentPermissionMode(operation, options.permissionMode),
|
|
5226
5623
|
status: targets.length > 0 ? 'running' : 'failed',
|
|
5227
5624
|
requestedAt: now,
|
|
5228
5625
|
updatedAt: now,
|
|
@@ -5249,7 +5646,7 @@ export function createRemoteHub(options = {}) {
|
|
|
5249
5646
|
}))
|
|
5250
5647
|
};
|
|
5251
5648
|
|
|
5252
|
-
taskBatches.set(batchId, batch);
|
|
5649
|
+
taskBatches.set(batchId, markOwnedTaskBatch(batch));
|
|
5253
5650
|
trimTaskBatches();
|
|
5254
5651
|
return batch;
|
|
5255
5652
|
}
|
|
@@ -5394,11 +5791,11 @@ export function createRemoteHub(options = {}) {
|
|
|
5394
5791
|
return null;
|
|
5395
5792
|
}
|
|
5396
5793
|
|
|
5397
|
-
const task = device.pendingTaskCommands.get(key);
|
|
5398
|
-
if (!task) {
|
|
5399
|
-
clearTaskTimeout(device, key);
|
|
5400
|
-
return null;
|
|
5401
|
-
}
|
|
5794
|
+
const task = device.pendingTaskCommands.get(key);
|
|
5795
|
+
if (!task || !isPendingAgentTaskOwner(device, task, key)) {
|
|
5796
|
+
clearTaskTimeout(device, key);
|
|
5797
|
+
return null;
|
|
5798
|
+
}
|
|
5402
5799
|
|
|
5403
5800
|
const now = new Date().toISOString();
|
|
5404
5801
|
clearTaskTimeout(device, key);
|
|
@@ -5915,62 +6312,66 @@ export function createRemoteHub(options = {}) {
|
|
|
5915
6312
|
return;
|
|
5916
6313
|
}
|
|
5917
6314
|
|
|
5918
|
-
clearTaskTimeout(device, commandId);
|
|
5919
|
-
const timer = setTimeout(() => {
|
|
5920
|
-
|
|
5921
|
-
|
|
6315
|
+
clearTaskTimeout(device, commandId);
|
|
6316
|
+
const timer = setTimeout(() => {
|
|
6317
|
+
if (isPendingAgentTaskOwner(device, task, commandId)) {
|
|
6318
|
+
failPendingTask(device, commandId, 'task-timeout');
|
|
6319
|
+
}
|
|
6320
|
+
}, taskTimeoutMs);
|
|
5922
6321
|
timer.unref?.();
|
|
5923
6322
|
device.pendingTaskTimers.set(commandId, timer);
|
|
5924
6323
|
}
|
|
5925
6324
|
|
|
5926
|
-
function summarizeTaskResult(result) {
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|| result.
|
|
5932
|
-
|| result.
|
|
5933
|
-
||
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
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);
|
|
5938
6343
|
}
|
|
5939
6344
|
|
|
5940
|
-
function applyTaskResult(device, commandId, result, error) {
|
|
5941
|
-
if (!device || !commandId) {
|
|
5942
|
-
return null;
|
|
5943
|
-
}
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
const task = resultTaskId
|
|
5949
|
-
? device.recentTasks.find(item => item.taskId === resultTaskId)
|
|
5950
|
-
: device.pendingTaskCommands.get(commandId);
|
|
5951
|
-
if (!task) {
|
|
5952
|
-
return null;
|
|
5953
|
-
}
|
|
5954
|
-
|
|
5955
|
-
if (device.pendingTaskCommands.get(commandId) !== task) {
|
|
5956
|
-
return null;
|
|
5957
|
-
}
|
|
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
|
+
}
|
|
5958
6353
|
|
|
5959
6354
|
const now = device.lastSeenAt || new Date().toISOString();
|
|
5960
|
-
const resultFailed = Boolean(error)
|
|
5961
|
-
|| result?.ok === false
|
|
5962
|
-
|| safeString(result?.status, 40) === 'failed';
|
|
5963
|
-
const status = resultFailed ? 'failed' : safeString(result?.status, 40) || 'completed';
|
|
5964
|
-
const
|
|
5965
|
-
|
|
5966
|
-
|
|
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' : '');
|
|
5967
6366
|
clearTaskTimeout(device, commandId);
|
|
5968
6367
|
task.status = status;
|
|
5969
6368
|
task.updatedAt = now;
|
|
5970
6369
|
task.completedAt = safeString(result?.completedAt, 80) || now;
|
|
5971
6370
|
task.error = resultError;
|
|
5972
|
-
task.resultSummary = resultError || summarizeTaskResult(result);
|
|
5973
|
-
task.resultData =
|
|
6371
|
+
task.resultSummary = resultError || summarizeTaskResult(result, diagnostic);
|
|
6372
|
+
task.resultData = diagnostic
|
|
6373
|
+
? safeDiagnosticTaskData(result?.data)
|
|
6374
|
+
: safeTaskData(result?.data);
|
|
5974
6375
|
task.resultKind = safeString(result?.kind || result?.mode || 'agent-task', 80);
|
|
5975
6376
|
device.latestTask = task;
|
|
5976
6377
|
device.pendingTaskCommands.delete(commandId);
|
|
@@ -7689,7 +8090,7 @@ export function createRemoteHub(options = {}) {
|
|
|
7689
8090
|
};
|
|
7690
8091
|
}
|
|
7691
8092
|
|
|
7692
|
-
function writeFrameTransportProofResponse(socket, result) {
|
|
8093
|
+
function writeFrameTransportProofResponse(socket, result) {
|
|
7693
8094
|
const proof = result?.proof;
|
|
7694
8095
|
if (!proof?.proofId) return;
|
|
7695
8096
|
const accepted = result.proofAccepted === true;
|
|
@@ -7705,24 +8106,98 @@ export function createRemoteHub(options = {}) {
|
|
|
7705
8106
|
streamPurpose: proof.streamPurpose,
|
|
7706
8107
|
monitorIndex: proof.monitorIndex,
|
|
7707
8108
|
...(accepted ? {} : { error: safeString(result.error || 'frame-proof-rejected', 160) })
|
|
7708
|
-
});
|
|
7709
|
-
}
|
|
7710
|
-
|
|
7711
|
-
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) {
|
|
7712
8167
|
if (!state.authenticated || !state.device) {
|
|
7713
8168
|
writeJsonLine(socket, { type: 'error', error: 'hello-required' });
|
|
7714
8169
|
socket.destroy();
|
|
7715
8170
|
return;
|
|
7716
8171
|
}
|
|
7717
|
-
|
|
7718
|
-
const device = state.device;
|
|
7719
|
-
if (state
|
|
7720
|
-
|
|
7721
|
-
socket.destroy();
|
|
7722
|
-
return;
|
|
7723
|
-
}
|
|
7724
|
-
|
|
7725
|
-
|
|
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;
|
|
7726
8201
|
device.lastSeenAt = new Date().toISOString();
|
|
7727
8202
|
if (state.frameOnly) {
|
|
7728
8203
|
device.frameLastSeenAt = device.lastSeenAt;
|
|
@@ -7731,8 +8206,7 @@ export function createRemoteHub(options = {}) {
|
|
|
7731
8206
|
device.audioLastSeenAt = device.lastSeenAt;
|
|
7732
8207
|
}
|
|
7733
8208
|
|
|
7734
|
-
const
|
|
7735
|
-
const binaryTransport = socket.__liveDeskRelayControl === true
|
|
8209
|
+
const binaryTransport = socket.__liveDeskRelayControl === true
|
|
7736
8210
|
? 'relay-binary'
|
|
7737
8211
|
: state.binaryTransport === 'ws-binary'
|
|
7738
8212
|
? 'ws-binary'
|
|
@@ -7959,9 +8433,12 @@ export function createRemoteHub(options = {}) {
|
|
|
7959
8433
|
return state.binaryIngress.enqueue(header, payload);
|
|
7960
8434
|
}
|
|
7961
8435
|
|
|
7962
|
-
function handleAgentMessage(socket, state, message) {
|
|
7963
|
-
if (!message || typeof message !== 'object') {
|
|
7964
|
-
|
|
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;
|
|
7965
8442
|
}
|
|
7966
8443
|
|
|
7967
8444
|
if (!state.authenticated) {
|
|
@@ -8022,9 +8499,10 @@ export function createRemoteHub(options = {}) {
|
|
|
8022
8499
|
return;
|
|
8023
8500
|
}
|
|
8024
8501
|
|
|
8025
|
-
state.authenticated = true;
|
|
8026
|
-
state.device = device;
|
|
8027
|
-
state.
|
|
8502
|
+
state.authenticated = true;
|
|
8503
|
+
state.device = device;
|
|
8504
|
+
state.sessionId = device.sessionId;
|
|
8505
|
+
state.inputOnly = true;
|
|
8028
8506
|
return;
|
|
8029
8507
|
}
|
|
8030
8508
|
if (channel === 'frame') {
|
|
@@ -8038,9 +8516,10 @@ export function createRemoteHub(options = {}) {
|
|
|
8038
8516
|
return;
|
|
8039
8517
|
}
|
|
8040
8518
|
|
|
8041
|
-
state.authenticated = true;
|
|
8042
|
-
state.device = device;
|
|
8043
|
-
state.
|
|
8519
|
+
state.authenticated = true;
|
|
8520
|
+
state.device = device;
|
|
8521
|
+
state.sessionId = device.sessionId;
|
|
8522
|
+
state.frameOnly = true;
|
|
8044
8523
|
return;
|
|
8045
8524
|
}
|
|
8046
8525
|
if (channel === 'audio') {
|
|
@@ -8049,9 +8528,10 @@ export function createRemoteHub(options = {}) {
|
|
|
8049
8528
|
return;
|
|
8050
8529
|
}
|
|
8051
8530
|
|
|
8052
|
-
state.authenticated = true;
|
|
8053
|
-
state.device = device;
|
|
8054
|
-
state.
|
|
8531
|
+
state.authenticated = true;
|
|
8532
|
+
state.device = device;
|
|
8533
|
+
state.sessionId = device.sessionId;
|
|
8534
|
+
state.audioOnly = true;
|
|
8055
8535
|
return;
|
|
8056
8536
|
}
|
|
8057
8537
|
if (channel === 'file') {
|
|
@@ -8060,9 +8540,10 @@ export function createRemoteHub(options = {}) {
|
|
|
8060
8540
|
return;
|
|
8061
8541
|
}
|
|
8062
8542
|
|
|
8063
|
-
state.authenticated = true;
|
|
8064
|
-
state.device = device;
|
|
8065
|
-
state.
|
|
8543
|
+
state.authenticated = true;
|
|
8544
|
+
state.device = device;
|
|
8545
|
+
state.sessionId = device.sessionId;
|
|
8546
|
+
state.fileOnly = true;
|
|
8066
8547
|
return;
|
|
8067
8548
|
}
|
|
8068
8549
|
|
|
@@ -8071,21 +8552,23 @@ export function createRemoteHub(options = {}) {
|
|
|
8071
8552
|
return;
|
|
8072
8553
|
}
|
|
8073
8554
|
|
|
8074
|
-
state.authenticated = true;
|
|
8075
|
-
state.device = device;
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
const device = state.device;
|
|
8080
|
-
if (!device) {
|
|
8081
|
-
socket.destroy();
|
|
8082
|
-
return;
|
|
8555
|
+
state.authenticated = true;
|
|
8556
|
+
state.device = device;
|
|
8557
|
+
state.sessionId = device.sessionId;
|
|
8558
|
+
return;
|
|
8083
8559
|
}
|
|
8084
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
|
+
|
|
8085
8571
|
if (state.inputOnly) {
|
|
8086
|
-
if (device.inputSocket !== socket) {
|
|
8087
|
-
return;
|
|
8088
|
-
}
|
|
8089
8572
|
device.inputLastSeenAt = new Date().toISOString();
|
|
8090
8573
|
if (message.type === 'input.applied' || message.type === 'input.error') {
|
|
8091
8574
|
acknowledgePendingDedicatedInput(device, message);
|
|
@@ -8095,21 +8578,50 @@ export function createRemoteHub(options = {}) {
|
|
|
8095
8578
|
}, 'input');
|
|
8096
8579
|
}
|
|
8097
8580
|
return;
|
|
8098
|
-
}
|
|
8099
|
-
if (state.frameOnly) {
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
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
|
+
}
|
|
8107
8618
|
if (state.fileOnly) {
|
|
8108
8619
|
device.fileLastSeenAt = new Date().toISOString();
|
|
8109
8620
|
}
|
|
8110
8621
|
|
|
8111
|
-
device.counters.messagesReceived += 1;
|
|
8112
|
-
device.lastSeenAt = new Date().toISOString();
|
|
8622
|
+
device.counters.messagesReceived += 1;
|
|
8623
|
+
device.lastSeenAt = new Date().toISOString();
|
|
8624
|
+
const channel = getAgentMessageChannel(state);
|
|
8113
8625
|
|
|
8114
8626
|
switch (message.type) {
|
|
8115
8627
|
case 'heartbeat':
|
|
@@ -8137,12 +8649,14 @@ export function createRemoteHub(options = {}) {
|
|
|
8137
8649
|
message.error || (message.type === 'command.error' ? 'command-failed' : ''),
|
|
8138
8650
|
500);
|
|
8139
8651
|
const result = message.result ?? null;
|
|
8140
|
-
settlePendingCommandResult(device, message);
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
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);
|
|
8146
8660
|
if (task) {
|
|
8147
8661
|
emitRemoteEvent('RemoteTaskResult', device, {
|
|
8148
8662
|
commandId,
|
|
@@ -8158,8 +8672,16 @@ export function createRemoteHub(options = {}) {
|
|
|
8158
8672
|
error: safeString(message.error, 500)
|
|
8159
8673
|
});
|
|
8160
8674
|
break;
|
|
8161
|
-
case 'client.update.verified': {
|
|
8162
|
-
|
|
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();
|
|
8163
8685
|
const proof = normalizeClientUpdateVerifiedProof(device, message, receivedAt);
|
|
8164
8686
|
if (!proof) {
|
|
8165
8687
|
logWarn(
|
|
@@ -8205,10 +8727,11 @@ export function createRemoteHub(options = {}) {
|
|
|
8205
8727
|
socket.setNoDelay(true);
|
|
8206
8728
|
socket.setKeepAlive(true, heartbeatMs);
|
|
8207
8729
|
|
|
8208
|
-
const state = {
|
|
8209
|
-
authenticated: false,
|
|
8210
|
-
device: null,
|
|
8211
|
-
|
|
8730
|
+
const state = {
|
|
8731
|
+
authenticated: false,
|
|
8732
|
+
device: null,
|
|
8733
|
+
sessionId: '',
|
|
8734
|
+
binaryTransport: 'ws-binary',
|
|
8212
8735
|
inputOnly: false,
|
|
8213
8736
|
frameOnly: false,
|
|
8214
8737
|
audioOnly: false,
|
|
@@ -8228,33 +8751,56 @@ export function createRemoteHub(options = {}) {
|
|
|
8228
8751
|
}
|
|
8229
8752
|
}, 10000);
|
|
8230
8753
|
|
|
8231
|
-
socket.onTextMessage = text => {
|
|
8232
|
-
try {
|
|
8233
|
-
handleAgentMessage(socket, state, parseJsonLine(String(text || '')));
|
|
8234
|
-
} catch (err) {
|
|
8235
|
-
|
|
8236
|
-
|
|
8237
|
-
|
|
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
|
+
}
|
|
8238
8765
|
};
|
|
8239
8766
|
|
|
8240
8767
|
socket.onBinaryMessage = payload => {
|
|
8241
8768
|
try {
|
|
8242
8769
|
const packet = parseRemoteHubWebSocketBinaryFrame(payload);
|
|
8243
|
-
if (!packet?.header || !Buffer.isBuffer(packet.payload)) {
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
const
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
|
|
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' });
|
|
8258
8804
|
logWarn('remote', 'invalid websocket binary frame from agent.');
|
|
8259
8805
|
return;
|
|
8260
8806
|
}
|
|
@@ -8382,10 +8928,11 @@ export function createRemoteHub(options = {}) {
|
|
|
8382
8928
|
socket.setNoDelay(true);
|
|
8383
8929
|
socket.setKeepAlive(true, heartbeatMs);
|
|
8384
8930
|
|
|
8385
|
-
const state = {
|
|
8386
|
-
authenticated: false,
|
|
8387
|
-
device: null,
|
|
8388
|
-
|
|
8931
|
+
const state = {
|
|
8932
|
+
authenticated: false,
|
|
8933
|
+
device: null,
|
|
8934
|
+
sessionId: '',
|
|
8935
|
+
binaryTransport: 'binary',
|
|
8389
8936
|
inputOnly: false,
|
|
8390
8937
|
frameOnly: false,
|
|
8391
8938
|
audioOnly: false,
|
|
@@ -8451,24 +8998,14 @@ export function createRemoteHub(options = {}) {
|
|
|
8451
8998
|
}
|
|
8452
8999
|
|
|
8453
9000
|
if (startsWithTcpBinaryFrameMagic(state.buffer)) {
|
|
8454
|
-
const packet = parseTcpBinaryFramePacket(state.buffer);
|
|
8455
|
-
if (packet?.incomplete) {
|
|
8456
|
-
return;
|
|
8457
|
-
}
|
|
8458
|
-
if (!packet || packet.invalid) {
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
8462
|
-
state.lineScanOffset = 0;
|
|
8463
|
-
continue;
|
|
8464
|
-
}
|
|
8465
|
-
|
|
8466
|
-
const syncTail = keepTcpFrameOnlySyncTail(state.buffer);
|
|
8467
|
-
state.buffer.clear();
|
|
8468
|
-
state.buffer.append(syncTail);
|
|
8469
|
-
state.lineScanOffset = 0;
|
|
8470
|
-
return;
|
|
8471
|
-
}
|
|
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
|
+
}
|
|
8472
9009
|
|
|
8473
9010
|
state.buffer.discard(packet.packetLength);
|
|
8474
9011
|
state.lineScanOffset = 0;
|
|
@@ -8476,20 +9013,10 @@ export function createRemoteHub(options = {}) {
|
|
|
8476
9013
|
continue;
|
|
8477
9014
|
}
|
|
8478
9015
|
|
|
8479
|
-
if (!isJsonObjectStartByte(state.buffer.byteAt(0))) {
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
state.lineScanOffset = 0;
|
|
8484
|
-
continue;
|
|
8485
|
-
}
|
|
8486
|
-
|
|
8487
|
-
const syncTail = keepTcpFrameOnlySyncTail(state.buffer);
|
|
8488
|
-
state.buffer.clear();
|
|
8489
|
-
state.buffer.append(syncTail);
|
|
8490
|
-
state.lineScanOffset = 0;
|
|
8491
|
-
return;
|
|
8492
|
-
}
|
|
9016
|
+
if (!isJsonObjectStartByte(state.buffer.byteAt(0))) {
|
|
9017
|
+
failCloseMediaSideChannel(socket, state, 'media-side-channel-malformed-binary', 'invalid-binary-frame');
|
|
9018
|
+
return;
|
|
9019
|
+
}
|
|
8493
9020
|
}
|
|
8494
9021
|
|
|
8495
9022
|
const newlineIndex = state.buffer.indexOfByte(0x0a, state.lineScanOffset);
|
|
@@ -8511,18 +9038,33 @@ export function createRemoteHub(options = {}) {
|
|
|
8511
9038
|
return;
|
|
8512
9039
|
}
|
|
8513
9040
|
|
|
8514
|
-
try {
|
|
8515
|
-
const message = parseJsonLine(lineBuffer.toString('utf8'));
|
|
8516
|
-
if (message?.type === 'frame.binary') {
|
|
8517
|
-
const
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
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;
|
|
8526
9068
|
}
|
|
8527
9069
|
|
|
8528
9070
|
state.pendingBinaryFrame = {
|
|
@@ -8532,11 +9074,12 @@ export function createRemoteHub(options = {}) {
|
|
|
8532
9074
|
continue;
|
|
8533
9075
|
}
|
|
8534
9076
|
|
|
8535
|
-
handleAgentMessage(socket, state, message);
|
|
8536
|
-
} catch (err) {
|
|
8537
|
-
if (state.authenticated && (state.frameOnly || state.audioOnly)) {
|
|
8538
|
-
|
|
8539
|
-
|
|
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
|
+
}
|
|
8540
9083
|
writeJsonLine(socket, { type: 'error', error: 'invalid-json' });
|
|
8541
9084
|
logWarn('remote', `invalid agent message: ${err?.message || err}`);
|
|
8542
9085
|
}
|
|
@@ -8650,20 +9193,36 @@ export function createRemoteHub(options = {}) {
|
|
|
8650
9193
|
});
|
|
8651
9194
|
}
|
|
8652
9195
|
|
|
8653
|
-
async function start() {
|
|
8654
|
-
if (!enabled || started) {
|
|
8655
|
-
return getStatus({ includeSecrets: false });
|
|
8656
|
-
}
|
|
8657
|
-
|
|
8658
|
-
try {
|
|
8659
|
-
|
|
8660
|
-
await listenOnPort(requestedPort);
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
|
|
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();
|
|
8667
9226
|
} catch (error) {
|
|
8668
9227
|
if (error?.code === 'EADDRINUSE') {
|
|
8669
9228
|
const conflict = new Error(
|
|
@@ -8680,8 +9239,14 @@ export function createRemoteHub(options = {}) {
|
|
|
8680
9239
|
return getStatus({ includeSecrets: false });
|
|
8681
9240
|
}
|
|
8682
9241
|
|
|
8683
|
-
async function close() {
|
|
8684
|
-
|
|
9242
|
+
async function close() {
|
|
9243
|
+
closing = true;
|
|
9244
|
+
if (udpStartRetryTimer) {
|
|
9245
|
+
clearTimeout(udpStartRetryTimer);
|
|
9246
|
+
udpStartRetryTimer = null;
|
|
9247
|
+
}
|
|
9248
|
+
const pendingUdpStart = udpStartPromise;
|
|
9249
|
+
if (deviceConnectionSweepTimer) {
|
|
8685
9250
|
clearTimeout(deviceConnectionSweepTimer);
|
|
8686
9251
|
deviceConnectionSweepTimer = null;
|
|
8687
9252
|
}
|
|
@@ -8693,9 +9258,12 @@ export function createRemoteHub(options = {}) {
|
|
|
8693
9258
|
abortTransportDiagnosticsForDevice(device, 'hub-shutdown');
|
|
8694
9259
|
failAllPendingTasks(device, 'hub-shutdown');
|
|
8695
9260
|
failAllPendingInputFallbacks(device, 'hub-shutdown');
|
|
8696
|
-
failPendingCommandResultWaiters(device, 'hub-shutdown');
|
|
8697
|
-
closeInputSocket(device, 'hub-shutdown');
|
|
8698
|
-
|
|
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) {
|
|
8699
9267
|
writeJsonLine(device.socket, { type: 'disconnect', reason: 'hub-shutdown' });
|
|
8700
9268
|
device.socket.destroy();
|
|
8701
9269
|
}
|
|
@@ -8706,10 +9274,13 @@ export function createRemoteHub(options = {}) {
|
|
|
8706
9274
|
socket.destroy();
|
|
8707
9275
|
}
|
|
8708
9276
|
}
|
|
8709
|
-
|
|
8710
|
-
sockets.clear();
|
|
8711
|
-
await relayControl?.close?.();
|
|
8712
|
-
|
|
9277
|
+
|
|
9278
|
+
sockets.clear();
|
|
9279
|
+
await relayControl?.close?.();
|
|
9280
|
+
if (pendingUdpStart) {
|
|
9281
|
+
await pendingUdpStart;
|
|
9282
|
+
}
|
|
9283
|
+
await udpTransport?.close?.();
|
|
8713
9284
|
if (!server) {
|
|
8714
9285
|
started = false;
|
|
8715
9286
|
return;
|
|
@@ -8843,7 +9414,7 @@ export function createRemoteHub(options = {}) {
|
|
|
8843
9414
|
: '';
|
|
8844
9415
|
const denied = policyError(device, requiredPermission, commandName);
|
|
8845
9416
|
if (denied) return { ok: false, error: denied };
|
|
8846
|
-
if (device?.synthetic === true && device.connected) {
|
|
9417
|
+
if (device?.synthetic === true && device.connected) {
|
|
8847
9418
|
const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
|
|
8848
9419
|
device.counters.commandsSent += 1;
|
|
8849
9420
|
device.lastSeenAt = new Date().toISOString();
|
|
@@ -8888,12 +9459,17 @@ export function createRemoteHub(options = {}) {
|
|
|
8888
9459
|
&& !device.fileSocket.destroyed
|
|
8889
9460
|
? device.fileSocket
|
|
8890
9461
|
: null;
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
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
|
+
}
|
|
8897
9473
|
device.counters.commandsSent += 1;
|
|
8898
9474
|
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
8899
9475
|
commandId,
|
|
@@ -9734,14 +10310,23 @@ export function createRemoteHub(options = {}) {
|
|
|
9734
10310
|
}
|
|
9735
10311
|
const device = devices.get(String(deviceId || ''));
|
|
9736
10312
|
const requestedOperation = safeString(options.operation, 80);
|
|
9737
|
-
const operation = normalizeAgentOperation(requestedOperation);
|
|
9738
|
-
if (requestedOperation && !operation) {
|
|
9739
|
-
return { ok: false, error: 'unsupported-agent-operation' };
|
|
9740
|
-
}
|
|
9741
|
-
|
|
9742
|
-
|
|
9743
|
-
|
|
9744
|
-
|
|
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');
|
|
9745
10330
|
if (denied) return { ok: false, error: denied };
|
|
9746
10331
|
|
|
9747
10332
|
if (device?.synthetic === true && device.connected) {
|
|
@@ -9752,21 +10337,21 @@ export function createRemoteHub(options = {}) {
|
|
|
9752
10337
|
|
|
9753
10338
|
const now = new Date().toISOString();
|
|
9754
10339
|
const approvalLevel = normalizeApprovalLevel(options.approvalLevel);
|
|
9755
|
-
const commandId =
|
|
9756
|
-
const taskId =
|
|
10340
|
+
const commandId = crypto.randomUUID();
|
|
10341
|
+
const taskId = crypto.randomUUID();
|
|
10342
|
+
const batch = getOwnedTaskBatch(options);
|
|
9757
10343
|
const title = safeString(options.title, 120)
|
|
9758
10344
|
|| safeString(instruction.split(/\r?\n/)[0], 120)
|
|
9759
10345
|
|| 'Remote task';
|
|
9760
10346
|
const task = {
|
|
9761
|
-
batchId:
|
|
10347
|
+
batchId: batch?.batchId || '',
|
|
9762
10348
|
taskId,
|
|
9763
10349
|
commandId,
|
|
9764
10350
|
title,
|
|
9765
|
-
|
|
9766
|
-
|
|
9767
|
-
|
|
9768
|
-
|
|
9769
|
-
instructionPreview: safeText(instruction, 320),
|
|
10351
|
+
operation,
|
|
10352
|
+
targetQuery: safeText(options.targetQuery, 160),
|
|
10353
|
+
permissionMode,
|
|
10354
|
+
instructionPreview: safeText(instruction, 320),
|
|
9770
10355
|
status: 'completed',
|
|
9771
10356
|
stage: 'Completed',
|
|
9772
10357
|
approvalLevel,
|
|
@@ -9779,8 +10364,14 @@ export function createRemoteHub(options = {}) {
|
|
|
9779
10364
|
resultSummary: operation === 'process.list'
|
|
9780
10365
|
? 'Process list collected.'
|
|
9781
10366
|
: `Synthetic task accepted by ${device.deviceName}.`,
|
|
9782
|
-
resultData: undefined
|
|
9783
|
-
};
|
|
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
|
+
});
|
|
9784
10375
|
|
|
9785
10376
|
device.counters.commandsSent += 1;
|
|
9786
10377
|
device.counters.commandResultsReceived += 1;
|
|
@@ -9811,9 +10402,12 @@ export function createRemoteHub(options = {}) {
|
|
|
9811
10402
|
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
9812
10403
|
return { ok: false, error: 'device-not-connected' };
|
|
9813
10404
|
}
|
|
9814
|
-
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
9815
|
-
return { ok: false, error: 'device-not-connected' };
|
|
9816
|
-
}
|
|
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
|
+
}
|
|
9817
10411
|
|
|
9818
10412
|
const instruction = safeText(options.instruction, MAX_AGENT_TASK_CHARS);
|
|
9819
10413
|
if (!instruction) {
|
|
@@ -9822,21 +10416,21 @@ export function createRemoteHub(options = {}) {
|
|
|
9822
10416
|
|
|
9823
10417
|
const now = new Date().toISOString();
|
|
9824
10418
|
const approvalLevel = normalizeApprovalLevel(options.approvalLevel);
|
|
9825
|
-
const commandId =
|
|
9826
|
-
const taskId =
|
|
10419
|
+
const commandId = crypto.randomUUID();
|
|
10420
|
+
const taskId = crypto.randomUUID();
|
|
10421
|
+
const batch = getOwnedTaskBatch(options);
|
|
9827
10422
|
const title = safeString(options.title, 120)
|
|
9828
10423
|
|| safeString(instruction.split(/\r?\n/)[0], 120)
|
|
9829
10424
|
|| 'Remote task';
|
|
9830
10425
|
const task = {
|
|
9831
|
-
batchId:
|
|
10426
|
+
batchId: batch?.batchId || '',
|
|
9832
10427
|
taskId,
|
|
9833
10428
|
commandId,
|
|
9834
10429
|
title,
|
|
9835
|
-
|
|
9836
|
-
|
|
9837
|
-
|
|
9838
|
-
|
|
9839
|
-
instructionPreview: safeText(instruction, 320),
|
|
10430
|
+
operation,
|
|
10431
|
+
targetQuery: safeText(options.targetQuery, 160),
|
|
10432
|
+
permissionMode,
|
|
10433
|
+
instructionPreview: safeText(instruction, 320),
|
|
9840
10434
|
status: 'queued',
|
|
9841
10435
|
stage: operation === 'process.list' ? 'Checking processes' : 'Queued',
|
|
9842
10436
|
approvalLevel,
|
|
@@ -9847,8 +10441,14 @@ export function createRemoteHub(options = {}) {
|
|
|
9847
10441
|
error: '',
|
|
9848
10442
|
resultKind: '',
|
|
9849
10443
|
resultSummary: '',
|
|
9850
|
-
resultData: undefined
|
|
9851
|
-
};
|
|
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
|
+
});
|
|
9852
10452
|
|
|
9853
10453
|
const commandName = operation || 'agent.task';
|
|
9854
10454
|
const sent = writeJsonLine(device.socket, {
|
|
@@ -9859,11 +10459,11 @@ export function createRemoteHub(options = {}) {
|
|
|
9859
10459
|
taskId,
|
|
9860
10460
|
title,
|
|
9861
10461
|
instruction,
|
|
9862
|
-
|
|
9863
|
-
|
|
9864
|
-
|
|
9865
|
-
|
|
9866
|
-
|
|
10462
|
+
operation,
|
|
10463
|
+
targetQuery: safeText(options.targetQuery, 160),
|
|
10464
|
+
toolArguments,
|
|
10465
|
+
permissionMode,
|
|
10466
|
+
approvalLevel,
|
|
9867
10467
|
requestedAt: now
|
|
9868
10468
|
},
|
|
9869
10469
|
issuedAt: now
|
|
@@ -9905,23 +10505,52 @@ export function createRemoteHub(options = {}) {
|
|
|
9905
10505
|
};
|
|
9906
10506
|
}
|
|
9907
10507
|
|
|
9908
|
-
if (targets.length === 0) {
|
|
10508
|
+
if (targets.length === 0) {
|
|
9909
10509
|
return {
|
|
9910
10510
|
ok: false,
|
|
9911
10511
|
error: 'no-target-devices',
|
|
9912
10512
|
total: 0,
|
|
9913
10513
|
queued: 0,
|
|
9914
10514
|
approvalLevel: normalizeApprovalLevel(options.approvalLevel),
|
|
9915
|
-
results: []
|
|
9916
|
-
};
|
|
9917
|
-
}
|
|
9918
|
-
|
|
9919
|
-
const
|
|
9920
|
-
const
|
|
9921
|
-
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
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
|
+
});
|
|
9925
10554
|
|
|
9926
10555
|
if (result.ok !== true) {
|
|
9927
10556
|
syncTaskBatchDispatchFailure(batch.batchId, deviceId, result.error, result);
|