livedesk 0.1.207 → 0.1.208

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.
@@ -68,7 +68,9 @@ export function createAgentManager({
68
68
  restrictedEnvironment: false,
69
69
  livedeskMcpOnly: false,
70
70
  selectedDeviceScopeEnforced: false,
71
- failClosed: true
71
+ failClosed: true,
72
+ verified: false,
73
+ state: 'unavailable'
72
74
  };
73
75
  return {
74
76
  ...exposedSettings,
@@ -247,19 +247,35 @@ export function createCodexAgentRuntime({
247
247
  const runs = new Map();
248
248
  let activeCount = 0;
249
249
  let codexModulePromise;
250
+ let securityVerificationState = 'configured';
250
251
  const workspace = path.resolve(dataDir, 'agent-workspace');
251
252
  const codexHome = path.resolve(dataDir, 'codex-home');
252
253
 
253
254
  function securityStatus() {
255
+ const configured = codexHome !== path.resolve(os.homedir(), '.codex')
256
+ && typeof createMcpSession === 'function'
257
+ && Boolean(mcpServerPath);
254
258
  return {
255
259
  isolatedCodexHome: codexHome !== path.resolve(os.homedir(), '.codex'),
256
260
  restrictedEnvironment: true,
257
261
  livedeskMcpOnly: typeof createMcpSession === 'function' && Boolean(mcpServerPath),
258
262
  selectedDeviceScopeEnforced: typeof createMcpSession === 'function',
259
- failClosed: true
263
+ failClosed: true,
264
+ verified: configured && securityVerificationState === 'verified',
265
+ state: configured ? securityVerificationState : 'unavailable'
260
266
  };
261
267
  }
262
268
 
269
+ async function prepareCodexHome() {
270
+ try {
271
+ await ensureCodexHome(codexHome);
272
+ securityVerificationState = 'verified';
273
+ } catch (error) {
274
+ securityVerificationState = 'unavailable';
275
+ throw error;
276
+ }
277
+ }
278
+
263
279
  function assertSecurityConfiguration() {
264
280
  const status = securityStatus();
265
281
  if (!status.isolatedCodexHome || !status.restrictedEnvironment || !status.livedeskMcpOnly || !status.selectedDeviceScopeEnforced) {
@@ -320,7 +336,7 @@ export function createCodexAgentRuntime({
320
336
  let authenticated = 'unknown';
321
337
  let detail = '';
322
338
  try {
323
- await ensureCodexHome(codexHome);
339
+ await prepareCodexHome();
324
340
  const result = await runCli(pathToCli, ['login', 'status'], 5000, safeCodexEnv(codexHome));
325
341
  detail = safeText(result.stdout || result.stderr, 300);
326
342
  authenticated = /logged in|authenticated|chatgpt/i.test(`${result.stdout}\n${result.stderr}`)
@@ -341,7 +357,7 @@ export function createCodexAgentRuntime({
341
357
  if (status.authenticated !== 'signed-in') throw new AgentProviderError('codex-auth-required', 'Sign in with the Codex CLI first.', { status: 401 });
342
358
  const settings = await settingsStore.get();
343
359
  await mkdir(workspace, { recursive: true });
344
- await ensureCodexHome(codexHome);
360
+ await prepareCodexHome();
345
361
  const codex = await createCodexClient({ env: safeCodexEnv(codexHome), config: baseCodexConfig() });
346
362
  const controller = new AbortController();
347
363
  const timer = setTimeout(() => controller.abort(), Math.min(30000, settings.codexTaskTimeoutMs));
@@ -425,14 +441,14 @@ export function createCodexAgentRuntime({
425
441
  const status = await getStatus();
426
442
  if (!status.installed) throw new AgentProviderError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
427
443
  if (status.authenticated !== 'signed-in') throw new AgentProviderError('codex-auth-required', 'Sign in with the Codex CLI first.', { status: 401 });
428
- session = createMcpSession({ runId: run.runId, signal: run.abortController.signal, allowedDeviceIds: input.deviceIds });
444
+ session = createMcpSession({ runId: run.runId, signal: run.abortController.signal, allowedDeviceIds: input.deviceIds, maxToolCalls: settings.codexMaxToolCalls });
429
445
  run.mcpToken = session.token;
430
446
  if (run.abortController.signal.aborted) {
431
447
  session.cancel();
432
448
  if (run.abortReason === 'timeout') throw codexAbortError('codex-run-timeout', 'The Codex task exceeded its timeout.');
433
449
  throw Object.assign(new Error('The Codex task was cancelled.'), { name: 'AbortError' });
434
450
  }
435
- await ensureCodexHome(codexHome);
451
+ await prepareCodexHome();
436
452
  const codex = await createCodexClient({ env: safeCodexEnv(codexHome), config: codexConfig({ mcpServerPath, mcpUrl: session.url, token: session.token, workspace }) });
437
453
  const threadOptions = codexThreadOptions(settings, workspace);
438
454
  const thread = input.resumeThreadId && settings.codexResumeSessions
package/hub/src/server.js CHANGED
@@ -215,6 +215,8 @@ const agentMcpRunBatches = new Map();
215
215
  const agentMcpRunCleanupTimers = new Map();
216
216
  const AGENT_MCP_RUN_TTL_MS = 60 * 60 * 1000;
217
217
  const MAX_AGENT_MCP_RUN_RECORDS = 500;
218
+ const DEFAULT_AGENT_MCP_TOOL_CALL_LIMIT = 20;
219
+ const MAX_AGENT_MCP_TOOL_CALL_LIMIT = 100;
218
220
 
219
221
  function connectedAgentDeviceIds() {
220
222
  const devices = remoteHub.listDevices({ includeDataUrl: false });
@@ -247,7 +249,13 @@ function scheduleAgentMcpRunCleanup(runId) {
247
249
  agentMcpRunCleanupTimers.set(runId, timer);
248
250
  }
249
251
 
250
- function createAgentMcpSession({ runId, signal, allowedDeviceIds: allowedDeviceIdsInput = [] }) {
252
+ function normalizeAgentMcpToolCallLimit(value) {
253
+ const limit = Number(value);
254
+ if (!Number.isFinite(limit)) return DEFAULT_AGENT_MCP_TOOL_CALL_LIMIT;
255
+ return Math.min(MAX_AGENT_MCP_TOOL_CALL_LIMIT, Math.max(1, Math.floor(limit)));
256
+ }
257
+
258
+ function createAgentMcpSession({ runId, signal, allowedDeviceIds: allowedDeviceIdsInput = [], maxToolCalls }) {
251
259
  const token = crypto.randomBytes(32).toString('hex');
252
260
  const allowedDeviceIds = createAgentDeviceScope(allowedDeviceIdsInput);
253
261
  const initialResolution = resolveAgentTargetIds({ allowedDeviceIds, connectedDeviceIds: connectedAgentDeviceIds() });
@@ -260,6 +268,9 @@ function createAgentMcpSession({ runId, signal, allowedDeviceIds: allowedDeviceI
260
268
  runId: String(runId || ''),
261
269
  signal,
262
270
  allowedDeviceIds,
271
+ toolCallCount: 0,
272
+ maxToolCalls: normalizeAgentMcpToolCallLimit(maxToolCalls),
273
+ toolLimitReached: false,
263
274
  cancelled: false,
264
275
  cancel() {
265
276
  if (this.cancelled) return;
@@ -300,6 +311,13 @@ function validateAgentMcpArguments(name, args) {
300
311
 
301
312
  async function dispatchAgentMcpTool(session, name, args = {}) {
302
313
  if (session.cancelled || session.signal?.aborted) return { ok: false, error: 'cancelled-by-user' };
314
+ // This check runs synchronously before the first await, so concurrent Node
315
+ // requests cannot pass the same remaining budget and queue extra Client work.
316
+ if (session.toolCallCount >= session.maxToolCalls) {
317
+ session.toolLimitReached = true;
318
+ return { ok: false, error: 'codex-tool-limit-reached' };
319
+ }
320
+ session.toolCallCount += 1;
303
321
  const validationError = validateAgentMcpArguments(name, args);
304
322
  if (validationError) return { ok: false, error: validationError };
305
323
  if (name === 'livedesk.list_devices') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.207",
3
+ "version": "0.1.208",
4
4
  "description": "LiveDesk Hub and client launcher",
5
5
  "type": "module",
6
6
  "bin": {