aegis-desktop 0.8.7 → 0.8.8
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/main.js +25 -1
- package/package.json +1 -1
- package/vendor/aegis.js +16 -1
- package/vendor/auto-provision.js +52 -0
- package/vendor/device-fingerprint.js +63 -0
package/main.js
CHANGED
|
@@ -47,6 +47,16 @@ try {
|
|
|
47
47
|
credentials = require('./vendor/credentials.js');
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// Same auto-provision helper the MCP plugin and the CLI use (see
|
|
51
|
+
// client/auto-provision.js) — one implementation so all three hosts ask
|
|
52
|
+
// aegis1 for the same account on the same machine, with the same timeout.
|
|
53
|
+
let autoProvision;
|
|
54
|
+
try {
|
|
55
|
+
autoProvision = require('../client/auto-provision.js');
|
|
56
|
+
} catch {
|
|
57
|
+
autoProvision = require('./vendor/auto-provision.js');
|
|
58
|
+
}
|
|
59
|
+
|
|
50
60
|
// The shared `~/.aegiscode/.env` loader (client/env-file.js, staged to
|
|
51
61
|
// ./vendor/env-file.js when packaged). It is what makes the key instruction the
|
|
52
62
|
// same in every host — "put it in ~/.aegiscode/.env" — because AEGIS_API_KEY is
|
|
@@ -2972,7 +2982,7 @@ function bootstrap() {
|
|
|
2972
2982
|
return win;
|
|
2973
2983
|
}
|
|
2974
2984
|
|
|
2975
|
-
app.whenReady().then(() => {
|
|
2985
|
+
app.whenReady().then(async () => {
|
|
2976
2986
|
// Hydrate the client from the persisted key BEFORE the renderer issues any
|
|
2977
2987
|
// status/model call, so class/model dropdowns populate immediately when a
|
|
2978
2988
|
// key was saved in-app (safeStorage is usable only after app ready).
|
|
@@ -2987,6 +2997,20 @@ function bootstrap() {
|
|
|
2987
2997
|
// The shared store is the newer write: bring this host's encrypted copy
|
|
2988
2998
|
// up to date so a CLI rotation does not have to be redone in Settings.
|
|
2989
2999
|
if (chosen.source === 'shared') settings.setAegisKey(chosen.key);
|
|
3000
|
+
} else {
|
|
3001
|
+
// Nothing configured anywhere (env, this app's encrypted copy, or the
|
|
3002
|
+
// shared store): a fresh install. Auto-provision a free device key
|
|
3003
|
+
// from aegis1 — same shared helper mcp/server.js and the CLI use, so
|
|
3004
|
+
// the desktop app works with no manual sign-in step. It starts at
|
|
3005
|
+
// zero token-bank balance server-side (register-device), and pooled
|
|
3006
|
+
// aegiscloud models additionally require an upgraded plan (aegis1's
|
|
3007
|
+
// is_pro() gate) — this only unblocks BYOK and account status/
|
|
3008
|
+
// settings until the account is funded and/or upgraded. Failure
|
|
3009
|
+
// (offline, aegis1 unreachable) falls through exactly as before: the
|
|
3010
|
+
// renderer's normal "not signed in" state handles it, and the next
|
|
3011
|
+
// launch tries again.
|
|
3012
|
+
const result = await autoProvision.autoProvisionApiKey(aegis, credentials);
|
|
3013
|
+
if (result.key) settings.setAegisKey(result.key);
|
|
2990
3014
|
}
|
|
2991
3015
|
}
|
|
2992
3016
|
// Apply whatever was last saved (or the default): ship the shortcut only
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegis-desktop",
|
|
3
3
|
"productName": "AEGIS Desktop",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.8",
|
|
5
5
|
"description": "Thin Electron host for AEGIS \u2014 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",
|
package/vendor/aegis.js
CHANGED
|
@@ -189,11 +189,12 @@ function createClient(opts = {}) {
|
|
|
189
189
|
return headers;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
async function apiPost(path, body, headers) {
|
|
192
|
+
async function apiPost(path, body, headers, opts) {
|
|
193
193
|
const res = await fetch(`${apiBase}${path}`, {
|
|
194
194
|
method: 'POST',
|
|
195
195
|
headers: headers || authHeaders(),
|
|
196
196
|
body: JSON.stringify(body || {}),
|
|
197
|
+
signal: opts && opts.signal,
|
|
197
198
|
});
|
|
198
199
|
return parseResponse(res);
|
|
199
200
|
}
|
|
@@ -371,6 +372,19 @@ function createClient(opts = {}) {
|
|
|
371
372
|
return apiPost('/api/verify-api-key', { api_key: apiKey });
|
|
372
373
|
}
|
|
373
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Ask aegis1 for a free device key when the caller has none configured yet
|
|
377
|
+
* (`POST /api/v1/auth/register-device`). Idempotent server-side on
|
|
378
|
+
* `fingerprint` — the same fingerprint always returns the same account's
|
|
379
|
+
* key rather than minting a new one. The account starts at zero token-bank
|
|
380
|
+
* balance: this hands out an account, not free inference, so the existing
|
|
381
|
+
* balance=0 gate on `/api/v1/chat/completions` still applies unchanged.
|
|
382
|
+
* Sends no API key of its own — there isn't one yet.
|
|
383
|
+
*/
|
|
384
|
+
async function registerDevice(fingerprint, opts) {
|
|
385
|
+
return apiPost('/api/v1/auth/register-device', { fingerprint }, undefined, opts);
|
|
386
|
+
}
|
|
387
|
+
|
|
374
388
|
/** Build the OpenAI-style messages array from either a full history or the
|
|
375
389
|
* single-shot { system, prompt } shorthand.
|
|
376
390
|
*
|
|
@@ -994,6 +1008,7 @@ function createClient(opts = {}) {
|
|
|
994
1008
|
},
|
|
995
1009
|
promptUrl,
|
|
996
1010
|
verifyApiKey,
|
|
1011
|
+
registerDevice,
|
|
997
1012
|
chatCompletion,
|
|
998
1013
|
byokChatCompletion,
|
|
999
1014
|
listModels,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Auto-provision a free aegis1 device key when a host finds no key configured
|
|
5
|
+
* anywhere (env, the shared 0600 store, legacy config.json). One
|
|
6
|
+
* implementation shared by every host (MCP plugin, desktop, CLI) — see
|
|
7
|
+
* client/credentials.js's own header for why that matters here: three copies
|
|
8
|
+
* of the same timeout/endpoint/save logic is how they drift.
|
|
9
|
+
*
|
|
10
|
+
* Calls aegis1's `POST /api/v1/auth/register-device` (see client/aegis.js's
|
|
11
|
+
* `registerDevice`), idempotent per machine fingerprint (client/
|
|
12
|
+
* device-fingerprint.js) so a wiped credential store still resolves back to
|
|
13
|
+
* the same account rather than minting a new, unfunded one.
|
|
14
|
+
*
|
|
15
|
+
* The provisioned account starts at ZERO token-bank balance server-side, and
|
|
16
|
+
* aegis1 additionally requires an upgraded (paid, active) plan to use pooled
|
|
17
|
+
* aegiscloud models at all (nexus_chat_completions' is_pro() gate) — this
|
|
18
|
+
* only unblocks BYOK and account status/settings until the account is funded
|
|
19
|
+
* and/or upgraded. It is account creation, not a grant of free inference.
|
|
20
|
+
*/
|
|
21
|
+
const { machineFingerprint } = require('./device-fingerprint.js');
|
|
22
|
+
|
|
23
|
+
const DEVICE_REGISTER_TIMEOUT_MS = 8000;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} aegisClient A client from client/aegis.js (createClient()).
|
|
27
|
+
* @param {object} credentials client/credentials.js (or a compatible double).
|
|
28
|
+
* @returns {Promise<{ok: boolean, key: string, error?: Error}>}
|
|
29
|
+
* `ok` is true only when a usable key was obtained AND persisted to the
|
|
30
|
+
* shared store; `aegisClient.setApiKey()` has already been called on
|
|
31
|
+
* success, so the caller's client is immediately usable either way it
|
|
32
|
+
* inspects `ok`.
|
|
33
|
+
*/
|
|
34
|
+
async function autoProvisionApiKey(aegisClient, credentials) {
|
|
35
|
+
if (aegisClient.apiKey) return { ok: true, key: aegisClient.apiKey };
|
|
36
|
+
const controller = new AbortController();
|
|
37
|
+
const timer = setTimeout(() => controller.abort(), DEVICE_REGISTER_TIMEOUT_MS);
|
|
38
|
+
try {
|
|
39
|
+
const data = await aegisClient.registerDevice(machineFingerprint(), { signal: controller.signal });
|
|
40
|
+
const key = data && typeof data.api_key === 'string' ? data.api_key.trim() : '';
|
|
41
|
+
if (!key) return { ok: false, key: '' };
|
|
42
|
+
const saved = credentials.saveApiKey(key);
|
|
43
|
+
aegisClient.setApiKey(key);
|
|
44
|
+
return { ok: !!saved.ok, key };
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return { ok: false, key: '', error: err };
|
|
47
|
+
} finally {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { autoProvisionApiKey, DEVICE_REGISTER_TIMEOUT_MS };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A stable per-machine identifier, used ONLY to ask aegis1 for a free device
|
|
5
|
+
* key (`POST /api/v1/auth/register-device`) when no key is configured yet. It
|
|
6
|
+
* is not a credential itself — the endpoint treats it as an idempotency key,
|
|
7
|
+
* so a wiped `~/.aegiscode` still resolves back to the same aegis1 account
|
|
8
|
+
* (and whatever balance it holds) instead of minting a fresh, unfunded one.
|
|
9
|
+
*
|
|
10
|
+
* Derivation mirrors ae-guix's `getMachineFingerprint()` (main.js) so the two
|
|
11
|
+
* hosts don't invent divergent conventions for the same idea: the OS's own
|
|
12
|
+
* machine identity where one exists, hashed with a purpose-specific prefix so
|
|
13
|
+
* it can never collide with a fingerprint minted for another purpose.
|
|
14
|
+
*/
|
|
15
|
+
const crypto = require('node:crypto');
|
|
16
|
+
const os = require('node:os');
|
|
17
|
+
|
|
18
|
+
/** Best-effort OS machine identity. Empty string, never throws, if none is reachable. */
|
|
19
|
+
function readMachineId() {
|
|
20
|
+
try {
|
|
21
|
+
if (process.platform === 'linux') {
|
|
22
|
+
const fs = require('node:fs');
|
|
23
|
+
try {
|
|
24
|
+
return fs.readFileSync('/etc/machine-id', 'utf8').trim();
|
|
25
|
+
} catch {
|
|
26
|
+
return fs.readFileSync('/var/lib/dbus/machine-id', 'utf8').trim();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (process.platform === 'darwin') {
|
|
30
|
+
const { execSync } = require('node:child_process');
|
|
31
|
+
const out = execSync('ioreg -rd1 -c IOPlatformExpertDevice', {
|
|
32
|
+
encoding: 'utf8',
|
|
33
|
+
timeout: 3000,
|
|
34
|
+
});
|
|
35
|
+
const m = out.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
|
|
36
|
+
if (m) return m[1];
|
|
37
|
+
}
|
|
38
|
+
if (process.platform === 'win32') {
|
|
39
|
+
const { execSync } = require('node:child_process');
|
|
40
|
+
const out = execSync('reg query HKLM\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid', {
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
timeout: 3000,
|
|
43
|
+
});
|
|
44
|
+
const m = out.match(/MachineGuid\s+REG_SZ\s+(\S+)/);
|
|
45
|
+
if (m) return m[1];
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
/* no reachable machine id on this host — the caller falls back below */
|
|
49
|
+
}
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* sha256("aegiscode-plugin-device-v1:" + machine identity), hex.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} [overrideId] - test-only override for the raw machine identity.
|
|
57
|
+
*/
|
|
58
|
+
function machineFingerprint(overrideId) {
|
|
59
|
+
const id = overrideId || readMachineId() || `${os.hostname()}:${os.platform()}:${os.arch()}`;
|
|
60
|
+
return crypto.createHash('sha256').update(`aegiscode-plugin-device-v1:${id}`).digest('hex');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { machineFingerprint, readMachineId };
|