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.
- package/hub/src/agents/agent-audit-store.js +24 -7
- package/hub/src/agents/agent-manager.js +2 -1
- package/hub/src/agents/agent-permissions.js +31 -6
- package/hub/src/agents/codex-agent-runtime.js +13 -2
- package/hub/src/server.js +46 -2
- package/package.json +2 -2
- package/web/dist/assets/{index-CJSXSfSl.css → index-ClhUQLYr.css} +1 -1
- package/web/dist/assets/index-DWh-qMz_.js +15 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-D8Z7w2jS.js +0 -15
|
@@ -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
|
|
32
|
-
|
|
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
|
-
|
|
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: '
|
|
48
|
-
serviceControl: '
|
|
49
|
-
applicationControl: '
|
|
47
|
+
processControl: 'allow',
|
|
48
|
+
serviceControl: 'allow',
|
|
49
|
+
applicationControl: 'allow',
|
|
50
50
|
fileRead: 'allow',
|
|
51
|
-
fileWrite: '
|
|
52
|
-
fileDelete: '
|
|
51
|
+
fileWrite: 'allow',
|
|
52
|
+
fileDelete: 'ask',
|
|
53
53
|
shell: 'ask',
|
|
54
|
-
script: '
|
|
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 >
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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",
|