@livedesk/hub 0.1.49 → 0.1.50
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 +1 -1
- package/package.json +4 -4
- package/src/agents/agent-manager.js +2 -2
- package/src/agents/agent-permissions.js +2 -2
- package/src/agents/agent-tool-registry.js +36 -36
- package/src/agents/codex-agent-runtime.js +16 -16
- package/src/agents/codex-mcp-server.js +5 -5
- package/src/filesystem/roots.js +20 -20
- package/src/filesystem/shared-folders.js +2 -2
- package/src/live-desk-update.js +9 -9
- package/src/remote-clipboard-contract.mjs +482 -482
- package/src/remote-hub.js +1025 -1025
- package/src/server.js +146 -146
- package/src/settings/effective-device-policy.js +31 -31
- package/src/settings/settings-schema.js +2 -2
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/hub",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.50",
|
|
4
|
+
"description": "VuvoDesk local Hub API and browser frame bridge",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/server.js",
|
|
7
7
|
"files": [
|
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
"scripts": {
|
|
12
12
|
"dev": "node src/server.js",
|
|
13
13
|
"start": "node src/server.js",
|
|
14
|
-
"check": "node --check src/server.js && node --check src/remote-hub.js && node --check src/remote-clipboard-contract.mjs && node --check src/console-relay.js",
|
|
14
|
+
"check": "node --check src/server.js && node --check src/remote-hub.js && node --check src/remote-clipboard-contract.mjs && node --check src/console-relay.js",
|
|
15
15
|
"prepublishOnly": "node ../../scripts/livedesk-release-git-gate.mjs"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
|
19
|
-
"@livedesk/runtime-core": "0.1.
|
|
19
|
+
"@livedesk/runtime-core": "0.1.6",
|
|
20
20
|
"@openai/codex-sdk": "0.145.0",
|
|
21
21
|
"cors": "^2.8.5",
|
|
22
22
|
"express": "^4.21.2",
|
|
@@ -87,12 +87,12 @@ export function createAgentManager({
|
|
|
87
87
|
const source = input && typeof input === 'object' ? input : {};
|
|
88
88
|
const results = Array.isArray(source.results) ? source.results : [];
|
|
89
89
|
return {
|
|
90
|
-
summary: String(results.length ? `${source.completed || 0} completed, ${source.failed || 0} failed.` : '
|
|
90
|
+
summary: String(results.length ? `${source.completed || 0} completed, ${source.failed || 0} failed.` : 'VuvoDesk Agent run completed.').slice(0, 1200)
|
|
91
91
|
};
|
|
92
92
|
},
|
|
93
93
|
async startRun(input) {
|
|
94
94
|
if (!runtime) {
|
|
95
|
-
throw new AgentRuntimeError('agent-codex-not-active', 'Codex SDK is not available for
|
|
95
|
+
throw new AgentRuntimeError('agent-codex-not-active', 'Codex SDK is not available for VuvoDesk Agent commands.', { status: 409 });
|
|
96
96
|
}
|
|
97
97
|
return runtime.start(input);
|
|
98
98
|
},
|
|
@@ -164,7 +164,7 @@ function isSafeAutoRelativePath(value) {
|
|
|
164
164
|
function safeAutoConstraint(tool, args, mode) {
|
|
165
165
|
if (mode !== 'safe-auto') return '';
|
|
166
166
|
if (tool.category === 'fileWrite' && !isSafeAutoRelativePath(args?.path)) {
|
|
167
|
-
return 'Safe Auto file writes are limited to relative paths inside the
|
|
167
|
+
return 'Safe Auto file writes are limited to relative paths inside the VuvoDesk files directory.';
|
|
168
168
|
}
|
|
169
169
|
if (tool.category === 'fileWrite') {
|
|
170
170
|
const filePath = String(args?.path || '').trim().replaceAll('\\', '/').toLowerCase();
|
|
@@ -175,7 +175,7 @@ function safeAutoConstraint(tool, args, mode) {
|
|
|
175
175
|
if (tool.category === 'script') {
|
|
176
176
|
const scriptPath = String(args?.path || '').trim().toLowerCase();
|
|
177
177
|
if (!isSafeAutoRelativePath(scriptPath) || !scriptPath.replaceAll('\\', '/').startsWith('scripts/') || !/\.(ps1|psm1|sh|bash|py|js|mjs|cmd|bat)$/.test(scriptPath)) {
|
|
178
|
-
return 'Safe Auto scripts must be registered relative script files inside the
|
|
178
|
+
return 'Safe Auto scripts must be registered relative script files inside the VuvoDesk files directory.';
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
return '';
|
|
@@ -27,7 +27,7 @@ const readTaskPermission = { ask: 'allow', safeAuto: 'allow', fullAccess: 'allow
|
|
|
27
27
|
export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
28
28
|
{
|
|
29
29
|
name: 'livedesk.list_devices',
|
|
30
|
-
description: 'List connected
|
|
30
|
+
description: 'List connected VuvoDesk Clients. This is read-only.',
|
|
31
31
|
category: 'read',
|
|
32
32
|
readOnly: true,
|
|
33
33
|
mutating: false,
|
|
@@ -105,7 +105,7 @@ export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
105
105
|
},
|
|
106
106
|
{
|
|
107
107
|
name: 'livedesk.collect_diagnostics',
|
|
108
|
-
description: 'Collect the existing safe
|
|
108
|
+
description: 'Collect the existing safe VuvoDesk diagnostics payload from the selected Clients.',
|
|
109
109
|
category: 'read',
|
|
110
110
|
readOnly: true,
|
|
111
111
|
mutating: false,
|
|
@@ -207,8 +207,8 @@ export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
207
207
|
inputSchema: withRequiredInput({ path: { type: 'string', minLength: 1, maxLength: 600 }, recursive: { type: 'boolean' } }, ['path']),
|
|
208
208
|
defaultPermission: taskPermission
|
|
209
209
|
},
|
|
210
|
-
{
|
|
211
|
-
name: 'livedesk.list_directory',
|
|
210
|
+
{
|
|
211
|
+
name: 'livedesk.list_directory',
|
|
212
212
|
description: 'List bounded file metadata from a directory on the selected Clients.',
|
|
213
213
|
category: 'fileRead',
|
|
214
214
|
readOnly: true,
|
|
@@ -218,37 +218,37 @@ export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
218
218
|
supportsBatch: true,
|
|
219
219
|
supportedPlatforms: ['windows', 'macos', 'linux'],
|
|
220
220
|
inputSchema: withRequiredInput({ path: { type: 'string', minLength: 1, maxLength: 600 }, recursive: { type: 'boolean' }, maxEntries: { type: 'integer', minimum: 1, maximum: 500 } }, ['path']),
|
|
221
|
-
defaultPermission: readTaskPermission
|
|
222
|
-
},
|
|
223
|
-
{
|
|
224
|
-
name: 'livedesk.search_files',
|
|
225
|
-
description: 'Search file and folder names under one bounded Client directory without reading file contents.',
|
|
226
|
-
category: 'fileRead',
|
|
227
|
-
readOnly: true,
|
|
228
|
-
mutating: false,
|
|
229
|
-
risk: 'low',
|
|
230
|
-
reversible: true,
|
|
231
|
-
supportsBatch: true,
|
|
232
|
-
supportedPlatforms: ['windows', 'macos', 'linux'],
|
|
233
|
-
inputSchema: withRequiredInput({ path: { type: 'string', minLength: 1, maxLength: 600 }, query: { type: 'string', minLength: 1, maxLength: 160 }, maxResults: { type: 'integer', minimum: 1, maximum: 200 }, maxDepth: { type: 'integer', minimum: 0, maximum: 8 }, maxScannedEntries: { type: 'integer', minimum: 1, maximum: 5000 } }, ['path', 'query']),
|
|
234
|
-
defaultPermission: readTaskPermission
|
|
235
|
-
},
|
|
236
|
-
{
|
|
237
|
-
name: 'livedesk.create_directory',
|
|
238
|
-
description: 'Create a directory on the selected Clients. Safe Auto remains limited to the
|
|
239
|
-
category: 'fileWrite',
|
|
240
|
-
readOnly: false,
|
|
241
|
-
mutating: true,
|
|
242
|
-
risk: 'medium',
|
|
243
|
-
reversible: true,
|
|
244
|
-
supportsBatch: true,
|
|
245
|
-
supportedPlatforms: ['windows', 'macos', 'linux'],
|
|
246
|
-
inputSchema: withRequiredInput({ path: { type: 'string', minLength: 1, maxLength: 600 } }, ['path']),
|
|
247
|
-
defaultPermission: taskPermission
|
|
248
|
-
},
|
|
249
|
-
{
|
|
250
|
-
name: 'livedesk.run_command',
|
|
251
|
-
description: 'Run a bounded PowerShell, cmd, sh, bash, zsh, or pwsh command on one selected Client. Choose the shell explicitly when syntax matters. This is an audited high-risk action.',
|
|
221
|
+
defaultPermission: readTaskPermission
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'livedesk.search_files',
|
|
225
|
+
description: 'Search file and folder names under one bounded Client directory without reading file contents.',
|
|
226
|
+
category: 'fileRead',
|
|
227
|
+
readOnly: true,
|
|
228
|
+
mutating: false,
|
|
229
|
+
risk: 'low',
|
|
230
|
+
reversible: true,
|
|
231
|
+
supportsBatch: true,
|
|
232
|
+
supportedPlatforms: ['windows', 'macos', 'linux'],
|
|
233
|
+
inputSchema: withRequiredInput({ path: { type: 'string', minLength: 1, maxLength: 600 }, query: { type: 'string', minLength: 1, maxLength: 160 }, maxResults: { type: 'integer', minimum: 1, maximum: 200 }, maxDepth: { type: 'integer', minimum: 0, maximum: 8 }, maxScannedEntries: { type: 'integer', minimum: 1, maximum: 5000 } }, ['path', 'query']),
|
|
234
|
+
defaultPermission: readTaskPermission
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'livedesk.create_directory',
|
|
238
|
+
description: 'Create a directory on the selected Clients. Safe Auto remains limited to the VuvoDesk files directory.',
|
|
239
|
+
category: 'fileWrite',
|
|
240
|
+
readOnly: false,
|
|
241
|
+
mutating: true,
|
|
242
|
+
risk: 'medium',
|
|
243
|
+
reversible: true,
|
|
244
|
+
supportsBatch: true,
|
|
245
|
+
supportedPlatforms: ['windows', 'macos', 'linux'],
|
|
246
|
+
inputSchema: withRequiredInput({ path: { type: 'string', minLength: 1, maxLength: 600 } }, ['path']),
|
|
247
|
+
defaultPermission: taskPermission
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: 'livedesk.run_command',
|
|
251
|
+
description: 'Run a bounded PowerShell, cmd, sh, bash, zsh, or pwsh command on one selected Client. Choose the shell explicitly when syntax matters. This is an audited high-risk action.',
|
|
252
252
|
category: 'shell',
|
|
253
253
|
readOnly: false,
|
|
254
254
|
mutating: true,
|
|
@@ -256,7 +256,7 @@ export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
256
256
|
reversible: false,
|
|
257
257
|
supportsBatch: false,
|
|
258
258
|
supportedPlatforms: ['windows', 'macos', 'linux'],
|
|
259
|
-
inputSchema: withRequiredInput({ command: { type: 'string', minLength: 1, maxLength: 16000 }, shell: { type: 'string', enum: ['auto', 'powershell', 'pwsh', 'cmd', 'sh', 'bash', 'zsh'] }, workingDirectory: { type: 'string', maxLength: 600 }, timeoutMs: { type: 'integer', minimum: 1000, maximum: 300000 } }, ['command']),
|
|
259
|
+
inputSchema: withRequiredInput({ command: { type: 'string', minLength: 1, maxLength: 16000 }, shell: { type: 'string', enum: ['auto', 'powershell', 'pwsh', 'cmd', 'sh', 'bash', 'zsh'] }, workingDirectory: { type: 'string', maxLength: 600 }, timeoutMs: { type: 'integer', minimum: 1000, maximum: 300000 } }, ['command']),
|
|
260
260
|
defaultPermission: taskPermission
|
|
261
261
|
},
|
|
262
262
|
{
|
|
@@ -75,9 +75,9 @@ function normalizeCodexError(error) {
|
|
|
75
75
|
const message = safeText(error?.message, 800) || 'Codex run failed.';
|
|
76
76
|
if (error instanceof AgentRuntimeError && error.code !== 'codex-run-failed') return error;
|
|
77
77
|
if (/out of credits|usage limit|rate limit|too many requests/i.test(message)) return new AgentRuntimeError('codex-usage-limit-reached', 'The Codex workspace has no remaining usage.', { status: 402, retryable: true });
|
|
78
|
-
if (/refresh[_ -]?token[_ -]?(?:already[_ -]?)?used|token refresh|refresh credential/i.test(message)) return new AgentRuntimeError('codex-auth-stale', 'The saved Codex sign-in is stale.
|
|
78
|
+
if (/refresh[_ -]?token[_ -]?(?:already[_ -]?)?used|token refresh|refresh credential/i.test(message)) return new AgentRuntimeError('codex-auth-stale', 'The saved Codex sign-in is stale. VuvoDesk refreshed its Codex authentication bridge and will retry.', { status: 401, retryable: true });
|
|
79
79
|
if (/not authenticated|not logged in|authentication|unauthorized|login required/i.test(message)) return new AgentRuntimeError('codex-auth-required', 'Sign in with the Codex CLI first.', { status: 401 });
|
|
80
|
-
if (/user cancelled MCP tool call/i.test(message)) return new AgentRuntimeError('codex-mcp-tool-rejected', 'Codex rejected the
|
|
80
|
+
if (/user cancelled MCP tool call/i.test(message)) return new AgentRuntimeError('codex-mcp-tool-rejected', 'Codex rejected the VuvoDesk MCP tool before it reached the Hub.', { status: 502 });
|
|
81
81
|
return new AgentRuntimeError('codex-run-failed', message, { status: 502, retryable: true });
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -180,7 +180,7 @@ async function ensureCodexHome(codexHome, globalCodexHome, { preferGlobalAuth =
|
|
|
180
180
|
const resolvedHome = path.resolve(codexHome);
|
|
181
181
|
const globalHome = path.resolve(globalCodexHome);
|
|
182
182
|
if (!resolvedHome || resolvedHome === globalHome) {
|
|
183
|
-
throw new AgentRuntimeError('codex-isolation-unavailable', 'Codex requires a dedicated
|
|
183
|
+
throw new AgentRuntimeError('codex-isolation-unavailable', 'Codex requires a dedicated VuvoDesk home.', { status: 503 });
|
|
184
184
|
}
|
|
185
185
|
try {
|
|
186
186
|
await mkdir(resolvedHome, { recursive: true, mode: 0o700 });
|
|
@@ -628,7 +628,7 @@ export function createCodexAgentRuntime({
|
|
|
628
628
|
|
|
629
629
|
async function loadCodex() {
|
|
630
630
|
if (!codexModulePromise) codexModulePromise = import('@openai/codex-sdk').catch(error => {
|
|
631
|
-
const wrapped = new AgentRuntimeError('codex-sdk-not-installed', 'Codex SDK is not installed in this
|
|
631
|
+
const wrapped = new AgentRuntimeError('codex-sdk-not-installed', 'Codex SDK is not installed in this VuvoDesk package.', { status: 503 });
|
|
632
632
|
wrapped.cause = error;
|
|
633
633
|
throw wrapped;
|
|
634
634
|
});
|
|
@@ -723,13 +723,13 @@ export function createCodexAgentRuntime({
|
|
|
723
723
|
|
|
724
724
|
function promptFor({ instruction, deviceIds, permissionPolicy }) {
|
|
725
725
|
return [
|
|
726
|
-
'You are the
|
|
727
|
-
'Use only the registered livedesk.* MCP tools exposed by the
|
|
726
|
+
'You are the VuvoDesk Agent orchestrator.',
|
|
727
|
+
'Use only the registered livedesk.* MCP tools exposed by the VuvoDesk server.',
|
|
728
728
|
'Never use arbitrary MCP servers, change permissions, forge approvals, request credentials, or invent a tool result.',
|
|
729
729
|
`The Hub has fixed this run to permission mode ${permissionPolicy?.mode || 'ask'} and enforces the policy independently of your instructions.`,
|
|
730
730
|
'Use only the selected connected device IDs below. If the Hub asks for user approval, wait for that approval result and do not work around it.',
|
|
731
731
|
'You may perform multiple safe read-only checks when the request requires a sequence. For example, find Clients missing a named process, then check a related service only on those Clients.',
|
|
732
|
-
'If one read-only result omits a fact the user requested, do not treat that missing field as proof that the fact is unavailable. Try another applicable registered
|
|
732
|
+
'If one read-only result omits a fact the user requested, do not treat that missing field as proof that the fact is unavailable. Try another applicable registered VuvoDesk read-only tool.',
|
|
733
733
|
'If only a high-risk registered tool such as livedesk.run_command can obtain the missing fact, use it only when necessary, one selected device per call, and let the Hub approval policy ask the user. If approval is denied or no applicable tool exists, explain that exact limit.',
|
|
734
734
|
`Selected device IDs: ${JSON.stringify(deviceIds)}`,
|
|
735
735
|
'Return a concise Korean or English summary grounded only in tool results. Do not invent results.',
|
|
@@ -754,7 +754,7 @@ export function createCodexAgentRuntime({
|
|
|
754
754
|
timeoutTimer.unref?.();
|
|
755
755
|
await mkdir(workspace, { recursive: true });
|
|
756
756
|
if (activeCount > settings.maxConcurrentRequests) {
|
|
757
|
-
throw new AgentRuntimeError('agent-concurrency-limit', 'The
|
|
757
|
+
throw new AgentRuntimeError('agent-concurrency-limit', 'The VuvoDesk Agent concurrency limit is reached.', { status: 429, retryable: true });
|
|
758
758
|
}
|
|
759
759
|
if (run.abortController.signal.aborted) {
|
|
760
760
|
if (run.abortReason === 'timeout') throw codexAbortError('codex-run-timeout', 'The Codex task exceeded its timeout.');
|
|
@@ -763,7 +763,7 @@ export function createCodexAgentRuntime({
|
|
|
763
763
|
const status = await getStatus();
|
|
764
764
|
if (!status.installed) throw new AgentRuntimeError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
|
|
765
765
|
if (status.authenticated !== 'signed-in') throw new AgentRuntimeError('codex-auth-required', 'Sign in with the Codex CLI first.', { status: 401 });
|
|
766
|
-
reportProgress(run, 'preparing-tools', 'Codex is ready. Preparing the
|
|
766
|
+
reportProgress(run, 'preparing-tools', 'Codex is ready. Preparing the VuvoDesk tools for this Client.');
|
|
767
767
|
session = createMcpSession({
|
|
768
768
|
runId: run.runId,
|
|
769
769
|
signal: run.abortController.signal,
|
|
@@ -805,7 +805,7 @@ export function createCodexAgentRuntime({
|
|
|
805
805
|
for await (const event of streamed.events) {
|
|
806
806
|
emit(run, event);
|
|
807
807
|
if (event.type === 'item.started' && event.item?.type === 'mcp_tool_call') {
|
|
808
|
-
reportProgress(run, 'tool-running', `Running ${safeText(event.item.tool, 120) || 'a
|
|
808
|
+
reportProgress(run, 'tool-running', `Running ${safeText(event.item.tool, 120) || 'a VuvoDesk tool'}.`, {
|
|
809
809
|
toolName: safeText(event.item.tool, 120)
|
|
810
810
|
});
|
|
811
811
|
}
|
|
@@ -815,17 +815,17 @@ export function createCodexAgentRuntime({
|
|
|
815
815
|
run.abortReason = 'turn-limit';
|
|
816
816
|
run.abortController.abort();
|
|
817
817
|
session.cancel();
|
|
818
|
-
throw new AgentRuntimeError('codex-turn-limit-reached', 'Codex exceeded the
|
|
818
|
+
throw new AgentRuntimeError('codex-turn-limit-reached', 'Codex exceeded the VuvoDesk turn limit.', { status: 409 });
|
|
819
819
|
}
|
|
820
820
|
}
|
|
821
821
|
if (run.toolCallCount > run.permissionPolicy.maxToolCalls) {
|
|
822
822
|
run.abortReason = 'tool-limit';
|
|
823
823
|
run.abortController.abort();
|
|
824
824
|
session.cancel();
|
|
825
|
-
throw new AgentRuntimeError('codex-tool-limit-reached', 'Codex exceeded the
|
|
825
|
+
throw new AgentRuntimeError('codex-tool-limit-reached', 'Codex exceeded the VuvoDesk tool-call limit.', { status: 409 });
|
|
826
826
|
}
|
|
827
827
|
if (event.type === 'item.completed' && event.item?.type === 'mcp_tool_call' && event.item?.status === 'failed') {
|
|
828
|
-
throw new Error(safeText(event.item?.error?.message, 800) || `
|
|
828
|
+
throw new Error(safeText(event.item?.error?.message, 800) || `VuvoDesk tool ${safeText(event.item?.tool, 120) || 'call'} failed.`);
|
|
829
829
|
}
|
|
830
830
|
if (event.type === 'item.completed' && event.item?.type === 'agent_message') run.finalResponse = safeText(event.item.text, 4000);
|
|
831
831
|
if (event.type === 'turn.completed') turnCompleted = true;
|
|
@@ -885,12 +885,12 @@ export function createCodexAgentRuntime({
|
|
|
885
885
|
} else if (run.abortReason === 'tool-limit') {
|
|
886
886
|
run.status = 'failed';
|
|
887
887
|
run.error = 'codex-tool-limit-reached';
|
|
888
|
-
run.errorMessage = 'Codex exceeded the
|
|
888
|
+
run.errorMessage = 'Codex exceeded the VuvoDesk tool-call limit.';
|
|
889
889
|
run.errorStage = run.stage || 'unknown';
|
|
890
890
|
} else if (run.abortReason === 'turn-limit') {
|
|
891
891
|
run.status = 'failed';
|
|
892
892
|
run.error = 'codex-turn-limit-reached';
|
|
893
|
-
run.errorMessage = 'Codex exceeded the
|
|
893
|
+
run.errorMessage = 'Codex exceeded the VuvoDesk turn limit.';
|
|
894
894
|
run.errorStage = run.stage || 'unknown';
|
|
895
895
|
} else if (isAbortError(error) || run.abortController.signal.aborted) {
|
|
896
896
|
run.status = 'cancelled';
|
|
@@ -934,7 +934,7 @@ export function createCodexAgentRuntime({
|
|
|
934
934
|
if (!settings.enabled) throw new AgentRuntimeError('agent-disabled', 'Codex Agent is disabled in Settings.', { status: 409 });
|
|
935
935
|
assertSecurityConfiguration();
|
|
936
936
|
pruneRuns();
|
|
937
|
-
if (runs.size >= MAX_RUNS) throw new AgentRuntimeError('agent-run-limit', 'The
|
|
937
|
+
if (runs.size >= MAX_RUNS) throw new AgentRuntimeError('agent-run-limit', 'The VuvoDesk Agent run history is full.', { status: 429, retryable: true });
|
|
938
938
|
const permissionPolicy = freezeAgentPermissionPolicy(input.permissionPolicy && typeof input.permissionPolicy === 'object'
|
|
939
939
|
? input.permissionPolicy
|
|
940
940
|
: createAgentPermissionPolicy({ mode: input.permissionMode || 'safe-auto', deviceIds, maxToolCalls: settings.codexMaxToolCalls }));
|
|
@@ -13,7 +13,7 @@ const toolDefinitions = AGENT_TOOL_DEFINITIONS.map(tool => ({
|
|
|
13
13
|
readOnlyHint: tool.readOnly === true,
|
|
14
14
|
// Codex exec cannot service an interactive MCP approval prompt. Device
|
|
15
15
|
// mutations are proposals at this boundary and remain fail-closed behind
|
|
16
|
-
//
|
|
16
|
+
// VuvoDesk's own signed permission policy and Hub approval workflow.
|
|
17
17
|
destructiveHint: false,
|
|
18
18
|
idempotentHint: tool.readOnly === true || tool.reversible === true,
|
|
19
19
|
openWorldHint: false
|
|
@@ -29,14 +29,14 @@ function errorResponse(id, code, message) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
async function callHub(name, args) {
|
|
32
|
-
if (!hubUrl || !token) throw new Error('
|
|
32
|
+
if (!hubUrl || !token) throw new Error('VuvoDesk MCP session is not configured.');
|
|
33
33
|
const result = await fetch(`${hubUrl}/api/internal/agent-mcp/tool`, {
|
|
34
34
|
method: 'POST',
|
|
35
35
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
36
36
|
body: JSON.stringify({ name, arguments: args && typeof args === 'object' ? args : {} })
|
|
37
37
|
});
|
|
38
38
|
const body = await result.json().catch(() => ({}));
|
|
39
|
-
if (!result.ok || body.ok !== true) throw new Error(String(body.error || `
|
|
39
|
+
if (!result.ok || body.ok !== true) throw new Error(String(body.error || `VuvoDesk tool failed (${result.status}).`));
|
|
40
40
|
return body.result;
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -63,7 +63,7 @@ async function handle(message) {
|
|
|
63
63
|
if (method === 'tools/call') {
|
|
64
64
|
const name = String(message?.params?.name || '');
|
|
65
65
|
if (!toolDefinitions.some(tool => tool.name === name)) {
|
|
66
|
-
errorResponse(id, -32602, 'Unknown
|
|
66
|
+
errorResponse(id, -32602, 'Unknown VuvoDesk tool.');
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
69
|
try {
|
|
@@ -75,7 +75,7 @@ async function handle(message) {
|
|
|
75
75
|
});
|
|
76
76
|
} catch (error) {
|
|
77
77
|
response(id, {
|
|
78
|
-
content: [{ type: 'text', text: String(error?.message || '
|
|
78
|
+
content: [{ type: 'text', text: String(error?.message || 'VuvoDesk tool failed.') }],
|
|
79
79
|
isError: true
|
|
80
80
|
});
|
|
81
81
|
}
|
package/src/filesystem/roots.js
CHANGED
|
@@ -98,26 +98,26 @@ async function mountedRoots() {
|
|
|
98
98
|
return roots;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
export async function discoverFilesystemRoots() {
|
|
102
|
-
const isolatedTestRoot = process.env.LIVEDESK_TEST_MODE === '1'
|
|
103
|
-
? normalizeRootPath(process.env.LIVEDESK_TEST_FILESYSTEM_ROOT)
|
|
104
|
-
: '';
|
|
105
|
-
if (isolatedTestRoot) {
|
|
106
|
-
const rootStat = await fs.stat(isolatedTestRoot);
|
|
107
|
-
if (!rootStat.isDirectory()) {
|
|
108
|
-
throw new Error('LIVEDESK_TEST_FILESYSTEM_ROOT must be a directory.');
|
|
109
|
-
}
|
|
110
|
-
return [{
|
|
111
|
-
path: isolatedTestRoot,
|
|
112
|
-
name: 'Test drive',
|
|
113
|
-
displayPath: isolatedTestRoot,
|
|
114
|
-
type: 'drive',
|
|
115
|
-
driveType: 'fixed',
|
|
116
|
-
...await statfsCapacity(isolatedTestRoot),
|
|
117
|
-
hasChildren: true
|
|
118
|
-
}];
|
|
119
|
-
}
|
|
120
|
-
const roots = process.platform === 'win32' ? await windowsRoots() : await mountedRoots();
|
|
101
|
+
export async function discoverFilesystemRoots() {
|
|
102
|
+
const isolatedTestRoot = process.env.LIVEDESK_TEST_MODE === '1'
|
|
103
|
+
? normalizeRootPath(process.env.LIVEDESK_TEST_FILESYSTEM_ROOT)
|
|
104
|
+
: '';
|
|
105
|
+
if (isolatedTestRoot) {
|
|
106
|
+
const rootStat = await fs.stat(isolatedTestRoot);
|
|
107
|
+
if (!rootStat.isDirectory()) {
|
|
108
|
+
throw new Error('LIVEDESK_TEST_FILESYSTEM_ROOT must be a directory.');
|
|
109
|
+
}
|
|
110
|
+
return [{
|
|
111
|
+
path: isolatedTestRoot,
|
|
112
|
+
name: 'Test drive',
|
|
113
|
+
displayPath: isolatedTestRoot,
|
|
114
|
+
type: 'drive',
|
|
115
|
+
driveType: 'fixed',
|
|
116
|
+
...await statfsCapacity(isolatedTestRoot),
|
|
117
|
+
hasChildren: true
|
|
118
|
+
}];
|
|
119
|
+
}
|
|
120
|
+
const roots = process.platform === 'win32' ? await windowsRoots() : await mountedRoots();
|
|
121
121
|
if (roots.length > 0) return roots;
|
|
122
122
|
const fallback = process.platform === 'win32' ? `${process.env.SystemDrive || 'C:'}\\` : os.homedir();
|
|
123
123
|
return [{
|
|
@@ -44,7 +44,7 @@ export class HubSharedFolders {
|
|
|
44
44
|
if (folder?.id && folder?.sourcePath) this.folders.set(folder.id, { ...folder, manifestVersion: MANIFEST_VERSION });
|
|
45
45
|
}
|
|
46
46
|
} catch (error) {
|
|
47
|
-
if (error?.code !== 'ENOENT') console.warn(`[
|
|
47
|
+
if (error?.code !== 'ENOENT') console.warn(`[VuvoDesk Hub] shared folder store unavailable: ${error?.message || error}`);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -111,7 +111,7 @@ export class HubSharedFolders {
|
|
|
111
111
|
if (this.syncing.has(folder.id)) throw new Error('sync-already-running');
|
|
112
112
|
const source = await this.filesystem.registerPersistedFolder(folder.sourcePath, folder.displayPath);
|
|
113
113
|
if (!source) throw new Error('sync-source-unavailable');
|
|
114
|
-
folder.remoteDirectory = String(remoteDirectory || folder.remoteDirectory || 'Desktop/
|
|
114
|
+
folder.remoteDirectory = String(remoteDirectory || folder.remoteDirectory || 'Desktop/VuvoDeskFiles').replace(/[\0\r\n\t]/g, ' ').trim().slice(0, 600);
|
|
115
115
|
const scan = await this.filesystem.scan([source.id]);
|
|
116
116
|
folder.fileCount = scan.files.length;
|
|
117
117
|
folder.totalBytes = scan.totalBytes;
|
package/src/live-desk-update.js
CHANGED
|
@@ -79,14 +79,14 @@ function buildLegacyPackageSupervisorStarterSource() {
|
|
|
79
79
|
"const{spawn}=require('node:child_process'),f=require('node:fs'),p=require('node:path'),o=require('node:os'),c=JSON.parse(Buffer.from(process.env.C,'base64')),e={...process.env,LIVEDESK_UPDATE_STARTER_PID:String(process.pid),LIVEDESK_UPDATE_ORIGINAL_CWD:process.cwd()},w=p.join(o.tmpdir(),'livedesk-update-cwd','client-'+c[0]),k=p.join(o.tmpdir(),'livedesk-update-cache','client-'+c[0]),u=e.LIVEDESK_NPX_EXECUTABLE,s=process.platform,j=process.execPath,n=u||(s==='win32'?'npx.cmd':'npx'),x=[e.LIVEDESK_NPX_CLI_PATH,e.npm_execpath&&p.join(p.dirname(e.npm_execpath),'npx-cli.js'),u&&p.join(p.dirname(u),'node_modules/npm/bin/npx-cli.js'),p.join(p.dirname(j),'node_modules/npm/bin/npx-cli.js')].find(v=>v&&f.existsSync(v)),a=['-y','--prefer-online','--prefix',w,'--workspaces=false','livedesk@'+c[6],'--internal-legacy-client-update'],z=String.fromCharCode(32),q=v=>'\"'+String(v).replaceAll('\"','\"\"')+'\"';if(!(+c[7]>0))process.exit(1);c[7]=String(Date.now()+Number(c[7]));e.C=Buffer.from(JSON.stringify(c)).toString('base64');f.mkdirSync(w,{recursive:true});if(f.readdirSync(w)[0])process.exit(1);f.mkdirSync(k,{recursive:true});Object.keys(e).forEach(n=>/^(INIT_CWD|npm_config_(cache|local_prefix|workspaces?|include_workspace_root))$/i.test(n)&&Reflect.deleteProperty(e,n));Object.assign(e,{INIT_CWD:w,npm_config_cache:k,npm_config_local_prefix:w,npm_config_workspaces:'false',npm_config_include_workspace_root:'false',LIVEDESK_UPDATE_NEUTRAL_CWD:w});e.i=x?{c:j,a:[x,...a]}:s==='win32'?{c:e.ComSpec||'cmd.exe',a:['/d','/s','/c','call'+z+q(n)+z+a.map(q).join(z)]}:{c:n,a};e.r=spawn(e.i.c,e.i.a,{cwd:w,env:e,detached:true,stdio:'ignore',windowsHide:true});",
|
|
80
80
|
"e.r.on('error',()=>process.exit(1));e.r.unref();"
|
|
81
81
|
].join('');
|
|
82
|
-
if (source.includes(' ')) throw new Error('
|
|
82
|
+
if (source.includes(' ')) throw new Error('VuvoDesk legacy starter source must not contain spaces.');
|
|
83
83
|
return source;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
function assertLegacyCommandLength(command, platform) {
|
|
87
87
|
if (command.length > LIVE_DESK_LEGACY_COMMAND_MAX_LENGTH) {
|
|
88
88
|
throw new Error(
|
|
89
|
-
`
|
|
89
|
+
`VuvoDesk legacy ${platform} update command is ${command.length} characters; `
|
|
90
90
|
+ `the compatibility limit is ${LIVE_DESK_LEGACY_COMMAND_MAX_LENGTH}.`
|
|
91
91
|
);
|
|
92
92
|
}
|
|
@@ -112,10 +112,10 @@ function buildLegacyPlatformCommand(platform, payload) {
|
|
|
112
112
|
"$ErrorActionPreference='Stop'",
|
|
113
113
|
'$n=[string]$env:LIVEDESK_NODE_EXECUTABLE',
|
|
114
114
|
'if(-not $n){$n=(Get-Command node.exe -ErrorAction SilentlyContinue).Source}',
|
|
115
|
-
"if(-not $n){throw '
|
|
115
|
+
"if(-not $n){throw 'VuvoDesk node executable was not found.'}",
|
|
116
116
|
"$j=\"eval(Buffer.from(process.env.S,'base64').toString('utf8'))\"",
|
|
117
117
|
"$p=Start-Process -FilePath $n -ArgumentList @('-e',$j) -WindowStyle Hidden -PassThru",
|
|
118
|
-
"if(-not $p){throw '
|
|
118
|
+
"if(-not $p){throw 'VuvoDesk update starter failed.'}"
|
|
119
119
|
].join(';');
|
|
120
120
|
const command = windows
|
|
121
121
|
? `set C=${configBase64}&set S=${starterBase64}&powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encodePowerShell(powerShellStarter)}`
|
|
@@ -159,7 +159,7 @@ async function fetchLatestPackage(packageName, fetchImpl) {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
export async function fetchLatestLiveDeskRelease(fetchImpl = globalThis.fetch) {
|
|
162
|
-
if (typeof fetchImpl !== 'function') throw new Error('fetch is unavailable for
|
|
162
|
+
if (typeof fetchImpl !== 'function') throw new Error('fetch is unavailable for VuvoDesk update checks.');
|
|
163
163
|
const [manager, client] = await Promise.all([
|
|
164
164
|
fetchLatestPackage('livedesk', fetchImpl),
|
|
165
165
|
fetchLatestPackage('@livedesk/client', fetchImpl)
|
|
@@ -379,7 +379,7 @@ export function createLiveDeskUpdateManager({
|
|
|
379
379
|
const failRun = (message) => {
|
|
380
380
|
if (!run) return;
|
|
381
381
|
run.state = 'failed';
|
|
382
|
-
run.error = String(message || '
|
|
382
|
+
run.error = String(message || 'VuvoDesk update failed.');
|
|
383
383
|
for (const target of run.targets) {
|
|
384
384
|
if (target.state === 'completed') continue;
|
|
385
385
|
target.state = 'failed';
|
|
@@ -428,7 +428,7 @@ export function createLiveDeskUpdateManager({
|
|
|
428
428
|
|
|
429
429
|
const requestRestart = () => {
|
|
430
430
|
if (!restartSupported) {
|
|
431
|
-
failRun('Hub launcher restart is unavailable. Start
|
|
431
|
+
failRun('Hub launcher restart is unavailable. Start VuvoDesk through npx livedesk@latest.');
|
|
432
432
|
return false;
|
|
433
433
|
}
|
|
434
434
|
const result = requestHubRestart?.({
|
|
@@ -686,7 +686,7 @@ export function createLiveDeskUpdateManager({
|
|
|
686
686
|
};
|
|
687
687
|
}
|
|
688
688
|
const release = latestRelease || await checkLatest();
|
|
689
|
-
if (!release) return { ok: false, error: checkError || '
|
|
689
|
+
if (!release) return { ok: false, error: checkError || 'VuvoDesk update check failed.', ...getStatus() };
|
|
690
690
|
const managerNeedsUpdate = compareVersions(release.latestManagerVersion, currentManagerVersion) > 0;
|
|
691
691
|
const clientPackageNeedsUpdate = compareVersions(release.latestClientVersion, currentClientVersion) > 0;
|
|
692
692
|
const connected = connectedClientDevices();
|
|
@@ -700,7 +700,7 @@ export function createLiveDeskUpdateManager({
|
|
|
700
700
|
return { ok: true, state: 'clients-updated', ...getStatus() };
|
|
701
701
|
}
|
|
702
702
|
if (needsHubRestart && !restartSupported) {
|
|
703
|
-
return { ok: false, error: 'Hub launcher restart is unavailable. Start
|
|
703
|
+
return { ok: false, error: 'Hub launcher restart is unavailable. Start VuvoDesk through npx livedesk@latest.', ...getStatus() };
|
|
704
704
|
}
|
|
705
705
|
const targets = outdatedClients;
|
|
706
706
|
run = {
|