livedesk 0.1.591 → 0.1.593
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 +8 -8
- package/bin/livedesk.js +115 -112
- package/bootstrap/client-process-identity.js +1 -1
- package/bootstrap/device-role.js +1 -1
- package/bootstrap/legacy-client-update.js +15 -15
- package/bootstrap/role-bootstrap-server.js +2 -2
- package/bootstrap/runtime-lock.js +4 -4
- package/bootstrap/runtime-shutdown.js +3 -3
- package/bootstrap/update-host-client.mjs +9 -9
- package/bootstrap/update-host.mjs +6 -6
- package/client/README.md +10 -10
- package/client/THIRD_PARTY_NOTICES.md +1 -1
- package/client/bin/client-version.js +1 -1
- package/client/bin/livedesk-client-node.js +102 -102
- package/client/bin/livedesk-client-update-bootstrap.cjs +12 -12
- package/client/bin/livedesk-client.js +136 -122
- package/client/package.json +7 -7
- package/client/src/runtime/agent-process-lifecycle.js +10 -10
- package/client/src/runtime/agent-shell.js +66 -66
- package/client/src/runtime/client-runtime-server.js +12 -11
- package/client/src/runtime/linux-video-acceleration.js +1 -1
- package/client/src/runtime/windows-owned-process-manifest.js +9 -9
- package/client/tests/client-version.test.mjs +2 -2
- package/diagnostics/livedesk-mac-physical-gate-core.mjs +5 -5
- package/diagnostics/livedesk-mode3-capture-smoke.mjs +7 -7
- package/diagnostics/livedesk-transport-physical.mjs +3 -3
- package/electron/auth-session-owner.mjs +24 -24
- package/electron/desktop-clipboard-owner.mjs +382 -382
- package/electron/electron-builder.e2e.yml +1 -1
- package/electron/electron-builder.mac-arm64.yml +1 -1
- package/electron/electron-builder.mac-x64.yml +1 -1
- package/electron/electron-builder.win-dev.yml +1 -1
- package/electron/electron-builder.win.yml +2 -2
- package/electron/electron-builder.yml +2 -2
- package/electron/main.mjs +175 -174
- package/electron/preload.cjs +13 -13
- package/electron/runtime-role-transition.mjs +17 -17
- package/hub/package.json +4 -4
- package/hub/src/agents/agent-manager.js +2 -2
- package/hub/src/agents/agent-permissions.js +2 -2
- package/hub/src/agents/agent-tool-registry.js +36 -36
- package/hub/src/agents/codex-agent-runtime.js +16 -16
- package/hub/src/agents/codex-mcp-server.js +5 -5
- package/hub/src/filesystem/roots.js +20 -20
- package/hub/src/filesystem/shared-folders.js +2 -2
- package/hub/src/live-desk-update.js +9 -9
- package/hub/src/remote-clipboard-contract.mjs +482 -482
- package/hub/src/remote-hub.js +1025 -1025
- package/hub/src/server.js +146 -146
- package/hub/src/settings/effective-device-policy.js +31 -31
- package/hub/src/settings/settings-schema.js +2 -2
- package/package.json +7 -7
- package/runtime-core/package.json +2 -2
- package/runtime-core/src/auth-config.js +1 -1
- package/scripts/sync-web-dist.js +9 -9
- package/web/dist/app.webmanifest +4 -4
- package/web/dist/assets/LiveDeskApp-Djx1CsIp.js +178 -0
- package/web/dist/assets/{icons-CebEqIp7.js → icons-DwJmccVC.js} +1 -1
- package/web/dist/assets/{index-BPgqiKB-.js → index-DjIbOQxl.js} +6 -6
- package/web/dist/assets/{react-BHQWCi9c.js → react-DOZUPHF8.js} +1 -1
- package/web/dist/assets/supabase-C7qjtLN3.js +29 -0
- package/web/dist/desktop-auth-callback.html +4 -4
- package/web/dist/desktop-auth-callback.js +13 -13
- package/web/dist/index.html +11 -10
- package/web/dist/livedesk-build-evidence.json +30 -30
- package/web/dist/offline.html +2 -2
- package/web/dist/sw.js +11 -11
- package/web/dist/assets/LiveDeskApp-BnYbdi9v.js +0 -178
- package/web/dist/assets/supabase-C0Z0FS3K.js +0 -29
|
@@ -7,8 +7,8 @@ import crypto from 'crypto';
|
|
|
7
7
|
import { existsSync, promises as fs, statfsSync } from 'fs';
|
|
8
8
|
import { spawn } from 'child_process';
|
|
9
9
|
import { createRequire } from 'node:module';
|
|
10
|
-
import { fileURLToPath } from 'node:url';
|
|
11
|
-
import { resolveAgentShellCommand } from '../src/runtime/agent-shell.js';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { resolveAgentShellCommand } from '../src/runtime/agent-shell.js';
|
|
12
12
|
|
|
13
13
|
const require = createRequire(import.meta.url);
|
|
14
14
|
const CLIENT_UPDATE_BOOTSTRAP_PATH = fileURLToPath(
|
|
@@ -40,7 +40,7 @@ function normalizeClientUpdateTargetVersion(value) {
|
|
|
40
40
|
const version = String(value || '').trim().replace(/^v/i, '');
|
|
41
41
|
if (!version) return '';
|
|
42
42
|
if (!CLIENT_UPDATE_VERSION_PATTERN.test(version)) {
|
|
43
|
-
throw new Error(`Invalid
|
|
43
|
+
throw new Error(`Invalid VuvoDesk product update version: ${version}`);
|
|
44
44
|
}
|
|
45
45
|
return version;
|
|
46
46
|
}
|
|
@@ -64,7 +64,7 @@ async function prepareClientUpdateNeutralCwd(operationId) {
|
|
|
64
64
|
const unexpectedEntry = (await fs.readdir(neutralCwd))[0];
|
|
65
65
|
if (unexpectedEntry) {
|
|
66
66
|
const shadowPath = path.join(neutralCwd, unexpectedEntry);
|
|
67
|
-
throw new Error(`
|
|
67
|
+
throw new Error(`VuvoDesk update neutral working directory is shadowed by ${shadowPath}: ${neutralCwd}`);
|
|
68
68
|
}
|
|
69
69
|
return neutralCwd;
|
|
70
70
|
}
|
|
@@ -228,8 +228,8 @@ async function waitForClientUpdateShutdownSignal(
|
|
|
228
228
|
return { ready: true, error: '' };
|
|
229
229
|
}
|
|
230
230
|
if (state.stage === 'failed') {
|
|
231
|
-
const error = String(state.error || '
|
|
232
|
-
console.error(`
|
|
231
|
+
const error = String(state.error || 'VuvoDesk client update preparation failed.');
|
|
232
|
+
console.error(`VuvoDesk client update preparation failed: ${error}`);
|
|
233
233
|
return { ready: false, error };
|
|
234
234
|
}
|
|
235
235
|
} catch {
|
|
@@ -239,8 +239,8 @@ async function waitForClientUpdateShutdownSignal(
|
|
|
239
239
|
}
|
|
240
240
|
const deadlineExpired = Date.now() >= Number(updateDeadlineEpochMs);
|
|
241
241
|
const error = deadlineExpired
|
|
242
|
-
? 'The absolute
|
|
243
|
-
: '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.';
|
|
244
244
|
await writeClientUpdateState('failed', {
|
|
245
245
|
operationId,
|
|
246
246
|
error,
|
|
@@ -248,7 +248,7 @@ async function waitForClientUpdateShutdownSignal(
|
|
|
248
248
|
deadlineExpired,
|
|
249
249
|
failedAt: new Date().toISOString()
|
|
250
250
|
}).catch(() => undefined);
|
|
251
|
-
console.error('
|
|
251
|
+
console.error('VuvoDesk client update preparation timed out; the current Agent will remain running.');
|
|
252
252
|
return { ready: false, error };
|
|
253
253
|
}
|
|
254
254
|
|
|
@@ -262,7 +262,7 @@ function markClientUpdateConnected() {
|
|
|
262
262
|
restartVerified: true,
|
|
263
263
|
error: ''
|
|
264
264
|
}).catch(error => {
|
|
265
|
-
console.error(`
|
|
265
|
+
console.error(`VuvoDesk client update diagnostics could not be persisted: ${error?.message || error}`);
|
|
266
266
|
});
|
|
267
267
|
}
|
|
268
268
|
|
|
@@ -319,14 +319,14 @@ async function publishClientUpdateSupervisorProof(socket) {
|
|
|
319
319
|
|
|
320
320
|
function printHelp() {
|
|
321
321
|
console.log(`
|
|
322
|
-
|
|
322
|
+
VuvoDesk Client
|
|
323
323
|
|
|
324
324
|
Usage:
|
|
325
325
|
npx @livedesk/client connect --manager 127.0.0.1:5197 --pair <token>
|
|
326
326
|
|
|
327
327
|
Options:
|
|
328
|
-
--manager <host:port>
|
|
329
|
-
--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.
|
|
330
330
|
--slot <number> Screen wall slot number for this computer.
|
|
331
331
|
--name <name> Friendly device name. Default: OS hostname.
|
|
332
332
|
--heartbeat <ms> Status heartbeat interval. Default: ${DEFAULT_HEARTBEAT_MS}
|
|
@@ -337,7 +337,7 @@ Options:
|
|
|
337
337
|
--no-live Disable focused live screen streaming.
|
|
338
338
|
--tasks Enable safe remote task inbox. Default on.
|
|
339
339
|
--no-tasks Disable remote task dispatch capability.
|
|
340
|
-
--files-dir <path> Default folder for received files. Default: ~/Desktop/
|
|
340
|
+
--files-dir <path> Default folder for received files. Default: ~/Desktop/VuvoDeskFiles.
|
|
341
341
|
--fake-thumbnail Use generated thumbnail frames for smoke tests.
|
|
342
342
|
--exit-on-disconnect Exit after Hub disconnect. Useful for tests.
|
|
343
343
|
--exit-on-invalid-pair Exit when the Hub rejects the pair token.
|
|
@@ -685,7 +685,7 @@ async function waitForNodeServiceState(executable, service, expectedState, timeo
|
|
|
685
685
|
}
|
|
686
686
|
|
|
687
687
|
function getDefaultFilesDir() {
|
|
688
|
-
return path.join(os.homedir(), 'Desktop', '
|
|
688
|
+
return path.join(os.homedir(), 'Desktop', 'VuvoDeskFiles');
|
|
689
689
|
}
|
|
690
690
|
|
|
691
691
|
function normalizeDirectoryPath(value, fallback = getDefaultFilesDir()) {
|
|
@@ -862,7 +862,7 @@ function createFakeThumbnailFrame(options, payload, frameSeq) {
|
|
|
862
862
|
'<rect width="100%" height="100%" fill="#0f172a"/>',
|
|
863
863
|
'<rect x="14" y="14" width="calc(100% - 28px)" height="calc(100% - 28px)" rx="12" fill="#1e293b" stroke="#38bdf8" stroke-width="2"/>',
|
|
864
864
|
`<text x="28" y="48" fill="#e2e8f0" font-family="Arial, sans-serif" font-size="20" font-weight="700">${title}</text>`,
|
|
865
|
-
`<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>`,
|
|
866
866
|
`<text x="28" y="${height - 30}" fill="#94a3b8" font-family="Consolas, monospace" font-size="12">${now}</text>`,
|
|
867
867
|
'</svg>'
|
|
868
868
|
].join('');
|
|
@@ -1119,7 +1119,7 @@ async function assertAgentPathDoesNotTraverseLink(base, resolved) {
|
|
|
1119
1119
|
}
|
|
1120
1120
|
const canonicalCurrent = await fs.realpath(current);
|
|
1121
1121
|
if (!isPathWithinAgentRoot(canonicalRoot, canonicalCurrent)) {
|
|
1122
|
-
throw new Error('Safe Agent path resolves outside the
|
|
1122
|
+
throw new Error('Safe Agent path resolves outside the VuvoDesk files directory.');
|
|
1123
1123
|
}
|
|
1124
1124
|
return;
|
|
1125
1125
|
} catch (error) {
|
|
@@ -1137,16 +1137,16 @@ async function resolveAgentPath(options, value, permissionMode, rejectSensitive
|
|
|
1137
1137
|
if (!text || text.length > 600) throw new Error('path is invalid');
|
|
1138
1138
|
const resolved = path.resolve(path.isAbsolute(text) ? text : path.join(base, text));
|
|
1139
1139
|
const relative = path.relative(base, resolved);
|
|
1140
|
-
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');
|
|
1141
1141
|
if (permissionMode !== 'full-access') await assertAgentPathDoesNotTraverseLink(base, resolved);
|
|
1142
1142
|
if (rejectSensitive && isSensitiveAgentPath(resolved)) throw new Error('credential and secret paths are not available');
|
|
1143
1143
|
return resolved;
|
|
1144
1144
|
}
|
|
1145
1145
|
|
|
1146
|
-
function runNodeAgentProcess(executable, args, timeoutMs = 15000, cwd = undefined, maxTimeoutMs = 30000) {
|
|
1147
|
-
return new Promise((resolve, reject) => {
|
|
1148
|
-
const startedAt = Date.now();
|
|
1149
|
-
const child = spawn(executable, args, { cwd, windowsHide: true, shell: false, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1146
|
+
function runNodeAgentProcess(executable, args, timeoutMs = 15000, cwd = undefined, maxTimeoutMs = 30000) {
|
|
1147
|
+
return new Promise((resolve, reject) => {
|
|
1148
|
+
const startedAt = Date.now();
|
|
1149
|
+
const child = spawn(executable, args, { cwd, windowsHide: true, shell: false, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1150
1150
|
let output = '';
|
|
1151
1151
|
let timedOut = false;
|
|
1152
1152
|
const append = chunk => { output = `${output}${String(chunk || '')}`.replace(/[\0\r]/g, ' ').slice(0, MAX_AGENT_OUTPUT_CHARS); };
|
|
@@ -1156,55 +1156,55 @@ function runNodeAgentProcess(executable, args, timeoutMs = 15000, cwd = undefine
|
|
|
1156
1156
|
timedOut = true;
|
|
1157
1157
|
child.kill('SIGTERM');
|
|
1158
1158
|
setTimeout(() => child.kill('SIGKILL'), 1000).unref?.();
|
|
1159
|
-
}, Math.max(1000, Math.min(maxTimeoutMs, Number(timeoutMs) || 15000)));
|
|
1160
|
-
child.once('error', error => { clearTimeout(timer); reject(error); });
|
|
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 }); });
|
|
1162
|
-
});
|
|
1163
|
-
}
|
|
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
|
-
}
|
|
1159
|
+
}, Math.max(1000, Math.min(maxTimeoutMs, Number(timeoutMs) || 15000)));
|
|
1160
|
+
child.once('error', error => { clearTimeout(timer); reject(error); });
|
|
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 }); });
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
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
1208
|
|
|
1209
1209
|
function redactAgentOutput(value) {
|
|
1210
1210
|
let output = String(value || '').replace(/[\0\r]/g, ' ');
|
|
@@ -1330,7 +1330,7 @@ async function persistNodeEnvironmentVariable(name, value) {
|
|
|
1330
1330
|
return { persisted: true, scope: 'user', output: result.output };
|
|
1331
1331
|
}
|
|
1332
1332
|
const profilePath = path.join(os.homedir(), '.profile');
|
|
1333
|
-
const marker = `#
|
|
1333
|
+
const marker = `# VuvoDesk managed environment: ${variableName}`;
|
|
1334
1334
|
const quoted = `'${variableValue.replaceAll("'", "'\\\"'\\\"'")}'`;
|
|
1335
1335
|
let profile = '';
|
|
1336
1336
|
try { profile = await fs.readFile(profilePath, 'utf8'); } catch { /* create on demand */ }
|
|
@@ -1370,7 +1370,7 @@ async function executeNodeAgentOperation(options, operation, payload = {}) {
|
|
|
1370
1370
|
else await fs.unlink(filePath);
|
|
1371
1371
|
return { summary: `Deleted ${path.basename(filePath)}.`, data: { path: filePath, recursive: args.recursive === true } };
|
|
1372
1372
|
}
|
|
1373
|
-
if (operation === 'file.list') {
|
|
1373
|
+
if (operation === 'file.list') {
|
|
1374
1374
|
const directory = await resolveAgentPath(options, args.path, permissionMode);
|
|
1375
1375
|
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
1376
1376
|
const maxEntries = Math.max(1, Math.min(500, Number(args.maxEntries) || 200));
|
|
@@ -1380,15 +1380,15 @@ async function executeNodeAgentOperation(options, operation, payload = {}) {
|
|
|
1380
1380
|
if (isSensitiveAgentPath(entryPath)) continue;
|
|
1381
1381
|
data.push({ name: entry.name, path: entryPath, type: entry.isDirectory() ? 'directory' : 'file', sizeBytes: entry.isDirectory() ? 0 : (await fs.stat(entryPath)).size });
|
|
1382
1382
|
}
|
|
1383
|
-
return { summary: `Listed ${data.length} entries.`, data: { path: directory, entries: data, truncated: entries.length > maxEntries } };
|
|
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
|
-
}
|
|
1383
|
+
return { summary: `Listed ${data.length} entries.`, data: { path: directory, entries: data, truncated: entries.length > maxEntries } };
|
|
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
|
+
}
|
|
1392
1392
|
if (operation === 'application.launch') {
|
|
1393
1393
|
const executable = String(args.executable || '').trim();
|
|
1394
1394
|
if (!executable || executable.length > 400 || /[\0\r\n]/.test(executable)) throw new Error('executable is invalid');
|
|
@@ -1442,13 +1442,13 @@ async function executeNodeAgentOperation(options, operation, payload = {}) {
|
|
|
1442
1442
|
if (!ok) throw new Error(`Service ${action} failed for ${service}.`);
|
|
1443
1443
|
return { summary: `Service ${action} completed for ${service}.`, data: { service, action, results } };
|
|
1444
1444
|
}
|
|
1445
|
-
if (operation === 'command.run') {
|
|
1446
|
-
const command = String(args.command || '');
|
|
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() } };
|
|
1445
|
+
if (operation === 'command.run') {
|
|
1446
|
+
const command = String(args.command || '');
|
|
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() } };
|
|
1452
1452
|
}
|
|
1453
1453
|
if (operation === 'script.run') {
|
|
1454
1454
|
const scriptPath = await resolveAgentPath(options, args.path, permissionMode);
|
|
@@ -1535,7 +1535,7 @@ function normalizeNodeAgentTaskResult(result) {
|
|
|
1535
1535
|
return { ...result, ok, status: ok ? 'completed' : 'failed', error };
|
|
1536
1536
|
}
|
|
1537
1537
|
|
|
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']);
|
|
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']);
|
|
1539
1539
|
|
|
1540
1540
|
function remotePolicyAllows(options, command) {
|
|
1541
1541
|
const policy = options.effectivePolicy;
|
|
@@ -1587,7 +1587,7 @@ async function scheduleClientUpdate(options, payload = {}) {
|
|
|
1587
1587
|
if (!Number.isSafeInteger(updateTimeoutMs)
|
|
1588
1588
|
|| updateTimeoutMs < 1_000
|
|
1589
1589
|
|| updateTimeoutMs > 90 * 60_000) {
|
|
1590
|
-
throw new Error('
|
|
1590
|
+
throw new Error('VuvoDesk Client update requires a bounded deadline budget.');
|
|
1591
1591
|
}
|
|
1592
1592
|
updateDeadlineEpochMs = Date.now() + updateTimeoutMs;
|
|
1593
1593
|
const originalCwd = path.resolve(process.cwd());
|
|
@@ -1608,7 +1608,7 @@ async function scheduleClientUpdate(options, payload = {}) {
|
|
|
1608
1608
|
? configuredParentPid
|
|
1609
1609
|
: process.pid;
|
|
1610
1610
|
if (!Number.isInteger(parentPid) || parentPid <= 1) {
|
|
1611
|
-
throw new Error('
|
|
1611
|
+
throw new Error('VuvoDesk client launcher process id is unavailable.');
|
|
1612
1612
|
}
|
|
1613
1613
|
const nodeBinDir = path.dirname(process.execPath);
|
|
1614
1614
|
const npxCandidates = process.platform === 'win32'
|
|
@@ -1616,7 +1616,7 @@ async function scheduleClientUpdate(options, payload = {}) {
|
|
|
1616
1616
|
: [process.env.LIVEDESK_NPX_EXECUTABLE, path.join(nodeBinDir, 'npx'), 'npx'];
|
|
1617
1617
|
const command = npxCandidates.find(candidate => candidate && (!path.isAbsolute(candidate) || existsSync(candidate)));
|
|
1618
1618
|
if (!command) {
|
|
1619
|
-
throw new Error('
|
|
1619
|
+
throw new Error('VuvoDesk npx executable was not found.');
|
|
1620
1620
|
}
|
|
1621
1621
|
const frozenCommand = path.isAbsolute(command) || !/[\\/]/.test(command)
|
|
1622
1622
|
? command
|
|
@@ -1651,7 +1651,7 @@ async function scheduleClientUpdate(options, payload = {}) {
|
|
|
1651
1651
|
}, neutralCwd);
|
|
1652
1652
|
const bootstrapStat = await fs.stat(CLIENT_UPDATE_BOOTSTRAP_PATH);
|
|
1653
1653
|
if (!bootstrapStat.isFile() || bootstrapStat.size <= 0) {
|
|
1654
|
-
throw new Error(`
|
|
1654
|
+
throw new Error(`VuvoDesk Client update bootstrap is unavailable: ${CLIENT_UPDATE_BOOTSTRAP_PATH}`);
|
|
1655
1655
|
}
|
|
1656
1656
|
return await new Promise((resolve, reject) => {
|
|
1657
1657
|
const child = spawn(process.execPath, [CLIENT_UPDATE_BOOTSTRAP_PATH], {
|
|
@@ -1777,7 +1777,7 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
|
|
|
1777
1777
|
summary: result.summary,
|
|
1778
1778
|
error: result.error || undefined,
|
|
1779
1779
|
data: result.data,
|
|
1780
|
-
sideEffects: ['file.read', 'file.list', 'file.search', 'network.status', 'logs.collect'].includes(command) ? 'none' : 'audited',
|
|
1780
|
+
sideEffects: ['file.read', 'file.list', 'file.search', 'network.status', 'logs.collect'].includes(command) ? 'none' : 'audited',
|
|
1781
1781
|
completedAt: new Date().toISOString()
|
|
1782
1782
|
}
|
|
1783
1783
|
});
|
|
@@ -2008,7 +2008,7 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
|
|
|
2008
2008
|
function connectOnce(options, deviceId) {
|
|
2009
2009
|
const manager = parseManagerAddress(options.manager);
|
|
2010
2010
|
if (!isPrivateLanHost(manager.host) && !isTruthy(process.env.LIVEDESK_ALLOW_UNENCRYPTED_LAN)) {
|
|
2011
|
-
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.'));
|
|
2012
2012
|
}
|
|
2013
2013
|
return new Promise((resolve, reject) => {
|
|
2014
2014
|
const socket = net.createConnection({
|
|
@@ -2106,11 +2106,11 @@ function connectOnce(options, deviceId) {
|
|
|
2106
2106
|
options.effectivePolicy = message.effectivePolicy && typeof message.effectivePolicy === 'object'
|
|
2107
2107
|
? message.effectivePolicy
|
|
2108
2108
|
: null;
|
|
2109
|
-
console.log(`Connected to
|
|
2109
|
+
console.log(`Connected to VuvoDesk Hub as ${options.name} (${deviceId})`);
|
|
2110
2110
|
markClientUpdateConnected();
|
|
2111
2111
|
void publishClientUpdateSupervisorProof(socket).catch(error => {
|
|
2112
2112
|
if (!socket.destroyed) {
|
|
2113
|
-
console.error(`
|
|
2113
|
+
console.error(`VuvoDesk client update verification proof failed: ${error?.message || error}`);
|
|
2114
2114
|
}
|
|
2115
2115
|
});
|
|
2116
2116
|
writeJsonLine(socket, {
|
|
@@ -2147,7 +2147,7 @@ function connectOnce(options, deviceId) {
|
|
|
2147
2147
|
finish();
|
|
2148
2148
|
return;
|
|
2149
2149
|
} else if (message.type === 'error') {
|
|
2150
|
-
const remoteError = new Error(message.error || '
|
|
2150
|
+
const remoteError = new Error(message.error || 'VuvoDesk Hub rejected the connection.');
|
|
2151
2151
|
if (message.error === 'invalid-pair-token' && options.exitOnInvalidPair) {
|
|
2152
2152
|
remoteError.exitCode = EXIT_INVALID_PAIR_TOKEN;
|
|
2153
2153
|
}
|
|
@@ -2164,7 +2164,7 @@ function connectOnce(options, deviceId) {
|
|
|
2164
2164
|
|
|
2165
2165
|
async function connectWithRetry(options) {
|
|
2166
2166
|
if (!options.pair) {
|
|
2167
|
-
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.');
|
|
2168
2168
|
}
|
|
2169
2169
|
|
|
2170
2170
|
const deviceId = await getDeviceId(options.deviceId);
|
|
@@ -2173,7 +2173,7 @@ async function connectWithRetry(options) {
|
|
|
2173
2173
|
|
|
2174
2174
|
process.once('SIGINT', () => {
|
|
2175
2175
|
stopping = true;
|
|
2176
|
-
console.log('\nStopping
|
|
2176
|
+
console.log('\nStopping VuvoDesk Client...');
|
|
2177
2177
|
});
|
|
2178
2178
|
process.once('SIGTERM', () => {
|
|
2179
2179
|
stopping = true;
|
|
@@ -2192,7 +2192,7 @@ async function connectWithRetry(options) {
|
|
|
2192
2192
|
throw err;
|
|
2193
2193
|
}
|
|
2194
2194
|
const delayMs = DEFAULT_RECONNECT_MS;
|
|
2195
|
-
console.error(`
|
|
2195
|
+
console.error(`VuvoDesk Hub connection failed: ${err?.message || err}. Retrying in ${delayMs}ms.`);
|
|
2196
2196
|
await wait(delayMs);
|
|
2197
2197
|
}
|
|
2198
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)}`,
|