livedesk 0.1.209 → 0.1.210

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.
@@ -1,9 +1,10 @@
1
- import { appendFile, mkdir, readFile } from 'node:fs/promises';
1
+ import { appendFile, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
2
  import crypto from 'node:crypto';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
 
6
6
  const MAX_AUDIT_RECORDS = 2000;
7
+ const MAX_AUDIT_FILE_RECORDS = 2250;
7
8
  const MAX_AUDIT_FIELD = 4000;
8
9
 
9
10
  function safeText(value, max = MAX_AUDIT_FIELD) {
@@ -23,13 +24,17 @@ function redact(value, depth = 0) {
23
24
  export function createAgentAuditStore({ dataDir = path.join(os.homedir(), '.livedesk') } = {}) {
24
25
  const filePath = path.join(dataDir, 'agent-audit.jsonl');
25
26
  let records = null;
27
+ let rewriteNeeded = false;
28
+ let fileRecordCount = 0;
26
29
  let writeQueue = Promise.resolve();
27
30
 
28
31
  async function load() {
29
32
  if (records) return records;
30
33
  try {
31
- const lines = (await readFile(filePath, 'utf8')).split(/\r?\n/).filter(Boolean).slice(-MAX_AUDIT_RECORDS);
32
- records = lines.map(line => JSON.parse(line)).filter(item => item && typeof item === 'object');
34
+ const allLines = (await readFile(filePath, 'utf8')).split(/\r?\n/).filter(Boolean);
35
+ fileRecordCount = allLines.length;
36
+ rewriteNeeded = allLines.length > MAX_AUDIT_FILE_RECORDS;
37
+ records = allLines.slice(-MAX_AUDIT_RECORDS).map(line => JSON.parse(line)).filter(item => item && typeof item === 'object');
33
38
  } catch {
34
39
  records = [];
35
40
  }
@@ -57,12 +62,24 @@ export function createAgentAuditStore({ dataDir = path.join(os.homedir(), '.live
57
62
  return {
58
63
  async record(event) {
59
64
  const record = normalize(event);
60
- const current = await load();
61
- current.push(record);
62
- if (current.length > MAX_AUDIT_RECORDS) current.splice(0, current.length - MAX_AUDIT_RECORDS);
63
65
  writeQueue = writeQueue.then(async () => {
66
+ const current = await load();
67
+ current.push(record);
68
+ fileRecordCount += 1;
69
+ if (current.length > MAX_AUDIT_RECORDS) {
70
+ current.splice(0, current.length - MAX_AUDIT_RECORDS);
71
+ }
72
+ if (fileRecordCount > MAX_AUDIT_FILE_RECORDS) rewriteNeeded = true;
64
73
  await mkdir(path.dirname(filePath), { recursive: true });
65
- await appendFile(filePath, `${JSON.stringify(record)}\n`, { encoding: 'utf8', mode: 0o600 });
74
+ if (rewriteNeeded) {
75
+ const tempPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
76
+ await writeFile(tempPath, `${current.map(item => JSON.stringify(item)).join('\n')}\n`, { encoding: 'utf8', mode: 0o600 });
77
+ await rename(tempPath, filePath);
78
+ fileRecordCount = current.length;
79
+ rewriteNeeded = false;
80
+ } else {
81
+ await appendFile(filePath, `${JSON.stringify(record)}\n`, { encoding: 'utf8', mode: 0o600 });
82
+ }
66
83
  });
67
84
  await writeQueue;
68
85
  return record;
@@ -169,6 +169,7 @@ export function createAgentManager({
169
169
  return runtime.start(input);
170
170
  },
171
171
  getRun(runId) { return runtime?.get(runId) || null; },
172
- cancelRun(runId) { return runtime?.cancel(runId) || null; }
172
+ cancelRun(runId) { return runtime?.cancel(runId) || null; },
173
+ cancelAllRuns() { return runtime?.cancelAll?.() || 0; }
173
174
  };
174
175
  }
@@ -44,14 +44,14 @@ const PRESET_CATEGORIES = Object.freeze({
44
44
  },
45
45
  'safe-auto': {
46
46
  read: 'allow',
47
- processControl: 'ask',
48
- serviceControl: 'ask',
49
- applicationControl: 'ask',
47
+ processControl: 'allow',
48
+ serviceControl: 'allow',
49
+ applicationControl: 'allow',
50
50
  fileRead: 'allow',
51
- fileWrite: 'ask',
52
- fileDelete: 'deny',
51
+ fileWrite: 'allow',
52
+ fileDelete: 'ask',
53
53
  shell: 'ask',
54
- script: 'ask',
54
+ script: 'allow',
55
55
  softwareInstall: 'ask',
56
56
  network: 'allow',
57
57
  systemPower: 'ask',
@@ -151,6 +151,27 @@ function targetIdsFromArgs(args, fallback = []) {
151
151
  return normalizeIds(args?.deviceIds?.length ? args.deviceIds : fallback);
152
152
  }
153
153
 
154
+ function isSafeAutoRelativePath(value) {
155
+ const text = String(value || '').trim();
156
+ if (!text || text.length > 600 || text.startsWith('/') || /^[A-Za-z]:[\\/]/.test(text) || text.startsWith('\\\\')) return false;
157
+ const segments = text.replaceAll('\\', '/').split('/').filter(Boolean);
158
+ return segments.length > 0 && !segments.includes('..');
159
+ }
160
+
161
+ function safeAutoConstraint(tool, args, mode) {
162
+ if (mode !== 'safe-auto') return '';
163
+ if (tool.category === 'fileWrite' && !isSafeAutoRelativePath(args?.path)) {
164
+ return 'Safe Auto file writes are limited to relative paths inside the LiveDesk files directory.';
165
+ }
166
+ if (tool.category === 'script') {
167
+ const scriptPath = String(args?.path || '').trim().toLowerCase();
168
+ if (!isSafeAutoRelativePath(scriptPath) || !scriptPath.replaceAll('\\', '/').startsWith('scripts/') || !/\.(ps1|psm1|sh|bash|py|js|mjs|cmd|bat)$/.test(scriptPath)) {
169
+ return 'Safe Auto scripts must be registered relative script files inside the LiveDesk files directory.';
170
+ }
171
+ }
172
+ return '';
173
+ }
174
+
154
175
  export function evaluateAgentToolPermission({ policy, toolName, arguments: args = {}, deviceIds = [] } = {}) {
155
176
  const tool = getAgentToolDefinition(toolName);
156
177
  if (!tool) return { decision: 'deny', category: 'unknown', risk: 'critical', reason: 'Unknown or unregistered Agent tool.' };
@@ -163,6 +184,10 @@ export function evaluateAgentToolPermission({ policy, toolName, arguments: args
163
184
  return { decision: 'deny', category: tool.category, risk: 'high', reason: 'The Agent permission policy has expired.' };
164
185
  }
165
186
  const category = TOOL_CATEGORY_ALIASES[tool.name.replace(/^livedesk\./, '')] || tool.category;
187
+ const constraintError = safeAutoConstraint(tool, args, policy?.mode);
188
+ if (constraintError) {
189
+ return { decision: 'deny', category, risk: tool.risk, reason: constraintError };
190
+ }
166
191
  const decision = normalizeDecision(policy?.categories?.[category], 'deny');
167
192
  return {
168
193
  decision,
@@ -492,7 +492,7 @@ export function createCodexAgentRuntime({
492
492
  throw new AgentProviderError('codex-turn-limit-reached', 'Codex exceeded the LiveDesk turn limit.', { status: 409 });
493
493
  }
494
494
  }
495
- if (run.toolCallCount > settings.codexMaxToolCalls) {
495
+ if (run.toolCallCount > run.permissionPolicy.maxToolCalls) {
496
496
  run.abortReason = 'tool-limit';
497
497
  run.abortController.abort();
498
498
  session.cancel();
@@ -589,5 +589,16 @@ export function createCodexAgentRuntime({
589
589
  return publicRun(run);
590
590
  }
591
591
 
592
- return { getStatus, testConnection, start, get, cancel, workspace, codexHome, getSecurityStatus: securityStatus };
592
+ function cancelAll() {
593
+ let cancelled = 0;
594
+ for (const run of runs.values()) {
595
+ if (!isTerminalStatus(run.status)) {
596
+ cancel(run.runId);
597
+ cancelled += 1;
598
+ }
599
+ }
600
+ return cancelled;
601
+ }
602
+
603
+ return { getStatus, testConnection, start, get, cancel, cancelAll, workspace, codexHome, getSecurityStatus: securityStatus };
593
604
  }
package/hub/src/server.js CHANGED
@@ -396,7 +396,7 @@ function createAgentMcpSession({ runId, signal, allowedDeviceIds: allowedDeviceI
396
396
  signal,
397
397
  allowedDeviceIds,
398
398
  toolCallCount: 0,
399
- maxToolCalls: normalizeAgentMcpToolCallLimit(maxToolCalls),
399
+ maxToolCalls: normalizeAgentMcpToolCallLimit(effectivePermissionPolicy.maxToolCalls ?? maxToolCalls),
400
400
  toolLimitReached: false,
401
401
  permissionPolicy: effectivePermissionPolicy,
402
402
  permissionPolicyHash: hashAgentPermissionPolicy(effectivePermissionPolicy),
@@ -429,6 +429,14 @@ function delayAgentMcp(ms) {
429
429
  return new Promise(resolve => setTimeout(resolve, ms));
430
430
  }
431
431
 
432
+ function normalizeAgentPlatform(value) {
433
+ const platform = String(value || '').trim().toLowerCase();
434
+ if (platform === 'win32' || platform === 'windows') return 'windows';
435
+ if (platform === 'darwin' || platform === 'macos' || platform === 'osx') return 'macos';
436
+ if (platform === 'linux') return 'linux';
437
+ return platform;
438
+ }
439
+
432
440
  function validateAgentMcpArguments(name, args) {
433
441
  const value = args && typeof args === 'object' && !Array.isArray(args) ? args : null;
434
442
  if (!value) return 'agent-tool-arguments-invalid';
@@ -510,6 +518,24 @@ async function dispatchAgentMcpTool(session, name, args = {}) {
510
518
  const targetIds = resolution.deviceIds;
511
519
  if (targetIds.length === 0) return { ok: false, error: resolution.error };
512
520
  if (name !== 'livedesk.list_devices' && !operation) return { ok: false, error: 'codex-tool-not-allowed' };
521
+ if (!tool.supportsBatch && targetIds.length !== 1) {
522
+ return { ok: false, error: 'agent-tool-single-target-required', toolName: name, targetCount: targetIds.length };
523
+ }
524
+ const selectedDevices = remoteHub.listDevices({ includeDataUrl: false }).filter(device => targetIds.includes(device.deviceId));
525
+ const unsupportedPlatform = selectedDevices.find(device => {
526
+ const platform = normalizeAgentPlatform(device.platform);
527
+ return platform && Array.isArray(tool.supportedPlatforms) && tool.supportedPlatforms.length > 0 && !tool.supportedPlatforms.includes(platform);
528
+ });
529
+ if (unsupportedPlatform) {
530
+ return { ok: false, error: 'agent-tool-platform-unsupported', toolName: name, deviceId: unsupportedPlatform.deviceId, platform: normalizeAgentPlatform(unsupportedPlatform.platform) };
531
+ }
532
+ const unsupportedCapability = selectedDevices.find(device => {
533
+ const advertised = device.capabilities?.agentTools;
534
+ return Array.isArray(advertised) && advertised.length > 0 && operation && !advertised.includes(operation);
535
+ });
536
+ if (unsupportedCapability) {
537
+ return { ok: false, error: 'agent-tool-capability-unavailable', toolName: name, deviceId: unsupportedCapability.deviceId };
538
+ }
513
539
  const permission = evaluateAgentToolPermission({
514
540
  policy: session.permissionPolicy,
515
541
  toolName: name,
@@ -734,7 +760,7 @@ function sendAgentError(res, error) {
734
760
  const status = Number.isInteger(error?.status) && error.status >= 400 && error.status <= 599
735
761
  ? error.status
736
762
  : code === 'agent-api-key-missing' || code === 'agent-api-key-invalid' || code === 'codex-auth-required' ? 401
737
- : code === 'agent-no-target-devices' || code === 'agent-target-outside-selection' || code === 'codex-tool-not-allowed' || code === 'agent-permission-denied' || code === 'agent-approval-required' || code === 'agent-approval-rejected' || code === 'agent-approval-expired' ? 400
763
+ : code === 'agent-no-target-devices' || code === 'agent-target-outside-selection' || code === 'codex-tool-not-allowed' || code === 'agent-permission-denied' || code === 'agent-approval-required' || code === 'agent-approval-rejected' || code === 'agent-approval-expired' || code === 'agent-tool-single-target-required' || code === 'agent-tool-platform-unsupported' || code === 'agent-tool-capability-unavailable' ? 400
738
764
  : code === 'agent-mcp-loopback-only' ? 403
739
765
  : code === 'codex-isolation-unavailable' || code === 'codex-environment-isolation-failed' || code === 'codex-unsupported-security-config' ? 503
740
766
  : code === 'codex-run-timeout' ? 504
@@ -1753,6 +1779,24 @@ app.post('/api/settings/agent/runs/:runId/cancel', async (req, res) => {
1753
1779
  res.json({ ok: true, ...agentRunResponse(req.params.runId) });
1754
1780
  });
1755
1781
 
1782
+ app.post('/api/settings/agent/runs/cancel-all', async (_req, res) => {
1783
+ noStore(res);
1784
+ let cancelledSessions = 0;
1785
+ const cancelledBatchIds = new Set();
1786
+ for (const session of agentMcpSessions.values()) {
1787
+ if (!session.cancelled) cancelledSessions += 1;
1788
+ for (const batchId of agentMcpRunBatches.get(session.runId) || []) cancelledBatchIds.add(batchId);
1789
+ session.cancel();
1790
+ }
1791
+ for (const batchIds of agentMcpRunBatches.values()) {
1792
+ for (const batchId of batchIds) cancelledBatchIds.add(batchId);
1793
+ }
1794
+ for (const batchId of cancelledBatchIds) remoteHub.cancelTaskBatch(batchId);
1795
+ const cancelledRuns = agentManager.cancelAllRuns();
1796
+ recordAgentAudit({ event: 'runs-cancelled-all', status: 'cancelled', details: { cancelledRuns, cancelledSessions, cancelledBatches: cancelledBatchIds.size } });
1797
+ res.json({ ok: true, cancelledRuns, cancelledSessions, cancelledBatches: cancelledBatchIds.size });
1798
+ });
1799
+
1756
1800
  app.post('/api/settings/agent/plan', async (req, res) => {
1757
1801
  noStore(res);
1758
1802
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.209",
3
+ "version": "0.1.210",
4
4
  "description": "LiveDesk Hub and client launcher",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@ffmpeg-installer/ffmpeg": "^1.1.0",
34
- "@livedesk/client": "0.1.121",
34
+ "@livedesk/client": "0.1.122",
35
35
  "@openai/codex-sdk": "0.144.5",
36
36
  "cors": "^2.8.5",
37
37
  "express": "^4.21.2",