livedesk 0.1.206 → 0.1.207
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 +11 -0
- package/hub/src/agents/agent-manager.js +17 -3
- package/hub/src/agents/codex-agent-runtime.js +86 -24
- package/hub/src/remote-hub.js +13 -1
- package/hub/src/server.js +43 -10
- package/package.json +1 -1
- package/web/dist/assets/{index-BZn8XWvN.js → index-CHVI5IbK.js} +8 -8
- package/web/dist/index.html +1 -1
|
@@ -16,3 +16,14 @@ export function selectAgentDeviceIds({ allowedDeviceIds, requestedDeviceIds, con
|
|
|
16
16
|
const candidates = requested.length > 0 ? requested : [...allowed];
|
|
17
17
|
return candidates.filter(deviceId => allowed.has(deviceId) && connected.has(deviceId));
|
|
18
18
|
}
|
|
19
|
+
|
|
20
|
+
export function resolveAgentTargetIds({ allowedDeviceIds, requestedDeviceIds, connectedDeviceIds }) {
|
|
21
|
+
const requested = normalizeIds(requestedDeviceIds);
|
|
22
|
+
const allowed = allowedDeviceIds instanceof Set ? allowedDeviceIds : new Set(normalizeIds(allowedDeviceIds));
|
|
23
|
+
const deviceIds = selectAgentDeviceIds({ allowedDeviceIds: allowed, requestedDeviceIds: requested, connectedDeviceIds });
|
|
24
|
+
if (deviceIds.length > 0) return { deviceIds, error: '' };
|
|
25
|
+
if (requested.length > 0 && requested.every(deviceId => !allowed.has(deviceId))) {
|
|
26
|
+
return { deviceIds: [], error: 'agent-target-outside-selection' };
|
|
27
|
+
}
|
|
28
|
+
return { deviceIds: [], error: 'agent-no-target-devices' };
|
|
29
|
+
}
|
|
@@ -37,13 +37,19 @@ export function createAgentManager({
|
|
|
37
37
|
async function getCodexStatus() {
|
|
38
38
|
if (!runtime) return { installed: false, authenticated: 'unknown', status: 'unavailable', codexPath: '' };
|
|
39
39
|
if (statusCache.value && statusCache.expiresAt > Date.now()) return statusCache.value;
|
|
40
|
-
|
|
40
|
+
let value;
|
|
41
|
+
try {
|
|
42
|
+
value = publicCodexStatus(await runtime.getStatus());
|
|
43
|
+
} catch (error) {
|
|
44
|
+
value = publicCodexStatus({ installed: true, authenticated: 'unknown', status: error?.code || 'security-unavailable', detail: error?.message || 'Codex security isolation is unavailable.' });
|
|
45
|
+
}
|
|
41
46
|
statusCache = { value, expiresAt: Date.now() + 10000 };
|
|
42
47
|
return value;
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
async function publicSettings() {
|
|
46
51
|
const current = await settings.get();
|
|
52
|
+
const { codexMaxTurns: _codexMaxTurns, ...exposedSettings } = current;
|
|
47
53
|
let hasApiKey = false;
|
|
48
54
|
let apiKeyPreview = '';
|
|
49
55
|
let secretStoreAvailable = true;
|
|
@@ -57,8 +63,15 @@ export function createAgentManager({
|
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
const codex = await getCodexStatus();
|
|
66
|
+
const securityStatus = runtime?.getSecurityStatus?.() || {
|
|
67
|
+
isolatedCodexHome: false,
|
|
68
|
+
restrictedEnvironment: false,
|
|
69
|
+
livedeskMcpOnly: false,
|
|
70
|
+
selectedDeviceScopeEnforced: false,
|
|
71
|
+
failClosed: true
|
|
72
|
+
};
|
|
60
73
|
return {
|
|
61
|
-
...
|
|
74
|
+
...exposedSettings,
|
|
62
75
|
hasApiKey,
|
|
63
76
|
apiKeyPreview,
|
|
64
77
|
secretStoreAvailable,
|
|
@@ -66,7 +79,8 @@ export function createAgentManager({
|
|
|
66
79
|
codexAuth: codex.authenticated,
|
|
67
80
|
codexStatus: codex.status,
|
|
68
81
|
codexPath: codex.codexPath,
|
|
69
|
-
agentWorkspacePath: current.codexWorkingDirectory
|
|
82
|
+
agentWorkspacePath: current.codexWorkingDirectory,
|
|
83
|
+
securityStatus
|
|
70
84
|
};
|
|
71
85
|
}
|
|
72
86
|
|
|
@@ -19,10 +19,6 @@ const SAFE_ENV_KEYS = [
|
|
|
19
19
|
'ComSpec',
|
|
20
20
|
'TEMP',
|
|
21
21
|
'TMP',
|
|
22
|
-
'USERPROFILE',
|
|
23
|
-
'HOME',
|
|
24
|
-
'APPDATA',
|
|
25
|
-
'LOCALAPPDATA',
|
|
26
22
|
'WINDIR',
|
|
27
23
|
'LANG',
|
|
28
24
|
'LC_ALL',
|
|
@@ -103,6 +99,7 @@ function baseCodexConfig() {
|
|
|
103
99
|
web_search: false,
|
|
104
100
|
skill_mcp_dependency_install: false,
|
|
105
101
|
memories: false,
|
|
102
|
+
plugins: false,
|
|
106
103
|
remote_plugin: false
|
|
107
104
|
},
|
|
108
105
|
tools: {
|
|
@@ -139,21 +136,40 @@ function codexConfig({ mcpServerPath, mcpUrl, token, workspace }) {
|
|
|
139
136
|
}
|
|
140
137
|
|
|
141
138
|
function safeCodexEnv(codexHome) {
|
|
142
|
-
const env = {
|
|
139
|
+
const env = {
|
|
140
|
+
CODEX_HOME: codexHome,
|
|
141
|
+
HOME: codexHome,
|
|
142
|
+
USERPROFILE: codexHome,
|
|
143
|
+
APPDATA: path.join(codexHome, 'AppData', 'Roaming'),
|
|
144
|
+
LOCALAPPDATA: path.join(codexHome, 'AppData', 'Local')
|
|
145
|
+
};
|
|
143
146
|
for (const key of SAFE_ENV_KEYS) {
|
|
144
147
|
const value = process.env[key];
|
|
145
148
|
if (value !== undefined && value !== '') env[key] = value;
|
|
146
149
|
}
|
|
147
|
-
if (!env.HOME) env.HOME = os.homedir();
|
|
148
|
-
if (!env.USERPROFILE && process.platform === 'win32') env.USERPROFILE = os.homedir();
|
|
149
150
|
if (!env.Path && env.PATH) env.Path = env.PATH;
|
|
150
151
|
if (!env.PATH && env.Path) env.PATH = env.Path;
|
|
151
152
|
return env;
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
async function ensureCodexHome(codexHome) {
|
|
155
|
-
|
|
156
|
-
const
|
|
156
|
+
const resolvedHome = path.resolve(codexHome);
|
|
157
|
+
const globalHome = path.resolve(os.homedir(), '.codex');
|
|
158
|
+
if (!resolvedHome || resolvedHome === globalHome) {
|
|
159
|
+
throw new AgentProviderError('codex-isolation-unavailable', 'Codex requires a dedicated LiveDesk home.', { status: 503 });
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
await mkdir(resolvedHome, { recursive: true, mode: 0o700 });
|
|
163
|
+
if (await access(path.join(resolvedHome, 'config.toml')).then(() => true, () => false)) {
|
|
164
|
+
throw new AgentProviderError('codex-environment-isolation-failed', 'The dedicated Codex home contains an unmanaged config.toml.', { status: 503 });
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (error instanceof AgentProviderError) throw error;
|
|
168
|
+
const wrapped = new AgentProviderError('codex-environment-isolation-failed', 'The dedicated Codex home could not be prepared.', { status: 503 });
|
|
169
|
+
wrapped.cause = error;
|
|
170
|
+
throw wrapped;
|
|
171
|
+
}
|
|
172
|
+
const targetAuthPath = path.join(resolvedHome, 'auth.json');
|
|
157
173
|
try {
|
|
158
174
|
await access(targetAuthPath);
|
|
159
175
|
return;
|
|
@@ -162,11 +178,14 @@ async function ensureCodexHome(codexHome) {
|
|
|
162
178
|
// CLI-owned auth file when the user is already signed in. The runtime
|
|
163
179
|
// never reads or copies the credential contents.
|
|
164
180
|
}
|
|
181
|
+
const sourceAuthPath = path.join(os.homedir(), '.codex', 'auth.json');
|
|
182
|
+
if (!(await access(sourceAuthPath).then(() => true, () => false))) return;
|
|
165
183
|
try {
|
|
166
|
-
await link(
|
|
167
|
-
} catch {
|
|
168
|
-
|
|
169
|
-
|
|
184
|
+
await link(sourceAuthPath, targetAuthPath);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
const wrapped = new AgentProviderError('codex-environment-isolation-failed', 'The Codex authentication bridge could not be prepared.', { status: 503 });
|
|
187
|
+
wrapped.cause = error;
|
|
188
|
+
throw wrapped;
|
|
170
189
|
}
|
|
171
190
|
}
|
|
172
191
|
|
|
@@ -207,7 +226,7 @@ function runAgeMs(run) {
|
|
|
207
226
|
}
|
|
208
227
|
|
|
209
228
|
function codexAbortError(code, message) {
|
|
210
|
-
return new AgentProviderError(code, message, { status: code === 'codex-
|
|
229
|
+
return new AgentProviderError(code, message, { status: code === 'codex-run-timeout' ? 504 : 409, retryable: code === 'codex-run-timeout' });
|
|
211
230
|
}
|
|
212
231
|
|
|
213
232
|
function safeAgentDeviceIds(value) {
|
|
@@ -221,7 +240,9 @@ export function createCodexAgentRuntime({
|
|
|
221
240
|
dataDir = path.join(os.homedir(), '.livedesk'),
|
|
222
241
|
mcpServerPath,
|
|
223
242
|
createMcpSession,
|
|
224
|
-
destroyMcpSession
|
|
243
|
+
destroyMcpSession,
|
|
244
|
+
createCodexClient: injectedCodexClient,
|
|
245
|
+
getCodexStatus: injectedCodexStatus
|
|
225
246
|
} = {}) {
|
|
226
247
|
const runs = new Map();
|
|
227
248
|
let activeCount = 0;
|
|
@@ -229,6 +250,29 @@ export function createCodexAgentRuntime({
|
|
|
229
250
|
const workspace = path.resolve(dataDir, 'agent-workspace');
|
|
230
251
|
const codexHome = path.resolve(dataDir, 'codex-home');
|
|
231
252
|
|
|
253
|
+
function securityStatus() {
|
|
254
|
+
return {
|
|
255
|
+
isolatedCodexHome: codexHome !== path.resolve(os.homedir(), '.codex'),
|
|
256
|
+
restrictedEnvironment: true,
|
|
257
|
+
livedeskMcpOnly: typeof createMcpSession === 'function' && Boolean(mcpServerPath),
|
|
258
|
+
selectedDeviceScopeEnforced: typeof createMcpSession === 'function',
|
|
259
|
+
failClosed: true
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function assertSecurityConfiguration() {
|
|
264
|
+
const status = securityStatus();
|
|
265
|
+
if (!status.isolatedCodexHome || !status.restrictedEnvironment || !status.livedeskMcpOnly || !status.selectedDeviceScopeEnforced) {
|
|
266
|
+
throw new AgentProviderError('codex-unsupported-security-config', 'Codex security isolation is not available.', { status: 503 });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function createCodexClient(options) {
|
|
271
|
+
if (typeof injectedCodexClient === 'function') return injectedCodexClient(options);
|
|
272
|
+
const { Codex } = await loadCodex();
|
|
273
|
+
return new Codex(options);
|
|
274
|
+
}
|
|
275
|
+
|
|
232
276
|
function scheduleRunCleanup(run) {
|
|
233
277
|
const timer = setTimeout(() => {
|
|
234
278
|
if (runs.get(run.runId) === run && isTerminalStatus(run.status) && runAgeMs(run) >= RUN_TTL_MS) {
|
|
@@ -268,6 +312,7 @@ export function createCodexAgentRuntime({
|
|
|
268
312
|
}
|
|
269
313
|
|
|
270
314
|
async function getStatus() {
|
|
315
|
+
if (typeof injectedCodexStatus === 'function') return injectedCodexStatus();
|
|
271
316
|
const pathToCli = cliPath();
|
|
272
317
|
if (!pathToCli) {
|
|
273
318
|
return { installed: false, authenticated: 'unknown', status: 'not-installed', codexPath: '' };
|
|
@@ -282,12 +327,14 @@ export function createCodexAgentRuntime({
|
|
|
282
327
|
? 'signed-in'
|
|
283
328
|
: 'not-signed-in';
|
|
284
329
|
} catch (error) {
|
|
330
|
+
if (error instanceof AgentProviderError) throw error;
|
|
285
331
|
detail = safeText(error?.message, 300);
|
|
286
332
|
}
|
|
287
333
|
return { installed: true, authenticated, status: authenticated === 'signed-in' ? 'ready' : 'auth-required', codexPath: pathToCli, detail };
|
|
288
334
|
}
|
|
289
335
|
|
|
290
336
|
async function testConnection() {
|
|
337
|
+
assertSecurityConfiguration();
|
|
291
338
|
const startedAt = Date.now();
|
|
292
339
|
const status = await getStatus();
|
|
293
340
|
if (!status.installed) throw new AgentProviderError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
|
|
@@ -295,8 +342,7 @@ export function createCodexAgentRuntime({
|
|
|
295
342
|
const settings = await settingsStore.get();
|
|
296
343
|
await mkdir(workspace, { recursive: true });
|
|
297
344
|
await ensureCodexHome(codexHome);
|
|
298
|
-
const
|
|
299
|
-
const codex = new Codex({ env: safeCodexEnv(codexHome), config: baseCodexConfig() });
|
|
345
|
+
const codex = await createCodexClient({ env: safeCodexEnv(codexHome), config: baseCodexConfig() });
|
|
300
346
|
const controller = new AbortController();
|
|
301
347
|
const timer = setTimeout(() => controller.abort(), Math.min(30000, settings.codexTaskTimeoutMs));
|
|
302
348
|
try {
|
|
@@ -359,6 +405,7 @@ export function createCodexAgentRuntime({
|
|
|
359
405
|
let session;
|
|
360
406
|
let timeoutTimer;
|
|
361
407
|
try {
|
|
408
|
+
assertSecurityConfiguration();
|
|
362
409
|
const settings = await settingsStore.get();
|
|
363
410
|
const timeoutMs = Math.max(1000, Number(settings.codexTaskTimeoutMs) || 600000);
|
|
364
411
|
timeoutTimer = setTimeout(() => {
|
|
@@ -372,7 +419,7 @@ export function createCodexAgentRuntime({
|
|
|
372
419
|
throw new AgentProviderError('agent-concurrency-limit', 'The LiveDesk Agent concurrency limit is reached.', { status: 429, retryable: true });
|
|
373
420
|
}
|
|
374
421
|
if (run.abortController.signal.aborted) {
|
|
375
|
-
if (run.abortReason === 'timeout') throw codexAbortError('codex-
|
|
422
|
+
if (run.abortReason === 'timeout') throw codexAbortError('codex-run-timeout', 'The Codex task exceeded its timeout.');
|
|
376
423
|
throw Object.assign(new Error('The Codex task was cancelled.'), { name: 'AbortError' });
|
|
377
424
|
}
|
|
378
425
|
const status = await getStatus();
|
|
@@ -382,12 +429,11 @@ export function createCodexAgentRuntime({
|
|
|
382
429
|
run.mcpToken = session.token;
|
|
383
430
|
if (run.abortController.signal.aborted) {
|
|
384
431
|
session.cancel();
|
|
385
|
-
if (run.abortReason === 'timeout') throw codexAbortError('codex-
|
|
432
|
+
if (run.abortReason === 'timeout') throw codexAbortError('codex-run-timeout', 'The Codex task exceeded its timeout.');
|
|
386
433
|
throw Object.assign(new Error('The Codex task was cancelled.'), { name: 'AbortError' });
|
|
387
434
|
}
|
|
388
435
|
await ensureCodexHome(codexHome);
|
|
389
|
-
const
|
|
390
|
-
const codex = new Codex({ env: safeCodexEnv(codexHome), config: codexConfig({ mcpServerPath, mcpUrl: session.url, token: session.token, workspace }) });
|
|
436
|
+
const codex = await createCodexClient({ env: safeCodexEnv(codexHome), config: codexConfig({ mcpServerPath, mcpUrl: session.url, token: session.token, workspace }) });
|
|
391
437
|
const threadOptions = codexThreadOptions(settings, workspace);
|
|
392
438
|
const thread = input.resumeThreadId && settings.codexResumeSessions
|
|
393
439
|
? codex.resumeThread(String(input.resumeThreadId), threadOptions)
|
|
@@ -397,6 +443,15 @@ export function createCodexAgentRuntime({
|
|
|
397
443
|
const streamed = await thread.runStreamed(promptFor({ instruction: run.instruction, deviceIds: input.deviceIds }), { signal: run.abortController.signal });
|
|
398
444
|
for await (const event of streamed.events) {
|
|
399
445
|
emit(run, event);
|
|
446
|
+
if (event.type === 'turn.started') {
|
|
447
|
+
run.turnCount += 1;
|
|
448
|
+
if (run.turnCount > settings.codexMaxTurns) {
|
|
449
|
+
run.abortReason = 'turn-limit';
|
|
450
|
+
run.abortController.abort();
|
|
451
|
+
session.cancel();
|
|
452
|
+
throw new AgentProviderError('codex-turn-limit-reached', 'Codex exceeded the LiveDesk turn limit.', { status: 409 });
|
|
453
|
+
}
|
|
454
|
+
}
|
|
400
455
|
if (run.toolCallCount > settings.codexMaxToolCalls) {
|
|
401
456
|
run.abortReason = 'tool-limit';
|
|
402
457
|
run.abortController.abort();
|
|
@@ -412,10 +467,13 @@ export function createCodexAgentRuntime({
|
|
|
412
467
|
} catch (error) {
|
|
413
468
|
if (run.abortReason === 'timeout') {
|
|
414
469
|
run.status = 'failed';
|
|
415
|
-
run.error = 'codex-
|
|
470
|
+
run.error = 'codex-run-timeout';
|
|
416
471
|
} else if (run.abortReason === 'tool-limit') {
|
|
417
472
|
run.status = 'failed';
|
|
418
473
|
run.error = 'codex-tool-limit-reached';
|
|
474
|
+
} else if (run.abortReason === 'turn-limit') {
|
|
475
|
+
run.status = 'failed';
|
|
476
|
+
run.error = 'codex-turn-limit-reached';
|
|
419
477
|
} else if (isAbortError(error) || run.abortController.signal.aborted) {
|
|
420
478
|
run.status = 'cancelled';
|
|
421
479
|
run.error = 'cancelled-by-user';
|
|
@@ -439,8 +497,11 @@ export function createCodexAgentRuntime({
|
|
|
439
497
|
async function start(input = {}) {
|
|
440
498
|
const instruction = safeText(input.instruction, 4000);
|
|
441
499
|
if (!instruction) throw new AgentProviderError('agent-invalid-instruction', 'Instruction is required.', { status: 400 });
|
|
500
|
+
const deviceIds = safeAgentDeviceIds(input.deviceIds);
|
|
501
|
+
if (deviceIds.length === 0) throw new AgentProviderError('agent-no-target-devices', 'Select at least one connected device.', { status: 400 });
|
|
442
502
|
const settings = await settingsStore.get();
|
|
443
503
|
if (!settings.enabled) throw new AgentProviderError('agent-ai-disabled', 'Agent AI is disabled in Settings.', { status: 409 });
|
|
504
|
+
assertSecurityConfiguration();
|
|
444
505
|
pruneRuns();
|
|
445
506
|
if (runs.size >= MAX_RUNS) throw new AgentProviderError('agent-run-limit', 'The LiveDesk Agent run history is full.', { status: 429, retryable: true });
|
|
446
507
|
const run = {
|
|
@@ -454,6 +515,7 @@ export function createCodexAgentRuntime({
|
|
|
454
515
|
createdAt: new Date().toISOString(),
|
|
455
516
|
updatedAt: new Date().toISOString(),
|
|
456
517
|
toolCallCount: 0,
|
|
518
|
+
turnCount: 0,
|
|
457
519
|
batchIds: new Set(),
|
|
458
520
|
events: [],
|
|
459
521
|
abortController: new AbortController(),
|
|
@@ -461,7 +523,7 @@ export function createCodexAgentRuntime({
|
|
|
461
523
|
mcpToken: ''
|
|
462
524
|
};
|
|
463
525
|
runs.set(run.runId, run);
|
|
464
|
-
void execute(run, { deviceIds
|
|
526
|
+
void execute(run, { deviceIds, resumeThreadId: input.resumeThreadId });
|
|
465
527
|
return publicRun(run);
|
|
466
528
|
}
|
|
467
529
|
|
|
@@ -482,5 +544,5 @@ export function createCodexAgentRuntime({
|
|
|
482
544
|
return publicRun(run);
|
|
483
545
|
}
|
|
484
546
|
|
|
485
|
-
return { getStatus, testConnection, start, get, cancel, workspace, codexHome };
|
|
547
|
+
return { getStatus, testConnection, start, get, cancel, workspace, codexHome, getSecurityStatus: securityStatus };
|
|
486
548
|
}
|
package/hub/src/remote-hub.js
CHANGED
|
@@ -1832,6 +1832,11 @@ export function createRemoteHub(options = {}) {
|
|
|
1832
1832
|
if (!batch) {
|
|
1833
1833
|
return { ok: false, error: 'task-not-found' };
|
|
1834
1834
|
}
|
|
1835
|
+
if (!['running', 'queued'].includes(batch.status)) {
|
|
1836
|
+
return { ok: true, ...serializeTaskBatch(batch) };
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
batch.cancelRequested = true;
|
|
1835
1840
|
|
|
1836
1841
|
for (const item of batch.results) {
|
|
1837
1842
|
if (!['targeted', 'queued', 'running'].includes(item.status)) {
|
|
@@ -2821,6 +2826,7 @@ export function createRemoteHub(options = {}) {
|
|
|
2821
2826
|
completed: 0,
|
|
2822
2827
|
failed: targets.length > 0 ? 0 : 1,
|
|
2823
2828
|
timedOut: 0,
|
|
2829
|
+
cancelRequested: false,
|
|
2824
2830
|
results: targets.map(deviceId => ({
|
|
2825
2831
|
deviceId,
|
|
2826
2832
|
deviceName: safeString(devices.get(deviceId)?.deviceName || devices.get(deviceId)?.hostname || deviceId, 160),
|
|
@@ -2890,7 +2896,9 @@ export function createRemoteHub(options = {}) {
|
|
|
2890
2896
|
batch.pending = results.filter(item => ['targeted', 'queued', 'running'].includes(item.status)).length;
|
|
2891
2897
|
batch.updatedAt = new Date().toISOString();
|
|
2892
2898
|
|
|
2893
|
-
if (batch.
|
|
2899
|
+
if (batch.cancelRequested && batch.pending === 0) {
|
|
2900
|
+
batch.status = 'cancelled';
|
|
2901
|
+
} else if (batch.total === 0) {
|
|
2894
2902
|
batch.status = 'failed';
|
|
2895
2903
|
} else if (batch.completed + batch.failed + batch.cancelled >= batch.total) {
|
|
2896
2904
|
batch.status = batch.failed > 0 ? 'completed-with-failures' : 'completed';
|
|
@@ -2919,6 +2927,10 @@ export function createRemoteHub(options = {}) {
|
|
|
2919
2927
|
return null;
|
|
2920
2928
|
}
|
|
2921
2929
|
|
|
2930
|
+
if (batch.cancelRequested && item.status === 'cancelled') {
|
|
2931
|
+
return batch;
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2922
2934
|
item.ok = true;
|
|
2923
2935
|
item.status = safeString(task.status, 40) || 'queued';
|
|
2924
2936
|
item.stage = safeString(task.stage, 160) || (item.status === 'running' ? 'Working' : '');
|
package/hub/src/server.js
CHANGED
|
@@ -16,7 +16,8 @@ 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,
|
|
19
|
+
import { createAgentDeviceScope, resolveAgentTargetIds } from './agents/agent-device-scope.js';
|
|
20
|
+
import { AgentProviderError } from './agents/provider-errors.js';
|
|
20
21
|
|
|
21
22
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
23
|
const webDistCandidates = [
|
|
@@ -248,7 +249,11 @@ function scheduleAgentMcpRunCleanup(runId) {
|
|
|
248
249
|
|
|
249
250
|
function createAgentMcpSession({ runId, signal, allowedDeviceIds: allowedDeviceIdsInput = [] }) {
|
|
250
251
|
const token = crypto.randomBytes(32).toString('hex');
|
|
251
|
-
const allowedDeviceIds = createAgentDeviceScope(allowedDeviceIdsInput
|
|
252
|
+
const allowedDeviceIds = createAgentDeviceScope(allowedDeviceIdsInput);
|
|
253
|
+
const initialResolution = resolveAgentTargetIds({ allowedDeviceIds, connectedDeviceIds: connectedAgentDeviceIds() });
|
|
254
|
+
if (initialResolution.deviceIds.length === 0) {
|
|
255
|
+
throw new AgentProviderError(initialResolution.error || 'agent-no-target-devices', 'No selected connected device is available.', { status: 400 });
|
|
256
|
+
}
|
|
252
257
|
pruneAgentMcpRunBatches();
|
|
253
258
|
const session = {
|
|
254
259
|
token,
|
|
@@ -278,16 +283,34 @@ function delayAgentMcp(ms) {
|
|
|
278
283
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
279
284
|
}
|
|
280
285
|
|
|
286
|
+
function validateAgentMcpArguments(name, args) {
|
|
287
|
+
const value = args && typeof args === 'object' && !Array.isArray(args) ? args : null;
|
|
288
|
+
if (!value) return 'agent-tool-arguments-invalid';
|
|
289
|
+
if (value.deviceIds !== undefined) {
|
|
290
|
+
if (!Array.isArray(value.deviceIds) || value.deviceIds.length > 500 || value.deviceIds.some(item => typeof item !== 'string' || !item.trim())) {
|
|
291
|
+
return 'agent-tool-arguments-invalid';
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (name === 'livedesk.list_processes' && value.processName !== undefined
|
|
295
|
+
&& (typeof value.processName !== 'string' || value.processName.length > 120)) return 'agent-tool-arguments-invalid';
|
|
296
|
+
if (name === 'livedesk.get_service_status' && value.serviceName !== undefined
|
|
297
|
+
&& (typeof value.serviceName !== 'string' || value.serviceName.length > 120)) return 'agent-tool-arguments-invalid';
|
|
298
|
+
return '';
|
|
299
|
+
}
|
|
300
|
+
|
|
281
301
|
async function dispatchAgentMcpTool(session, name, args = {}) {
|
|
282
302
|
if (session.cancelled || session.signal?.aborted) return { ok: false, error: 'cancelled-by-user' };
|
|
303
|
+
const validationError = validateAgentMcpArguments(name, args);
|
|
304
|
+
if (validationError) return { ok: false, error: validationError };
|
|
283
305
|
if (name === 'livedesk.list_devices') {
|
|
284
|
-
const
|
|
306
|
+
const resolution = resolveAgentTargetIds({
|
|
285
307
|
allowedDeviceIds: session.allowedDeviceIds,
|
|
286
308
|
requestedDeviceIds: args.deviceIds,
|
|
287
309
|
connectedDeviceIds: connectedAgentDeviceIds()
|
|
288
310
|
});
|
|
311
|
+
if (resolution.deviceIds.length === 0) return { ok: false, error: resolution.error };
|
|
289
312
|
const devices = remoteHub.listDevices({ includeDataUrl: false })
|
|
290
|
-
.filter(device =>
|
|
313
|
+
.filter(device => resolution.deviceIds.includes(device.deviceId))
|
|
291
314
|
.slice(0, 500)
|
|
292
315
|
.map(device => ({ deviceId: device.deviceId, deviceName: device.deviceName, hostname: device.hostname, connected: device.connected === true, platform: device.platform, capabilities: device.capabilities }));
|
|
293
316
|
return { ok: true, devices };
|
|
@@ -301,13 +324,14 @@ async function dispatchAgentMcpTool(session, name, args = {}) {
|
|
|
301
324
|
'livedesk.collect_diagnostics': 'diagnostics.collect'
|
|
302
325
|
};
|
|
303
326
|
const operation = operationByTool[name];
|
|
304
|
-
if (!operation) return { ok: false, error: '
|
|
305
|
-
const
|
|
327
|
+
if (!operation) return { ok: false, error: 'codex-tool-not-allowed' };
|
|
328
|
+
const resolution = resolveAgentTargetIds({
|
|
306
329
|
allowedDeviceIds: session.allowedDeviceIds,
|
|
307
330
|
requestedDeviceIds: args.deviceIds,
|
|
308
331
|
connectedDeviceIds: connectedAgentDeviceIds()
|
|
309
332
|
});
|
|
310
|
-
|
|
333
|
+
const targetIds = resolution.deviceIds;
|
|
334
|
+
if (targetIds.length === 0) return { ok: false, error: resolution.error };
|
|
311
335
|
const targetQuery = operation === 'process.list'
|
|
312
336
|
? String(args.processName || '').replace(/[\0\r\n]/g, ' ').trim().slice(0, 120)
|
|
313
337
|
: operation === 'service.status'
|
|
@@ -475,7 +499,12 @@ function sendAgentError(res, error) {
|
|
|
475
499
|
const code = String(error?.code || 'agent-request-failed').replace(/[^a-z0-9-]/gi, '-').toLowerCase().slice(0, 80);
|
|
476
500
|
const status = Number.isInteger(error?.status) && error.status >= 400 && error.status <= 599
|
|
477
501
|
? error.status
|
|
478
|
-
: code === 'agent-api-key-missing' || code === 'agent-api-key-invalid' || code === 'codex-auth-required' ? 401
|
|
502
|
+
: code === 'agent-api-key-missing' || code === 'agent-api-key-invalid' || code === 'codex-auth-required' ? 401
|
|
503
|
+
: code === 'agent-no-target-devices' || code === 'agent-target-outside-selection' || code === 'codex-tool-not-allowed' ? 400
|
|
504
|
+
: code === 'agent-mcp-loopback-only' ? 403
|
|
505
|
+
: code === 'codex-isolation-unavailable' || code === 'codex-environment-isolation-failed' || code === 'codex-unsupported-security-config' ? 503
|
|
506
|
+
: code === 'codex-run-timeout' ? 504
|
|
507
|
+
: 502;
|
|
479
508
|
res.status(status).json({ ok: false, error: code });
|
|
480
509
|
}
|
|
481
510
|
|
|
@@ -1351,9 +1380,13 @@ app.post('/api/settings/agent/test', async (_req, res) => {
|
|
|
1351
1380
|
|
|
1352
1381
|
app.post('/api/internal/agent-mcp/tool', async (req, res) => {
|
|
1353
1382
|
noStore(res);
|
|
1383
|
+
if (!isLoopbackAddress(req.socket?.remoteAddress)) {
|
|
1384
|
+
res.status(403).json({ ok: false, error: 'agent-mcp-loopback-only' });
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1354
1387
|
const authorization = String(req.headers.authorization || '');
|
|
1355
1388
|
const token = authorization.startsWith('Bearer ') ? authorization.slice(7).trim() : '';
|
|
1356
|
-
const session = agentMcpSessions.get(token);
|
|
1389
|
+
const session = /^[a-f0-9]{64}$/i.test(token) ? agentMcpSessions.get(token) : null;
|
|
1357
1390
|
if (!session || session.cancelled || session.signal?.aborted) {
|
|
1358
1391
|
res.status(401).json({ ok: false, error: 'agent-mcp-session-invalid' });
|
|
1359
1392
|
return;
|
|
@@ -1365,7 +1398,7 @@ app.post('/api/internal/agent-mcp/tool', async (req, res) => {
|
|
|
1365
1398
|
try {
|
|
1366
1399
|
const name = String(req.body?.name || '').slice(0, 120);
|
|
1367
1400
|
const result = await dispatchAgentMcpTool(session, name, req.body?.arguments || {});
|
|
1368
|
-
res.json({ ok: result.ok !== false, result });
|
|
1401
|
+
res.status(result.ok === false ? 400 : 200).json({ ok: result.ok !== false, error: result.ok === false ? result.error : undefined, result });
|
|
1369
1402
|
} catch {
|
|
1370
1403
|
res.status(502).json({ ok: false, error: 'agent-mcp-tool-failed' });
|
|
1371
1404
|
}
|