livedesk 0.1.205 → 0.1.206

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.
@@ -0,0 +1,18 @@
1
+ function normalizeIds(value) {
2
+ const source = Array.isArray(value) ? value : [];
3
+ return [...new Set(source.map(item => String(item || '').trim()).filter(Boolean))].slice(0, 500);
4
+ }
5
+
6
+ export function createAgentDeviceScope(allowedDeviceIds, fallbackDeviceIds = []) {
7
+ const requested = normalizeIds(allowedDeviceIds);
8
+ const fallback = normalizeIds(fallbackDeviceIds);
9
+ return new Set(requested.length > 0 ? requested : fallback);
10
+ }
11
+
12
+ export function selectAgentDeviceIds({ allowedDeviceIds, requestedDeviceIds, connectedDeviceIds }) {
13
+ const allowed = allowedDeviceIds instanceof Set ? allowedDeviceIds : new Set(normalizeIds(allowedDeviceIds));
14
+ const requested = normalizeIds(requestedDeviceIds);
15
+ const connected = new Set(normalizeIds(connectedDeviceIds));
16
+ const candidates = requested.length > 0 ? requested : [...allowed];
17
+ return candidates.filter(deviceId => allowed.has(deviceId) && connected.has(deviceId));
18
+ }
@@ -136,7 +136,9 @@ export function createAgentManager({
136
136
  const { current, active } = await currentProvider();
137
137
  if (!current.enabled || !current.generateSummary) throw new AgentProviderError('agent-summary-disabled', 'Agent summaries are disabled in Settings.', { status: 409 });
138
138
  if (current.provider === AGENT_PROVIDER_CODEX) {
139
- return { summary: String(input?.summary?.results?.length ? `${input.summary.completed || 0} completed, ${input.summary.failed || 0} failed.` : 'LiveDesk Agent run completed.').slice(0, 1200), modelId: current.codexModelId || 'Codex default' };
139
+ const source = input && typeof input === 'object' ? input : {};
140
+ const results = Array.isArray(source.results) ? source.results : [];
141
+ return { summary: String(results.length ? `${source.completed || 0} completed, ${source.failed || 0} failed.` : 'LiveDesk Agent run completed.').slice(0, 1200), modelId: current.codexModelId || 'Codex default' };
140
142
  }
141
143
  const source = input && typeof input === 'object' ? input : {};
142
144
  const results = Array.isArray(source.results) ? source.results : [];
@@ -98,11 +98,9 @@ function normalizeBaseUrl(value) {
98
98
  }
99
99
 
100
100
  function normalizeWorkingDirectory(value) {
101
- const candidate = stringValue(value, DEFAULT_AGENT_WORKSPACE, 500);
102
- if (!candidate || candidate.includes('\0')) {
103
- throw new AgentSettingsError('agent-invalid-working-directory', 'Codex working directory is invalid.');
104
- }
105
- return path.resolve(candidate);
101
+ // The agent runtime owns this directory. Do not allow a browser/API caller
102
+ // to move Codex into a user project or an arbitrary filesystem root.
103
+ return DEFAULT_AGENT_WORKSPACE;
106
104
  }
107
105
 
108
106
  export function normalizeAgentSettings(value = {}) {
@@ -1,5 +1,5 @@
1
1
  import crypto from 'node:crypto';
2
- import { mkdir } from 'node:fs/promises';
2
+ import { access, link, mkdir } from 'node:fs/promises';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { spawn } from 'node:child_process';
@@ -9,6 +9,34 @@ import { AGENT_PROVIDER_CODEX } from './agent-settings.js';
9
9
 
10
10
  const require = createRequire(import.meta.url);
11
11
  const MAX_EVENTS = 240;
12
+ const MAX_RUNS = 500;
13
+ const RUN_TTL_MS = 60 * 60 * 1000;
14
+ const SAFE_ENV_KEYS = [
15
+ 'Path',
16
+ 'PATH',
17
+ 'SystemRoot',
18
+ 'SYSTEMROOT',
19
+ 'ComSpec',
20
+ 'TEMP',
21
+ 'TMP',
22
+ 'USERPROFILE',
23
+ 'HOME',
24
+ 'APPDATA',
25
+ 'LOCALAPPDATA',
26
+ 'WINDIR',
27
+ 'LANG',
28
+ 'LC_ALL',
29
+ 'TMPDIR'
30
+ ];
31
+ const LIVEDESK_MCP_TOOLS = [
32
+ 'livedesk.list_devices',
33
+ 'livedesk.get_system_health',
34
+ 'livedesk.get_gpu_status',
35
+ 'livedesk.get_disk_status',
36
+ 'livedesk.list_processes',
37
+ 'livedesk.get_service_status',
38
+ 'livedesk.collect_diagnostics'
39
+ ];
12
40
 
13
41
  function safeText(value, max = 2400) {
14
42
  return String(value || '').replace(/[\0\r]/g, ' ').trim().slice(0, max);
@@ -61,42 +89,91 @@ function codexThreadOptions(settings, workspace) {
61
89
  };
62
90
  }
63
91
 
64
- function codexConfig({ mcpServerPath, mcpUrl, token, workspace }) {
92
+ function baseCodexConfig() {
65
93
  return {
66
94
  approval_policy: 'never',
67
95
  sandbox_mode: 'read-only',
68
96
  web_search: 'disabled',
69
97
  features: {
70
98
  shell_tool: false,
71
- web_search: false,
99
+ unified_exec: false,
100
+ shell_snapshot: false,
72
101
  apps: false,
73
- multi_agent: false
102
+ multi_agent: false,
103
+ web_search: false,
104
+ skill_mcp_dependency_install: false,
105
+ memories: false,
106
+ remote_plugin: false
74
107
  },
75
108
  tools: {
76
109
  web_search: false,
77
110
  view_image: false
78
111
  },
79
- mcp_servers: {
80
- livedesk: {
81
- command: process.execPath,
82
- args: [mcpServerPath],
83
- cwd: workspace,
84
- env: {
85
- LIVEDESK_AGENT_MCP_URL: mcpUrl,
86
- LIVEDESK_AGENT_MCP_TOKEN: token
87
- },
88
- enabled: true,
89
- required: true,
90
- startup_timeout_sec: 10,
91
- tool_timeout_sec: 150
92
- }
112
+ apps: {
113
+ _default: { enabled: false }
114
+ },
115
+ plugins: {},
116
+ mcp_servers: {}
117
+ };
118
+ }
119
+
120
+ function codexConfig({ mcpServerPath, mcpUrl, token, workspace }) {
121
+ const config = baseCodexConfig();
122
+ config.mcp_servers = {
123
+ livedesk: {
124
+ command: process.execPath,
125
+ args: [mcpServerPath],
126
+ cwd: workspace,
127
+ env: {
128
+ LIVEDESK_AGENT_MCP_URL: mcpUrl,
129
+ LIVEDESK_AGENT_MCP_TOKEN: token
130
+ },
131
+ enabled: true,
132
+ required: true,
133
+ enabled_tools: LIVEDESK_MCP_TOOLS,
134
+ startup_timeout_sec: 10,
135
+ tool_timeout_sec: 150
93
136
  }
94
137
  };
138
+ return config;
95
139
  }
96
140
 
97
- function runCli(cliPath, args, timeoutMs = 5000) {
141
+ function safeCodexEnv(codexHome) {
142
+ const env = { CODEX_HOME: codexHome };
143
+ for (const key of SAFE_ENV_KEYS) {
144
+ const value = process.env[key];
145
+ if (value !== undefined && value !== '') env[key] = value;
146
+ }
147
+ if (!env.HOME) env.HOME = os.homedir();
148
+ if (!env.USERPROFILE && process.platform === 'win32') env.USERPROFILE = os.homedir();
149
+ if (!env.Path && env.PATH) env.Path = env.PATH;
150
+ if (!env.PATH && env.Path) env.PATH = env.Path;
151
+ return env;
152
+ }
153
+
154
+ async function ensureCodexHome(codexHome) {
155
+ await mkdir(codexHome, { recursive: true, mode: 0o700 });
156
+ const targetAuthPath = path.join(codexHome, 'auth.json');
157
+ try {
158
+ await access(targetAuthPath);
159
+ return;
160
+ } catch {
161
+ // Keep the SDK isolated from the user's global config while reusing the
162
+ // CLI-owned auth file when the user is already signed in. The runtime
163
+ // never reads or copies the credential contents.
164
+ }
165
+ try {
166
+ await link(path.join(os.homedir(), '.codex', 'auth.json'), targetAuthPath);
167
+ } catch {
168
+ // A fresh installation may not have a CLI auth file yet. Codex will then
169
+ // report the normal auth-required state through getStatus().
170
+ }
171
+ }
172
+
173
+ function runCli(cliPath, args, timeoutMs = 5000, env = process.env) {
98
174
  return new Promise((resolve, reject) => {
99
175
  const child = spawn(process.execPath, [cliPath, ...args], {
176
+ env,
100
177
  windowsHide: true,
101
178
  stdio: ['ignore', 'pipe', 'pipe']
102
179
  });
@@ -121,6 +198,24 @@ function runCli(cliPath, args, timeoutMs = 5000) {
121
198
  });
122
199
  }
123
200
 
201
+ function isTerminalStatus(status) {
202
+ return ['completed', 'failed', 'cancelled'].includes(status);
203
+ }
204
+
205
+ function runAgeMs(run) {
206
+ return Math.max(0, Date.now() - Date.parse(run.updatedAt || run.createdAt || 0));
207
+ }
208
+
209
+ function codexAbortError(code, message) {
210
+ return new AgentProviderError(code, message, { status: code === 'codex-task-timeout' ? 504 : 409, retryable: code === 'codex-task-timeout' });
211
+ }
212
+
213
+ function safeAgentDeviceIds(value) {
214
+ return [...new Set((Array.isArray(value) ? value : [])
215
+ .map(item => String(item || '').trim())
216
+ .filter(Boolean))].slice(0, 500);
217
+ }
218
+
124
219
  export function createCodexAgentRuntime({
125
220
  settingsStore,
126
221
  dataDir = path.join(os.homedir(), '.livedesk'),
@@ -132,6 +227,28 @@ export function createCodexAgentRuntime({
132
227
  let activeCount = 0;
133
228
  let codexModulePromise;
134
229
  const workspace = path.resolve(dataDir, 'agent-workspace');
230
+ const codexHome = path.resolve(dataDir, 'codex-home');
231
+
232
+ function scheduleRunCleanup(run) {
233
+ const timer = setTimeout(() => {
234
+ if (runs.get(run.runId) === run && isTerminalStatus(run.status) && runAgeMs(run) >= RUN_TTL_MS) {
235
+ runs.delete(run.runId);
236
+ }
237
+ }, RUN_TTL_MS + 1000);
238
+ timer.unref?.();
239
+ }
240
+
241
+ function pruneRuns() {
242
+ for (const [runId, run] of runs) {
243
+ if (isTerminalStatus(run.status) && runAgeMs(run) >= RUN_TTL_MS) runs.delete(runId);
244
+ }
245
+ if (runs.size < MAX_RUNS) return;
246
+ for (const [runId, run] of runs) {
247
+ if (!isTerminalStatus(run.status)) continue;
248
+ runs.delete(runId);
249
+ if (runs.size < MAX_RUNS) break;
250
+ }
251
+ }
135
252
 
136
253
  async function loadCodex() {
137
254
  if (!codexModulePromise) codexModulePromise = import('@openai/codex-sdk').catch(error => {
@@ -158,7 +275,8 @@ export function createCodexAgentRuntime({
158
275
  let authenticated = 'unknown';
159
276
  let detail = '';
160
277
  try {
161
- const result = await runCli(pathToCli, ['login', 'status']);
278
+ await ensureCodexHome(codexHome);
279
+ const result = await runCli(pathToCli, ['login', 'status'], 5000, safeCodexEnv(codexHome));
162
280
  detail = safeText(result.stdout || result.stderr, 300);
163
281
  authenticated = /logged in|authenticated|chatgpt/i.test(`${result.stdout}\n${result.stderr}`)
164
282
  ? 'signed-in'
@@ -176,8 +294,9 @@ export function createCodexAgentRuntime({
176
294
  if (status.authenticated !== 'signed-in') throw new AgentProviderError('codex-auth-required', 'Sign in with the Codex CLI first.', { status: 401 });
177
295
  const settings = await settingsStore.get();
178
296
  await mkdir(workspace, { recursive: true });
297
+ await ensureCodexHome(codexHome);
179
298
  const { Codex } = await loadCodex();
180
- const codex = new Codex({ config: { approval_policy: 'never', sandbox_mode: 'read-only', web_search: 'disabled', features: { shell_tool: false, web_search: false, apps: false, multi_agent: false }, tools: { web_search: false, view_image: false } } });
299
+ const codex = new Codex({ env: safeCodexEnv(codexHome), config: baseCodexConfig() });
181
300
  const controller = new AbortController();
182
301
  const timer = setTimeout(() => controller.abort(), Math.min(30000, settings.codexTaskTimeoutMs));
183
302
  try {
@@ -238,19 +357,37 @@ export function createCodexAgentRuntime({
238
357
  async function execute(run, input) {
239
358
  activeCount += 1;
240
359
  let session;
360
+ let timeoutTimer;
241
361
  try {
242
362
  const settings = await settingsStore.get();
363
+ const timeoutMs = Math.max(1000, Number(settings.codexTaskTimeoutMs) || 600000);
364
+ timeoutTimer = setTimeout(() => {
365
+ run.abortReason = 'timeout';
366
+ run.abortController.abort();
367
+ try { session?.cancel(); } catch { /* best effort */ }
368
+ }, timeoutMs);
369
+ timeoutTimer.unref?.();
243
370
  await mkdir(workspace, { recursive: true });
244
371
  if (activeCount > settings.maxConcurrentRequests) {
245
372
  throw new AgentProviderError('agent-concurrency-limit', 'The LiveDesk Agent concurrency limit is reached.', { status: 429, retryable: true });
246
373
  }
374
+ if (run.abortController.signal.aborted) {
375
+ if (run.abortReason === 'timeout') throw codexAbortError('codex-task-timeout', 'The Codex task exceeded its timeout.');
376
+ throw Object.assign(new Error('The Codex task was cancelled.'), { name: 'AbortError' });
377
+ }
247
378
  const status = await getStatus();
248
379
  if (!status.installed) throw new AgentProviderError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
249
380
  if (status.authenticated !== 'signed-in') throw new AgentProviderError('codex-auth-required', 'Sign in with the Codex CLI first.', { status: 401 });
250
- session = createMcpSession({ runId: run.runId, signal: run.abortController.signal });
381
+ session = createMcpSession({ runId: run.runId, signal: run.abortController.signal, allowedDeviceIds: input.deviceIds });
251
382
  run.mcpToken = session.token;
383
+ if (run.abortController.signal.aborted) {
384
+ session.cancel();
385
+ if (run.abortReason === 'timeout') throw codexAbortError('codex-task-timeout', 'The Codex task exceeded its timeout.');
386
+ throw Object.assign(new Error('The Codex task was cancelled.'), { name: 'AbortError' });
387
+ }
388
+ await ensureCodexHome(codexHome);
252
389
  const { Codex } = await loadCodex();
253
- const codex = new Codex({ config: codexConfig({ mcpServerPath, mcpUrl: session.url, token: session.token, workspace }) });
390
+ const codex = new Codex({ env: safeCodexEnv(codexHome), config: codexConfig({ mcpServerPath, mcpUrl: session.url, token: session.token, workspace }) });
254
391
  const threadOptions = codexThreadOptions(settings, workspace);
255
392
  const thread = input.resumeThreadId && settings.codexResumeSessions
256
393
  ? codex.resumeThread(String(input.resumeThreadId), threadOptions)
@@ -261,6 +398,7 @@ export function createCodexAgentRuntime({
261
398
  for await (const event of streamed.events) {
262
399
  emit(run, event);
263
400
  if (run.toolCallCount > settings.codexMaxToolCalls) {
401
+ run.abortReason = 'tool-limit';
264
402
  run.abortController.abort();
265
403
  session.cancel();
266
404
  throw new AgentProviderError('codex-tool-limit-reached', 'Codex exceeded the LiveDesk tool-call limit.', { status: 409 });
@@ -272,7 +410,13 @@ export function createCodexAgentRuntime({
272
410
  }
273
411
  if (run.status === 'running') run.status = 'completed';
274
412
  } catch (error) {
275
- if (isAbortError(error) || run.abortController.signal.aborted) {
413
+ if (run.abortReason === 'timeout') {
414
+ run.status = 'failed';
415
+ run.error = 'codex-task-timeout';
416
+ } else if (run.abortReason === 'tool-limit') {
417
+ run.status = 'failed';
418
+ run.error = 'codex-tool-limit-reached';
419
+ } else if (isAbortError(error) || run.abortController.signal.aborted) {
276
420
  run.status = 'cancelled';
277
421
  run.error = 'cancelled-by-user';
278
422
  } else {
@@ -286,7 +430,9 @@ export function createCodexAgentRuntime({
286
430
  try { session.cancel(); } catch { /* best effort */ }
287
431
  destroyMcpSession(session.token);
288
432
  }
433
+ clearTimeout(timeoutTimer);
289
434
  run.updatedAt = new Date().toISOString();
435
+ if (isTerminalStatus(run.status)) scheduleRunCleanup(run);
290
436
  }
291
437
  }
292
438
 
@@ -295,6 +441,8 @@ export function createCodexAgentRuntime({
295
441
  if (!instruction) throw new AgentProviderError('agent-invalid-instruction', 'Instruction is required.', { status: 400 });
296
442
  const settings = await settingsStore.get();
297
443
  if (!settings.enabled) throw new AgentProviderError('agent-ai-disabled', 'Agent AI is disabled in Settings.', { status: 409 });
444
+ pruneRuns();
445
+ if (runs.size >= MAX_RUNS) throw new AgentProviderError('agent-run-limit', 'The LiveDesk Agent run history is full.', { status: 429, retryable: true });
298
446
  const run = {
299
447
  runId: crypto.randomUUID(),
300
448
  status: 'queued',
@@ -309,10 +457,11 @@ export function createCodexAgentRuntime({
309
457
  batchIds: new Set(),
310
458
  events: [],
311
459
  abortController: new AbortController(),
460
+ abortReason: '',
312
461
  mcpToken: ''
313
462
  };
314
463
  runs.set(run.runId, run);
315
- void execute(run, { deviceIds: Array.isArray(input.deviceIds) ? input.deviceIds.slice(0, 500) : [], resumeThreadId: input.resumeThreadId });
464
+ void execute(run, { deviceIds: safeAgentDeviceIds(input.deviceIds), resumeThreadId: input.resumeThreadId });
316
465
  return publicRun(run);
317
466
  }
318
467
 
@@ -324,6 +473,8 @@ export function createCodexAgentRuntime({
324
473
  function cancel(runId) {
325
474
  const run = runs.get(String(runId || ''));
326
475
  if (!run) return null;
476
+ if (isTerminalStatus(run.status)) return publicRun(run);
477
+ run.abortReason = 'user';
327
478
  run.abortController.abort();
328
479
  run.status = 'cancelled';
329
480
  run.error = 'cancelled-by-user';
@@ -331,5 +482,5 @@ export function createCodexAgentRuntime({
331
482
  return publicRun(run);
332
483
  }
333
484
 
334
- return { getStatus, testConnection, start, get, cancel, workspace };
485
+ return { getStatus, testConnection, start, get, cancel, workspace, codexHome };
335
486
  }
package/hub/src/server.js CHANGED
@@ -16,6 +16,7 @@ import { toFilesystemHttpError } from './filesystem/directory-reader.js';
16
16
  import { createAgentManager } from './agents/agent-manager.js';
17
17
  import { AgentSettingsStore } from './agents/agent-settings.js';
18
18
  import { createCodexAgentRuntime } from './agents/codex-agent-runtime.js';
19
+ import { createAgentDeviceScope, selectAgentDeviceIds } from './agents/agent-device-scope.js';
19
20
 
20
21
  const __dirname = dirname(fileURLToPath(import.meta.url));
21
22
  const webDistCandidates = [
@@ -210,23 +211,50 @@ const remoteHub = createRemoteHub({
210
211
  const agentDataDir = process.env.LIVEDESK_DATA_DIR || undefined;
211
212
  const agentMcpSessions = new Map();
212
213
  const agentMcpRunBatches = new Map();
214
+ const agentMcpRunCleanupTimers = new Map();
215
+ const AGENT_MCP_RUN_TTL_MS = 60 * 60 * 1000;
216
+ const MAX_AGENT_MCP_RUN_RECORDS = 500;
213
217
 
214
- function connectedAgentDevices(requestedIds = []) {
218
+ function connectedAgentDeviceIds() {
215
219
  const devices = remoteHub.listDevices({ includeDataUrl: false });
216
- const connected = devices.filter(device => device.connected === true);
217
- const requested = [...new Set((Array.isArray(requestedIds) ? requestedIds : [])
218
- .map(value => String(value || '').trim())
219
- .filter(Boolean))].slice(0, 500);
220
- const allowed = new Set(connected.map(device => device.deviceId));
221
- return (requested.length > 0 ? requested : [...allowed]).filter(deviceId => allowed.has(deviceId));
220
+ return devices
221
+ .filter(device => device.connected === true)
222
+ .map(device => String(device.deviceId || '').trim())
223
+ .filter(Boolean)
224
+ .slice(0, 500);
222
225
  }
223
226
 
224
- function createAgentMcpSession({ runId, signal }) {
227
+ function deleteAgentMcpRunBatches(runId) {
228
+ agentMcpRunBatches.delete(runId);
229
+ const timer = agentMcpRunCleanupTimers.get(runId);
230
+ if (timer) clearTimeout(timer);
231
+ agentMcpRunCleanupTimers.delete(runId);
232
+ }
233
+
234
+ function pruneAgentMcpRunBatches() {
235
+ while (agentMcpRunBatches.size >= MAX_AGENT_MCP_RUN_RECORDS) {
236
+ const oldestRunId = agentMcpRunBatches.keys().next().value;
237
+ if (!oldestRunId) break;
238
+ deleteAgentMcpRunBatches(oldestRunId);
239
+ }
240
+ }
241
+
242
+ function scheduleAgentMcpRunCleanup(runId) {
243
+ if (agentMcpRunCleanupTimers.has(runId)) return;
244
+ const timer = setTimeout(() => deleteAgentMcpRunBatches(runId), AGENT_MCP_RUN_TTL_MS);
245
+ timer.unref?.();
246
+ agentMcpRunCleanupTimers.set(runId, timer);
247
+ }
248
+
249
+ function createAgentMcpSession({ runId, signal, allowedDeviceIds: allowedDeviceIdsInput = [] }) {
225
250
  const token = crypto.randomBytes(32).toString('hex');
251
+ const allowedDeviceIds = createAgentDeviceScope(allowedDeviceIdsInput, connectedAgentDeviceIds());
252
+ pruneAgentMcpRunBatches();
226
253
  const session = {
227
254
  token,
228
255
  runId: String(runId || ''),
229
256
  signal,
257
+ allowedDeviceIds,
230
258
  cancelled: false,
231
259
  cancel() {
232
260
  if (this.cancelled) return;
@@ -235,7 +263,7 @@ function createAgentMcpSession({ runId, signal }) {
235
263
  }
236
264
  };
237
265
  agentMcpSessions.set(token, session);
238
- agentMcpRunBatches.set(session.runId, new Set());
266
+ if (!agentMcpRunBatches.has(session.runId)) agentMcpRunBatches.set(session.runId, new Set());
239
267
  return { token, url: `http://127.0.0.1:${httpPort}`, cancel: () => session.cancel() };
240
268
  }
241
269
 
@@ -243,6 +271,7 @@ function destroyAgentMcpSession(token) {
243
271
  const session = agentMcpSessions.get(token);
244
272
  if (session) session.cancel();
245
273
  agentMcpSessions.delete(token);
274
+ if (session) scheduleAgentMcpRunCleanup(session.runId);
246
275
  }
247
276
 
248
277
  function delayAgentMcp(ms) {
@@ -252,9 +281,13 @@ function delayAgentMcp(ms) {
252
281
  async function dispatchAgentMcpTool(session, name, args = {}) {
253
282
  if (session.cancelled || session.signal?.aborted) return { ok: false, error: 'cancelled-by-user' };
254
283
  if (name === 'livedesk.list_devices') {
255
- const requested = connectedAgentDevices(args.deviceIds);
284
+ const requested = selectAgentDeviceIds({
285
+ allowedDeviceIds: session.allowedDeviceIds,
286
+ requestedDeviceIds: args.deviceIds,
287
+ connectedDeviceIds: connectedAgentDeviceIds()
288
+ });
256
289
  const devices = remoteHub.listDevices({ includeDataUrl: false })
257
- .filter(device => requested.length === 0 ? device.connected === true : requested.includes(device.deviceId))
290
+ .filter(device => requested.includes(device.deviceId))
258
291
  .slice(0, 500)
259
292
  .map(device => ({ deviceId: device.deviceId, deviceName: device.deviceName, hostname: device.hostname, connected: device.connected === true, platform: device.platform, capabilities: device.capabilities }));
260
293
  return { ok: true, devices };
@@ -269,7 +302,11 @@ async function dispatchAgentMcpTool(session, name, args = {}) {
269
302
  };
270
303
  const operation = operationByTool[name];
271
304
  if (!operation) return { ok: false, error: 'unsupported-livedesk-tool' };
272
- const targetIds = connectedAgentDevices(args.deviceIds);
305
+ const targetIds = selectAgentDeviceIds({
306
+ allowedDeviceIds: session.allowedDeviceIds,
307
+ requestedDeviceIds: args.deviceIds,
308
+ connectedDeviceIds: connectedAgentDeviceIds()
309
+ });
273
310
  if (targetIds.length === 0) return { ok: false, error: 'no-target-devices' };
274
311
  const targetQuery = operation === 'process.list'
275
312
  ? String(args.processName || '').replace(/[\0\r\n]/g, ' ').trim().slice(0, 120)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.205",
3
+ "version": "0.1.206",
4
4
  "description": "LiveDesk Hub and client launcher",
5
5
  "type": "module",
6
6
  "bin": {