@livedesk/hub 0.1.18 → 0.1.20
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/package.json +1 -1
- package/src/agents/agent-manager.js +7 -52
- package/src/agents/agent-runtime-error.js +9 -0
- package/src/agents/agent-settings.js +2 -35
- package/src/agents/codex-agent-runtime.js +37 -40
- package/src/remote-hub.js +14 -0
- package/src/server.js +15 -59
- package/src/agents/provider-errors.js +0 -12
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { AgentSettingsStore
|
|
4
|
-
import {
|
|
3
|
+
import { AgentSettingsStore } from './agent-settings.js';
|
|
4
|
+
import { AgentRuntimeError } from './agent-runtime-error.js';
|
|
5
5
|
|
|
6
6
|
function publicCodexStatus(status = {}) {
|
|
7
7
|
return {
|
|
@@ -42,66 +42,22 @@ export function createAgentManager({
|
|
|
42
42
|
|
|
43
43
|
async function publicSettings() {
|
|
44
44
|
const current = await settings.get();
|
|
45
|
-
const { codexMaxTurns: _codexMaxTurns, ...exposedSettings } = current;
|
|
46
45
|
const codex = await getCodexStatus();
|
|
47
|
-
const securityStatus = runtime?.getSecurityStatus?.() || {
|
|
48
|
-
isolatedCodexHome: false,
|
|
49
|
-
restrictedEnvironment: false,
|
|
50
|
-
livedeskMcpOnly: false,
|
|
51
|
-
selectedDeviceScopeEnforced: false,
|
|
52
|
-
failClosed: true,
|
|
53
|
-
verified: false,
|
|
54
|
-
state: 'unavailable'
|
|
55
|
-
};
|
|
56
46
|
return {
|
|
57
|
-
|
|
58
|
-
provider: AGENT_PROVIDER_CODEX,
|
|
47
|
+
enabled: current.enabled === true,
|
|
59
48
|
codexInstallation: codex.installed ? 'installed' : 'not-installed',
|
|
60
49
|
codexAuth: codex.authenticated,
|
|
61
|
-
codexStatus: codex.status
|
|
62
|
-
codexPath: codex.codexPath,
|
|
63
|
-
agentWorkspacePath: current.codexWorkingDirectory,
|
|
64
|
-
securityStatus
|
|
50
|
+
codexStatus: codex.status
|
|
65
51
|
};
|
|
66
52
|
}
|
|
67
53
|
|
|
68
54
|
return {
|
|
69
55
|
getSettings: publicSettings,
|
|
70
|
-
async updateSettings(patch = {}) {
|
|
71
|
-
const nextPatch = { ...patch };
|
|
72
|
-
const requestedProvider = String(nextPatch.provider || AGENT_PROVIDER_CODEX).trim().toLowerCase();
|
|
73
|
-
const suppliedLegacyKey = typeof nextPatch.apiKey === 'string' && nextPatch.apiKey.trim().length > 0;
|
|
74
|
-
delete nextPatch.provider;
|
|
75
|
-
delete nextPatch.apiKey;
|
|
76
|
-
if (requestedProvider !== AGENT_PROVIDER_CODEX || suppliedLegacyKey) {
|
|
77
|
-
throw new AgentProviderError(
|
|
78
|
-
'agent-provider-retired',
|
|
79
|
-
'LiveDesk uses the Hub-hosted Codex Agent and does not configure alternate model providers.',
|
|
80
|
-
{ status: 400 }
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
await settings.update({ ...nextPatch, provider: AGENT_PROVIDER_CODEX });
|
|
84
|
-
statusCache = { expiresAt: 0, value: null };
|
|
85
|
-
return publicSettings();
|
|
86
|
-
},
|
|
87
|
-
async resetSettings() {
|
|
88
|
-
await settings.reset();
|
|
89
|
-
statusCache = { expiresAt: 0, value: null };
|
|
90
|
-
return publicSettings();
|
|
91
|
-
},
|
|
92
|
-
// Kept as a harmless compatibility endpoint for an older cached web UI.
|
|
93
|
-
// No credential is read, written, or returned by the current Hub.
|
|
94
|
-
async deleteApiKey() {
|
|
95
|
-
return publicSettings();
|
|
96
|
-
},
|
|
97
56
|
async testConnection() {
|
|
98
|
-
if (!runtime) throw new
|
|
57
|
+
if (!runtime) throw new AgentRuntimeError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
|
|
99
58
|
statusCache = { expiresAt: 0, value: null };
|
|
100
59
|
return runtime.testConnection();
|
|
101
60
|
},
|
|
102
|
-
async createPlan() {
|
|
103
|
-
throw new AgentProviderError('agent-codex-run-required', 'Codex Agent requests use the LiveDesk tool loop.', { status: 409 });
|
|
104
|
-
},
|
|
105
61
|
async createSummary(input) {
|
|
106
62
|
const source = input && typeof input === 'object' ? input : {};
|
|
107
63
|
const results = Array.isArray(source.results) ? source.results : [];
|
|
@@ -110,9 +66,8 @@ export function createAgentManager({
|
|
|
110
66
|
};
|
|
111
67
|
},
|
|
112
68
|
async startRun(input) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
throw new AgentProviderError('agent-codex-not-active', 'Codex SDK is not the active Agent runtime.', { status: 409 });
|
|
69
|
+
if (!runtime) {
|
|
70
|
+
throw new AgentRuntimeError('agent-codex-not-active', 'Codex SDK is not available for LiveDesk Agent commands.', { status: 409 });
|
|
116
71
|
}
|
|
117
72
|
return runtime.start(input);
|
|
118
73
|
},
|
|
@@ -2,23 +2,13 @@ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
|
|
5
|
-
export const AGENT_PROVIDER_CODEX = 'codex';
|
|
6
|
-
export const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_CODEX;
|
|
7
|
-
export const DEFAULT_AGENT_WORKSPACE = path.join(os.homedir(), '.livedesk', 'agent-workspace');
|
|
8
5
|
export const DEFAULT_AGENT_SETTINGS = Object.freeze({
|
|
9
|
-
settingsVersion:
|
|
6
|
+
settingsVersion: 5,
|
|
10
7
|
enabled: true,
|
|
11
|
-
provider: DEFAULT_AGENT_PROVIDER,
|
|
12
|
-
codexSandboxMode: 'read-only',
|
|
13
|
-
codexApprovalPolicy: 'never',
|
|
14
|
-
codexWorkingDirectory: DEFAULT_AGENT_WORKSPACE,
|
|
15
8
|
codexTaskTimeoutMs: 600000,
|
|
16
9
|
codexMaxTurns: 12,
|
|
17
10
|
codexMaxToolCalls: 20,
|
|
18
11
|
codexResumeSessions: true,
|
|
19
|
-
codexShowDetailedEvents: true,
|
|
20
|
-
codexNetworkAccessEnabled: false,
|
|
21
|
-
codexWebSearchMode: 'disabled',
|
|
22
12
|
maxConcurrentRequests: 2
|
|
23
13
|
});
|
|
24
14
|
|
|
@@ -33,32 +23,15 @@ function numberValue(value, min, max, fallback, integer = false) {
|
|
|
33
23
|
return integer ? Math.round(clamped) : clamped;
|
|
34
24
|
}
|
|
35
25
|
|
|
36
|
-
function normalizeWorkingDirectory() {
|
|
37
|
-
// The agent runtime owns this directory. Do not allow a browser/API caller
|
|
38
|
-
// to move Codex into a user project or an arbitrary filesystem root.
|
|
39
|
-
return DEFAULT_AGENT_WORKSPACE;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
26
|
export function normalizeAgentSettings(value = {}) {
|
|
43
27
|
const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
44
28
|
return {
|
|
45
|
-
settingsVersion:
|
|
29
|
+
settingsVersion: 5,
|
|
46
30
|
enabled: booleanValue(source.enabled, DEFAULT_AGENT_SETTINGS.enabled),
|
|
47
|
-
// LiveDesk has one Agent runtime. Old provider values are deliberately
|
|
48
|
-
// normalized to Codex so an upgrade cannot reactivate a retired provider.
|
|
49
|
-
provider: AGENT_PROVIDER_CODEX,
|
|
50
|
-
// These are safety settings, not user-controlled execution toggles. They
|
|
51
|
-
// are accepted for compatibility but always normalized to the safe floor.
|
|
52
|
-
codexSandboxMode: 'read-only',
|
|
53
|
-
codexApprovalPolicy: 'never',
|
|
54
|
-
codexWorkingDirectory: normalizeWorkingDirectory(),
|
|
55
31
|
codexTaskTimeoutMs: numberValue(source.codexTaskTimeoutMs, 30000, 1800000, DEFAULT_AGENT_SETTINGS.codexTaskTimeoutMs, true),
|
|
56
32
|
codexMaxTurns: numberValue(source.codexMaxTurns, 1, 40, DEFAULT_AGENT_SETTINGS.codexMaxTurns, true),
|
|
57
33
|
codexMaxToolCalls: numberValue(source.codexMaxToolCalls, 1, 100, DEFAULT_AGENT_SETTINGS.codexMaxToolCalls, true),
|
|
58
34
|
codexResumeSessions: booleanValue(source.codexResumeSessions, DEFAULT_AGENT_SETTINGS.codexResumeSessions),
|
|
59
|
-
codexShowDetailedEvents: booleanValue(source.codexShowDetailedEvents, DEFAULT_AGENT_SETTINGS.codexShowDetailedEvents),
|
|
60
|
-
codexNetworkAccessEnabled: false,
|
|
61
|
-
codexWebSearchMode: 'disabled',
|
|
62
35
|
maxConcurrentRequests: numberValue(source.maxConcurrentRequests, 1, 8, DEFAULT_AGENT_SETTINGS.maxConcurrentRequests, true)
|
|
63
36
|
};
|
|
64
37
|
}
|
|
@@ -95,10 +68,4 @@ export class AgentSettingsStore {
|
|
|
95
68
|
return { ...next };
|
|
96
69
|
}
|
|
97
70
|
|
|
98
|
-
async reset() {
|
|
99
|
-
const next = normalizeAgentSettings(DEFAULT_AGENT_SETTINGS);
|
|
100
|
-
await writeJsonAtomic(this.filePath, next);
|
|
101
|
-
this.settings = next;
|
|
102
|
-
return { ...next };
|
|
103
|
-
}
|
|
104
71
|
}
|
|
@@ -3,10 +3,9 @@ import { access, link, mkdir, stat, unlink } 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';
|
|
6
|
-
import { createRequire } from 'node:module';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { AGENT_TOOL_NAMES } from './agent-tool-registry.js';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import { AgentRuntimeError } from './agent-runtime-error.js';
|
|
8
|
+
import { AGENT_TOOL_NAMES } from './agent-tool-registry.js';
|
|
10
9
|
import { createAgentPermissionPolicy, hashAgentPermissionPolicy } from './agent-permissions.js';
|
|
11
10
|
|
|
12
11
|
const require = createRequire(import.meta.url);
|
|
@@ -58,12 +57,12 @@ function isAbortError(error) {
|
|
|
58
57
|
|
|
59
58
|
function normalizeCodexError(error) {
|
|
60
59
|
const message = safeText(error?.message, 800) || 'Codex run failed.';
|
|
61
|
-
if (error instanceof
|
|
62
|
-
if (/out of credits|usage limit|rate limit|too many requests/i.test(message)) return new
|
|
63
|
-
if (/refresh[_ -]?token[_ -]?(?:already[_ -]?)?used|token refresh|refresh credential/i.test(message)) return new
|
|
64
|
-
if (/not authenticated|not logged in|authentication|unauthorized|login required/i.test(message)) return new
|
|
65
|
-
if (/user cancelled MCP tool call/i.test(message)) return new
|
|
66
|
-
return new
|
|
60
|
+
if (error instanceof AgentRuntimeError && error.code !== 'codex-run-failed') return error;
|
|
61
|
+
if (/out of credits|usage limit|rate limit|too many requests/i.test(message)) return new AgentRuntimeError('codex-usage-limit-reached', 'The Codex workspace has no remaining usage.', { status: 402, retryable: true });
|
|
62
|
+
if (/refresh[_ -]?token[_ -]?(?:already[_ -]?)?used|token refresh|refresh credential/i.test(message)) return new AgentRuntimeError('codex-auth-stale', 'The saved Codex sign-in is stale. LiveDesk refreshed its Codex authentication bridge and will retry.', { status: 401, retryable: true });
|
|
63
|
+
if (/not authenticated|not logged in|authentication|unauthorized|login required/i.test(message)) return new AgentRuntimeError('codex-auth-required', 'Sign in with the Codex CLI first.', { status: 401 });
|
|
64
|
+
if (/user cancelled MCP tool call/i.test(message)) return new AgentRuntimeError('codex-mcp-tool-rejected', 'Codex rejected the LiveDesk MCP tool before it reached the Hub.', { status: 502 });
|
|
65
|
+
return new AgentRuntimeError('codex-run-failed', message, { status: 502, retryable: true });
|
|
67
66
|
}
|
|
68
67
|
|
|
69
68
|
function codexThreadOptions(workspace) {
|
|
@@ -165,16 +164,16 @@ async function ensureCodexHome(codexHome, globalCodexHome, { preferGlobalAuth =
|
|
|
165
164
|
const resolvedHome = path.resolve(codexHome);
|
|
166
165
|
const globalHome = path.resolve(globalCodexHome);
|
|
167
166
|
if (!resolvedHome || resolvedHome === globalHome) {
|
|
168
|
-
throw new
|
|
167
|
+
throw new AgentRuntimeError('codex-isolation-unavailable', 'Codex requires a dedicated LiveDesk home.', { status: 503 });
|
|
169
168
|
}
|
|
170
169
|
try {
|
|
171
170
|
await mkdir(resolvedHome, { recursive: true, mode: 0o700 });
|
|
172
171
|
if (await access(path.join(resolvedHome, 'config.toml')).then(() => true, () => false)) {
|
|
173
|
-
throw new
|
|
172
|
+
throw new AgentRuntimeError('codex-environment-isolation-failed', 'The dedicated Codex home contains an unmanaged config.toml.', { status: 503 });
|
|
174
173
|
}
|
|
175
174
|
} catch (error) {
|
|
176
|
-
if (error instanceof
|
|
177
|
-
const wrapped = new
|
|
175
|
+
if (error instanceof AgentRuntimeError) throw error;
|
|
176
|
+
const wrapped = new AgentRuntimeError('codex-environment-isolation-failed', 'The dedicated Codex home could not be prepared.', { status: 503 });
|
|
178
177
|
wrapped.cause = error;
|
|
179
178
|
throw wrapped;
|
|
180
179
|
}
|
|
@@ -201,7 +200,7 @@ async function ensureCodexHome(codexHome, globalCodexHome, { preferGlobalAuth =
|
|
|
201
200
|
await replaceAuthHardLink(sourceAuthPath, targetAuthPath);
|
|
202
201
|
}
|
|
203
202
|
} catch (error) {
|
|
204
|
-
const wrapped = new
|
|
203
|
+
const wrapped = new AgentRuntimeError('codex-environment-isolation-failed', 'The Codex authentication bridge could not be prepared.', { status: 503 });
|
|
205
204
|
wrapped.cause = error;
|
|
206
205
|
throw wrapped;
|
|
207
206
|
}
|
|
@@ -244,7 +243,7 @@ function runAgeMs(run) {
|
|
|
244
243
|
}
|
|
245
244
|
|
|
246
245
|
function codexAbortError(code, message) {
|
|
247
|
-
return new
|
|
246
|
+
return new AgentRuntimeError(code, message, { status: code === 'codex-run-timeout' ? 504 : 409, retryable: code === 'codex-run-timeout' });
|
|
248
247
|
}
|
|
249
248
|
|
|
250
249
|
function safeAgentDeviceIds(value) {
|
|
@@ -332,7 +331,7 @@ export function createCodexAgentRuntime({
|
|
|
332
331
|
function assertSecurityConfiguration() {
|
|
333
332
|
const status = securityStatus();
|
|
334
333
|
if (!status.isolatedCodexHome || !status.restrictedEnvironment || !status.livedeskMcpOnly || !status.selectedDeviceScopeEnforced) {
|
|
335
|
-
throw new
|
|
334
|
+
throw new AgentRuntimeError('codex-unsupported-security-config', 'Codex security isolation is not available.', { status: 503 });
|
|
336
335
|
}
|
|
337
336
|
}
|
|
338
337
|
|
|
@@ -365,7 +364,7 @@ export function createCodexAgentRuntime({
|
|
|
365
364
|
|
|
366
365
|
async function loadCodex() {
|
|
367
366
|
if (!codexModulePromise) codexModulePromise = import('@openai/codex-sdk').catch(error => {
|
|
368
|
-
const wrapped = new
|
|
367
|
+
const wrapped = new AgentRuntimeError('codex-sdk-not-installed', 'Codex SDK is not installed in this LiveDesk package.', { status: 503 });
|
|
369
368
|
wrapped.cause = error;
|
|
370
369
|
throw wrapped;
|
|
371
370
|
});
|
|
@@ -396,7 +395,7 @@ export function createCodexAgentRuntime({
|
|
|
396
395
|
? 'signed-in'
|
|
397
396
|
: 'not-signed-in';
|
|
398
397
|
} catch (error) {
|
|
399
|
-
if (error instanceof
|
|
398
|
+
if (error instanceof AgentRuntimeError) throw error;
|
|
400
399
|
detail = safeText(error?.message, 300);
|
|
401
400
|
}
|
|
402
401
|
return { installed: true, authenticated, status: authenticated === 'signed-in' ? 'ready' : 'auth-required', codexPath: pathToCli, detail };
|
|
@@ -406,8 +405,8 @@ export function createCodexAgentRuntime({
|
|
|
406
405
|
assertSecurityConfiguration();
|
|
407
406
|
const startedAt = Date.now();
|
|
408
407
|
const status = await getStatus();
|
|
409
|
-
if (!status.installed) throw new
|
|
410
|
-
if (status.authenticated !== 'signed-in') throw new
|
|
408
|
+
if (!status.installed) throw new AgentRuntimeError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
|
|
409
|
+
if (status.authenticated !== 'signed-in') throw new AgentRuntimeError('codex-auth-required', 'Sign in with the Codex CLI first.', { status: 401 });
|
|
411
410
|
const settings = await settingsStore.get();
|
|
412
411
|
await mkdir(workspace, { recursive: true });
|
|
413
412
|
await prepareCodexHome();
|
|
@@ -417,7 +416,7 @@ export function createCodexAgentRuntime({
|
|
|
417
416
|
try {
|
|
418
417
|
const thread = codex.startThread(codexThreadOptions(workspace));
|
|
419
418
|
const turn = await thread.run('Reply with the single word READY. Do not use tools.', { signal: controller.signal });
|
|
420
|
-
if (!String(turn.finalResponse || '').trim()) throw new
|
|
419
|
+
if (!String(turn.finalResponse || '').trim()) throw new AgentRuntimeError('codex-empty-response', 'Codex returned an empty response.', { status: 502 });
|
|
421
420
|
return { ok: true, latencyMs: Date.now() - startedAt, threadId: thread.id || '' };
|
|
422
421
|
} catch (error) {
|
|
423
422
|
throw normalizeCodexError(error);
|
|
@@ -427,11 +426,10 @@ export function createCodexAgentRuntime({
|
|
|
427
426
|
}
|
|
428
427
|
|
|
429
428
|
function publicRun(run) {
|
|
430
|
-
return {
|
|
431
|
-
runId: run.runId,
|
|
432
|
-
status: run.status,
|
|
433
|
-
|
|
434
|
-
instruction: run.instruction,
|
|
429
|
+
return {
|
|
430
|
+
runId: run.runId,
|
|
431
|
+
status: run.status,
|
|
432
|
+
instruction: run.instruction,
|
|
435
433
|
threadId: run.threadId,
|
|
436
434
|
finalResponse: run.finalResponse,
|
|
437
435
|
error: run.error,
|
|
@@ -494,15 +492,15 @@ export function createCodexAgentRuntime({
|
|
|
494
492
|
timeoutTimer.unref?.();
|
|
495
493
|
await mkdir(workspace, { recursive: true });
|
|
496
494
|
if (activeCount > settings.maxConcurrentRequests) {
|
|
497
|
-
throw new
|
|
495
|
+
throw new AgentRuntimeError('agent-concurrency-limit', 'The LiveDesk Agent concurrency limit is reached.', { status: 429, retryable: true });
|
|
498
496
|
}
|
|
499
497
|
if (run.abortController.signal.aborted) {
|
|
500
498
|
if (run.abortReason === 'timeout') throw codexAbortError('codex-run-timeout', 'The Codex task exceeded its timeout.');
|
|
501
499
|
throw Object.assign(new Error('The Codex task was cancelled.'), { name: 'AbortError' });
|
|
502
500
|
}
|
|
503
501
|
const status = await getStatus();
|
|
504
|
-
if (!status.installed) throw new
|
|
505
|
-
if (status.authenticated !== 'signed-in') throw new
|
|
502
|
+
if (!status.installed) throw new AgentRuntimeError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
|
|
503
|
+
if (status.authenticated !== 'signed-in') throw new AgentRuntimeError('codex-auth-required', 'Sign in with the Codex CLI first.', { status: 401 });
|
|
506
504
|
reportProgress(run, 'preparing-tools', 'Codex is ready. Preparing the LiveDesk tools for this Client.');
|
|
507
505
|
session = createMcpSession({
|
|
508
506
|
runId: run.runId,
|
|
@@ -554,14 +552,14 @@ export function createCodexAgentRuntime({
|
|
|
554
552
|
run.abortReason = 'turn-limit';
|
|
555
553
|
run.abortController.abort();
|
|
556
554
|
session.cancel();
|
|
557
|
-
throw new
|
|
555
|
+
throw new AgentRuntimeError('codex-turn-limit-reached', 'Codex exceeded the LiveDesk turn limit.', { status: 409 });
|
|
558
556
|
}
|
|
559
557
|
}
|
|
560
558
|
if (run.toolCallCount > run.permissionPolicy.maxToolCalls) {
|
|
561
559
|
run.abortReason = 'tool-limit';
|
|
562
560
|
run.abortController.abort();
|
|
563
561
|
session.cancel();
|
|
564
|
-
throw new
|
|
562
|
+
throw new AgentRuntimeError('codex-tool-limit-reached', 'Codex exceeded the LiveDesk tool-call limit.', { status: 409 });
|
|
565
563
|
}
|
|
566
564
|
if (event.type === 'item.completed' && event.item?.type === 'mcp_tool_call' && event.item?.status === 'failed') {
|
|
567
565
|
throw new Error(safeText(event.item?.error?.message, 800) || `LiveDesk tool ${safeText(event.item?.tool, 120) || 'call'} failed.`);
|
|
@@ -643,21 +641,20 @@ export function createCodexAgentRuntime({
|
|
|
643
641
|
|
|
644
642
|
async function start(input = {}) {
|
|
645
643
|
const instruction = safeText(input.instruction, 4000);
|
|
646
|
-
if (!instruction) throw new
|
|
644
|
+
if (!instruction) throw new AgentRuntimeError('agent-invalid-instruction', 'Instruction is required.', { status: 400 });
|
|
647
645
|
const deviceIds = safeAgentDeviceIds(input.deviceIds);
|
|
648
|
-
if (deviceIds.length === 0) throw new
|
|
646
|
+
if (deviceIds.length === 0) throw new AgentRuntimeError('agent-no-target-devices', 'Select at least one connected device.', { status: 400 });
|
|
649
647
|
const settings = await settingsStore.get();
|
|
650
|
-
if (!settings.enabled) throw new
|
|
648
|
+
if (!settings.enabled) throw new AgentRuntimeError('agent-disabled', 'Codex Agent is disabled in Settings.', { status: 409 });
|
|
651
649
|
assertSecurityConfiguration();
|
|
652
650
|
pruneRuns();
|
|
653
|
-
if (runs.size >= MAX_RUNS) throw new
|
|
651
|
+
if (runs.size >= MAX_RUNS) throw new AgentRuntimeError('agent-run-limit', 'The LiveDesk Agent run history is full.', { status: 429, retryable: true });
|
|
654
652
|
const permissionPolicy = freezeAgentPermissionPolicy(input.permissionPolicy && typeof input.permissionPolicy === 'object'
|
|
655
653
|
? input.permissionPolicy
|
|
656
654
|
: createAgentPermissionPolicy({ mode: input.permissionMode || 'safe-auto', deviceIds, maxToolCalls: settings.codexMaxToolCalls }));
|
|
657
|
-
const run = {
|
|
658
|
-
runId: crypto.randomUUID(),
|
|
659
|
-
status: 'queued',
|
|
660
|
-
provider: AGENT_PROVIDER_CODEX,
|
|
655
|
+
const run = {
|
|
656
|
+
runId: crypto.randomUUID(),
|
|
657
|
+
status: 'queued',
|
|
661
658
|
instruction,
|
|
662
659
|
deviceIds,
|
|
663
660
|
threadId: '',
|
package/src/remote-hub.js
CHANGED
|
@@ -3898,14 +3898,21 @@ export function createRemoteHub(options = {}) {
|
|
|
3898
3898
|
: null,
|
|
3899
3899
|
nativeEncoderHardwareQueryStatus: safeString(message.nativeEncoderHardwareQueryStatus, 80),
|
|
3900
3900
|
nativeCaptureFps: Number.isFinite(Number(message.nativeCaptureFps)) ? Number(message.nativeCaptureFps) : 0,
|
|
3901
|
+
nativeCompressionFps: Number.isFinite(Number(message.nativeCompressionFps)) ? Number(message.nativeCompressionFps) : 0,
|
|
3901
3902
|
nativeEncodedFps: Number.isFinite(Number(message.nativeEncodedFps)) ? Number(message.nativeEncodedFps) : 0,
|
|
3902
3903
|
nativeCaptureSampleCount: Number.isFinite(Number(message.nativeCaptureSampleCount)) ? Number(message.nativeCaptureSampleCount) : 0,
|
|
3903
3904
|
nativeEncodableFrameCount: Number.isFinite(Number(message.nativeEncodableFrameCount)) ? Number(message.nativeEncodableFrameCount) : 0,
|
|
3904
3905
|
nativeSkippedFrameCount: Number.isFinite(Number(message.nativeSkippedFrameCount)) ? Number(message.nativeSkippedFrameCount) : 0,
|
|
3906
|
+
nativeCompressionOutputCount: Number.isFinite(Number(message.nativeCompressionOutputCount)) ? Number(message.nativeCompressionOutputCount) : 0,
|
|
3905
3907
|
nativeEncodedFrameCount: Number.isFinite(Number(message.nativeEncodedFrameCount)) ? Number(message.nativeEncodedFrameCount) : 0,
|
|
3906
3908
|
nativeEncodeFramesInFlight: Number.isFinite(Number(message.nativeEncodeFramesInFlight)) ? Number(message.nativeEncodeFramesInFlight) : 0,
|
|
3907
3909
|
nativeBackpressureSkippedFrameCount: Number.isFinite(Number(message.nativeBackpressureSkippedFrameCount)) ? Number(message.nativeBackpressureSkippedFrameCount) : 0,
|
|
3908
3910
|
nativeIdleSkippedFrameCount: Number.isFinite(Number(message.nativeIdleSkippedFrameCount)) ? Number(message.nativeIdleSkippedFrameCount) : 0,
|
|
3911
|
+
nativeOutputQueueDepth: Number.isFinite(Number(message.nativeOutputQueueDepth)) ? Number(message.nativeOutputQueueDepth) : 0,
|
|
3912
|
+
nativeOutputQueueBytes: Number.isFinite(Number(message.nativeOutputQueueBytes)) ? Number(message.nativeOutputQueueBytes) : 0,
|
|
3913
|
+
nativeOutputQueueHighWatermark: Number.isFinite(Number(message.nativeOutputQueueHighWatermark)) ? Number(message.nativeOutputQueueHighWatermark) : 0,
|
|
3914
|
+
nativeOutputQueueDroppedCount: Number.isFinite(Number(message.nativeOutputQueueDroppedCount)) ? Number(message.nativeOutputQueueDroppedCount) : 0,
|
|
3915
|
+
nativeOutputQueueAwaitingKeyFrame: message.nativeOutputQueueAwaitingKeyFrame === true,
|
|
3909
3916
|
nativeCaptureTelemetryAt: safeString(message.nativeCaptureTelemetryAt, 80),
|
|
3910
3917
|
droppedByAgent: Number.isFinite(Number(message.droppedByAgent)) ? Number(message.droppedByAgent) : 0,
|
|
3911
3918
|
hardwareEncoder: safeString(message.hardwareEncoder, 80),
|
|
@@ -4280,14 +4287,21 @@ export function createRemoteHub(options = {}) {
|
|
|
4280
4287
|
: null,
|
|
4281
4288
|
nativeEncoderHardwareQueryStatus: safeString(message.nativeEncoderHardwareQueryStatus, 80),
|
|
4282
4289
|
nativeCaptureFps: Number.isFinite(Number(message.nativeCaptureFps)) ? Number(message.nativeCaptureFps) : 0,
|
|
4290
|
+
nativeCompressionFps: Number.isFinite(Number(message.nativeCompressionFps)) ? Number(message.nativeCompressionFps) : 0,
|
|
4283
4291
|
nativeEncodedFps: Number.isFinite(Number(message.nativeEncodedFps)) ? Number(message.nativeEncodedFps) : 0,
|
|
4284
4292
|
nativeCaptureSampleCount: Number.isFinite(Number(message.nativeCaptureSampleCount)) ? Number(message.nativeCaptureSampleCount) : 0,
|
|
4285
4293
|
nativeEncodableFrameCount: Number.isFinite(Number(message.nativeEncodableFrameCount)) ? Number(message.nativeEncodableFrameCount) : 0,
|
|
4286
4294
|
nativeSkippedFrameCount: Number.isFinite(Number(message.nativeSkippedFrameCount)) ? Number(message.nativeSkippedFrameCount) : 0,
|
|
4295
|
+
nativeCompressionOutputCount: Number.isFinite(Number(message.nativeCompressionOutputCount)) ? Number(message.nativeCompressionOutputCount) : 0,
|
|
4287
4296
|
nativeEncodedFrameCount: Number.isFinite(Number(message.nativeEncodedFrameCount)) ? Number(message.nativeEncodedFrameCount) : 0,
|
|
4288
4297
|
nativeEncodeFramesInFlight: Number.isFinite(Number(message.nativeEncodeFramesInFlight)) ? Number(message.nativeEncodeFramesInFlight) : 0,
|
|
4289
4298
|
nativeBackpressureSkippedFrameCount: Number.isFinite(Number(message.nativeBackpressureSkippedFrameCount)) ? Number(message.nativeBackpressureSkippedFrameCount) : 0,
|
|
4290
4299
|
nativeIdleSkippedFrameCount: Number.isFinite(Number(message.nativeIdleSkippedFrameCount)) ? Number(message.nativeIdleSkippedFrameCount) : 0,
|
|
4300
|
+
nativeOutputQueueDepth: Number.isFinite(Number(message.nativeOutputQueueDepth)) ? Number(message.nativeOutputQueueDepth) : 0,
|
|
4301
|
+
nativeOutputQueueBytes: Number.isFinite(Number(message.nativeOutputQueueBytes)) ? Number(message.nativeOutputQueueBytes) : 0,
|
|
4302
|
+
nativeOutputQueueHighWatermark: Number.isFinite(Number(message.nativeOutputQueueHighWatermark)) ? Number(message.nativeOutputQueueHighWatermark) : 0,
|
|
4303
|
+
nativeOutputQueueDroppedCount: Number.isFinite(Number(message.nativeOutputQueueDroppedCount)) ? Number(message.nativeOutputQueueDroppedCount) : 0,
|
|
4304
|
+
nativeOutputQueueAwaitingKeyFrame: message.nativeOutputQueueAwaitingKeyFrame === true,
|
|
4291
4305
|
nativeCaptureTelemetryAt: safeString(message.nativeCaptureTelemetryAt, 80),
|
|
4292
4306
|
droppedByAgent: Number.isFinite(Number(message.droppedByAgent)) ? Number(message.droppedByAgent) : 0,
|
|
4293
4307
|
hubIngressDropped: Number.isFinite(Number(message.hubIngressDropped)) ? Number(message.hubIngressDropped) : 0,
|
package/src/server.js
CHANGED
|
@@ -19,7 +19,7 @@ import { createAgentManager } from './agents/agent-manager.js';
|
|
|
19
19
|
import { AgentSettingsStore } from './agents/agent-settings.js';
|
|
20
20
|
import { createCodexAgentRuntime } from './agents/codex-agent-runtime.js';
|
|
21
21
|
import { createAgentDeviceScope, resolveAgentTargetIds } from './agents/agent-device-scope.js';
|
|
22
|
-
import {
|
|
22
|
+
import { AgentRuntimeError } from './agents/agent-runtime-error.js';
|
|
23
23
|
import { AGENT_PERMISSION_MODES, createAgentPermissionPolicy, evaluateAgentToolPermission, hashAgentPermissionPolicy } from './agents/agent-permissions.js';
|
|
24
24
|
import { createAgentPermissionStore } from './agents/agent-permission-store.js';
|
|
25
25
|
import { createAgentAuditStore } from './agents/agent-audit-store.js';
|
|
@@ -931,7 +931,7 @@ function createAgentMcpSession({ runId, signal, allowedDeviceIds: allowedDeviceI
|
|
|
931
931
|
const allowedDeviceIds = createAgentDeviceScope(allowedDeviceIdsInput);
|
|
932
932
|
const initialResolution = resolveAgentTargetIds({ allowedDeviceIds, connectedDeviceIds: connectedAgentDeviceIds() });
|
|
933
933
|
if (initialResolution.deviceIds.length === 0) {
|
|
934
|
-
throw new
|
|
934
|
+
throw new AgentRuntimeError(initialResolution.error || 'agent-no-target-devices', 'No selected connected device is available.', { status: 400 });
|
|
935
935
|
}
|
|
936
936
|
pruneAgentMcpRunBatches();
|
|
937
937
|
const effectivePermissionPolicy = permissionPolicy || createAgentPermissionPolicy({ mode: 'ask', deviceIds: allowedDeviceIdsInput, maxToolCalls });
|
|
@@ -1376,7 +1376,7 @@ function sendAgentError(res, error) {
|
|
|
1376
1376
|
const code = String(error?.code || 'agent-request-failed').replace(/[^a-z0-9-]/gi, '-').toLowerCase().slice(0, 80);
|
|
1377
1377
|
const status = Number.isInteger(error?.status) && error.status >= 400 && error.status <= 599
|
|
1378
1378
|
? error.status
|
|
1379
|
-
: code === '
|
|
1379
|
+
: code === 'codex-auth-required' ? 401
|
|
1380
1380
|
: 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
|
|
1381
1381
|
: code === 'agent-mcp-loopback-only' ? 403
|
|
1382
1382
|
: code === 'codex-isolation-unavailable' || code === 'codex-environment-isolation-failed' || code === 'codex-unsupported-security-config' ? 503
|
|
@@ -2108,14 +2108,21 @@ function buildRemoteFrameBinaryPacket(frameEvent, diagnostics = {}) {
|
|
|
2108
2108
|
: null,
|
|
2109
2109
|
nativeEncoderHardwareQueryStatus: frame.nativeEncoderHardwareQueryStatus || '',
|
|
2110
2110
|
nativeCaptureFps: Number(frame.nativeCaptureFps || 0) || 0,
|
|
2111
|
+
nativeCompressionFps: Number(frame.nativeCompressionFps || 0) || 0,
|
|
2111
2112
|
nativeEncodedFps: Number(frame.nativeEncodedFps || 0) || 0,
|
|
2112
2113
|
nativeCaptureSampleCount: Number(frame.nativeCaptureSampleCount || 0) || 0,
|
|
2113
2114
|
nativeEncodableFrameCount: Number(frame.nativeEncodableFrameCount || 0) || 0,
|
|
2114
2115
|
nativeSkippedFrameCount: Number(frame.nativeSkippedFrameCount || 0) || 0,
|
|
2116
|
+
nativeCompressionOutputCount: Number(frame.nativeCompressionOutputCount || 0) || 0,
|
|
2115
2117
|
nativeEncodedFrameCount: Number(frame.nativeEncodedFrameCount || 0) || 0,
|
|
2116
2118
|
nativeEncodeFramesInFlight: Number(frame.nativeEncodeFramesInFlight || 0) || 0,
|
|
2117
2119
|
nativeBackpressureSkippedFrameCount: Number(frame.nativeBackpressureSkippedFrameCount || 0) || 0,
|
|
2118
2120
|
nativeIdleSkippedFrameCount: Number(frame.nativeIdleSkippedFrameCount || 0) || 0,
|
|
2121
|
+
nativeOutputQueueDepth: Number(frame.nativeOutputQueueDepth || 0) || 0,
|
|
2122
|
+
nativeOutputQueueBytes: Number(frame.nativeOutputQueueBytes || 0) || 0,
|
|
2123
|
+
nativeOutputQueueHighWatermark: Number(frame.nativeOutputQueueHighWatermark || 0) || 0,
|
|
2124
|
+
nativeOutputQueueDroppedCount: Number(frame.nativeOutputQueueDroppedCount || 0) || 0,
|
|
2125
|
+
nativeOutputQueueAwaitingKeyFrame: frame.nativeOutputQueueAwaitingKeyFrame === true,
|
|
2119
2126
|
nativeCaptureTelemetryAt: frame.nativeCaptureTelemetryAt || '',
|
|
2120
2127
|
droppedByAgent: Number(frame.droppedByAgent || 0) || 0,
|
|
2121
2128
|
hubIngressDropped: Number(frame.hubIngressDropped || 0) || 0,
|
|
@@ -2925,48 +2932,7 @@ app.get('/api/settings/agent/audit', async (req, res) => {
|
|
|
2925
2932
|
res.json({ ok: true, audit: await agentAuditStore.list({ runId: req.query?.runId || '', limit: req.query?.limit }) });
|
|
2926
2933
|
});
|
|
2927
2934
|
|
|
2928
|
-
app.
|
|
2929
|
-
noStore(res);
|
|
2930
|
-
try {
|
|
2931
|
-
const patch = req.body && typeof req.body === 'object' && !Array.isArray(req.body) ? { ...req.body } : {};
|
|
2932
|
-
delete patch.enabled;
|
|
2933
|
-
patch.enabled = await synchronizeAgentEnablement();
|
|
2934
|
-
res.json({ ok: true, settings: await agentManager.updateSettings(patch) });
|
|
2935
|
-
} catch (error) {
|
|
2936
|
-
sendAgentError(res, error);
|
|
2937
|
-
}
|
|
2938
|
-
});
|
|
2939
|
-
|
|
2940
|
-
app.post('/api/settings/agent/reset', async (_req, res) => {
|
|
2941
|
-
noStore(res);
|
|
2942
|
-
try {
|
|
2943
|
-
await agentManager.resetSettings();
|
|
2944
|
-
await synchronizeAgentEnablement();
|
|
2945
|
-
res.json({ ok: true, settings: await agentManager.getSettings() });
|
|
2946
|
-
} catch (error) {
|
|
2947
|
-
sendAgentError(res, error);
|
|
2948
|
-
}
|
|
2949
|
-
});
|
|
2950
|
-
|
|
2951
|
-
app.delete('/api/settings/agent/api-key', async (_req, res) => {
|
|
2952
|
-
noStore(res);
|
|
2953
|
-
try {
|
|
2954
|
-
res.json({ ok: true, settings: await agentManager.deleteApiKey() });
|
|
2955
|
-
} catch (error) {
|
|
2956
|
-
sendAgentError(res, error);
|
|
2957
|
-
}
|
|
2958
|
-
});
|
|
2959
|
-
|
|
2960
|
-
app.get('/api/settings/agent/models', async (_req, res) => {
|
|
2961
|
-
noStore(res);
|
|
2962
|
-
res.status(410).json({
|
|
2963
|
-
ok: false,
|
|
2964
|
-
error: 'agent-model-catalog-retired',
|
|
2965
|
-
message: 'LiveDesk uses the Codex SDK defaults and does not manage models.'
|
|
2966
|
-
});
|
|
2967
|
-
});
|
|
2968
|
-
|
|
2969
|
-
app.post('/api/settings/agent/test', async (_req, res) => {
|
|
2935
|
+
app.post('/api/settings/agent/test', async (_req, res) => {
|
|
2970
2936
|
noStore(res);
|
|
2971
2937
|
try {
|
|
2972
2938
|
res.json({ ok: true, connection: await agentManager.testConnection() });
|
|
@@ -3001,17 +2967,17 @@ app.post('/api/settings/agent/run', async (req, res) => {
|
|
|
3001
2967
|
noStore(res);
|
|
3002
2968
|
try {
|
|
3003
2969
|
if (!(await synchronizeAgentEnablement())) {
|
|
3004
|
-
throw new
|
|
2970
|
+
throw new AgentRuntimeError('agent-disabled', 'Enable Codex Agent in Settings before running commands.', { status: 409 });
|
|
3005
2971
|
}
|
|
3006
2972
|
const instruction = typeof req.body?.instruction === 'string' ? req.body.instruction.slice(0, 4000) : '';
|
|
3007
2973
|
const deviceIds = normalizeDeviceIds(req.body?.deviceIds).slice(0, 500);
|
|
3008
2974
|
const connectedDeviceIds = new Set(connectedAgentDeviceIds());
|
|
3009
2975
|
if (deviceIds.length === 0 || deviceIds.some(deviceId => !connectedDeviceIds.has(deviceId))) {
|
|
3010
|
-
throw new
|
|
2976
|
+
throw new AgentRuntimeError('agent-target-not-connected', 'Every selected Client must have an active Agent control channel.', { status: 409 });
|
|
3011
2977
|
}
|
|
3012
2978
|
const permissionMode = String(req.body?.permissionMode || 'ask').trim().toLowerCase();
|
|
3013
2979
|
if (!AGENT_PERMISSION_MODES.includes(permissionMode)) {
|
|
3014
|
-
throw new
|
|
2980
|
+
throw new AgentRuntimeError('agent-policy-invalid', 'Select a valid Agent permission mode.', { status: 400 });
|
|
3015
2981
|
}
|
|
3016
2982
|
const settings = await agentSettingsStore.get();
|
|
3017
2983
|
const customPolicy = permissionMode === 'custom' ? await agentPermissionStore.get(req.body?.customPolicyId || 'custom-default') : null;
|
|
@@ -3109,17 +3075,7 @@ app.post('/api/settings/agent/runs/cancel-all', async (_req, res) => {
|
|
|
3109
3075
|
res.json({ ok: true, cancelledRuns, cancelledSessions, cancelledBatches: cancelledBatchIds.size });
|
|
3110
3076
|
});
|
|
3111
3077
|
|
|
3112
|
-
app.post('/api/settings/agent/
|
|
3113
|
-
noStore(res);
|
|
3114
|
-
try {
|
|
3115
|
-
const instruction = typeof req.body?.instruction === 'string' ? req.body.instruction.slice(0, 2000) : '';
|
|
3116
|
-
res.json({ ok: true, plan: await agentManager.createPlan({ instruction }) });
|
|
3117
|
-
} catch (error) {
|
|
3118
|
-
sendAgentError(res, error);
|
|
3119
|
-
}
|
|
3120
|
-
});
|
|
3121
|
-
|
|
3122
|
-
app.post('/api/settings/agent/summary', async (req, res) => {
|
|
3078
|
+
app.post('/api/settings/agent/summary', async (req, res) => {
|
|
3123
3079
|
noStore(res);
|
|
3124
3080
|
try {
|
|
3125
3081
|
res.json({ ok: true, ...await agentManager.createSummary(req.body || {}) });
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export class AgentProviderError extends Error {
|
|
2
|
-
constructor(code, message, { status = 502, retryable = false } = {}) {
|
|
3
|
-
super(message);
|
|
4
|
-
this.name = 'AgentProviderError';
|
|
5
|
-
this.code = code;
|
|
6
|
-
this.status = status;
|
|
7
|
-
this.retryable = retryable;
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
export function isRetryableProviderError(error) {
|
|
11
|
-
return error?.retryable === true || ['agent-provider-timeout', 'agent-provider-network', 'agent-provider-rate-limited', 'agent-provider-upstream'].includes(error?.code);
|
|
12
|
-
}
|