livedesk 0.1.209 → 0.1.211
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 +93 -76
- package/hub/src/agents/agent-manager.js +175 -174
- package/hub/src/agents/agent-permissions.js +201 -167
- package/hub/src/agents/codex-agent-runtime.js +604 -593
- package/hub/src/remote-hub.js +11 -7
- package/hub/src/server.js +2556 -2501
- package/package.json +45 -45
- 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,76 +1,93 @@
|
|
|
1
|
-
import { appendFile, mkdir, readFile } from 'node:fs/promises';
|
|
2
|
-
import crypto from 'node:crypto';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
|
|
6
|
-
const MAX_AUDIT_RECORDS = 2000;
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
let
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
1
|
+
import { appendFile, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const MAX_AUDIT_RECORDS = 2000;
|
|
7
|
+
const MAX_AUDIT_FILE_RECORDS = 2250;
|
|
8
|
+
const MAX_AUDIT_FIELD = 4000;
|
|
9
|
+
|
|
10
|
+
function safeText(value, max = MAX_AUDIT_FIELD) {
|
|
11
|
+
return String(value ?? '').replace(/[\0\r\n]/g, ' ').trim().slice(0, max);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function redact(value, depth = 0) {
|
|
15
|
+
if (depth > 4) return '[truncated]';
|
|
16
|
+
if (Array.isArray(value)) return value.slice(0, 100).map(item => redact(item, depth + 1));
|
|
17
|
+
if (!value || typeof value !== 'object') return typeof value === 'string' ? safeText(value) : value;
|
|
18
|
+
return Object.fromEntries(Object.entries(value).slice(0, 100).map(([key, child]) => [
|
|
19
|
+
key,
|
|
20
|
+
/content|command|script|token|secret|password|credential|api[-_]?key|authorization/i.test(key) ? '[redacted]' : redact(child, depth + 1)
|
|
21
|
+
]));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createAgentAuditStore({ dataDir = path.join(os.homedir(), '.livedesk') } = {}) {
|
|
25
|
+
const filePath = path.join(dataDir, 'agent-audit.jsonl');
|
|
26
|
+
let records = null;
|
|
27
|
+
let rewriteNeeded = false;
|
|
28
|
+
let fileRecordCount = 0;
|
|
29
|
+
let writeQueue = Promise.resolve();
|
|
30
|
+
|
|
31
|
+
async function load() {
|
|
32
|
+
if (records) return records;
|
|
33
|
+
try {
|
|
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');
|
|
38
|
+
} catch {
|
|
39
|
+
records = [];
|
|
40
|
+
}
|
|
41
|
+
return records;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalize(event = {}) {
|
|
45
|
+
return {
|
|
46
|
+
auditId: safeText(event.auditId || crypto.randomUUID(), 100),
|
|
47
|
+
timestamp: new Date().toISOString(),
|
|
48
|
+
runId: safeText(event.runId, 100),
|
|
49
|
+
deviceIds: Array.isArray(event.deviceIds) ? [...new Set(event.deviceIds.map(item => safeText(item, 128)).filter(Boolean))].slice(0, 500) : [],
|
|
50
|
+
event: safeText(event.event, 100),
|
|
51
|
+
toolName: safeText(event.toolName, 120),
|
|
52
|
+
category: safeText(event.category, 80),
|
|
53
|
+
decision: safeText(event.decision, 40),
|
|
54
|
+
status: safeText(event.status, 40),
|
|
55
|
+
permissionMode: safeText(event.permissionMode, 40),
|
|
56
|
+
policyHash: safeText(event.policyHash, 100),
|
|
57
|
+
argumentsHash: safeText(event.argumentsHash, 100),
|
|
58
|
+
details: redact(event.details || {})
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
async record(event) {
|
|
64
|
+
const record = normalize(event);
|
|
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;
|
|
73
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
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
|
+
}
|
|
83
|
+
});
|
|
84
|
+
await writeQueue;
|
|
85
|
+
return record;
|
|
86
|
+
},
|
|
87
|
+
async list({ runId = '', limit = 200 } = {}) {
|
|
88
|
+
const current = await load();
|
|
89
|
+
const normalizedLimit = Math.max(1, Math.min(500, Number(limit) || 200));
|
|
90
|
+
return current.filter(item => !runId || item.runId === String(runId)).slice(-normalizedLimit).reverse();
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -1,174 +1,175 @@
|
|
|
1
|
-
import os from 'node:os';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { AgentSettingsStore, AGENT_PROVIDER_CODEX, AGENT_PROVIDER_OPENCODE_GO } from './agent-settings.js';
|
|
4
|
-
import { createOsSecretStore } from './secret-store.js';
|
|
5
|
-
import { createOpenCodeGoProvider } from './opencode-go-provider.js';
|
|
6
|
-
import { AgentProviderError } from './provider-errors.js';
|
|
7
|
-
import { createCodexAgentRuntime } from './codex-agent-runtime.js';
|
|
8
|
-
|
|
9
|
-
function maskApiKey(value) {
|
|
10
|
-
const key = String(value || '');
|
|
11
|
-
return key.length >= 4 ? `••••${key.slice(-4)}` : '';
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function publicCodexStatus(status = {}) {
|
|
15
|
-
return {
|
|
16
|
-
installed: status.installed === true,
|
|
17
|
-
authenticated: ['signed-in', 'not-signed-in'].includes(status.authenticated) ? status.authenticated : 'unknown',
|
|
18
|
-
status: String(status.status || 'unknown').slice(0, 40),
|
|
19
|
-
codexPath: String(status.codexPath || '').slice(0, 500),
|
|
20
|
-
detail: String(status.detail || '').slice(0, 300)
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function createAgentManager({
|
|
25
|
-
dataDir = path.join(os.homedir(), '.livedesk'),
|
|
26
|
-
settingsStore,
|
|
27
|
-
secretStore,
|
|
28
|
-
provider,
|
|
29
|
-
codexRuntime
|
|
30
|
-
} = {}) {
|
|
31
|
-
const settings = settingsStore || new AgentSettingsStore({ dataDir });
|
|
32
|
-
const secrets = secretStore || createOsSecretStore({ dataDir });
|
|
33
|
-
const legacyProvider = provider || createOpenCodeGoProvider();
|
|
34
|
-
const runtime = codexRuntime;
|
|
35
|
-
let statusCache = { expiresAt: 0, value: null };
|
|
36
|
-
|
|
37
|
-
async function getCodexStatus() {
|
|
38
|
-
if (!runtime) return { installed: false, authenticated: 'unknown', status: 'unavailable', codexPath: '' };
|
|
39
|
-
if (statusCache.value && statusCache.expiresAt > Date.now()) return statusCache.value;
|
|
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
|
-
}
|
|
46
|
-
statusCache = { value, expiresAt: Date.now() + 10000 };
|
|
47
|
-
return value;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function publicSettings() {
|
|
51
|
-
const current = await settings.get();
|
|
52
|
-
const { codexMaxTurns: _codexMaxTurns, ...exposedSettings } = current;
|
|
53
|
-
let hasApiKey = false;
|
|
54
|
-
let apiKeyPreview = '';
|
|
55
|
-
let secretStoreAvailable = true;
|
|
56
|
-
if (current.provider === AGENT_PROVIDER_OPENCODE_GO) {
|
|
57
|
-
try {
|
|
58
|
-
const key = await secrets.getApiKey(AGENT_PROVIDER_OPENCODE_GO);
|
|
59
|
-
hasApiKey = Boolean(key);
|
|
60
|
-
apiKeyPreview = maskApiKey(key);
|
|
61
|
-
} catch {
|
|
62
|
-
secretStoreAvailable = false;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
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
|
-
verified: false,
|
|
73
|
-
state: 'unavailable'
|
|
74
|
-
};
|
|
75
|
-
return {
|
|
76
|
-
...exposedSettings,
|
|
77
|
-
hasApiKey,
|
|
78
|
-
apiKeyPreview,
|
|
79
|
-
secretStoreAvailable,
|
|
80
|
-
codexInstallation: codex.installed ? 'installed' : 'not-installed',
|
|
81
|
-
codexAuth: codex.authenticated,
|
|
82
|
-
codexStatus: codex.status,
|
|
83
|
-
codexPath: codex.codexPath,
|
|
84
|
-
agentWorkspacePath: current.codexWorkingDirectory,
|
|
85
|
-
securityStatus
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async function legacyApiKey() {
|
|
90
|
-
const key = await secrets.getApiKey(AGENT_PROVIDER_OPENCODE_GO);
|
|
91
|
-
if (!key) throw new AgentProviderError('agent-api-key-missing', 'OpenCode Go API key is not configured.', { status: 401 });
|
|
92
|
-
return key;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function currentProvider() {
|
|
96
|
-
const current = await settings.get();
|
|
97
|
-
return { current, active: current.provider === AGENT_PROVIDER_CODEX ? runtime : legacyProvider };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
getSettings: publicSettings,
|
|
102
|
-
async updateSettings(patch = {}) {
|
|
103
|
-
const nextPatch = { ...patch };
|
|
104
|
-
const apiKey = typeof nextPatch.apiKey === 'string' ? nextPatch.apiKey.trim() : '';
|
|
105
|
-
delete nextPatch.apiKey;
|
|
106
|
-
const current = await settings.get();
|
|
107
|
-
const requestedProvider = String(nextPatch.provider || current.provider);
|
|
108
|
-
if (apiKey) {
|
|
109
|
-
if (requestedProvider !== AGENT_PROVIDER_OPENCODE_GO) {
|
|
110
|
-
throw new AgentProviderError('agent-codex-does-not-use-api-key', 'Codex authentication is owned by the installed Codex CLI.', { status: 400 });
|
|
111
|
-
}
|
|
112
|
-
await secrets.setApiKey(AGENT_PROVIDER_OPENCODE_GO, apiKey);
|
|
113
|
-
}
|
|
114
|
-
await settings.update(nextPatch);
|
|
115
|
-
statusCache = { expiresAt: 0, value: null };
|
|
116
|
-
return publicSettings();
|
|
117
|
-
},
|
|
118
|
-
async resetSettings() {
|
|
119
|
-
await settings.reset();
|
|
120
|
-
statusCache = { expiresAt: 0, value: null };
|
|
121
|
-
return publicSettings();
|
|
122
|
-
},
|
|
123
|
-
async deleteApiKey() {
|
|
124
|
-
await secrets.deleteApiKey(AGENT_PROVIDER_OPENCODE_GO);
|
|
125
|
-
return publicSettings();
|
|
126
|
-
},
|
|
127
|
-
async getModels() {
|
|
128
|
-
const { current, active } = await currentProvider();
|
|
129
|
-
if (current.provider === AGENT_PROVIDER_CODEX) {
|
|
130
|
-
return [{ id: '', name: 'Codex default', protocol: 'codex-sdk', available: true, capabilities: { supportsReasoningLevel: true, supportsStreaming: true, supportsTools: true } }];
|
|
131
|
-
}
|
|
132
|
-
return active.getModels(current, await legacyApiKey());
|
|
133
|
-
},
|
|
134
|
-
async testConnection() {
|
|
135
|
-
const { current, active } = await currentProvider();
|
|
136
|
-
if (current.provider === AGENT_PROVIDER_CODEX) {
|
|
137
|
-
if (!active) throw new AgentProviderError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
|
|
138
|
-
statusCache = { expiresAt: 0, value: null };
|
|
139
|
-
return active.testConnection();
|
|
140
|
-
}
|
|
141
|
-
return active.testConnection(current, await legacyApiKey());
|
|
142
|
-
},
|
|
143
|
-
async createPlan(input) {
|
|
144
|
-
const { current, active } = await currentProvider();
|
|
145
|
-
if (!current.enabled) throw new AgentProviderError('agent-ai-disabled', 'Agent AI is disabled in Settings.', { status: 409 });
|
|
146
|
-
if (current.provider === AGENT_PROVIDER_CODEX) {
|
|
147
|
-
throw new AgentProviderError('agent-codex-run-required', 'Codex runs use the LiveDesk tool loop.', { status: 409 });
|
|
148
|
-
}
|
|
149
|
-
return active.createPlan(input, current, await legacyApiKey());
|
|
150
|
-
},
|
|
151
|
-
async createSummary(input) {
|
|
152
|
-
const { current, active } = await currentProvider();
|
|
153
|
-
if (!current.enabled || !current.generateSummary) throw new AgentProviderError('agent-summary-disabled', 'Agent summaries are disabled in Settings.', { status: 409 });
|
|
154
|
-
if (current.provider === AGENT_PROVIDER_CODEX) {
|
|
155
|
-
const source = input && typeof input === 'object' ? input : {};
|
|
156
|
-
const results = Array.isArray(source.results) ? source.results : [];
|
|
157
|
-
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' };
|
|
158
|
-
}
|
|
159
|
-
const source = input && typeof input === 'object' ? input : {};
|
|
160
|
-
const results = Array.isArray(source.results) ? source.results : [];
|
|
161
|
-
const safeResults = current.includeRawDeviceDetails
|
|
162
|
-
? results.map(result => ({ deviceName: String(result?.deviceName || '').slice(0, 120), status: String(result?.status || '').slice(0, 40), result: String(result?.result || '').slice(0, 1200), error: String(result?.error || '').slice(0, 500) }))
|
|
163
|
-
: results.map(result => ({ deviceName: String(result?.deviceName || '').slice(0, 120), status: String(result?.status || '').slice(0, 40), hasResult: Boolean(result?.result), hasError: Boolean(result?.error) }));
|
|
164
|
-
return active.createSummary({ instruction: String(source.instruction || '').slice(0, 500), operation: String(source.operation || '').slice(0, 80), summary: { total: Number(source.total || results.length), completed: Number(source.completed || 0), failed: Number(source.failed || 0), results: safeResults } }, current, await legacyApiKey());
|
|
165
|
-
},
|
|
166
|
-
async startRun(input) {
|
|
167
|
-
const current = await settings.get();
|
|
168
|
-
if (current.provider !== AGENT_PROVIDER_CODEX || !runtime) throw new AgentProviderError('agent-codex-not-active', 'Codex SDK is not the active Agent provider.', { status: 409 });
|
|
169
|
-
return runtime.start(input);
|
|
170
|
-
},
|
|
171
|
-
getRun(runId) { return runtime?.get(runId) || null; },
|
|
172
|
-
cancelRun(runId) { return runtime?.cancel(runId) || null; }
|
|
173
|
-
|
|
174
|
-
}
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { AgentSettingsStore, AGENT_PROVIDER_CODEX, AGENT_PROVIDER_OPENCODE_GO } from './agent-settings.js';
|
|
4
|
+
import { createOsSecretStore } from './secret-store.js';
|
|
5
|
+
import { createOpenCodeGoProvider } from './opencode-go-provider.js';
|
|
6
|
+
import { AgentProviderError } from './provider-errors.js';
|
|
7
|
+
import { createCodexAgentRuntime } from './codex-agent-runtime.js';
|
|
8
|
+
|
|
9
|
+
function maskApiKey(value) {
|
|
10
|
+
const key = String(value || '');
|
|
11
|
+
return key.length >= 4 ? `••••${key.slice(-4)}` : '';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function publicCodexStatus(status = {}) {
|
|
15
|
+
return {
|
|
16
|
+
installed: status.installed === true,
|
|
17
|
+
authenticated: ['signed-in', 'not-signed-in'].includes(status.authenticated) ? status.authenticated : 'unknown',
|
|
18
|
+
status: String(status.status || 'unknown').slice(0, 40),
|
|
19
|
+
codexPath: String(status.codexPath || '').slice(0, 500),
|
|
20
|
+
detail: String(status.detail || '').slice(0, 300)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createAgentManager({
|
|
25
|
+
dataDir = path.join(os.homedir(), '.livedesk'),
|
|
26
|
+
settingsStore,
|
|
27
|
+
secretStore,
|
|
28
|
+
provider,
|
|
29
|
+
codexRuntime
|
|
30
|
+
} = {}) {
|
|
31
|
+
const settings = settingsStore || new AgentSettingsStore({ dataDir });
|
|
32
|
+
const secrets = secretStore || createOsSecretStore({ dataDir });
|
|
33
|
+
const legacyProvider = provider || createOpenCodeGoProvider();
|
|
34
|
+
const runtime = codexRuntime;
|
|
35
|
+
let statusCache = { expiresAt: 0, value: null };
|
|
36
|
+
|
|
37
|
+
async function getCodexStatus() {
|
|
38
|
+
if (!runtime) return { installed: false, authenticated: 'unknown', status: 'unavailable', codexPath: '' };
|
|
39
|
+
if (statusCache.value && statusCache.expiresAt > Date.now()) return statusCache.value;
|
|
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
|
+
}
|
|
46
|
+
statusCache = { value, expiresAt: Date.now() + 10000 };
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function publicSettings() {
|
|
51
|
+
const current = await settings.get();
|
|
52
|
+
const { codexMaxTurns: _codexMaxTurns, ...exposedSettings } = current;
|
|
53
|
+
let hasApiKey = false;
|
|
54
|
+
let apiKeyPreview = '';
|
|
55
|
+
let secretStoreAvailable = true;
|
|
56
|
+
if (current.provider === AGENT_PROVIDER_OPENCODE_GO) {
|
|
57
|
+
try {
|
|
58
|
+
const key = await secrets.getApiKey(AGENT_PROVIDER_OPENCODE_GO);
|
|
59
|
+
hasApiKey = Boolean(key);
|
|
60
|
+
apiKeyPreview = maskApiKey(key);
|
|
61
|
+
} catch {
|
|
62
|
+
secretStoreAvailable = false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
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
|
+
verified: false,
|
|
73
|
+
state: 'unavailable'
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
...exposedSettings,
|
|
77
|
+
hasApiKey,
|
|
78
|
+
apiKeyPreview,
|
|
79
|
+
secretStoreAvailable,
|
|
80
|
+
codexInstallation: codex.installed ? 'installed' : 'not-installed',
|
|
81
|
+
codexAuth: codex.authenticated,
|
|
82
|
+
codexStatus: codex.status,
|
|
83
|
+
codexPath: codex.codexPath,
|
|
84
|
+
agentWorkspacePath: current.codexWorkingDirectory,
|
|
85
|
+
securityStatus
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function legacyApiKey() {
|
|
90
|
+
const key = await secrets.getApiKey(AGENT_PROVIDER_OPENCODE_GO);
|
|
91
|
+
if (!key) throw new AgentProviderError('agent-api-key-missing', 'OpenCode Go API key is not configured.', { status: 401 });
|
|
92
|
+
return key;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function currentProvider() {
|
|
96
|
+
const current = await settings.get();
|
|
97
|
+
return { current, active: current.provider === AGENT_PROVIDER_CODEX ? runtime : legacyProvider };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
getSettings: publicSettings,
|
|
102
|
+
async updateSettings(patch = {}) {
|
|
103
|
+
const nextPatch = { ...patch };
|
|
104
|
+
const apiKey = typeof nextPatch.apiKey === 'string' ? nextPatch.apiKey.trim() : '';
|
|
105
|
+
delete nextPatch.apiKey;
|
|
106
|
+
const current = await settings.get();
|
|
107
|
+
const requestedProvider = String(nextPatch.provider || current.provider);
|
|
108
|
+
if (apiKey) {
|
|
109
|
+
if (requestedProvider !== AGENT_PROVIDER_OPENCODE_GO) {
|
|
110
|
+
throw new AgentProviderError('agent-codex-does-not-use-api-key', 'Codex authentication is owned by the installed Codex CLI.', { status: 400 });
|
|
111
|
+
}
|
|
112
|
+
await secrets.setApiKey(AGENT_PROVIDER_OPENCODE_GO, apiKey);
|
|
113
|
+
}
|
|
114
|
+
await settings.update(nextPatch);
|
|
115
|
+
statusCache = { expiresAt: 0, value: null };
|
|
116
|
+
return publicSettings();
|
|
117
|
+
},
|
|
118
|
+
async resetSettings() {
|
|
119
|
+
await settings.reset();
|
|
120
|
+
statusCache = { expiresAt: 0, value: null };
|
|
121
|
+
return publicSettings();
|
|
122
|
+
},
|
|
123
|
+
async deleteApiKey() {
|
|
124
|
+
await secrets.deleteApiKey(AGENT_PROVIDER_OPENCODE_GO);
|
|
125
|
+
return publicSettings();
|
|
126
|
+
},
|
|
127
|
+
async getModels() {
|
|
128
|
+
const { current, active } = await currentProvider();
|
|
129
|
+
if (current.provider === AGENT_PROVIDER_CODEX) {
|
|
130
|
+
return [{ id: '', name: 'Codex default', protocol: 'codex-sdk', available: true, capabilities: { supportsReasoningLevel: true, supportsStreaming: true, supportsTools: true } }];
|
|
131
|
+
}
|
|
132
|
+
return active.getModels(current, await legacyApiKey());
|
|
133
|
+
},
|
|
134
|
+
async testConnection() {
|
|
135
|
+
const { current, active } = await currentProvider();
|
|
136
|
+
if (current.provider === AGENT_PROVIDER_CODEX) {
|
|
137
|
+
if (!active) throw new AgentProviderError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
|
|
138
|
+
statusCache = { expiresAt: 0, value: null };
|
|
139
|
+
return active.testConnection();
|
|
140
|
+
}
|
|
141
|
+
return active.testConnection(current, await legacyApiKey());
|
|
142
|
+
},
|
|
143
|
+
async createPlan(input) {
|
|
144
|
+
const { current, active } = await currentProvider();
|
|
145
|
+
if (!current.enabled) throw new AgentProviderError('agent-ai-disabled', 'Agent AI is disabled in Settings.', { status: 409 });
|
|
146
|
+
if (current.provider === AGENT_PROVIDER_CODEX) {
|
|
147
|
+
throw new AgentProviderError('agent-codex-run-required', 'Codex runs use the LiveDesk tool loop.', { status: 409 });
|
|
148
|
+
}
|
|
149
|
+
return active.createPlan(input, current, await legacyApiKey());
|
|
150
|
+
},
|
|
151
|
+
async createSummary(input) {
|
|
152
|
+
const { current, active } = await currentProvider();
|
|
153
|
+
if (!current.enabled || !current.generateSummary) throw new AgentProviderError('agent-summary-disabled', 'Agent summaries are disabled in Settings.', { status: 409 });
|
|
154
|
+
if (current.provider === AGENT_PROVIDER_CODEX) {
|
|
155
|
+
const source = input && typeof input === 'object' ? input : {};
|
|
156
|
+
const results = Array.isArray(source.results) ? source.results : [];
|
|
157
|
+
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' };
|
|
158
|
+
}
|
|
159
|
+
const source = input && typeof input === 'object' ? input : {};
|
|
160
|
+
const results = Array.isArray(source.results) ? source.results : [];
|
|
161
|
+
const safeResults = current.includeRawDeviceDetails
|
|
162
|
+
? results.map(result => ({ deviceName: String(result?.deviceName || '').slice(0, 120), status: String(result?.status || '').slice(0, 40), result: String(result?.result || '').slice(0, 1200), error: String(result?.error || '').slice(0, 500) }))
|
|
163
|
+
: results.map(result => ({ deviceName: String(result?.deviceName || '').slice(0, 120), status: String(result?.status || '').slice(0, 40), hasResult: Boolean(result?.result), hasError: Boolean(result?.error) }));
|
|
164
|
+
return active.createSummary({ instruction: String(source.instruction || '').slice(0, 500), operation: String(source.operation || '').slice(0, 80), summary: { total: Number(source.total || results.length), completed: Number(source.completed || 0), failed: Number(source.failed || 0), results: safeResults } }, current, await legacyApiKey());
|
|
165
|
+
},
|
|
166
|
+
async startRun(input) {
|
|
167
|
+
const current = await settings.get();
|
|
168
|
+
if (current.provider !== AGENT_PROVIDER_CODEX || !runtime) throw new AgentProviderError('agent-codex-not-active', 'Codex SDK is not the active Agent provider.', { status: 409 });
|
|
169
|
+
return runtime.start(input);
|
|
170
|
+
},
|
|
171
|
+
getRun(runId) { return runtime?.get(runId) || null; },
|
|
172
|
+
cancelRun(runId) { return runtime?.cancel(runId) || null; },
|
|
173
|
+
cancelAllRuns() { return runtime?.cancelAll?.() || 0; }
|
|
174
|
+
};
|
|
175
|
+
}
|