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.
- package/hub/src/agents/agent-device-scope.js +18 -0
- package/hub/src/agents/agent-manager.js +3 -1
- package/hub/src/agents/agent-settings.js +3 -5
- package/hub/src/agents/codex-agent-runtime.js +177 -26
- package/hub/src/server.js +49 -12
- package/package.json +1 -1
- package/web/dist/assets/{index-CD98YUHv.js → index-BZn8XWvN.js} +1 -1
- package/web/dist/index.html +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
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
|
-
|
|
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({
|
|
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 (
|
|
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:
|
|
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
|
|
218
|
+
function connectedAgentDeviceIds() {
|
|
215
219
|
const devices = remoteHub.listDevices({ includeDataUrl: false });
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
.map(
|
|
219
|
-
.filter(Boolean)
|
|
220
|
-
|
|
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
|
|
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 =
|
|
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.
|
|
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 =
|
|
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)
|