livedesk 0.1.451 → 0.1.453
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/client/bin/livedesk-client.js +118 -48
- package/client/package.json +5 -5
- package/hub/package.json +1 -1
- package/hub/src/agents/agent-manager.js +7 -55
- package/hub/src/agents/agent-runtime-error.js +9 -0
- package/hub/src/agents/agent-settings.js +2 -35
- package/hub/src/agents/codex-agent-runtime.js +28 -28
- package/hub/src/remote-hub.js +14 -0
- package/hub/src/server.js +15 -59
- package/package.json +6 -6
- package/web/dist/assets/{index-9QTFmh8R.css → index-BHIS8fyi.css} +1 -1
- package/web/dist/assets/index-Byr1IQTG.js +25 -0
- package/web/dist/index.html +2 -2
- package/hub/src/agents/provider-errors.js +0 -12
- package/web/dist/assets/index-BFwWY4pa.js +0 -25
|
@@ -47,8 +47,7 @@ const FRESH_REGISTRY_TIMEOUT_MAX_MS = 10000;
|
|
|
47
47
|
// polling must stay interactive instead of backing off to multi-minute waits.
|
|
48
48
|
const DISCOVERY_RETRY_SCHEDULE_MS = [750, 750, 750, 750, 750, 750, 750, 750, 5000];
|
|
49
49
|
const DISCOVERY_RETRY_JITTER_RATIO = 0.2;
|
|
50
|
-
const
|
|
51
|
-
const EXIT_INVALID_PAIR_TOKEN = 23;
|
|
50
|
+
const EXIT_INVALID_PAIR_TOKEN = 23;
|
|
52
51
|
const EXIT_CLIENT_UPDATE = 42;
|
|
53
52
|
const SESSION_REFRESH_SKEW_SECONDS = 60;
|
|
54
53
|
const HUB_TARGET_CACHE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
@@ -2706,9 +2705,10 @@ export async function resolveManagerFromPin(supabase, pin, options = {}) {
|
|
|
2706
2705
|
allowRelayFallback: options.allowRelayFallback === true,
|
|
2707
2706
|
relayEndpoint: options.relayEndpoint,
|
|
2708
2707
|
probeEndpoint: options.probeEndpoint,
|
|
2709
|
-
resolveFreshTarget: async
|
|
2708
|
+
resolveFreshTarget: async signal => {
|
|
2710
2709
|
const { data, error } = await supabase.functions.invoke('livedesk-resolve-remote-host', {
|
|
2711
|
-
body: { pin: normalizedPin }
|
|
2710
|
+
body: { pin: normalizedPin },
|
|
2711
|
+
signal
|
|
2712
2712
|
});
|
|
2713
2713
|
if (error) {
|
|
2714
2714
|
throw await normalizePinResolverInvocationError(error);
|
|
@@ -3667,28 +3667,73 @@ export function resolveManagerFromCachedRelayFallback(ownerKey, options = {}) {
|
|
|
3667
3667
|
};
|
|
3668
3668
|
}
|
|
3669
3669
|
|
|
3670
|
+
function normalizeFreshRegistryTimeoutMs(value) {
|
|
3671
|
+
const configured = Number(value);
|
|
3672
|
+
return Number.isFinite(configured)
|
|
3673
|
+
? Math.max(FRESH_REGISTRY_TIMEOUT_MIN_MS, Math.min(FRESH_REGISTRY_TIMEOUT_MAX_MS, configured))
|
|
3674
|
+
: FRESH_REGISTRY_TIMEOUT_MS;
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
export async function runFreshHubRegistryLookup(resolveFreshTarget, options = {}) {
|
|
3678
|
+
if (typeof resolveFreshTarget !== 'function') {
|
|
3679
|
+
throw createHubDiscoveryError(
|
|
3680
|
+
'fresh-registry-resolver-required',
|
|
3681
|
+
'Fresh Hub registry resolver is required.'
|
|
3682
|
+
);
|
|
3683
|
+
}
|
|
3684
|
+
const timeoutMs = normalizeFreshRegistryTimeoutMs(options.timeoutMs);
|
|
3685
|
+
const controller = new AbortController();
|
|
3686
|
+
let timeoutHandle = null;
|
|
3687
|
+
const lookupOutcome = Promise.resolve()
|
|
3688
|
+
.then(() => resolveFreshTarget(controller.signal))
|
|
3689
|
+
.then(
|
|
3690
|
+
target => ({ type: 'target', target }),
|
|
3691
|
+
error => ({ type: 'error', error })
|
|
3692
|
+
);
|
|
3693
|
+
const timeoutOutcome = new Promise(resolveTimeout => {
|
|
3694
|
+
timeoutHandle = setTimeout(
|
|
3695
|
+
() => resolveTimeout({ type: 'timeout' }),
|
|
3696
|
+
timeoutMs
|
|
3697
|
+
);
|
|
3698
|
+
});
|
|
3699
|
+
const outcomes = [lookupOutcome, timeoutOutcome];
|
|
3700
|
+
if (options.wakePromise && typeof options.wakePromise.then === 'function') {
|
|
3701
|
+
outcomes.push(Promise.resolve(options.wakePromise).then(
|
|
3702
|
+
event => ({ type: 'hub-online', event }),
|
|
3703
|
+
() => new Promise(() => {})
|
|
3704
|
+
));
|
|
3705
|
+
}
|
|
3706
|
+
const outcome = await Promise.race(outcomes).finally(() => {
|
|
3707
|
+
clearTimeout(timeoutHandle);
|
|
3708
|
+
});
|
|
3709
|
+
if (outcome.type === 'hub-online') {
|
|
3710
|
+
controller.abort('hub-online');
|
|
3711
|
+
return outcome;
|
|
3712
|
+
}
|
|
3713
|
+
if (outcome.type === 'timeout') {
|
|
3714
|
+
controller.abort('hub-registry-timeout');
|
|
3715
|
+
throw createHubDiscoveryError(
|
|
3716
|
+
'hub-registry-timeout',
|
|
3717
|
+
`Fresh LiveDesk Hub registry lookup exceeded ${timeoutMs}ms.`
|
|
3718
|
+
);
|
|
3719
|
+
}
|
|
3720
|
+
if (outcome.type === 'error') {
|
|
3721
|
+
controller.abort('hub-registry-error');
|
|
3722
|
+
throw outcome.error;
|
|
3723
|
+
}
|
|
3724
|
+
return outcome;
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3670
3727
|
export async function resolveManagerWithCachedRecovery(ownerKey, options = {}) {
|
|
3671
3728
|
const cached = Object.prototype.hasOwnProperty.call(options, 'cachedTarget')
|
|
3672
3729
|
? normalizeCachedHubTarget(options.cachedTarget, ownerKey, options.now)
|
|
3673
3730
|
: readCachedHubTarget(ownerKey);
|
|
3674
3731
|
try {
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
const
|
|
3680
|
-
? Math.max(FRESH_REGISTRY_TIMEOUT_MIN_MS, Math.min(FRESH_REGISTRY_TIMEOUT_MAX_MS, configuredFreshTimeoutMs))
|
|
3681
|
-
: FRESH_REGISTRY_TIMEOUT_MS;
|
|
3682
|
-
let timeoutHandle = null;
|
|
3683
|
-
const fresh = await Promise.race([
|
|
3684
|
-
Promise.resolve().then(() => options.resolveFreshTarget()),
|
|
3685
|
-
new Promise((_, reject) => {
|
|
3686
|
-
timeoutHandle = setTimeout(() => reject(createHubDiscoveryError(
|
|
3687
|
-
'hub-registry-timeout',
|
|
3688
|
-
`Fresh LiveDesk Hub registry lookup exceeded ${freshTimeoutMs}ms.`
|
|
3689
|
-
)), freshTimeoutMs);
|
|
3690
|
-
})
|
|
3691
|
-
]).finally(() => clearTimeout(timeoutHandle));
|
|
3732
|
+
const freshOutcome = await runFreshHubRegistryLookup(
|
|
3733
|
+
options.resolveFreshTarget,
|
|
3734
|
+
{ timeoutMs: options.freshTimeoutMs }
|
|
3735
|
+
);
|
|
3736
|
+
const fresh = freshOutcome.target;
|
|
3692
3737
|
if (!fresh?.manager || !fresh?.pair) {
|
|
3693
3738
|
throw createHubDiscoveryError('fresh-registry-target-invalid', 'Fresh Hub registry target is incomplete.');
|
|
3694
3739
|
}
|
|
@@ -3770,12 +3815,21 @@ function normalizeEndpointCandidates(target) {
|
|
|
3770
3815
|
}
|
|
3771
3816
|
|
|
3772
3817
|
export async function resolveManagerFromSupabase(supabase, options = {}) {
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
.
|
|
3818
|
+
if (options.signal?.aborted) {
|
|
3819
|
+
throw createHubDiscoveryError('hub-registry-aborted', 'LiveDesk Hub registry lookup was cancelled.');
|
|
3820
|
+
}
|
|
3821
|
+
await refreshSessionIfNeeded(supabase);
|
|
3822
|
+
if (options.signal?.aborted) {
|
|
3823
|
+
throw createHubDiscoveryError('hub-registry-aborted', 'LiveDesk Hub registry lookup was cancelled.');
|
|
3824
|
+
}
|
|
3825
|
+
let query = supabase
|
|
3826
|
+
.from('livedesk_remote_host_targets')
|
|
3827
|
+
.select('node_id, endpoint, endpoint_candidates, pair_token, active, expires_at, updated_at, manager_version')
|
|
3828
|
+
.eq('product_key', 'livedesk');
|
|
3829
|
+
if (options.signal && typeof query.abortSignal === 'function') {
|
|
3830
|
+
query = query.abortSignal(options.signal);
|
|
3831
|
+
}
|
|
3832
|
+
const { data, error } = await query.maybeSingle();
|
|
3779
3833
|
if (error) {
|
|
3780
3834
|
throw error;
|
|
3781
3835
|
}
|
|
@@ -3849,9 +3903,12 @@ async function registerClientDeviceWithSupabase(supabase, options = {}) {
|
|
|
3849
3903
|
}
|
|
3850
3904
|
}
|
|
3851
3905
|
|
|
3852
|
-
async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
3906
|
+
export async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
3853
3907
|
const configuredIntervalMs = Number(options.intervalMs || 0);
|
|
3854
3908
|
const shouldStop = typeof options.shouldStop === 'function' ? options.shouldStop : () => false;
|
|
3909
|
+
const createWakeListener = typeof options.createWakeListener === 'function'
|
|
3910
|
+
? options.createWakeListener
|
|
3911
|
+
: createHubWakeListener;
|
|
3855
3912
|
let attempts = 0;
|
|
3856
3913
|
let lastMessage = '';
|
|
3857
3914
|
let wakeListener = null;
|
|
@@ -3862,6 +3919,17 @@ async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
|
3862
3919
|
if (shouldStop()) {
|
|
3863
3920
|
return null;
|
|
3864
3921
|
}
|
|
3922
|
+
if (!wakeListener) {
|
|
3923
|
+
try {
|
|
3924
|
+
wakeListener = await createWakeListener({
|
|
3925
|
+
getAccessToken: async () => (await refreshSessionIfNeeded(supabase))?.access_token || ''
|
|
3926
|
+
});
|
|
3927
|
+
} catch (error) {
|
|
3928
|
+
if (attempts === 0) {
|
|
3929
|
+
console.warn(`[LiveDesk Client] Hub-online event channel unavailable; adaptive registry retries remain active: ${error?.message || error}`);
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3865
3933
|
attempts += 1;
|
|
3866
3934
|
try {
|
|
3867
3935
|
if (initialError) {
|
|
@@ -3869,9 +3937,24 @@ async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
|
3869
3937
|
initialError = null;
|
|
3870
3938
|
throw error;
|
|
3871
3939
|
}
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3940
|
+
const outcome = await runFreshHubRegistryLookup(
|
|
3941
|
+
signal => resolveManagerFromSupabase(supabase, {
|
|
3942
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
3943
|
+
probeEndpoint: options.probeEndpoint,
|
|
3944
|
+
signal
|
|
3945
|
+
}),
|
|
3946
|
+
{
|
|
3947
|
+
timeoutMs: options.freshTimeoutMs,
|
|
3948
|
+
wakePromise: wakeListener?.promise
|
|
3949
|
+
}
|
|
3950
|
+
);
|
|
3951
|
+
if (outcome.type === 'hub-online') {
|
|
3952
|
+
wakeListener?.close();
|
|
3953
|
+
wakeListener = null;
|
|
3954
|
+
console.log('[LiveDesk Client] Hub-online event received. Rechecking the registry now.');
|
|
3955
|
+
continue;
|
|
3956
|
+
}
|
|
3957
|
+
return outcome.target;
|
|
3875
3958
|
} catch (err) {
|
|
3876
3959
|
const message = formatDiscoveryError(err);
|
|
3877
3960
|
if (message !== lastMessage || attempts === 1 || attempts % 6 === 0) {
|
|
@@ -3881,18 +3964,6 @@ async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
|
3881
3964
|
}
|
|
3882
3965
|
}
|
|
3883
3966
|
|
|
3884
|
-
if (!wakeListener) {
|
|
3885
|
-
try {
|
|
3886
|
-
wakeListener = await createHubWakeListener({
|
|
3887
|
-
getAccessToken: async () => (await refreshSessionIfNeeded(supabase))?.access_token || ''
|
|
3888
|
-
});
|
|
3889
|
-
} catch (error) {
|
|
3890
|
-
if (attempts === 1) {
|
|
3891
|
-
console.warn(`[LiveDesk Client] Hub-online event channel unavailable; adaptive registry retries remain active: ${error?.message || error}`);
|
|
3892
|
-
}
|
|
3893
|
-
}
|
|
3894
|
-
}
|
|
3895
|
-
|
|
3896
3967
|
const trigger = await waitForDiscoveryTrigger(
|
|
3897
3968
|
getDiscoveryRetryDelay(attempts, configuredIntervalMs),
|
|
3898
3969
|
wakeListener?.promise,
|
|
@@ -3901,9 +3972,7 @@ async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
|
3901
3972
|
if (trigger.type === 'hub-online') {
|
|
3902
3973
|
wakeListener?.close();
|
|
3903
3974
|
wakeListener = null;
|
|
3904
|
-
|
|
3905
|
-
console.log(`[LiveDesk Client] Hub-online event received. Rechecking the registry in ${jitterMs}ms.`);
|
|
3906
|
-
await waitForDiscoveryTrigger(jitterMs, null, shouldStop);
|
|
3975
|
+
console.log('[LiveDesk Client] Hub-online event received. Rechecking the registry now.');
|
|
3907
3976
|
}
|
|
3908
3977
|
}
|
|
3909
3978
|
} finally {
|
|
@@ -3993,7 +4062,7 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
|
|
|
3993
4062
|
resolved = await resolveManagerWithCachedRecovery(cacheOwnerKey, {
|
|
3994
4063
|
allowRelayFallback,
|
|
3995
4064
|
relayEndpoint: parsed.relay,
|
|
3996
|
-
resolveFreshTarget: async
|
|
4065
|
+
resolveFreshTarget: async signal => {
|
|
3997
4066
|
// The signed registry is authoritative for Hub replacement
|
|
3998
4067
|
// and pair-token rotation. Cache direct/relay recovery is
|
|
3999
4068
|
// considered only after this one current lookup reports
|
|
@@ -4006,7 +4075,8 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
|
|
|
4006
4075
|
console.warn('[LiveDesk Client] Another auth owner rotated the refresh token. Keeping the runtime alive while the saved session catches up.');
|
|
4007
4076
|
}
|
|
4008
4077
|
return await resolveManagerFromSupabase(supabase, {
|
|
4009
|
-
allowRelayFallback
|
|
4078
|
+
allowRelayFallback,
|
|
4079
|
+
signal
|
|
4010
4080
|
});
|
|
4011
4081
|
}
|
|
4012
4082
|
});
|
package/client/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.208",
|
|
4
4
|
"description": "LiveDesk local remote client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
"ws": "^8.18.3"
|
|
41
41
|
},
|
|
42
42
|
"optionalDependencies": {
|
|
43
|
-
"@livedesk/fast-linux-x64": "0.1.
|
|
44
|
-
"@livedesk/fast-osx-arm64": "0.1.
|
|
45
|
-
"@livedesk/fast-osx-x64": "0.1.
|
|
46
|
-
"@livedesk/fast-win-x64": "0.1.
|
|
43
|
+
"@livedesk/fast-linux-x64": "0.1.413",
|
|
44
|
+
"@livedesk/fast-osx-arm64": "0.1.413",
|
|
45
|
+
"@livedesk/fast-osx-x64": "0.1.413",
|
|
46
|
+
"@livedesk/fast-win-x64": "0.1.413"
|
|
47
47
|
},
|
|
48
48
|
"publishConfig": {
|
|
49
49
|
"access": "public"
|
package/hub/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,69 +42,22 @@ export function createAgentManager({
|
|
|
42
42
|
|
|
43
43
|
async function publicSettings() {
|
|
44
44
|
const current = await settings.get();
|
|
45
|
-
const {
|
|
46
|
-
codexMaxTurns: _codexMaxTurns,
|
|
47
|
-
provider: _provider,
|
|
48
|
-
...exposedSettings
|
|
49
|
-
} = current;
|
|
50
45
|
const codex = await getCodexStatus();
|
|
51
|
-
const securityStatus = runtime?.getSecurityStatus?.() || {
|
|
52
|
-
isolatedCodexHome: false,
|
|
53
|
-
restrictedEnvironment: false,
|
|
54
|
-
livedeskMcpOnly: false,
|
|
55
|
-
selectedDeviceScopeEnforced: false,
|
|
56
|
-
failClosed: true,
|
|
57
|
-
verified: false,
|
|
58
|
-
state: 'unavailable'
|
|
59
|
-
};
|
|
60
46
|
return {
|
|
61
|
-
|
|
47
|
+
enabled: current.enabled === true,
|
|
62
48
|
codexInstallation: codex.installed ? 'installed' : 'not-installed',
|
|
63
49
|
codexAuth: codex.authenticated,
|
|
64
|
-
codexStatus: codex.status
|
|
65
|
-
codexPath: codex.codexPath,
|
|
66
|
-
agentWorkspacePath: current.codexWorkingDirectory,
|
|
67
|
-
securityStatus
|
|
50
|
+
codexStatus: codex.status
|
|
68
51
|
};
|
|
69
52
|
}
|
|
70
53
|
|
|
71
54
|
return {
|
|
72
55
|
getSettings: publicSettings,
|
|
73
|
-
async updateSettings(patch = {}) {
|
|
74
|
-
const nextPatch = { ...patch };
|
|
75
|
-
const requestedProvider = String(nextPatch.provider || AGENT_PROVIDER_CODEX).trim().toLowerCase();
|
|
76
|
-
const suppliedLegacyKey = typeof nextPatch.apiKey === 'string' && nextPatch.apiKey.trim().length > 0;
|
|
77
|
-
delete nextPatch.provider;
|
|
78
|
-
delete nextPatch.apiKey;
|
|
79
|
-
if (requestedProvider !== AGENT_PROVIDER_CODEX || suppliedLegacyKey) {
|
|
80
|
-
throw new AgentProviderError(
|
|
81
|
-
'agent-provider-retired',
|
|
82
|
-
'LiveDesk uses the Hub-hosted Codex Agent and does not configure alternate model providers.',
|
|
83
|
-
{ status: 400 }
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
await settings.update({ ...nextPatch, provider: AGENT_PROVIDER_CODEX });
|
|
87
|
-
statusCache = { expiresAt: 0, value: null };
|
|
88
|
-
return publicSettings();
|
|
89
|
-
},
|
|
90
|
-
async resetSettings() {
|
|
91
|
-
await settings.reset();
|
|
92
|
-
statusCache = { expiresAt: 0, value: null };
|
|
93
|
-
return publicSettings();
|
|
94
|
-
},
|
|
95
|
-
// Kept as a harmless compatibility endpoint for an older cached web UI.
|
|
96
|
-
// No credential is read, written, or returned by the current Hub.
|
|
97
|
-
async deleteApiKey() {
|
|
98
|
-
return publicSettings();
|
|
99
|
-
},
|
|
100
56
|
async testConnection() {
|
|
101
|
-
if (!runtime) throw new
|
|
57
|
+
if (!runtime) throw new AgentRuntimeError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
|
|
102
58
|
statusCache = { expiresAt: 0, value: null };
|
|
103
59
|
return runtime.testConnection();
|
|
104
60
|
},
|
|
105
|
-
async createPlan() {
|
|
106
|
-
throw new AgentProviderError('agent-codex-run-required', 'Codex Agent requests use the LiveDesk tool loop.', { status: 409 });
|
|
107
|
-
},
|
|
108
61
|
async createSummary(input) {
|
|
109
62
|
const source = input && typeof input === 'object' ? input : {};
|
|
110
63
|
const results = Array.isArray(source.results) ? source.results : [];
|
|
@@ -113,9 +66,8 @@ export function createAgentManager({
|
|
|
113
66
|
};
|
|
114
67
|
},
|
|
115
68
|
async startRun(input) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
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 });
|
|
119
71
|
}
|
|
120
72
|
return runtime.start(input);
|
|
121
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
|
}
|
|
@@ -4,7 +4,7 @@ import os from 'node:os';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
7
|
-
import {
|
|
7
|
+
import { AgentRuntimeError } from './agent-runtime-error.js';
|
|
8
8
|
import { AGENT_TOOL_NAMES } from './agent-tool-registry.js';
|
|
9
9
|
import { createAgentPermissionPolicy, hashAgentPermissionPolicy } from './agent-permissions.js';
|
|
10
10
|
|
|
@@ -57,12 +57,12 @@ function isAbortError(error) {
|
|
|
57
57
|
|
|
58
58
|
function normalizeCodexError(error) {
|
|
59
59
|
const message = safeText(error?.message, 800) || 'Codex run failed.';
|
|
60
|
-
if (error instanceof
|
|
61
|
-
if (/out of credits|usage limit|rate limit|too many requests/i.test(message)) return new
|
|
62
|
-
if (/refresh[_ -]?token[_ -]?(?:already[_ -]?)?used|token refresh|refresh credential/i.test(message)) return new
|
|
63
|
-
if (/not authenticated|not logged in|authentication|unauthorized|login required/i.test(message)) return new
|
|
64
|
-
if (/user cancelled MCP tool call/i.test(message)) return new
|
|
65
|
-
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 });
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
function codexThreadOptions(workspace) {
|
|
@@ -164,16 +164,16 @@ async function ensureCodexHome(codexHome, globalCodexHome, { preferGlobalAuth =
|
|
|
164
164
|
const resolvedHome = path.resolve(codexHome);
|
|
165
165
|
const globalHome = path.resolve(globalCodexHome);
|
|
166
166
|
if (!resolvedHome || resolvedHome === globalHome) {
|
|
167
|
-
throw new
|
|
167
|
+
throw new AgentRuntimeError('codex-isolation-unavailable', 'Codex requires a dedicated LiveDesk home.', { status: 503 });
|
|
168
168
|
}
|
|
169
169
|
try {
|
|
170
170
|
await mkdir(resolvedHome, { recursive: true, mode: 0o700 });
|
|
171
171
|
if (await access(path.join(resolvedHome, 'config.toml')).then(() => true, () => false)) {
|
|
172
|
-
throw new
|
|
172
|
+
throw new AgentRuntimeError('codex-environment-isolation-failed', 'The dedicated Codex home contains an unmanaged config.toml.', { status: 503 });
|
|
173
173
|
}
|
|
174
174
|
} catch (error) {
|
|
175
|
-
if (error instanceof
|
|
176
|
-
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 });
|
|
177
177
|
wrapped.cause = error;
|
|
178
178
|
throw wrapped;
|
|
179
179
|
}
|
|
@@ -200,7 +200,7 @@ async function ensureCodexHome(codexHome, globalCodexHome, { preferGlobalAuth =
|
|
|
200
200
|
await replaceAuthHardLink(sourceAuthPath, targetAuthPath);
|
|
201
201
|
}
|
|
202
202
|
} catch (error) {
|
|
203
|
-
const wrapped = new
|
|
203
|
+
const wrapped = new AgentRuntimeError('codex-environment-isolation-failed', 'The Codex authentication bridge could not be prepared.', { status: 503 });
|
|
204
204
|
wrapped.cause = error;
|
|
205
205
|
throw wrapped;
|
|
206
206
|
}
|
|
@@ -243,7 +243,7 @@ function runAgeMs(run) {
|
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
function codexAbortError(code, message) {
|
|
246
|
-
return new
|
|
246
|
+
return new AgentRuntimeError(code, message, { status: code === 'codex-run-timeout' ? 504 : 409, retryable: code === 'codex-run-timeout' });
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
function safeAgentDeviceIds(value) {
|
|
@@ -331,7 +331,7 @@ export function createCodexAgentRuntime({
|
|
|
331
331
|
function assertSecurityConfiguration() {
|
|
332
332
|
const status = securityStatus();
|
|
333
333
|
if (!status.isolatedCodexHome || !status.restrictedEnvironment || !status.livedeskMcpOnly || !status.selectedDeviceScopeEnforced) {
|
|
334
|
-
throw new
|
|
334
|
+
throw new AgentRuntimeError('codex-unsupported-security-config', 'Codex security isolation is not available.', { status: 503 });
|
|
335
335
|
}
|
|
336
336
|
}
|
|
337
337
|
|
|
@@ -364,7 +364,7 @@ export function createCodexAgentRuntime({
|
|
|
364
364
|
|
|
365
365
|
async function loadCodex() {
|
|
366
366
|
if (!codexModulePromise) codexModulePromise = import('@openai/codex-sdk').catch(error => {
|
|
367
|
-
const wrapped = new
|
|
367
|
+
const wrapped = new AgentRuntimeError('codex-sdk-not-installed', 'Codex SDK is not installed in this LiveDesk package.', { status: 503 });
|
|
368
368
|
wrapped.cause = error;
|
|
369
369
|
throw wrapped;
|
|
370
370
|
});
|
|
@@ -395,7 +395,7 @@ export function createCodexAgentRuntime({
|
|
|
395
395
|
? 'signed-in'
|
|
396
396
|
: 'not-signed-in';
|
|
397
397
|
} catch (error) {
|
|
398
|
-
if (error instanceof
|
|
398
|
+
if (error instanceof AgentRuntimeError) throw error;
|
|
399
399
|
detail = safeText(error?.message, 300);
|
|
400
400
|
}
|
|
401
401
|
return { installed: true, authenticated, status: authenticated === 'signed-in' ? 'ready' : 'auth-required', codexPath: pathToCli, detail };
|
|
@@ -405,8 +405,8 @@ export function createCodexAgentRuntime({
|
|
|
405
405
|
assertSecurityConfiguration();
|
|
406
406
|
const startedAt = Date.now();
|
|
407
407
|
const status = await getStatus();
|
|
408
|
-
if (!status.installed) throw new
|
|
409
|
-
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 });
|
|
410
410
|
const settings = await settingsStore.get();
|
|
411
411
|
await mkdir(workspace, { recursive: true });
|
|
412
412
|
await prepareCodexHome();
|
|
@@ -416,7 +416,7 @@ export function createCodexAgentRuntime({
|
|
|
416
416
|
try {
|
|
417
417
|
const thread = codex.startThread(codexThreadOptions(workspace));
|
|
418
418
|
const turn = await thread.run('Reply with the single word READY. Do not use tools.', { signal: controller.signal });
|
|
419
|
-
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 });
|
|
420
420
|
return { ok: true, latencyMs: Date.now() - startedAt, threadId: thread.id || '' };
|
|
421
421
|
} catch (error) {
|
|
422
422
|
throw normalizeCodexError(error);
|
|
@@ -492,15 +492,15 @@ export function createCodexAgentRuntime({
|
|
|
492
492
|
timeoutTimer.unref?.();
|
|
493
493
|
await mkdir(workspace, { recursive: true });
|
|
494
494
|
if (activeCount > settings.maxConcurrentRequests) {
|
|
495
|
-
throw new
|
|
495
|
+
throw new AgentRuntimeError('agent-concurrency-limit', 'The LiveDesk Agent concurrency limit is reached.', { status: 429, retryable: true });
|
|
496
496
|
}
|
|
497
497
|
if (run.abortController.signal.aborted) {
|
|
498
498
|
if (run.abortReason === 'timeout') throw codexAbortError('codex-run-timeout', 'The Codex task exceeded its timeout.');
|
|
499
499
|
throw Object.assign(new Error('The Codex task was cancelled.'), { name: 'AbortError' });
|
|
500
500
|
}
|
|
501
501
|
const status = await getStatus();
|
|
502
|
-
if (!status.installed) throw new
|
|
503
|
-
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 });
|
|
504
504
|
reportProgress(run, 'preparing-tools', 'Codex is ready. Preparing the LiveDesk tools for this Client.');
|
|
505
505
|
session = createMcpSession({
|
|
506
506
|
runId: run.runId,
|
|
@@ -552,14 +552,14 @@ export function createCodexAgentRuntime({
|
|
|
552
552
|
run.abortReason = 'turn-limit';
|
|
553
553
|
run.abortController.abort();
|
|
554
554
|
session.cancel();
|
|
555
|
-
throw new
|
|
555
|
+
throw new AgentRuntimeError('codex-turn-limit-reached', 'Codex exceeded the LiveDesk turn limit.', { status: 409 });
|
|
556
556
|
}
|
|
557
557
|
}
|
|
558
558
|
if (run.toolCallCount > run.permissionPolicy.maxToolCalls) {
|
|
559
559
|
run.abortReason = 'tool-limit';
|
|
560
560
|
run.abortController.abort();
|
|
561
561
|
session.cancel();
|
|
562
|
-
throw new
|
|
562
|
+
throw new AgentRuntimeError('codex-tool-limit-reached', 'Codex exceeded the LiveDesk tool-call limit.', { status: 409 });
|
|
563
563
|
}
|
|
564
564
|
if (event.type === 'item.completed' && event.item?.type === 'mcp_tool_call' && event.item?.status === 'failed') {
|
|
565
565
|
throw new Error(safeText(event.item?.error?.message, 800) || `LiveDesk tool ${safeText(event.item?.tool, 120) || 'call'} failed.`);
|
|
@@ -641,14 +641,14 @@ export function createCodexAgentRuntime({
|
|
|
641
641
|
|
|
642
642
|
async function start(input = {}) {
|
|
643
643
|
const instruction = safeText(input.instruction, 4000);
|
|
644
|
-
if (!instruction) throw new
|
|
644
|
+
if (!instruction) throw new AgentRuntimeError('agent-invalid-instruction', 'Instruction is required.', { status: 400 });
|
|
645
645
|
const deviceIds = safeAgentDeviceIds(input.deviceIds);
|
|
646
|
-
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 });
|
|
647
647
|
const settings = await settingsStore.get();
|
|
648
|
-
if (!settings.enabled) throw new
|
|
648
|
+
if (!settings.enabled) throw new AgentRuntimeError('agent-disabled', 'Codex Agent is disabled in Settings.', { status: 409 });
|
|
649
649
|
assertSecurityConfiguration();
|
|
650
650
|
pruneRuns();
|
|
651
|
-
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 });
|
|
652
652
|
const permissionPolicy = freezeAgentPermissionPolicy(input.permissionPolicy && typeof input.permissionPolicy === 'object'
|
|
653
653
|
? input.permissionPolicy
|
|
654
654
|
: createAgentPermissionPolicy({ mode: input.permissionMode || 'safe-auto', deviceIds, maxToolCalls: settings.codexMaxToolCalls }));
|