aegis-desktop 1.0.3 → 1.0.5
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/lib/account-key.js +58 -0
- package/main.js +17 -0
- package/package.json +4 -3
- package/preload.js +8 -0
- package/renderer/app.js +50 -0
- package/renderer/index.html +11 -0
- package/vendor/auto-provision.js +31 -15
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The desktop's view of whether this machine has an AEGIS account key. When
|
|
7
|
+
* startup could not provision one, the banner says why and offers a retry.
|
|
8
|
+
* Pure: Electron, the store and the provisioner are injected.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
function provisionFailureText(result) {
|
|
12
|
+
const err = result && result.error;
|
|
13
|
+
if (!err) return 'the server returned no account key';
|
|
14
|
+
if (err.status === 429) return 'too many sign-ups from this network — try again later';
|
|
15
|
+
if (!err.status || err.status === 408 || err.status >= 500) return 'could not reach aegiscloud.org';
|
|
16
|
+
return `the server refused the request (HTTP ${err.status})`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createAccountKey({ aegis, credentials, persistKey, provision, onChange }) {
|
|
20
|
+
let failure = null;
|
|
21
|
+
let inFlight = null;
|
|
22
|
+
|
|
23
|
+
function status() {
|
|
24
|
+
if (aegis.apiKey || !failure) return null;
|
|
25
|
+
return { status: 'no-key', reason: failure };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function recordStartup(result) {
|
|
29
|
+
failure = (result.key || aegis.apiKey) ? null : provisionFailureText(result);
|
|
30
|
+
onChange(status());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function registerAccount() {
|
|
34
|
+
if (!inFlight) {
|
|
35
|
+
inFlight = (async () => {
|
|
36
|
+
try {
|
|
37
|
+
const result = await provision(aegis, credentials);
|
|
38
|
+
if (result.key) {
|
|
39
|
+
persistKey(result.key);
|
|
40
|
+
failure = null;
|
|
41
|
+
} else {
|
|
42
|
+
failure = provisionFailureText(result);
|
|
43
|
+
}
|
|
44
|
+
const state = status();
|
|
45
|
+
onChange(state);
|
|
46
|
+
return state;
|
|
47
|
+
} finally {
|
|
48
|
+
inFlight = null;
|
|
49
|
+
}
|
|
50
|
+
})();
|
|
51
|
+
}
|
|
52
|
+
return inFlight;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { status, recordStartup, registerAccount };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { createAccountKey, provisionFailureText };
|
package/main.js
CHANGED
|
@@ -152,6 +152,7 @@ try {
|
|
|
152
152
|
// backed by transport-only modules in desktop/lib/. No brain logic enters here.
|
|
153
153
|
const os = require('node:os');
|
|
154
154
|
const { createLocalEngine } = require('./lib/local/engine.js');
|
|
155
|
+
const { createAccountKey } = require('./lib/account-key.js');
|
|
155
156
|
const { createSettingsStore, isReservedNamespace } = require('./lib/settings.js');
|
|
156
157
|
const sessionStore = require('./lib/sync/sessions.js');
|
|
157
158
|
const memoryQueue = require('./lib/sync/memory-queue.js');
|
|
@@ -2633,6 +2634,7 @@ function registerAutoIpc(ipcMain, dispatch) {
|
|
|
2633
2634
|
* polling `aegis:updateStatus`.
|
|
2634
2635
|
*/
|
|
2635
2636
|
const UPDATE_STATUS_CHANNEL = `${IPC_PREFIX}updateStatus`;
|
|
2637
|
+
const ACCOUNT_STATUS_CHANNEL = `${IPC_PREFIX}accountStatusChanged`;
|
|
2636
2638
|
|
|
2637
2639
|
/** Re-check for a new release every few hours — frequent enough that a
|
|
2638
2640
|
* long-lived session still notices a release, rare enough to not hammer
|
|
@@ -3324,7 +3326,15 @@ function bootstrap() {
|
|
|
3324
3326
|
// store via credentials.preferNewest() and writes the encrypted copy once
|
|
3325
3327
|
// ready — this just guarantees there is something there for it to find
|
|
3326
3328
|
// even when 'ready' never comes.
|
|
3329
|
+
const accountKey = createAccountKey({
|
|
3330
|
+
aegis,
|
|
3331
|
+
credentials,
|
|
3332
|
+
persistKey: (key) => settings.setAegisKey(key),
|
|
3333
|
+
provision: autoProvision.autoProvisionApiKey,
|
|
3334
|
+
onChange: (state) => broadcastAccountStatus(state),
|
|
3335
|
+
});
|
|
3327
3336
|
const earlyKeyProvision = autoProvision.autoProvisionApiKey(aegis, credentials).then((result) => {
|
|
3337
|
+
accountKey.recordStartup(result);
|
|
3328
3338
|
if (!result.key && !aegis.apiKey) {
|
|
3329
3339
|
const reason =
|
|
3330
3340
|
(result.error && (result.error.status || result.error.message || String(result.error))) ||
|
|
@@ -3768,6 +3778,11 @@ function bootstrap() {
|
|
|
3768
3778
|
if (!win.isDestroyed()) win.webContents.send(UPDATE_STATUS_CHANNEL, state);
|
|
3769
3779
|
}
|
|
3770
3780
|
}
|
|
3781
|
+
function broadcastAccountStatus(state) {
|
|
3782
|
+
for (const win of openWindows) {
|
|
3783
|
+
if (!win.isDestroyed()) win.webContents.send(ACCOUNT_STATUS_CHANNEL, state);
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3771
3786
|
|
|
3772
3787
|
const updateManager = createUpdateManager({
|
|
3773
3788
|
autoUpdater: realAutoUpdater,
|
|
@@ -3775,6 +3790,8 @@ function bootstrap() {
|
|
|
3775
3790
|
onStatus: broadcastUpdateStatus,
|
|
3776
3791
|
});
|
|
3777
3792
|
registerUpdateIpc(ipcMain, updateManager);
|
|
3793
|
+
ipcMain.handle(`${IPC_PREFIX}accountStatus`, () => accountKey.status());
|
|
3794
|
+
ipcMain.handle(`${IPC_PREFIX}registerAccount`, () => accountKey.registerAccount());
|
|
3778
3795
|
registerExportIpc(
|
|
3779
3796
|
ipcMain,
|
|
3780
3797
|
sessionStore,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegis-desktop",
|
|
3
3
|
"productName": "AEGIS Desktop",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.5",
|
|
5
5
|
"description": "Thin Electron host for AEGIS — a local chat UI over the shared client/aegis.js transport. Ships transport + UI only; engine logic stays server-side.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "AEGIS Code",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"demo": "node scripts/record-demo-gif.mjs",
|
|
33
33
|
"dist": "npm run predist && electron-builder",
|
|
34
34
|
"dist:dir": "npm run predist && electron-builder --dir",
|
|
35
|
-
"check": "node --check main.js && node --check preload.js && node --check renderer/app.js && node --check renderer/quick.js && node --check renderer/budget.js && node --check renderer/usage.js && node --check renderer/stream-policy.js && node --check renderer/transcript-view.js && node --check renderer/diff.js && node --check renderer/diffview.js && node --check renderer/markdown.js && node --check renderer/vendor/aegis-highlight.js && node --check scripts/predist.mjs && node --check scripts/generate-icon.mjs && node --check scripts/bundle-fonts.mjs && node --check lib/local/local.js && node --check lib/local/context.js && node --check lib/local/engine.js && node --check lib/local/session-rounds.js && node --check lib/local/tools.js && node --check lib/local/prompt.js && node --check lib/local/shell.js && node --check lib/local/agents.js && node --check lib/local/queue.js && node --check lib/local/autonomous.js && node --check lib/settings.js && node --check lib/sync/sessions.js && node --check lib/sync/memory-queue.js && node --check lib/window-state.js && node --check lib/deep-link.js && node --check lib/quick-launcher.js && node --check lib/avatar/tiers.js && node --check lib/avatar/xp.js && node --check lib/avatar/level.js && node --check lib/avatar/events.js && node --check lib/avatar/persona.js && node --check bin/aegis.js",
|
|
35
|
+
"check": "node --check main.js && node --check preload.js && node --check renderer/app.js && node --check renderer/quick.js && node --check renderer/budget.js && node --check renderer/usage.js && node --check renderer/stream-policy.js && node --check renderer/transcript-view.js && node --check renderer/diff.js && node --check renderer/diffview.js && node --check renderer/markdown.js && node --check renderer/vendor/aegis-highlight.js && node --check scripts/predist.mjs && node --check scripts/generate-icon.mjs && node --check scripts/bundle-fonts.mjs && node --check lib/local/local.js && node --check lib/local/context.js && node --check lib/local/engine.js && node --check lib/local/session-rounds.js && node --check lib/local/tools.js && node --check lib/local/prompt.js && node --check lib/local/shell.js && node --check lib/local/agents.js && node --check lib/local/queue.js && node --check lib/local/autonomous.js && node --check lib/account-key.js && node --check lib/settings.js && node --check lib/sync/sessions.js && node --check lib/sync/memory-queue.js && node --check lib/window-state.js && node --check lib/deep-link.js && node --check lib/quick-launcher.js && node --check lib/avatar/tiers.js && node --check lib/avatar/xp.js && node --check lib/avatar/level.js && node --check lib/avatar/events.js && node --check lib/avatar/persona.js && node --check bin/aegis.js",
|
|
36
36
|
"test:shell": "node ../test/desktop-shell.mjs",
|
|
37
37
|
"test:model": "node ../test/model-dispatch.mjs",
|
|
38
38
|
"test:budget": "node ../test/budget.test.mjs",
|
|
@@ -55,7 +55,8 @@
|
|
|
55
55
|
"test:avatar-events": "node test/avatar-events.test.mjs",
|
|
56
56
|
"test:avatar-tiers": "node test/avatar-tiers.test.mjs",
|
|
57
57
|
"test:avatar-persona": "node test/avatar-persona.test.mjs",
|
|
58
|
-
"test:renderer-fonts": "node test/renderer-fonts.test.mjs"
|
|
58
|
+
"test:renderer-fonts": "node test/renderer-fonts.test.mjs",
|
|
59
|
+
"test:account-key": "node test/account-key.test.mjs"
|
|
59
60
|
},
|
|
60
61
|
"dependencies": {
|
|
61
62
|
"electron-updater": "^6.8.9"
|
package/preload.js
CHANGED
|
@@ -27,6 +27,7 @@ const AUTO_PREFIX = 'auto:';
|
|
|
27
27
|
const QUEUE_PROGRESS_CHANNEL = `${QUEUE_PREFIX}progress`;
|
|
28
28
|
const CHAT_DELTA_CHANNEL = `${IPC_PREFIX}chatDelta`;
|
|
29
29
|
const UPDATE_STATUS_CHANNEL = `${IPC_PREFIX}updateStatus`;
|
|
30
|
+
const ACCOUNT_STATUS_CHANNEL = `${IPC_PREFIX}accountStatusChanged`;
|
|
30
31
|
const MENU_NEW_CHAT_CHANNEL = `${IPC_PREFIX}menuNewChat`;
|
|
31
32
|
const MENU_SEARCH_CHANNEL = `${IPC_PREFIX}menuSearch`;
|
|
32
33
|
const MENU_EXPORT_MARKDOWN_CHANNEL = `${IPC_PREFIX}menuExportMarkdown`;
|
|
@@ -268,6 +269,13 @@ const api = {
|
|
|
268
269
|
ipcRenderer.on(UPDATE_STATUS_CHANNEL, listener);
|
|
269
270
|
return () => ipcRenderer.removeListener(UPDATE_STATUS_CHANNEL, listener);
|
|
270
271
|
},
|
|
272
|
+
accountStatus: () => invoke('accountStatus'),
|
|
273
|
+
registerAccount: () => invoke('registerAccount'),
|
|
274
|
+
onAccountStatus: (onStatus) => {
|
|
275
|
+
const listener = (_event, state) => onStatus(state);
|
|
276
|
+
ipcRenderer.on(ACCOUNT_STATUS_CHANNEL, listener);
|
|
277
|
+
return () => ipcRenderer.removeListener(ACCOUNT_STATUS_CHANNEL, listener);
|
|
278
|
+
},
|
|
271
279
|
// Native menu accelerators with no built-in Electron role (Cmd/Ctrl+N,
|
|
272
280
|
// Cmd/Ctrl+K — see main.js buildAppMenu): main.js just pings the channel,
|
|
273
281
|
// the renderer owns what "new chat" / "search" actually do.
|
package/renderer/app.js
CHANGED
|
@@ -73,6 +73,11 @@ const ELEMENT_IDS = {
|
|
|
73
73
|
updateRestartBtn: 'update-restart-btn',
|
|
74
74
|
updateRetryBtn: 'update-retry-btn',
|
|
75
75
|
updateLaterBtn: 'update-later-btn',
|
|
76
|
+
accountBanner: 'account-banner',
|
|
77
|
+
accountBannerText: 'account-banner-text',
|
|
78
|
+
accountRetryBtn: 'account-retry-btn',
|
|
79
|
+
keysAccount: 'keys-account',
|
|
80
|
+
keysAccountRetry: 'keys-account-retry',
|
|
76
81
|
connDot: 'conn-dot',
|
|
77
82
|
connText: 'conn-text',
|
|
78
83
|
app: 'st-app',
|
|
@@ -1140,6 +1145,35 @@ function renderUpdateBanner(state) {
|
|
|
1140
1145
|
els.updateBanner.hidden = false;
|
|
1141
1146
|
}
|
|
1142
1147
|
|
|
1148
|
+
/** Why the last account registration failed; null while the account is set up. */
|
|
1149
|
+
let accountFailureReason = null;
|
|
1150
|
+
|
|
1151
|
+
function renderAccountBanner(state) {
|
|
1152
|
+
accountFailureReason = state && state.status === 'no-key' ? state.reason : null;
|
|
1153
|
+
renderKeysPopup();
|
|
1154
|
+
if (!els.accountBanner) return;
|
|
1155
|
+
if (!accountFailureReason) {
|
|
1156
|
+
els.accountBanner.hidden = true;
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
els.accountBannerText.textContent = `No AEGIS account key on this machine: ${accountFailureReason}.`;
|
|
1160
|
+
els.accountBanner.hidden = false;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/** Retry registration from either surface; a success re-reads status so the sheet and key rows update. */
|
|
1164
|
+
function retryAccountSetup(button) {
|
|
1165
|
+
button.disabled = true;
|
|
1166
|
+
aegis.registerAccount()
|
|
1167
|
+
.then((state) => {
|
|
1168
|
+
renderAccountBanner(state);
|
|
1169
|
+
if (!state) return refreshAfterKeyChange();
|
|
1170
|
+
})
|
|
1171
|
+
.catch((err) => renderAccountBanner({ status: 'no-key', reason: String((err && err.message) || err) }))
|
|
1172
|
+
.finally(() => {
|
|
1173
|
+
button.disabled = false;
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1143
1177
|
async function loadAccountInfo() {
|
|
1144
1178
|
try {
|
|
1145
1179
|
const verify = await aegis.verifyApiKey();
|
|
@@ -2875,6 +2909,13 @@ function renderKeysPopup() {
|
|
|
2875
2909
|
if (els.keysRowAegis) els.keysRowAegis.classList.toggle('set', aegisSet);
|
|
2876
2910
|
if (els.keysRowByok) els.keysRowByok.classList.toggle('set', byokSet);
|
|
2877
2911
|
if (els.keysCount) els.keysCount.textContent = `${(aegisSet ? 1 : 0) + (byokSet ? 1 : 0)} / 2`;
|
|
2912
|
+
if (els.keysAccount) {
|
|
2913
|
+
els.keysAccount.hidden = !accountFailureReason;
|
|
2914
|
+
els.keysAccount.textContent = accountFailureReason
|
|
2915
|
+
? `The AEGIS account key could not be set up: ${accountFailureReason}.`
|
|
2916
|
+
: '';
|
|
2917
|
+
}
|
|
2918
|
+
if (els.keysAccountRetry) els.keysAccountRetry.hidden = !accountFailureReason;
|
|
2878
2919
|
if (els.keysHint) {
|
|
2879
2920
|
els.keysHint.textContent =
|
|
2880
2921
|
aegisSet && byokSet
|
|
@@ -5703,6 +5744,15 @@ async function init() {
|
|
|
5703
5744
|
|
|
5704
5745
|
// Auto-update banner: `?`-guarded like the memory inspector above, since
|
|
5705
5746
|
// the markup is optional and a missing node must never crash boot.
|
|
5747
|
+
if (els.accountBanner && aegis.onAccountStatus) {
|
|
5748
|
+
aegis.onAccountStatus(renderAccountBanner);
|
|
5749
|
+
aegis.accountStatus().then(renderAccountBanner).catch((err) => console.warn('account status unavailable', err));
|
|
5750
|
+
els.accountRetryBtn.addEventListener('click', () => retryAccountSetup(els.accountRetryBtn));
|
|
5751
|
+
if (els.keysAccountRetry) {
|
|
5752
|
+
els.keysAccountRetry.addEventListener('click', () => retryAccountSetup(els.keysAccountRetry));
|
|
5753
|
+
}
|
|
5754
|
+
}
|
|
5755
|
+
|
|
5706
5756
|
if (els.updateBanner && aegis.onUpdateStatus) {
|
|
5707
5757
|
aegis.onUpdateStatus(renderUpdateBanner);
|
|
5708
5758
|
aegis.updateStatus().then(renderUpdateBanner).catch(() => {});
|
package/renderer/index.html
CHANGED
|
@@ -48,6 +48,13 @@
|
|
|
48
48
|
</div>
|
|
49
49
|
</div>
|
|
50
50
|
|
|
51
|
+
<div class="update-banner account-banner" id="account-banner" role="alert" hidden>
|
|
52
|
+
<span id="account-banner-text"></span>
|
|
53
|
+
<div class="update-banner-actions">
|
|
54
|
+
<button type="button" id="account-retry-btn" class="ghost-btn">Retry account setup</button>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
51
58
|
<main>
|
|
52
59
|
<aside class="side">
|
|
53
60
|
<section class="card">
|
|
@@ -832,9 +839,13 @@
|
|
|
832
839
|
</div>
|
|
833
840
|
</div>
|
|
834
841
|
|
|
842
|
+
<p class="hint" id="keys-account" role="alert" hidden></p>
|
|
835
843
|
<p class="hint" id="keys-hint"></p>
|
|
836
844
|
|
|
837
845
|
<div class="keys-actions">
|
|
846
|
+
<button type="button" id="keys-account-retry" class="ghost-btn" hidden>
|
|
847
|
+
Retry account setup
|
|
848
|
+
</button>
|
|
838
849
|
<a
|
|
839
850
|
class="ghost-btn"
|
|
840
851
|
id="keys-get"
|
package/vendor/auto-provision.js
CHANGED
|
@@ -33,32 +33,48 @@
|
|
|
33
33
|
const { machineFingerprint } = require('./device-fingerprint.js');
|
|
34
34
|
|
|
35
35
|
const DEVICE_REGISTER_TIMEOUT_MS = 8000;
|
|
36
|
+
const DEVICE_REGISTER_RETRY_DELAYS_MS = [1000, 3000];
|
|
37
|
+
|
|
38
|
+
// Network failure, timeout (status 0), 408, or 5xx. A 4xx — including 429, the
|
|
39
|
+
// server's per-IP registration cap — is a refusal, and retrying it only spends
|
|
40
|
+
// more of that cap.
|
|
41
|
+
function isTransientRegisterError(err) {
|
|
42
|
+
const status = err && err.status;
|
|
43
|
+
return !status || status === 408 || status >= 500;
|
|
44
|
+
}
|
|
36
45
|
|
|
37
46
|
/**
|
|
38
47
|
* @param {object} aegisClient A client from client/aegis.js (createClient()).
|
|
39
48
|
* @param {object} credentials client/credentials.js (or a compatible double).
|
|
49
|
+
* @param {number[]} [retryDelaysMs] Wait before each retry of a transient failure.
|
|
40
50
|
* @returns {Promise<{ok: boolean, key: string, error?: Error}>}
|
|
41
51
|
* `ok` is true only when a usable key was obtained AND persisted to the
|
|
42
52
|
* shared store; `aegisClient.setApiKey()` has already been called on
|
|
43
53
|
* success, so the caller's client is immediately usable either way it
|
|
44
54
|
* inspects `ok`.
|
|
45
55
|
*/
|
|
46
|
-
async function autoProvisionApiKey(aegisClient, credentials) {
|
|
56
|
+
async function autoProvisionApiKey(aegisClient, credentials, retryDelaysMs = DEVICE_REGISTER_RETRY_DELAYS_MS) {
|
|
47
57
|
if (aegisClient.apiKey) return { ok: true, key: aegisClient.apiKey };
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
let lastError = null;
|
|
59
|
+
for (let attempt = 0; attempt <= retryDelaysMs.length; attempt++) {
|
|
60
|
+
if (attempt > 0) await new Promise((resolve) => setTimeout(resolve, retryDelaysMs[attempt - 1]));
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
const timer = setTimeout(() => controller.abort(), DEVICE_REGISTER_TIMEOUT_MS);
|
|
63
|
+
try {
|
|
64
|
+
const data = await aegisClient.registerDevice(machineFingerprint(), { signal: controller.signal });
|
|
65
|
+
const key = data && typeof data.api_key === 'string' ? data.api_key.trim() : '';
|
|
66
|
+
if (!key) return { ok: false, key: '' };
|
|
67
|
+
const saved = credentials.saveApiKey(key);
|
|
68
|
+
aegisClient.setApiKey(key);
|
|
69
|
+
return { ok: !!saved.ok, key };
|
|
70
|
+
} catch (err) {
|
|
71
|
+
lastError = err;
|
|
72
|
+
if (!isTransientRegisterError(err)) break;
|
|
73
|
+
} finally {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
}
|
|
61
76
|
}
|
|
77
|
+
return { ok: false, key: '', error: lastError };
|
|
62
78
|
}
|
|
63
79
|
|
|
64
|
-
module.exports = { autoProvisionApiKey, DEVICE_REGISTER_TIMEOUT_MS };
|
|
80
|
+
module.exports = { autoProvisionApiKey, DEVICE_REGISTER_TIMEOUT_MS, DEVICE_REGISTER_RETRY_DELAYS_MS };
|