livedesk 0.1.205 → 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 +29 -0
- package/hub/src/agents/agent-manager.js +20 -4
- package/hub/src/agents/agent-settings.js +3 -5
- package/hub/src/agents/codex-agent-runtime.js +242 -29
- package/hub/src/remote-hub.js +13 -1
- package/hub/src/server.js +87 -17
- package/package.json +1 -1
- package/web/dist/assets/{index-CD98YUHv.js → index-CHVI5IbK.js} +8 -8
- package/web/dist/index.html +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
}
|
|
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
|
|
|
@@ -136,7 +150,9 @@ export function createAgentManager({
|
|
|
136
150
|
const { current, active } = await currentProvider();
|
|
137
151
|
if (!current.enabled || !current.generateSummary) throw new AgentProviderError('agent-summary-disabled', 'Agent summaries are disabled in Settings.', { status: 409 });
|
|
138
152
|
if (current.provider === AGENT_PROVIDER_CODEX) {
|
|
139
|
-
|
|
153
|
+
const source = input && typeof input === 'object' ? input : {};
|
|
154
|
+
const results = Array.isArray(source.results) ? source.results : [];
|
|
155
|
+
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
156
|
}
|
|
141
157
|
const source = input && typeof input === 'object' ? input : {};
|
|
142
158
|
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,30 @@ 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
|
+
'WINDIR',
|
|
23
|
+
'LANG',
|
|
24
|
+
'LC_ALL',
|
|
25
|
+
'TMPDIR'
|
|
26
|
+
];
|
|
27
|
+
const LIVEDESK_MCP_TOOLS = [
|
|
28
|
+
'livedesk.list_devices',
|
|
29
|
+
'livedesk.get_system_health',
|
|
30
|
+
'livedesk.get_gpu_status',
|
|
31
|
+
'livedesk.get_disk_status',
|
|
32
|
+
'livedesk.list_processes',
|
|
33
|
+
'livedesk.get_service_status',
|
|
34
|
+
'livedesk.collect_diagnostics'
|
|
35
|
+
];
|
|
12
36
|
|
|
13
37
|
function safeText(value, max = 2400) {
|
|
14
38
|
return String(value || '').replace(/[\0\r]/g, ' ').trim().slice(0, max);
|
|
@@ -61,42 +85,114 @@ function codexThreadOptions(settings, workspace) {
|
|
|
61
85
|
};
|
|
62
86
|
}
|
|
63
87
|
|
|
64
|
-
function
|
|
88
|
+
function baseCodexConfig() {
|
|
65
89
|
return {
|
|
66
90
|
approval_policy: 'never',
|
|
67
91
|
sandbox_mode: 'read-only',
|
|
68
92
|
web_search: 'disabled',
|
|
69
93
|
features: {
|
|
70
94
|
shell_tool: false,
|
|
71
|
-
|
|
95
|
+
unified_exec: false,
|
|
96
|
+
shell_snapshot: false,
|
|
72
97
|
apps: false,
|
|
73
|
-
multi_agent: false
|
|
98
|
+
multi_agent: false,
|
|
99
|
+
web_search: false,
|
|
100
|
+
skill_mcp_dependency_install: false,
|
|
101
|
+
memories: false,
|
|
102
|
+
plugins: false,
|
|
103
|
+
remote_plugin: false
|
|
74
104
|
},
|
|
75
105
|
tools: {
|
|
76
106
|
web_search: false,
|
|
77
107
|
view_image: false
|
|
78
108
|
},
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
109
|
+
apps: {
|
|
110
|
+
_default: { enabled: false }
|
|
111
|
+
},
|
|
112
|
+
plugins: {},
|
|
113
|
+
mcp_servers: {}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function codexConfig({ mcpServerPath, mcpUrl, token, workspace }) {
|
|
118
|
+
const config = baseCodexConfig();
|
|
119
|
+
config.mcp_servers = {
|
|
120
|
+
livedesk: {
|
|
121
|
+
command: process.execPath,
|
|
122
|
+
args: [mcpServerPath],
|
|
123
|
+
cwd: workspace,
|
|
124
|
+
env: {
|
|
125
|
+
LIVEDESK_AGENT_MCP_URL: mcpUrl,
|
|
126
|
+
LIVEDESK_AGENT_MCP_TOKEN: token
|
|
127
|
+
},
|
|
128
|
+
enabled: true,
|
|
129
|
+
required: true,
|
|
130
|
+
enabled_tools: LIVEDESK_MCP_TOOLS,
|
|
131
|
+
startup_timeout_sec: 10,
|
|
132
|
+
tool_timeout_sec: 150
|
|
93
133
|
}
|
|
94
134
|
};
|
|
135
|
+
return config;
|
|
95
136
|
}
|
|
96
137
|
|
|
97
|
-
function
|
|
138
|
+
function safeCodexEnv(codexHome) {
|
|
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
|
+
};
|
|
146
|
+
for (const key of SAFE_ENV_KEYS) {
|
|
147
|
+
const value = process.env[key];
|
|
148
|
+
if (value !== undefined && value !== '') env[key] = value;
|
|
149
|
+
}
|
|
150
|
+
if (!env.Path && env.PATH) env.Path = env.PATH;
|
|
151
|
+
if (!env.PATH && env.Path) env.PATH = env.Path;
|
|
152
|
+
return env;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function ensureCodexHome(codexHome) {
|
|
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');
|
|
173
|
+
try {
|
|
174
|
+
await access(targetAuthPath);
|
|
175
|
+
return;
|
|
176
|
+
} catch {
|
|
177
|
+
// Keep the SDK isolated from the user's global config while reusing the
|
|
178
|
+
// CLI-owned auth file when the user is already signed in. The runtime
|
|
179
|
+
// never reads or copies the credential contents.
|
|
180
|
+
}
|
|
181
|
+
const sourceAuthPath = path.join(os.homedir(), '.codex', 'auth.json');
|
|
182
|
+
if (!(await access(sourceAuthPath).then(() => true, () => false))) return;
|
|
183
|
+
try {
|
|
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;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function runCli(cliPath, args, timeoutMs = 5000, env = process.env) {
|
|
98
193
|
return new Promise((resolve, reject) => {
|
|
99
194
|
const child = spawn(process.execPath, [cliPath, ...args], {
|
|
195
|
+
env,
|
|
100
196
|
windowsHide: true,
|
|
101
197
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
102
198
|
});
|
|
@@ -121,17 +217,82 @@ function runCli(cliPath, args, timeoutMs = 5000) {
|
|
|
121
217
|
});
|
|
122
218
|
}
|
|
123
219
|
|
|
220
|
+
function isTerminalStatus(status) {
|
|
221
|
+
return ['completed', 'failed', 'cancelled'].includes(status);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function runAgeMs(run) {
|
|
225
|
+
return Math.max(0, Date.now() - Date.parse(run.updatedAt || run.createdAt || 0));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function codexAbortError(code, message) {
|
|
229
|
+
return new AgentProviderError(code, message, { status: code === 'codex-run-timeout' ? 504 : 409, retryable: code === 'codex-run-timeout' });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function safeAgentDeviceIds(value) {
|
|
233
|
+
return [...new Set((Array.isArray(value) ? value : [])
|
|
234
|
+
.map(item => String(item || '').trim())
|
|
235
|
+
.filter(Boolean))].slice(0, 500);
|
|
236
|
+
}
|
|
237
|
+
|
|
124
238
|
export function createCodexAgentRuntime({
|
|
125
239
|
settingsStore,
|
|
126
240
|
dataDir = path.join(os.homedir(), '.livedesk'),
|
|
127
241
|
mcpServerPath,
|
|
128
242
|
createMcpSession,
|
|
129
|
-
destroyMcpSession
|
|
243
|
+
destroyMcpSession,
|
|
244
|
+
createCodexClient: injectedCodexClient,
|
|
245
|
+
getCodexStatus: injectedCodexStatus
|
|
130
246
|
} = {}) {
|
|
131
247
|
const runs = new Map();
|
|
132
248
|
let activeCount = 0;
|
|
133
249
|
let codexModulePromise;
|
|
134
250
|
const workspace = path.resolve(dataDir, 'agent-workspace');
|
|
251
|
+
const codexHome = path.resolve(dataDir, 'codex-home');
|
|
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
|
+
|
|
276
|
+
function scheduleRunCleanup(run) {
|
|
277
|
+
const timer = setTimeout(() => {
|
|
278
|
+
if (runs.get(run.runId) === run && isTerminalStatus(run.status) && runAgeMs(run) >= RUN_TTL_MS) {
|
|
279
|
+
runs.delete(run.runId);
|
|
280
|
+
}
|
|
281
|
+
}, RUN_TTL_MS + 1000);
|
|
282
|
+
timer.unref?.();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function pruneRuns() {
|
|
286
|
+
for (const [runId, run] of runs) {
|
|
287
|
+
if (isTerminalStatus(run.status) && runAgeMs(run) >= RUN_TTL_MS) runs.delete(runId);
|
|
288
|
+
}
|
|
289
|
+
if (runs.size < MAX_RUNS) return;
|
|
290
|
+
for (const [runId, run] of runs) {
|
|
291
|
+
if (!isTerminalStatus(run.status)) continue;
|
|
292
|
+
runs.delete(runId);
|
|
293
|
+
if (runs.size < MAX_RUNS) break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
135
296
|
|
|
136
297
|
async function loadCodex() {
|
|
137
298
|
if (!codexModulePromise) codexModulePromise = import('@openai/codex-sdk').catch(error => {
|
|
@@ -151,6 +312,7 @@ export function createCodexAgentRuntime({
|
|
|
151
312
|
}
|
|
152
313
|
|
|
153
314
|
async function getStatus() {
|
|
315
|
+
if (typeof injectedCodexStatus === 'function') return injectedCodexStatus();
|
|
154
316
|
const pathToCli = cliPath();
|
|
155
317
|
if (!pathToCli) {
|
|
156
318
|
return { installed: false, authenticated: 'unknown', status: 'not-installed', codexPath: '' };
|
|
@@ -158,26 +320,29 @@ export function createCodexAgentRuntime({
|
|
|
158
320
|
let authenticated = 'unknown';
|
|
159
321
|
let detail = '';
|
|
160
322
|
try {
|
|
161
|
-
|
|
323
|
+
await ensureCodexHome(codexHome);
|
|
324
|
+
const result = await runCli(pathToCli, ['login', 'status'], 5000, safeCodexEnv(codexHome));
|
|
162
325
|
detail = safeText(result.stdout || result.stderr, 300);
|
|
163
326
|
authenticated = /logged in|authenticated|chatgpt/i.test(`${result.stdout}\n${result.stderr}`)
|
|
164
327
|
? 'signed-in'
|
|
165
328
|
: 'not-signed-in';
|
|
166
329
|
} catch (error) {
|
|
330
|
+
if (error instanceof AgentProviderError) throw error;
|
|
167
331
|
detail = safeText(error?.message, 300);
|
|
168
332
|
}
|
|
169
333
|
return { installed: true, authenticated, status: authenticated === 'signed-in' ? 'ready' : 'auth-required', codexPath: pathToCli, detail };
|
|
170
334
|
}
|
|
171
335
|
|
|
172
336
|
async function testConnection() {
|
|
337
|
+
assertSecurityConfiguration();
|
|
173
338
|
const startedAt = Date.now();
|
|
174
339
|
const status = await getStatus();
|
|
175
340
|
if (!status.installed) throw new AgentProviderError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
|
|
176
341
|
if (status.authenticated !== 'signed-in') throw new AgentProviderError('codex-auth-required', 'Sign in with the Codex CLI first.', { status: 401 });
|
|
177
342
|
const settings = await settingsStore.get();
|
|
178
343
|
await mkdir(workspace, { recursive: true });
|
|
179
|
-
|
|
180
|
-
const codex =
|
|
344
|
+
await ensureCodexHome(codexHome);
|
|
345
|
+
const codex = await createCodexClient({ env: safeCodexEnv(codexHome), config: baseCodexConfig() });
|
|
181
346
|
const controller = new AbortController();
|
|
182
347
|
const timer = setTimeout(() => controller.abort(), Math.min(30000, settings.codexTaskTimeoutMs));
|
|
183
348
|
try {
|
|
@@ -238,19 +403,37 @@ export function createCodexAgentRuntime({
|
|
|
238
403
|
async function execute(run, input) {
|
|
239
404
|
activeCount += 1;
|
|
240
405
|
let session;
|
|
406
|
+
let timeoutTimer;
|
|
241
407
|
try {
|
|
408
|
+
assertSecurityConfiguration();
|
|
242
409
|
const settings = await settingsStore.get();
|
|
410
|
+
const timeoutMs = Math.max(1000, Number(settings.codexTaskTimeoutMs) || 600000);
|
|
411
|
+
timeoutTimer = setTimeout(() => {
|
|
412
|
+
run.abortReason = 'timeout';
|
|
413
|
+
run.abortController.abort();
|
|
414
|
+
try { session?.cancel(); } catch { /* best effort */ }
|
|
415
|
+
}, timeoutMs);
|
|
416
|
+
timeoutTimer.unref?.();
|
|
243
417
|
await mkdir(workspace, { recursive: true });
|
|
244
418
|
if (activeCount > settings.maxConcurrentRequests) {
|
|
245
419
|
throw new AgentProviderError('agent-concurrency-limit', 'The LiveDesk Agent concurrency limit is reached.', { status: 429, retryable: true });
|
|
246
420
|
}
|
|
421
|
+
if (run.abortController.signal.aborted) {
|
|
422
|
+
if (run.abortReason === 'timeout') throw codexAbortError('codex-run-timeout', 'The Codex task exceeded its timeout.');
|
|
423
|
+
throw Object.assign(new Error('The Codex task was cancelled.'), { name: 'AbortError' });
|
|
424
|
+
}
|
|
247
425
|
const status = await getStatus();
|
|
248
426
|
if (!status.installed) throw new AgentProviderError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
|
|
249
427
|
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 });
|
|
428
|
+
session = createMcpSession({ runId: run.runId, signal: run.abortController.signal, allowedDeviceIds: input.deviceIds });
|
|
251
429
|
run.mcpToken = session.token;
|
|
252
|
-
|
|
253
|
-
|
|
430
|
+
if (run.abortController.signal.aborted) {
|
|
431
|
+
session.cancel();
|
|
432
|
+
if (run.abortReason === 'timeout') throw codexAbortError('codex-run-timeout', 'The Codex task exceeded its timeout.');
|
|
433
|
+
throw Object.assign(new Error('The Codex task was cancelled.'), { name: 'AbortError' });
|
|
434
|
+
}
|
|
435
|
+
await ensureCodexHome(codexHome);
|
|
436
|
+
const codex = await createCodexClient({ env: safeCodexEnv(codexHome), config: codexConfig({ mcpServerPath, mcpUrl: session.url, token: session.token, workspace }) });
|
|
254
437
|
const threadOptions = codexThreadOptions(settings, workspace);
|
|
255
438
|
const thread = input.resumeThreadId && settings.codexResumeSessions
|
|
256
439
|
? codex.resumeThread(String(input.resumeThreadId), threadOptions)
|
|
@@ -260,7 +443,17 @@ export function createCodexAgentRuntime({
|
|
|
260
443
|
const streamed = await thread.runStreamed(promptFor({ instruction: run.instruction, deviceIds: input.deviceIds }), { signal: run.abortController.signal });
|
|
261
444
|
for await (const event of streamed.events) {
|
|
262
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
|
+
}
|
|
263
455
|
if (run.toolCallCount > settings.codexMaxToolCalls) {
|
|
456
|
+
run.abortReason = 'tool-limit';
|
|
264
457
|
run.abortController.abort();
|
|
265
458
|
session.cancel();
|
|
266
459
|
throw new AgentProviderError('codex-tool-limit-reached', 'Codex exceeded the LiveDesk tool-call limit.', { status: 409 });
|
|
@@ -272,7 +465,16 @@ export function createCodexAgentRuntime({
|
|
|
272
465
|
}
|
|
273
466
|
if (run.status === 'running') run.status = 'completed';
|
|
274
467
|
} catch (error) {
|
|
275
|
-
if (
|
|
468
|
+
if (run.abortReason === 'timeout') {
|
|
469
|
+
run.status = 'failed';
|
|
470
|
+
run.error = 'codex-run-timeout';
|
|
471
|
+
} else if (run.abortReason === 'tool-limit') {
|
|
472
|
+
run.status = 'failed';
|
|
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';
|
|
477
|
+
} else if (isAbortError(error) || run.abortController.signal.aborted) {
|
|
276
478
|
run.status = 'cancelled';
|
|
277
479
|
run.error = 'cancelled-by-user';
|
|
278
480
|
} else {
|
|
@@ -286,15 +488,22 @@ export function createCodexAgentRuntime({
|
|
|
286
488
|
try { session.cancel(); } catch { /* best effort */ }
|
|
287
489
|
destroyMcpSession(session.token);
|
|
288
490
|
}
|
|
491
|
+
clearTimeout(timeoutTimer);
|
|
289
492
|
run.updatedAt = new Date().toISOString();
|
|
493
|
+
if (isTerminalStatus(run.status)) scheduleRunCleanup(run);
|
|
290
494
|
}
|
|
291
495
|
}
|
|
292
496
|
|
|
293
497
|
async function start(input = {}) {
|
|
294
498
|
const instruction = safeText(input.instruction, 4000);
|
|
295
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 });
|
|
296
502
|
const settings = await settingsStore.get();
|
|
297
503
|
if (!settings.enabled) throw new AgentProviderError('agent-ai-disabled', 'Agent AI is disabled in Settings.', { status: 409 });
|
|
504
|
+
assertSecurityConfiguration();
|
|
505
|
+
pruneRuns();
|
|
506
|
+
if (runs.size >= MAX_RUNS) throw new AgentProviderError('agent-run-limit', 'The LiveDesk Agent run history is full.', { status: 429, retryable: true });
|
|
298
507
|
const run = {
|
|
299
508
|
runId: crypto.randomUUID(),
|
|
300
509
|
status: 'queued',
|
|
@@ -306,13 +515,15 @@ export function createCodexAgentRuntime({
|
|
|
306
515
|
createdAt: new Date().toISOString(),
|
|
307
516
|
updatedAt: new Date().toISOString(),
|
|
308
517
|
toolCallCount: 0,
|
|
518
|
+
turnCount: 0,
|
|
309
519
|
batchIds: new Set(),
|
|
310
520
|
events: [],
|
|
311
521
|
abortController: new AbortController(),
|
|
522
|
+
abortReason: '',
|
|
312
523
|
mcpToken: ''
|
|
313
524
|
};
|
|
314
525
|
runs.set(run.runId, run);
|
|
315
|
-
void execute(run, { deviceIds
|
|
526
|
+
void execute(run, { deviceIds, resumeThreadId: input.resumeThreadId });
|
|
316
527
|
return publicRun(run);
|
|
317
528
|
}
|
|
318
529
|
|
|
@@ -324,6 +535,8 @@ export function createCodexAgentRuntime({
|
|
|
324
535
|
function cancel(runId) {
|
|
325
536
|
const run = runs.get(String(runId || ''));
|
|
326
537
|
if (!run) return null;
|
|
538
|
+
if (isTerminalStatus(run.status)) return publicRun(run);
|
|
539
|
+
run.abortReason = 'user';
|
|
327
540
|
run.abortController.abort();
|
|
328
541
|
run.status = 'cancelled';
|
|
329
542
|
run.error = 'cancelled-by-user';
|
|
@@ -331,5 +544,5 @@ export function createCodexAgentRuntime({
|
|
|
331
544
|
return publicRun(run);
|
|
332
545
|
}
|
|
333
546
|
|
|
334
|
-
return { getStatus, testConnection, start, get, cancel, workspace };
|
|
547
|
+
return { getStatus, testConnection, start, get, cancel, workspace, codexHome, getSecurityStatus: securityStatus };
|
|
335
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' : '');
|