@livedesk/client 0.1.250 → 0.1.252
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/README.md +10 -10
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/bin/client-version.js +1 -1
- package/bin/livedesk-client-node.js +91 -37
- package/bin/livedesk-client-update-bootstrap.cjs +12 -12
- package/bin/livedesk-client.js +136 -122
- package/package.json +7 -7
- package/src/runtime/agent-process-lifecycle.js +10 -10
- package/src/runtime/agent-shell.js +66 -0
- package/src/runtime/client-runtime-server.js +12 -11
- package/src/runtime/linux-video-acceleration.js +1 -1
- package/src/runtime/windows-owned-process-manifest.js +9 -9
- package/tests/client-version.test.mjs +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# VuvoDesk Client
|
|
2
2
|
|
|
3
|
-
For normal users, run the unified
|
|
3
|
+
For normal users, run the unified VuvoDesk application:
|
|
4
4
|
|
|
5
5
|
```powershell
|
|
6
6
|
npx -y --prefer-online livedesk@latest
|
|
@@ -20,7 +20,7 @@ aligned.
|
|
|
20
20
|
The default flow lets the local Client runtime own Google authorization,
|
|
21
21
|
callback exchange, and saved authentication as one path. The browser only
|
|
22
22
|
shows progress; it does not maintain a second session that must be transferred
|
|
23
|
-
back to the Client.
|
|
23
|
+
back to the Client. VuvoDesk then waits for the active Hub published by the
|
|
24
24
|
same account and connects locally. If the Hub is not
|
|
25
25
|
ready yet, the client waits for a Hub-online event and polls the registry at
|
|
26
26
|
most every six seconds as a fallback. Missing a wake event can never leave a
|
|
@@ -34,10 +34,10 @@ On Windows, check **Start with Windows** on the connection page to reconnect thi
|
|
|
34
34
|
client automatically after reboot. The startup entry reuses the saved Google
|
|
35
35
|
session and only opens the connection page again if sign-in is needed.
|
|
36
36
|
|
|
37
|
-
On the Hub computer, open the
|
|
37
|
+
On the Hub computer, open the VuvoDesk dashboard, sign in, and run **Sync
|
|
38
38
|
Server** once. The dashboard transfers the signed-in session to the local Hub;
|
|
39
39
|
the Hub process then keeps its own local address and private pair token
|
|
40
|
-
refreshed in the
|
|
40
|
+
refreshed in the VuvoDesk Supabase registry. The dashboard can be closed after
|
|
41
41
|
that while the Hub remains running.
|
|
42
42
|
|
|
43
43
|
Supabase Auth must allow the CLI callback URL:
|
|
@@ -55,12 +55,12 @@ can receive safe task-only instructions. The C# RemoteFast engine also supports
|
|
|
55
55
|
keyboard/mouse control on Windows, macOS, and Linux/X11, file transfer, and
|
|
56
56
|
system-audio playback from Windows, macOS, and Linux. Linux captures the default
|
|
57
57
|
output through PipeWire `pw-record` or PulseAudio `parec`. macOS control requires Accessibility
|
|
58
|
-
permission for the terminal app that started
|
|
59
|
-
system-audio capture require Screen Recording permission.
|
|
58
|
+
permission for the terminal app that started VuvoDesk; screen streaming and
|
|
59
|
+
system-audio capture require Screen Recording permission. VuvoDesk checks these
|
|
60
60
|
permissions without opening repeated system dialogs and prints the matching
|
|
61
61
|
System Settings path when access is missing; after granting access, restart the
|
|
62
62
|
client. When file transfer is enabled by the Hub, received files are saved
|
|
63
|
-
to `Desktop/
|
|
63
|
+
to `Desktop/VuvoDeskFiles` unless the Hub sets another destination folder.
|
|
64
64
|
|
|
65
65
|
By default, the launcher uses the packaged C# RemoteFast engine when supported.
|
|
66
66
|
It falls back to the Node engine only when a compatible RemoteFast runtime is
|
|
@@ -80,7 +80,7 @@ npx -y --prefer-online livedesk@latest client 3
|
|
|
80
80
|
npx -y --prefer-online livedesk@latest client --no-thumbnail
|
|
81
81
|
npx -y --prefer-online livedesk@latest client --no-live
|
|
82
82
|
npx -y --prefer-online livedesk@latest client --no-audio
|
|
83
|
-
npx -y --prefer-online livedesk@latest client --files-dir "D:\
|
|
83
|
+
npx -y --prefer-online livedesk@latest client --files-dir "D:\VuvoDeskFiles"
|
|
84
84
|
npx -y --prefer-online livedesk@latest client --engine node
|
|
85
85
|
npx -y --prefer-online livedesk@latest client --engine fast --trace-frames
|
|
86
86
|
npx -y --prefer-online livedesk@latest client --auth-port 5200
|
|
@@ -91,7 +91,7 @@ Frame pipeline roadmap:
|
|
|
91
91
|
|
|
92
92
|
- Mode 1: `mode1-jpeg` - current test path using screen capture, resize, and JPEG binary frames.
|
|
93
93
|
- Mode 2: `mode2-lzo` - cross-platform wall path using independent RGB565LE frames capped at 320x180 and compressed as LZO1X blocks. The wall defaults to 8 fps. macOS and Linux keep one persistent local capture helper per client process. Legacy `mode2-lz4` settings migrate to this mode.
|
|
94
|
-
- Mode 3: `mode3-h264-hw` - OS-specific H.264 path. Windows tries Media Foundation/NVENC/QSV/AMF, macOS prefers ScreenCaptureKit plus VideoToolbox, and Linux tries NVENC/VAAPI/QSV before falling back to the bundled ffmpeg `libx264` encoder. Linux users do not need to install ffmpeg separately when starting
|
|
94
|
+
- Mode 3: `mode3-h264-hw` - OS-specific H.264 path. Windows tries Media Foundation/NVENC/QSV/AMF, macOS prefers ScreenCaptureKit plus VideoToolbox, and Linux tries NVENC/VAAPI/QSV before falling back to the bundled ffmpeg `libx264` encoder. Linux users do not need to install ffmpeg separately when starting VuvoDesk through the published npm package. macOS emits one startup key frame and then an approximately half-second recovery GOP. The launcher uses bundled ffmpeg for fallback paths unless `LIVEDESK_FFMPEG` points to a custom binary. On macOS, set `LIVEDESK_FFMPEG_AVFOUNDATION_INPUT` only when the ScreenCaptureKit helper is unavailable and the AVFoundation fallback input is not `1:none`.
|
|
95
95
|
|
|
96
96
|
Legacy mode names such as `remote-fast` and `remote-quality` are treated as
|
|
97
97
|
Mode 1 aliases.
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## lzokay
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
VuvoDesk's LZO1X block encoder and decoder follow the instruction layout of
|
|
6
6
|
[lzokay](https://github.com/AxioDL/lzokay), Copyright (c) 2018 Jack Andersen.
|
|
7
7
|
|
|
8
8
|
The MIT License
|
package/bin/client-version.js
CHANGED
|
@@ -19,5 +19,5 @@ export function formatClientVersionBanner(env = process.env) {
|
|
|
19
19
|
const launcherVersion = String(env.LIVEDESK_NPM_LAUNCHER_VERSION || '').trim();
|
|
20
20
|
const productName = launcherName || 'livedesk';
|
|
21
21
|
const productVersion = launcherVersion || readClientPackageVersion();
|
|
22
|
-
return `[
|
|
22
|
+
return `[VuvoDesk Client] ${productName}@${productVersion}`;
|
|
23
23
|
}
|
|
@@ -8,6 +8,7 @@ import { existsSync, promises as fs, statfsSync } from 'fs';
|
|
|
8
8
|
import { spawn } from 'child_process';
|
|
9
9
|
import { createRequire } from 'node:module';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { resolveAgentShellCommand } from '../src/runtime/agent-shell.js';
|
|
11
12
|
|
|
12
13
|
const require = createRequire(import.meta.url);
|
|
13
14
|
const CLIENT_UPDATE_BOOTSTRAP_PATH = fileURLToPath(
|
|
@@ -39,7 +40,7 @@ function normalizeClientUpdateTargetVersion(value) {
|
|
|
39
40
|
const version = String(value || '').trim().replace(/^v/i, '');
|
|
40
41
|
if (!version) return '';
|
|
41
42
|
if (!CLIENT_UPDATE_VERSION_PATTERN.test(version)) {
|
|
42
|
-
throw new Error(`Invalid
|
|
43
|
+
throw new Error(`Invalid VuvoDesk product update version: ${version}`);
|
|
43
44
|
}
|
|
44
45
|
return version;
|
|
45
46
|
}
|
|
@@ -63,7 +64,7 @@ async function prepareClientUpdateNeutralCwd(operationId) {
|
|
|
63
64
|
const unexpectedEntry = (await fs.readdir(neutralCwd))[0];
|
|
64
65
|
if (unexpectedEntry) {
|
|
65
66
|
const shadowPath = path.join(neutralCwd, unexpectedEntry);
|
|
66
|
-
throw new Error(`
|
|
67
|
+
throw new Error(`VuvoDesk update neutral working directory is shadowed by ${shadowPath}: ${neutralCwd}`);
|
|
67
68
|
}
|
|
68
69
|
return neutralCwd;
|
|
69
70
|
}
|
|
@@ -227,8 +228,8 @@ async function waitForClientUpdateShutdownSignal(
|
|
|
227
228
|
return { ready: true, error: '' };
|
|
228
229
|
}
|
|
229
230
|
if (state.stage === 'failed') {
|
|
230
|
-
const error = String(state.error || '
|
|
231
|
-
console.error(`
|
|
231
|
+
const error = String(state.error || 'VuvoDesk client update preparation failed.');
|
|
232
|
+
console.error(`VuvoDesk client update preparation failed: ${error}`);
|
|
232
233
|
return { ready: false, error };
|
|
233
234
|
}
|
|
234
235
|
} catch {
|
|
@@ -238,8 +239,8 @@ async function waitForClientUpdateShutdownSignal(
|
|
|
238
239
|
}
|
|
239
240
|
const deadlineExpired = Date.now() >= Number(updateDeadlineEpochMs);
|
|
240
241
|
const error = deadlineExpired
|
|
241
|
-
? 'The absolute
|
|
242
|
-
: 'Timed out waiting for the
|
|
242
|
+
? 'The absolute VuvoDesk Client update deadline expired before shutdown.'
|
|
243
|
+
: 'Timed out waiting for the VuvoDesk update supervisor to prepare the exact package.';
|
|
243
244
|
await writeClientUpdateState('failed', {
|
|
244
245
|
operationId,
|
|
245
246
|
error,
|
|
@@ -247,7 +248,7 @@ async function waitForClientUpdateShutdownSignal(
|
|
|
247
248
|
deadlineExpired,
|
|
248
249
|
failedAt: new Date().toISOString()
|
|
249
250
|
}).catch(() => undefined);
|
|
250
|
-
console.error('
|
|
251
|
+
console.error('VuvoDesk client update preparation timed out; the current Agent will remain running.');
|
|
251
252
|
return { ready: false, error };
|
|
252
253
|
}
|
|
253
254
|
|
|
@@ -261,7 +262,7 @@ function markClientUpdateConnected() {
|
|
|
261
262
|
restartVerified: true,
|
|
262
263
|
error: ''
|
|
263
264
|
}).catch(error => {
|
|
264
|
-
console.error(`
|
|
265
|
+
console.error(`VuvoDesk client update diagnostics could not be persisted: ${error?.message || error}`);
|
|
265
266
|
});
|
|
266
267
|
}
|
|
267
268
|
|
|
@@ -318,14 +319,14 @@ async function publishClientUpdateSupervisorProof(socket) {
|
|
|
318
319
|
|
|
319
320
|
function printHelp() {
|
|
320
321
|
console.log(`
|
|
321
|
-
|
|
322
|
+
VuvoDesk Client
|
|
322
323
|
|
|
323
324
|
Usage:
|
|
324
325
|
npx @livedesk/client connect --manager 127.0.0.1:5197 --pair <token>
|
|
325
326
|
|
|
326
327
|
Options:
|
|
327
|
-
--manager <host:port>
|
|
328
|
-
--pair <token>
|
|
328
|
+
--manager <host:port> VuvoDesk Hub address. Default: ${DEFAULT_MANAGER}
|
|
329
|
+
--pair <token> VuvoDesk Hub pairing token. Can also use LIVEDESK_CLIENT_PAIR_TOKEN.
|
|
329
330
|
--slot <number> Screen wall slot number for this computer.
|
|
330
331
|
--name <name> Friendly device name. Default: OS hostname.
|
|
331
332
|
--heartbeat <ms> Status heartbeat interval. Default: ${DEFAULT_HEARTBEAT_MS}
|
|
@@ -336,7 +337,7 @@ Options:
|
|
|
336
337
|
--no-live Disable focused live screen streaming.
|
|
337
338
|
--tasks Enable safe remote task inbox. Default on.
|
|
338
339
|
--no-tasks Disable remote task dispatch capability.
|
|
339
|
-
--files-dir <path> Default folder for received files. Default: ~/Desktop/
|
|
340
|
+
--files-dir <path> Default folder for received files. Default: ~/Desktop/VuvoDeskFiles.
|
|
340
341
|
--fake-thumbnail Use generated thumbnail frames for smoke tests.
|
|
341
342
|
--exit-on-disconnect Exit after Hub disconnect. Useful for tests.
|
|
342
343
|
--exit-on-invalid-pair Exit when the Hub rejects the pair token.
|
|
@@ -684,7 +685,7 @@ async function waitForNodeServiceState(executable, service, expectedState, timeo
|
|
|
684
685
|
}
|
|
685
686
|
|
|
686
687
|
function getDefaultFilesDir() {
|
|
687
|
-
return path.join(os.homedir(), 'Desktop', '
|
|
688
|
+
return path.join(os.homedir(), 'Desktop', 'VuvoDeskFiles');
|
|
688
689
|
}
|
|
689
690
|
|
|
690
691
|
function normalizeDirectoryPath(value, fallback = getDefaultFilesDir()) {
|
|
@@ -861,7 +862,7 @@ function createFakeThumbnailFrame(options, payload, frameSeq) {
|
|
|
861
862
|
'<rect width="100%" height="100%" fill="#0f172a"/>',
|
|
862
863
|
'<rect x="14" y="14" width="calc(100% - 28px)" height="calc(100% - 28px)" rx="12" fill="#1e293b" stroke="#38bdf8" stroke-width="2"/>',
|
|
863
864
|
`<text x="28" y="48" fill="#e2e8f0" font-family="Arial, sans-serif" font-size="20" font-weight="700">${title}</text>`,
|
|
864
|
-
`<text x="28" y="82" fill="#93c5fd" font-family="Arial, sans-serif" font-size="14">
|
|
865
|
+
`<text x="28" y="82" fill="#93c5fd" font-family="Arial, sans-serif" font-size="14">VuvoDesk Client Thumbnail</text>`,
|
|
865
866
|
`<text x="28" y="${height - 30}" fill="#94a3b8" font-family="Consolas, monospace" font-size="12">${now}</text>`,
|
|
866
867
|
'</svg>'
|
|
867
868
|
].join('');
|
|
@@ -1118,7 +1119,7 @@ async function assertAgentPathDoesNotTraverseLink(base, resolved) {
|
|
|
1118
1119
|
}
|
|
1119
1120
|
const canonicalCurrent = await fs.realpath(current);
|
|
1120
1121
|
if (!isPathWithinAgentRoot(canonicalRoot, canonicalCurrent)) {
|
|
1121
|
-
throw new Error('Safe Agent path resolves outside the
|
|
1122
|
+
throw new Error('Safe Agent path resolves outside the VuvoDesk files directory.');
|
|
1122
1123
|
}
|
|
1123
1124
|
return;
|
|
1124
1125
|
} catch (error) {
|
|
@@ -1136,14 +1137,15 @@ async function resolveAgentPath(options, value, permissionMode, rejectSensitive
|
|
|
1136
1137
|
if (!text || text.length > 600) throw new Error('path is invalid');
|
|
1137
1138
|
const resolved = path.resolve(path.isAbsolute(text) ? text : path.join(base, text));
|
|
1138
1139
|
const relative = path.relative(base, resolved);
|
|
1139
|
-
if (permissionMode !== 'full-access' && (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))) throw new Error('path is outside the
|
|
1140
|
+
if (permissionMode !== 'full-access' && (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))) throw new Error('path is outside the VuvoDesk files directory for this permission mode');
|
|
1140
1141
|
if (permissionMode !== 'full-access') await assertAgentPathDoesNotTraverseLink(base, resolved);
|
|
1141
1142
|
if (rejectSensitive && isSensitiveAgentPath(resolved)) throw new Error('credential and secret paths are not available');
|
|
1142
1143
|
return resolved;
|
|
1143
1144
|
}
|
|
1144
1145
|
|
|
1145
|
-
function runNodeAgentProcess(executable, args, timeoutMs = 15000, cwd = undefined) {
|
|
1146
|
+
function runNodeAgentProcess(executable, args, timeoutMs = 15000, cwd = undefined, maxTimeoutMs = 30000) {
|
|
1146
1147
|
return new Promise((resolve, reject) => {
|
|
1148
|
+
const startedAt = Date.now();
|
|
1147
1149
|
const child = spawn(executable, args, { cwd, windowsHide: true, shell: false, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1148
1150
|
let output = '';
|
|
1149
1151
|
let timedOut = false;
|
|
@@ -1154,12 +1156,56 @@ function runNodeAgentProcess(executable, args, timeoutMs = 15000, cwd = undefine
|
|
|
1154
1156
|
timedOut = true;
|
|
1155
1157
|
child.kill('SIGTERM');
|
|
1156
1158
|
setTimeout(() => child.kill('SIGKILL'), 1000).unref?.();
|
|
1157
|
-
}, Math.max(1000, Math.min(
|
|
1159
|
+
}, Math.max(1000, Math.min(maxTimeoutMs, Number(timeoutMs) || 15000)));
|
|
1158
1160
|
child.once('error', error => { clearTimeout(timer); reject(error); });
|
|
1159
|
-
child.once('close', code => { clearTimeout(timer); resolve({ output: redactAgentOutput(`${output}${timedOut ? '\n[timeout]' : ''}`.trim()), exitCode: timedOut ? -1 : code ?? -1, timedOut }); });
|
|
1161
|
+
child.once('close', code => { clearTimeout(timer); resolve({ output: redactAgentOutput(`${output}${timedOut ? '\n[timeout]' : ''}`.trim()), exitCode: timedOut ? -1 : code ?? -1, timedOut, durationMs: Date.now() - startedAt }); });
|
|
1160
1162
|
});
|
|
1161
1163
|
}
|
|
1162
1164
|
|
|
1165
|
+
async function searchNodeAgentFiles(options, args, permissionMode) {
|
|
1166
|
+
const root = await resolveAgentPath(options, args.path, permissionMode);
|
|
1167
|
+
const query = String(args.query || '').trim().toLowerCase();
|
|
1168
|
+
if (!query || query.length > 160 || /[\0\r\n]/.test(query)) throw new Error('search query is invalid');
|
|
1169
|
+
const maxResults = Math.max(1, Math.min(200, Number(args.maxResults) || 100));
|
|
1170
|
+
const maxDepth = Math.max(0, Math.min(8, Number(args.maxDepth) || 4));
|
|
1171
|
+
const maxScannedEntries = Math.max(maxResults, Math.min(5000, Number(args.maxScannedEntries) || 2000));
|
|
1172
|
+
const queue = [{ directory: root, depth: 0 }];
|
|
1173
|
+
const matches = [];
|
|
1174
|
+
let scannedEntries = 0;
|
|
1175
|
+
while (queue.length > 0 && matches.length < maxResults && scannedEntries < maxScannedEntries) {
|
|
1176
|
+
const current = queue.shift();
|
|
1177
|
+
let entries;
|
|
1178
|
+
try {
|
|
1179
|
+
entries = await fs.readdir(current.directory, { withFileTypes: true });
|
|
1180
|
+
} catch {
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
for (const entry of entries) {
|
|
1184
|
+
if (matches.length >= maxResults || scannedEntries >= maxScannedEntries) break;
|
|
1185
|
+
scannedEntries += 1;
|
|
1186
|
+
const entryPath = path.join(current.directory, entry.name);
|
|
1187
|
+
if (isSensitiveAgentPath(entryPath)) continue;
|
|
1188
|
+
const isDirectory = entry.isDirectory();
|
|
1189
|
+
if (entry.name.toLowerCase().includes(query)) {
|
|
1190
|
+
matches.push({ name: entry.name, path: entryPath, type: isDirectory ? 'directory' : 'file' });
|
|
1191
|
+
}
|
|
1192
|
+
if (isDirectory && current.depth < maxDepth && !entry.isSymbolicLink()) {
|
|
1193
|
+
queue.push({ directory: entryPath, depth: current.depth + 1 });
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
return {
|
|
1198
|
+
summary: `Found ${matches.length} matching path${matches.length === 1 ? '' : 's'}.`,
|
|
1199
|
+
data: {
|
|
1200
|
+
path: root,
|
|
1201
|
+
query,
|
|
1202
|
+
matches,
|
|
1203
|
+
scannedEntries,
|
|
1204
|
+
truncated: matches.length >= maxResults || scannedEntries >= maxScannedEntries
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1163
1209
|
function redactAgentOutput(value) {
|
|
1164
1210
|
let output = String(value || '').replace(/[\0\r]/g, ' ');
|
|
1165
1211
|
output = output.replace(/\b(token|secret|password|api[-_]?key|authorization|private[-_]?key|connection[-_]?string|access[-_]?key|client[-_]?secret)\b\s*[:=]\s*("[^"]*"|'[^']*'|[^\s,;]+)/gi, '$1=[redacted]');
|
|
@@ -1284,7 +1330,7 @@ async function persistNodeEnvironmentVariable(name, value) {
|
|
|
1284
1330
|
return { persisted: true, scope: 'user', output: result.output };
|
|
1285
1331
|
}
|
|
1286
1332
|
const profilePath = path.join(os.homedir(), '.profile');
|
|
1287
|
-
const marker = `#
|
|
1333
|
+
const marker = `# VuvoDesk managed environment: ${variableName}`;
|
|
1288
1334
|
const quoted = `'${variableValue.replaceAll("'", "'\\\"'\\\"'")}'`;
|
|
1289
1335
|
let profile = '';
|
|
1290
1336
|
try { profile = await fs.readFile(profilePath, 'utf8'); } catch { /* create on demand */ }
|
|
@@ -1336,6 +1382,13 @@ async function executeNodeAgentOperation(options, operation, payload = {}) {
|
|
|
1336
1382
|
}
|
|
1337
1383
|
return { summary: `Listed ${data.length} entries.`, data: { path: directory, entries: data, truncated: entries.length > maxEntries } };
|
|
1338
1384
|
}
|
|
1385
|
+
if (operation === 'file.search') return searchNodeAgentFiles(options, args, permissionMode);
|
|
1386
|
+
if (operation === 'directory.create') {
|
|
1387
|
+
let directory = await resolveAgentPath(options, args.path, permissionMode);
|
|
1388
|
+
await fs.mkdir(directory, { recursive: true });
|
|
1389
|
+
directory = await resolveAgentPath(options, args.path, permissionMode);
|
|
1390
|
+
return { summary: `Created directory ${path.basename(directory)}.`, data: { path: directory, created: true } };
|
|
1391
|
+
}
|
|
1339
1392
|
if (operation === 'application.launch') {
|
|
1340
1393
|
const executable = String(args.executable || '').trim();
|
|
1341
1394
|
if (!executable || executable.length > 400 || /[\0\r\n]/.test(executable)) throw new Error('executable is invalid');
|
|
@@ -1391,10 +1444,11 @@ async function executeNodeAgentOperation(options, operation, payload = {}) {
|
|
|
1391
1444
|
}
|
|
1392
1445
|
if (operation === 'command.run') {
|
|
1393
1446
|
const command = String(args.command || '');
|
|
1394
|
-
|
|
1395
|
-
const
|
|
1396
|
-
const
|
|
1397
|
-
|
|
1447
|
+
if (!command || command.length > 16000 || /\0/.test(command)) throw new Error('command is invalid');
|
|
1448
|
+
const shellCommand = resolveAgentShellCommand({ shell: args.shell, command });
|
|
1449
|
+
const workingDirectory = args.workingDirectory ? await resolveAgentPath(options, args.workingDirectory, permissionMode, false) : undefined;
|
|
1450
|
+
const result = await runNodeAgentProcess(shellCommand.executable, shellCommand.args, args.timeoutMs, workingDirectory, 300000);
|
|
1451
|
+
return { summary: result.timedOut ? `${shellCommand.shell} command timed out.` : `${shellCommand.shell} command exited with code ${result.exitCode}.`, data: { ...result, output: redactAgentOutput(result.output), shell: shellCommand.shell, workingDirectory: workingDirectory || process.cwd() } };
|
|
1398
1452
|
}
|
|
1399
1453
|
if (operation === 'script.run') {
|
|
1400
1454
|
const scriptPath = await resolveAgentPath(options, args.path, permissionMode);
|
|
@@ -1481,7 +1535,7 @@ function normalizeNodeAgentTaskResult(result) {
|
|
|
1481
1535
|
return { ...result, ok, status: ok ? 'completed' : 'failed', error };
|
|
1482
1536
|
}
|
|
1483
1537
|
|
|
1484
|
-
const NODE_AGENT_OPERATIONS = new Set(['process.control', 'service.control', 'application.launch', 'application.close', 'file.read', 'file.write', 'file.delete', 'file.list', 'command.run', 'script.run', 'software.install', 'network.status', 'system.power', 'system.configure', 'logs.collect']);
|
|
1538
|
+
const NODE_AGENT_OPERATIONS = new Set(['process.control', 'service.control', 'application.launch', 'application.close', 'file.read', 'file.write', 'file.delete', 'file.list', 'file.search', 'directory.create', 'command.run', 'script.run', 'software.install', 'network.status', 'system.power', 'system.configure', 'logs.collect']);
|
|
1485
1539
|
|
|
1486
1540
|
function remotePolicyAllows(options, command) {
|
|
1487
1541
|
const policy = options.effectivePolicy;
|
|
@@ -1533,7 +1587,7 @@ async function scheduleClientUpdate(options, payload = {}) {
|
|
|
1533
1587
|
if (!Number.isSafeInteger(updateTimeoutMs)
|
|
1534
1588
|
|| updateTimeoutMs < 1_000
|
|
1535
1589
|
|| updateTimeoutMs > 90 * 60_000) {
|
|
1536
|
-
throw new Error('
|
|
1590
|
+
throw new Error('VuvoDesk Client update requires a bounded deadline budget.');
|
|
1537
1591
|
}
|
|
1538
1592
|
updateDeadlineEpochMs = Date.now() + updateTimeoutMs;
|
|
1539
1593
|
const originalCwd = path.resolve(process.cwd());
|
|
@@ -1554,7 +1608,7 @@ async function scheduleClientUpdate(options, payload = {}) {
|
|
|
1554
1608
|
? configuredParentPid
|
|
1555
1609
|
: process.pid;
|
|
1556
1610
|
if (!Number.isInteger(parentPid) || parentPid <= 1) {
|
|
1557
|
-
throw new Error('
|
|
1611
|
+
throw new Error('VuvoDesk client launcher process id is unavailable.');
|
|
1558
1612
|
}
|
|
1559
1613
|
const nodeBinDir = path.dirname(process.execPath);
|
|
1560
1614
|
const npxCandidates = process.platform === 'win32'
|
|
@@ -1562,7 +1616,7 @@ async function scheduleClientUpdate(options, payload = {}) {
|
|
|
1562
1616
|
: [process.env.LIVEDESK_NPX_EXECUTABLE, path.join(nodeBinDir, 'npx'), 'npx'];
|
|
1563
1617
|
const command = npxCandidates.find(candidate => candidate && (!path.isAbsolute(candidate) || existsSync(candidate)));
|
|
1564
1618
|
if (!command) {
|
|
1565
|
-
throw new Error('
|
|
1619
|
+
throw new Error('VuvoDesk npx executable was not found.');
|
|
1566
1620
|
}
|
|
1567
1621
|
const frozenCommand = path.isAbsolute(command) || !/[\\/]/.test(command)
|
|
1568
1622
|
? command
|
|
@@ -1597,7 +1651,7 @@ async function scheduleClientUpdate(options, payload = {}) {
|
|
|
1597
1651
|
}, neutralCwd);
|
|
1598
1652
|
const bootstrapStat = await fs.stat(CLIENT_UPDATE_BOOTSTRAP_PATH);
|
|
1599
1653
|
if (!bootstrapStat.isFile() || bootstrapStat.size <= 0) {
|
|
1600
|
-
throw new Error(`
|
|
1654
|
+
throw new Error(`VuvoDesk Client update bootstrap is unavailable: ${CLIENT_UPDATE_BOOTSTRAP_PATH}`);
|
|
1601
1655
|
}
|
|
1602
1656
|
return await new Promise((resolve, reject) => {
|
|
1603
1657
|
const child = spawn(process.execPath, [CLIENT_UPDATE_BOOTSTRAP_PATH], {
|
|
@@ -1723,7 +1777,7 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
|
|
|
1723
1777
|
summary: result.summary,
|
|
1724
1778
|
error: result.error || undefined,
|
|
1725
1779
|
data: result.data,
|
|
1726
|
-
sideEffects: ['file.read', 'file.list', 'network.status', 'logs.collect'].includes(command) ? 'none' : 'audited',
|
|
1780
|
+
sideEffects: ['file.read', 'file.list', 'file.search', 'network.status', 'logs.collect'].includes(command) ? 'none' : 'audited',
|
|
1727
1781
|
completedAt: new Date().toISOString()
|
|
1728
1782
|
}
|
|
1729
1783
|
});
|
|
@@ -1954,7 +2008,7 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
|
|
|
1954
2008
|
function connectOnce(options, deviceId) {
|
|
1955
2009
|
const manager = parseManagerAddress(options.manager);
|
|
1956
2010
|
if (!isPrivateLanHost(manager.host) && !isTruthy(process.env.LIVEDESK_ALLOW_UNENCRYPTED_LAN)) {
|
|
1957
|
-
return Promise.reject(new Error('Plain TCP
|
|
2011
|
+
return Promise.reject(new Error('Plain TCP VuvoDesk connections are limited to loopback/private LAN. Use an encrypted endpoint for external connections.'));
|
|
1958
2012
|
}
|
|
1959
2013
|
return new Promise((resolve, reject) => {
|
|
1960
2014
|
const socket = net.createConnection({
|
|
@@ -2052,11 +2106,11 @@ function connectOnce(options, deviceId) {
|
|
|
2052
2106
|
options.effectivePolicy = message.effectivePolicy && typeof message.effectivePolicy === 'object'
|
|
2053
2107
|
? message.effectivePolicy
|
|
2054
2108
|
: null;
|
|
2055
|
-
console.log(`Connected to
|
|
2109
|
+
console.log(`Connected to VuvoDesk Hub as ${options.name} (${deviceId})`);
|
|
2056
2110
|
markClientUpdateConnected();
|
|
2057
2111
|
void publishClientUpdateSupervisorProof(socket).catch(error => {
|
|
2058
2112
|
if (!socket.destroyed) {
|
|
2059
|
-
console.error(`
|
|
2113
|
+
console.error(`VuvoDesk client update verification proof failed: ${error?.message || error}`);
|
|
2060
2114
|
}
|
|
2061
2115
|
});
|
|
2062
2116
|
writeJsonLine(socket, {
|
|
@@ -2093,7 +2147,7 @@ function connectOnce(options, deviceId) {
|
|
|
2093
2147
|
finish();
|
|
2094
2148
|
return;
|
|
2095
2149
|
} else if (message.type === 'error') {
|
|
2096
|
-
const remoteError = new Error(message.error || '
|
|
2150
|
+
const remoteError = new Error(message.error || 'VuvoDesk Hub rejected the connection.');
|
|
2097
2151
|
if (message.error === 'invalid-pair-token' && options.exitOnInvalidPair) {
|
|
2098
2152
|
remoteError.exitCode = EXIT_INVALID_PAIR_TOKEN;
|
|
2099
2153
|
}
|
|
@@ -2110,7 +2164,7 @@ function connectOnce(options, deviceId) {
|
|
|
2110
2164
|
|
|
2111
2165
|
async function connectWithRetry(options) {
|
|
2112
2166
|
if (!options.pair) {
|
|
2113
|
-
throw new Error('Missing --pair token. Sign in with Google or get it from
|
|
2167
|
+
throw new Error('Missing --pair token. Sign in with Google or get it from VuvoDesk Hub status.');
|
|
2114
2168
|
}
|
|
2115
2169
|
|
|
2116
2170
|
const deviceId = await getDeviceId(options.deviceId);
|
|
@@ -2119,7 +2173,7 @@ async function connectWithRetry(options) {
|
|
|
2119
2173
|
|
|
2120
2174
|
process.once('SIGINT', () => {
|
|
2121
2175
|
stopping = true;
|
|
2122
|
-
console.log('\nStopping
|
|
2176
|
+
console.log('\nStopping VuvoDesk Client...');
|
|
2123
2177
|
});
|
|
2124
2178
|
process.once('SIGTERM', () => {
|
|
2125
2179
|
stopping = true;
|
|
@@ -2138,7 +2192,7 @@ async function connectWithRetry(options) {
|
|
|
2138
2192
|
throw err;
|
|
2139
2193
|
}
|
|
2140
2194
|
const delayMs = DEFAULT_RECONNECT_MS;
|
|
2141
|
-
console.error(`
|
|
2195
|
+
console.error(`VuvoDesk Hub connection failed: ${err?.message || err}. Retrying in ${delayMs}ms.`);
|
|
2142
2196
|
await wait(delayMs);
|
|
2143
2197
|
}
|
|
2144
2198
|
}
|
|
@@ -79,7 +79,7 @@ function withStateLock(callback) {
|
|
|
79
79
|
}
|
|
80
80
|
if (error?.code !== 'EEXIST') throw error;
|
|
81
81
|
if (removeAbandonedStateLock()) continue;
|
|
82
|
-
if (Date.now() >= deadline) throw new Error(`Timed out waiting for the
|
|
82
|
+
if (Date.now() >= deadline) throw new Error(`Timed out waiting for the VuvoDesk update state lock: ${stateLockPath}`);
|
|
83
83
|
Atomics.wait(lockWaitBuffer, 0, 0, Math.min(25, Math.max(1, deadline - Date.now())));
|
|
84
84
|
}
|
|
85
85
|
}
|
|
@@ -125,26 +125,26 @@ function writeFailure(error, cancelRequested = false) {
|
|
|
125
125
|
};
|
|
126
126
|
}, 'handoff-failed');
|
|
127
127
|
} catch { /* preserve the original handoff failure */ }
|
|
128
|
-
process.stderr.write(`
|
|
128
|
+
process.stderr.write(`VuvoDesk update handoff failed: ${error?.message || error}\n`);
|
|
129
129
|
process.exitCode = 1;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
function prepareNeutralCwd() {
|
|
133
|
-
if (!neutralCwd) throw new Error('
|
|
133
|
+
if (!neutralCwd) throw new Error('VuvoDesk update neutral working directory is unavailable.');
|
|
134
134
|
fs.mkdirSync(neutralCwd, { recursive: true });
|
|
135
135
|
const unexpectedEntry = fs.readdirSync(neutralCwd)[0];
|
|
136
136
|
if (unexpectedEntry) {
|
|
137
|
-
throw new Error(`
|
|
137
|
+
throw new Error(`VuvoDesk update neutral working directory is shadowed by ${path.join(neutralCwd, unexpectedEntry)}.`);
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
function writeHandoffStarted() {
|
|
142
142
|
return writeState(previous => {
|
|
143
143
|
if (Date.now() >= updateDeadlineEpochMs) {
|
|
144
|
-
throw new Error('The absolute
|
|
144
|
+
throw new Error('The absolute VuvoDesk Client update deadline expired before handoff.');
|
|
145
145
|
}
|
|
146
146
|
if (previous.operationId !== operationId) {
|
|
147
|
-
throw new Error('
|
|
147
|
+
throw new Error('VuvoDesk update handoff was superseded before exact-package launch.');
|
|
148
148
|
}
|
|
149
149
|
const now = new Date().toISOString();
|
|
150
150
|
return {
|
|
@@ -175,7 +175,7 @@ function submitUpdateHostJob(job) {
|
|
|
175
175
|
|| status?.protocolVersion !== UPDATE_HOST_PROTOCOL_VERSION
|
|
176
176
|
|| !endpoint
|
|
177
177
|
|| !/^[a-f0-9]{64}$/i.test(token)) {
|
|
178
|
-
rejectRequest(new Error('The independent
|
|
178
|
+
rejectRequest(new Error('The independent VuvoDesk Update Host is unavailable; the existing Client was left running.'));
|
|
179
179
|
return;
|
|
180
180
|
}
|
|
181
181
|
const request = JSON.stringify({
|
|
@@ -185,7 +185,7 @@ function submitUpdateHostJob(job) {
|
|
|
185
185
|
job
|
|
186
186
|
});
|
|
187
187
|
if (Buffer.byteLength(request, 'utf8') > UPDATE_HOST_MAX_MESSAGE_BYTES) {
|
|
188
|
-
rejectRequest(new Error('The
|
|
188
|
+
rejectRequest(new Error('The VuvoDesk Client update request exceeded the Update Host message limit.'));
|
|
189
189
|
return;
|
|
190
190
|
}
|
|
191
191
|
const socket = net.createConnection(endpoint);
|
|
@@ -207,7 +207,7 @@ function submitUpdateHostJob(job) {
|
|
|
207
207
|
socket.on('data', chunk => {
|
|
208
208
|
responseText += chunk;
|
|
209
209
|
if (Buffer.byteLength(responseText, 'utf8') > UPDATE_HOST_MAX_MESSAGE_BYTES) {
|
|
210
|
-
finish(new Error('The
|
|
210
|
+
finish(new Error('The VuvoDesk Update Host response exceeded its message limit.'));
|
|
211
211
|
}
|
|
212
212
|
});
|
|
213
213
|
socket.once('error', error => finish(error));
|
|
@@ -215,7 +215,7 @@ function submitUpdateHostJob(job) {
|
|
|
215
215
|
try {
|
|
216
216
|
const response = JSON.parse(responseText || '{}');
|
|
217
217
|
if (response?.ok !== true || response?.accepted !== true || Number(response?.job?.workerPid || 0) <= 1) {
|
|
218
|
-
throw new Error(String(response?.error || 'The
|
|
218
|
+
throw new Error(String(response?.error || 'The VuvoDesk Update Host did not accept the Client update.'));
|
|
219
219
|
}
|
|
220
220
|
finish(null, response);
|
|
221
221
|
} catch (error) {
|
|
@@ -269,7 +269,7 @@ function buildWorkerInvocation(env) {
|
|
|
269
269
|
async function main() {
|
|
270
270
|
if (!operationId || !versionPattern.test(targetProductVersion)
|
|
271
271
|
|| !Number.isSafeInteger(updateDeadlineEpochMs) || Date.now() >= updateDeadlineEpochMs) {
|
|
272
|
-
throw new Error('Invalid or expired
|
|
272
|
+
throw new Error('Invalid or expired VuvoDesk exact-package update handoff.');
|
|
273
273
|
}
|
|
274
274
|
prepareNeutralCwd();
|
|
275
275
|
writeHandoffStarted();
|
|
@@ -289,7 +289,7 @@ async function main() {
|
|
|
289
289
|
|
|
290
290
|
const invocation = buildWorkerInvocation(env);
|
|
291
291
|
if (!path.isAbsolute(invocation.command) || !fs.existsSync(invocation.command)) {
|
|
292
|
-
throw new Error('
|
|
292
|
+
throw new Error('VuvoDesk could not prepare an absolute Client update worker command for the independent Update Host.');
|
|
293
293
|
}
|
|
294
294
|
const result = await submitUpdateHostJob({
|
|
295
295
|
jobId: `client-${operationId.replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 160)}`,
|