aegis-desktop 0.8.12 → 0.8.13
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/auto.js +423 -0
- package/lib/avatar/packs/core-wardrobe.pack.json +1 -1
- package/lib/avatar/packs/local-voice-core.pack.json +1 -1
- package/lib/avatar/packs/seasonal-winter.pack.json +1 -1
- package/lib/avatar/profile.js +10 -1
- package/lib/fingerprint.js +327 -0
- package/lib/local/autonomous.js +1 -0
- package/lib/local/prompt.js +6 -0
- package/lib/settings.js +154 -1
- package/main.js +316 -17
- package/package.json +2 -2
- package/preload.js +84 -4
- package/renderer/app.js +208 -4
- package/renderer/fingerprint.css +143 -0
- package/renderer/fingerprint.js +583 -0
- package/renderer/index.html +163 -1
- package/vendor/aegis.js +180 -18
- package/vendor/attest.js +439 -0
- package/vendor/authorship-key.js +366 -0
- package/vendor/device-fingerprint.js +85 -3
- package/vendor/fingerprint.js +1061 -0
- package/vendor/presence-constants.js +104 -0
- package/vendor/presence.js +435 -0
- package/vendor/record-vault.js +834 -0
- package/vendor/touch.js +455 -0
package/main.js
CHANGED
|
@@ -57,6 +57,49 @@ try {
|
|
|
57
57
|
autoProvision = require('./vendor/auto-provision.js');
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// The shared presence beacon (client/presence.js) — the client half of
|
|
61
|
+
// aegis1's `/api/v1/heartbeat` (app.py `heartbeat_v1`), which writes the
|
|
62
|
+
// `client_presence` row the admin panel's presence card reads. Started once the
|
|
63
|
+
// account key is known (see the beacon block in `app.whenReady`), stopped in
|
|
64
|
+
// `will-quit`. Same file the CLI beacons through, so the panel sees one
|
|
65
|
+
// vocabulary (`app: 'desktop'` / `'aegiscode'`) and not two.
|
|
66
|
+
let sharedPresence;
|
|
67
|
+
try {
|
|
68
|
+
sharedPresence = require('../client/presence.js');
|
|
69
|
+
} catch {
|
|
70
|
+
sharedPresence = require('./vendor/presence.js');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// The device fingerprint (client/device-fingerprint.js) — the SAME identity the
|
|
74
|
+
// auto-provision path registers a device-key against, now also carried in every
|
|
75
|
+
// presence beat so one laptop is one machine in the admin panel rather than one
|
|
76
|
+
// row per launch. Resolved here rather than left to presence.js's own fallback
|
|
77
|
+
// for the same reason the CLI resolves it at its call site: a host that cannot
|
|
78
|
+
// state its identity should fail visibly, not substitute a different one.
|
|
79
|
+
let sharedFingerprint;
|
|
80
|
+
try {
|
|
81
|
+
sharedFingerprint = require('../client/device-fingerprint.js');
|
|
82
|
+
} catch {
|
|
83
|
+
sharedFingerprint = require('./vendor/device-fingerprint.js');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// φ(α): the authorship key and sealed session records (lib/fingerprint.js).
|
|
87
|
+
// The service is built with the app's own data dir, so the key it mints is the
|
|
88
|
+
// key `aegiscode fingerprint` resolves in the terminal — one identity, two
|
|
89
|
+
// hosts, no second store. Everything cryptographic lives in the shared client
|
|
90
|
+
// modules it loads; this require only wires the IPC surface in registerIpc()
|
|
91
|
+
// below (`fingerprintStatus`/`keygen`/`mint`/`verify`/`backup`).
|
|
92
|
+
const { createFingerprintService } = require('./lib/fingerprint.js');
|
|
93
|
+
|
|
94
|
+
// The two opt-in automations (lib/auto.js): the auto-drain timer and the
|
|
95
|
+
// seal-on-quiet auto-mint. Both are DEFAULT OFF and read their switch from the
|
|
96
|
+
// settings store's reserved `__autoDrain`/`__autoMint` namespaces on every tick,
|
|
97
|
+
// so this require wires the TIMING only — the queue worker still owns the lock
|
|
98
|
+
// and lib/fingerprint.js still owns the seal. See the header of lib/auto.js for
|
|
99
|
+
// why the "nothing drains unless the user asked" rule became a default rather
|
|
100
|
+
// than an absolute.
|
|
101
|
+
const { createAutoDrain, createAutoMint } = require('./lib/auto.js');
|
|
102
|
+
|
|
60
103
|
// The shared `~/.aegiscode/.env` loader (client/env-file.js, staged to
|
|
61
104
|
// ./vendor/env-file.js when packaged). It is what makes the key instruction the
|
|
62
105
|
// same in every host — "put it in ~/.aegiscode/.env" — because AEGIS_API_KEY is
|
|
@@ -148,6 +191,7 @@ const SYNC_PREFIX = 'sync:';
|
|
|
148
191
|
const TOOLS_PREFIX = 'tools:';
|
|
149
192
|
const QUICK_PREFIX = 'quick:';
|
|
150
193
|
const QUEUE_PREFIX = 'queue:';
|
|
194
|
+
const AUTO_PREFIX = 'auto:';
|
|
151
195
|
|
|
152
196
|
/**
|
|
153
197
|
* Main -> renderer push channel for chat SSE deltas (D2.1 streaming render).
|
|
@@ -638,6 +682,11 @@ function providerRouteConfigured(engine) {
|
|
|
638
682
|
* import to stay unit-testable.
|
|
639
683
|
*/
|
|
640
684
|
function createIpcDispatch(aegis, dir, persistApiKey, openExternal, providerConfigured, avatar) {
|
|
685
|
+
// One service for the window's lifetime, bound to the app's data dir. It is
|
|
686
|
+
// created lazily inside its handlers rather than here, so a broken shared
|
|
687
|
+
// module surfaces as an error in the fingerprint panel instead of preventing
|
|
688
|
+
// the window from opening at all.
|
|
689
|
+
const fingerprint = createFingerprintService({ dir });
|
|
641
690
|
// `providerConfigured` is an injected thunk (bootstrap() passes one that reads
|
|
642
691
|
// the settings store) so this module keeps its no-Electron, unit-testable
|
|
643
692
|
// shape; absent in tests and older call sites, where it reads as "no".
|
|
@@ -701,6 +750,29 @@ function createIpcDispatch(aegis, dir, persistApiKey, openExternal, providerConf
|
|
|
701
750
|
onStream: payload && payload.onStream,
|
|
702
751
|
}),
|
|
703
752
|
|
|
753
|
+
// φ(α) — the authorship key and the sealed session records, the same ones
|
|
754
|
+
// `aegiscode fingerprint` mints in the terminal (lib/fingerprint.js).
|
|
755
|
+
// Every handler resolves `{ ok, … }` instead of rejecting, so the renderer
|
|
756
|
+
// can show the real message; an IPC rejection reaches it as an opaque
|
|
757
|
+
// "Error invoking remote method" with the reason buried in main-process
|
|
758
|
+
// logs, and the one error a user is likely to hit (a key file other users
|
|
759
|
+
// can read) is exactly the one that must be readable.
|
|
760
|
+
fingerprintStatus: () => fingerprint.status(),
|
|
761
|
+
fingerprintKeygen: (payload) => fingerprint.keygen(payload || {}),
|
|
762
|
+
fingerprintMint: (payload) => fingerprint.mint(payload || {}),
|
|
763
|
+
fingerprintVerify: (payload) => fingerprint.verify(payload || {}),
|
|
764
|
+
fingerprintBackup: (payload) => fingerprint.backup(payload || {}),
|
|
765
|
+
// The vault (client/record-vault.js). `publish` sends the SEALED BYTES to
|
|
766
|
+
// gdrive:AEGIS-AGPL/records and writes the `.anchor.json` link beside the
|
|
767
|
+
// local record; `anchor` checks the copy still matches; `vaultAudit`
|
|
768
|
+
// inspects the whole folder for key material. The key is never publishable —
|
|
769
|
+
// record-vault.js refuses it by path, by shape and by byte comparison, in
|
|
770
|
+
// the shared module rather than in this dispatch, so the CLI inherits the
|
|
771
|
+
// same refusal.
|
|
772
|
+
fingerprintPublish: (payload) => fingerprint.publish(payload || {}),
|
|
773
|
+
fingerprintAnchor: (payload) => fingerprint.anchor(payload || {}),
|
|
774
|
+
fingerprintVault: () => fingerprint.vaultList(),
|
|
775
|
+
fingerprintVaultAudit: () => fingerprint.vaultAudit(),
|
|
704
776
|
byokStatus: () => aegis.byokStatus(),
|
|
705
777
|
byokSet: (payload) =>
|
|
706
778
|
aegis.byokSet(
|
|
@@ -726,7 +798,7 @@ function createIpcDispatch(aegis, dir, persistApiKey, openExternal, providerConf
|
|
|
726
798
|
memorySaveBatch: (payload) =>
|
|
727
799
|
aegis.memorySaveBatch(payload && payload.entries),
|
|
728
800
|
memoryImport: (payload) =>
|
|
729
|
-
importForeignMemory(aegis, dir, payload),
|
|
801
|
+
importForeignMemory(aegis, dir, payload, avatar),
|
|
730
802
|
importConversation: (payload) =>
|
|
731
803
|
aegis.importConversation(payload || {}),
|
|
732
804
|
|
|
@@ -1943,12 +2015,85 @@ function createQueueDispatch({
|
|
|
1943
2015
|
};
|
|
1944
2016
|
}
|
|
1945
2017
|
|
|
2018
|
+
/**
|
|
2019
|
+
* The two opt-in automations, as an IPC surface (`auto:<name>`).
|
|
2020
|
+
*
|
|
2021
|
+
* lib/auto.js owns the TIMING; this owns the ANSWERS the renderer gets, and
|
|
2022
|
+
* deliberately nothing else: it has no timer of its own, it does not read the
|
|
2023
|
+
* settings store (the automations do, on every tick), and it does not touch the
|
|
2024
|
+
* queue file or a record (the queue worker and lib/fingerprint.js do). Split
|
|
2025
|
+
* out as a pure function so a test can drive it with stub automations and a
|
|
2026
|
+
* fake ipcMain, exactly like createQueueDispatch.
|
|
2027
|
+
*
|
|
2028
|
+
* `sealNow` is the panel's explicit "seal this session" shortcut through the
|
|
2029
|
+
* AUTOMATION rather than through the fingerprint service directly, so the
|
|
2030
|
+
* pending-turn counter is reset by the same call that files the record — a
|
|
2031
|
+
* manual seal followed by the timer must not file the same session twice.
|
|
2032
|
+
*/
|
|
2033
|
+
function createAutoDispatch({ autoDrain, autoMint } = {}) {
|
|
2034
|
+
if (!autoDrain || typeof autoDrain.state !== 'function') {
|
|
2035
|
+
throw new Error('createAutoDispatch: the auto-drain automation is required');
|
|
2036
|
+
}
|
|
2037
|
+
if (!autoMint || typeof autoMint.state !== 'function') {
|
|
2038
|
+
throw new Error('createAutoDispatch: the auto-mint automation is required');
|
|
2039
|
+
}
|
|
2040
|
+
return {
|
|
2041
|
+
/** Both switches and both live states, in one answer. */
|
|
2042
|
+
autoStatus: () => ({ ok: true, drain: autoDrain.state(), mint: autoMint.state() }),
|
|
2043
|
+
autoSetDrain: (payload) => {
|
|
2044
|
+
const p = payload || {};
|
|
2045
|
+
const patch = {};
|
|
2046
|
+
if (p.enabled != null) patch.enabled = Boolean(p.enabled);
|
|
2047
|
+
if (p.intervalMs != null) patch.intervalMs = Number(p.intervalMs);
|
|
2048
|
+
try {
|
|
2049
|
+
return { ok: true, drain: autoDrain.set(patch) };
|
|
2050
|
+
} catch (err) {
|
|
2051
|
+
return { ok: false, error: errorText(err) };
|
|
2052
|
+
}
|
|
2053
|
+
},
|
|
2054
|
+
autoSetMint: (payload) => {
|
|
2055
|
+
const p = payload || {};
|
|
2056
|
+
const patch = {};
|
|
2057
|
+
if (p.enabled != null) patch.enabled = Boolean(p.enabled);
|
|
2058
|
+
if (p.publish != null) patch.publish = Boolean(p.publish);
|
|
2059
|
+
try {
|
|
2060
|
+
return { ok: true, mint: autoMint.set(patch) };
|
|
2061
|
+
} catch (err) {
|
|
2062
|
+
return { ok: false, error: errorText(err) };
|
|
2063
|
+
}
|
|
2064
|
+
},
|
|
2065
|
+
/** Seal the open session now, ahead of the quiet timer. Resets the counter
|
|
2066
|
+
* either way: a seal that failed is still not something to retry from a
|
|
2067
|
+
* timer that was about to fire on the same turns, and the panel shows the
|
|
2068
|
+
* failure. */
|
|
2069
|
+
autoSealNow: async (payload) => {
|
|
2070
|
+
const p = payload || {};
|
|
2071
|
+
if (p.publish != null) autoMint.set({ publish: Boolean(p.publish) });
|
|
2072
|
+
try {
|
|
2073
|
+
const result = await autoMint.flush();
|
|
2074
|
+
autoMint.reset();
|
|
2075
|
+
return { ok: true, ...result };
|
|
2076
|
+
} catch (err) {
|
|
2077
|
+
autoMint.reset();
|
|
2078
|
+
return { ok: false, error: errorText(err) };
|
|
2079
|
+
}
|
|
2080
|
+
},
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
|
|
1946
2084
|
/**
|
|
1947
2085
|
* Register the queue surface as `queue:<name>` on ipcMain — the same
|
|
1948
2086
|
* per-namespace pattern as MODEL_PREFIX/SYNC_PREFIX/QUICK_PREFIX above, with
|
|
1949
|
-
* one addition:
|
|
1950
|
-
* frames are pushed to the window that ASKED for
|
|
2087
|
+
* one addition: every handler keeps the raw IPC event, because a drain's
|
|
2088
|
+
* progress frames are pushed to the window that ASKED for it, over
|
|
1951
2089
|
* QUEUE_PROGRESS_CHANNEL (never CHAT_DELTA_CHANNEL — see that constant).
|
|
2090
|
+
*
|
|
2091
|
+
* Every handler, not just drain/proceed: with the auto-drain switch armed the
|
|
2092
|
+
* drain is started by a TIMER, so there is no "window that asked for it" — the
|
|
2093
|
+
* sink has to be whichever window last touched this surface (the card that
|
|
2094
|
+
* armed the switch, or the one that just read queue.list()). Leaving only
|
|
2095
|
+
* drain/proceed to set `target` would make an automatic drain's progress
|
|
2096
|
+
* invisible in exactly that case, and an invisible drain reads as a hang.
|
|
1952
2097
|
*/
|
|
1953
2098
|
function registerQueueIpc(ipcMain, { engine, env, queue, autonomous, entitlement, aegis } = {}) {
|
|
1954
2099
|
// The window a drain's frames go to. One drain runs at a time process-wide
|
|
@@ -2004,14 +2149,26 @@ function registerQueueIpc(ipcMain, { engine, env, queue, autonomous, entitlement
|
|
|
2004
2149
|
},
|
|
2005
2150
|
});
|
|
2006
2151
|
for (const [name, handler] of Object.entries(dispatch)) {
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2152
|
+
ipcMain.handle(`${QUEUE_PREFIX}${name}`, (event, payload) => {
|
|
2153
|
+
target = (event && event.sender) || null;
|
|
2154
|
+
return handler(payload);
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
return dispatch;
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
/**
|
|
2161
|
+
* Register the automation surface as `auto:<name>` on ipcMain.
|
|
2162
|
+
*
|
|
2163
|
+
* Unlike the queue, no handler here needs the IPC event: these calls are
|
|
2164
|
+
* answers about a switch, not a stream, and the one long-running one
|
|
2165
|
+
* (`sealNow`) resolves its own result. Nothing is pushed to the renderer from
|
|
2166
|
+
* this file — the card and the panel re-read status after every action, which
|
|
2167
|
+
* is also what makes a stale paint impossible.
|
|
2168
|
+
*/
|
|
2169
|
+
function registerAutoIpc(ipcMain, dispatch) {
|
|
2170
|
+
for (const [name, handler] of Object.entries(dispatch)) {
|
|
2171
|
+
ipcMain.handle(`${AUTO_PREFIX}${name}`, (_event, payload) => handler(payload));
|
|
2015
2172
|
}
|
|
2016
2173
|
return dispatch;
|
|
2017
2174
|
}
|
|
@@ -2707,13 +2864,34 @@ function bootstrap() {
|
|
|
2707
2864
|
note.show();
|
|
2708
2865
|
}
|
|
2709
2866
|
|
|
2867
|
+
// Both automations (lib/auto.js) are built further down, AFTER the queue and
|
|
2868
|
+
// the fingerprint have been wired — they call into both. This `let` is how
|
|
2869
|
+
// the reply hook below (which has to be handed to registerIpc up here) can
|
|
2870
|
+
// reach one that does not exist yet: it fires when a turn FINISHES, which is
|
|
2871
|
+
// always long after bootstrap() has returned, and it is null-guarded so a
|
|
2872
|
+
// reply arriving in that theoretical gap is a no-op rather than a TypeError.
|
|
2873
|
+
let autoMint = null;
|
|
2874
|
+
|
|
2875
|
+
/**
|
|
2876
|
+
* The reply hook: the native notification (unchanged) plus the auto-mint's
|
|
2877
|
+
* quiet clock. The clock is restarted by every finished turn, never read as
|
|
2878
|
+
* "seal now" — lib/auto.js's flush() is what decides, and only once the
|
|
2879
|
+
* session has been quiet for its full interval.
|
|
2880
|
+
*/
|
|
2881
|
+
const handleReplyFinished = (event, result) => {
|
|
2882
|
+
notifyReplyIfUnfocused(event);
|
|
2883
|
+
if (autoMint && typeof autoMint.note === 'function') {
|
|
2884
|
+
autoMint.note({ sessionId: (result && result.sessionId) || null });
|
|
2885
|
+
}
|
|
2886
|
+
};
|
|
2887
|
+
|
|
2710
2888
|
registerIpc(
|
|
2711
2889
|
ipcMain,
|
|
2712
2890
|
aegis,
|
|
2713
2891
|
dataDir,
|
|
2714
2892
|
persistApiKey,
|
|
2715
2893
|
(url) => shell.openExternal(url),
|
|
2716
|
-
|
|
2894
|
+
handleReplyFinished,
|
|
2717
2895
|
() => providerRouteConfigured(engine)
|
|
2718
2896
|
);
|
|
2719
2897
|
registerModelIpc(
|
|
@@ -2721,7 +2899,7 @@ function bootstrap() {
|
|
|
2721
2899
|
engine,
|
|
2722
2900
|
sessionsDir,
|
|
2723
2901
|
aegis,
|
|
2724
|
-
|
|
2902
|
+
handleReplyFinished,
|
|
2725
2903
|
undefined,
|
|
2726
2904
|
settingsDir
|
|
2727
2905
|
);
|
|
@@ -2775,10 +2953,67 @@ function bootstrap() {
|
|
|
2775
2953
|
//
|
|
2776
2954
|
// It gets its OWN engine, built with the approval gate off: a queued task has
|
|
2777
2955
|
// nobody to answer a card, so the interactive engine is the wrong one to hand
|
|
2778
|
-
// a worker (see createQueueEngine).
|
|
2779
|
-
//
|
|
2780
|
-
//
|
|
2781
|
-
|
|
2956
|
+
// a worker (see createQueueEngine).
|
|
2957
|
+
//
|
|
2958
|
+
// Registration alone drains nothing. There are now exactly two ways a drain
|
|
2959
|
+
// starts, and both are visible in the UI: a click on the card's
|
|
2960
|
+
// Run one / Drain all, or the card's "drain automatically" switch — a
|
|
2961
|
+
// PERSISTED, DEFAULT-OFF setting (settings.js `__autoDrain`) read by
|
|
2962
|
+
// lib/auto.js on every tick. There is still no boot drain and no
|
|
2963
|
+
// drain-on-idle; the interval only exists while the switch is on.
|
|
2964
|
+
const queueDispatch = registerQueueIpc(ipcMain, {
|
|
2965
|
+
engine: createQueueEngine(aegis, settings),
|
|
2966
|
+
aegis,
|
|
2967
|
+
});
|
|
2968
|
+
|
|
2969
|
+
// ── the two opt-in automations (lib/auto.js) ─────────────────────────────
|
|
2970
|
+
//
|
|
2971
|
+
// Created here because this is the first point where every collaborator
|
|
2972
|
+
// exists: settings (the switches), the queue dispatch above (the drain), and
|
|
2973
|
+
// the fingerprint service (the seal). What they do NOT get is a vote — both
|
|
2974
|
+
// modules re-read the persisted switch on every tick, so flipping the toggle
|
|
2975
|
+
// in the card or the panel stops the very next one.
|
|
2976
|
+
const autoDrain = createAutoDrain({
|
|
2977
|
+
// The SAME entry point the card's "Drain all" button calls
|
|
2978
|
+
// (createQueueDispatch's `proceed` -> drainAll). Not a second loop: the
|
|
2979
|
+
// worker's own per-task lock, Stop, and the progress frames are all the
|
|
2980
|
+
// ones the button already uses.
|
|
2981
|
+
proceed: () => queueDispatch.proceed(),
|
|
2982
|
+
settings,
|
|
2983
|
+
queue: queueModule,
|
|
2984
|
+
env: process.env,
|
|
2985
|
+
log: (entry) => {
|
|
2986
|
+
// Visible in the main-process log, and nowhere else. An automation that
|
|
2987
|
+
// fails silently is indistinguishable from one that is switched off.
|
|
2988
|
+
if (entry && entry.type !== 'skip') console.log('[auto-drain]', entry.type, entry.reason || entry.error || '');
|
|
2989
|
+
},
|
|
2990
|
+
});
|
|
2991
|
+
|
|
2992
|
+
// The seal. `mint` is lib/fingerprint.js's own — the same call the panel's
|
|
2993
|
+
// "Seal this session" makes, reading the same session log through the same
|
|
2994
|
+
// shared adapter. The service is built LAZILY for the reason createIpcDispatch
|
|
2995
|
+
// states: a broken shared module must surface as an error in the panel, not as
|
|
2996
|
+
// a window that never opens. `aiSecret` is never passed, so the AI axis of the
|
|
2997
|
+
// record is attested from `$AEGIS_AI_SECRET` in the app's scope by
|
|
2998
|
+
// client/attest.js's own rungs — no key enters main.js, the settings store, or
|
|
2999
|
+
// the DOM for this path.
|
|
3000
|
+
let autoFingerprint = null;
|
|
3001
|
+
autoMint = createAutoMint({
|
|
3002
|
+
mint: (payload) => {
|
|
3003
|
+
if (!autoFingerprint) autoFingerprint = createFingerprintService({ dir: dataDir });
|
|
3004
|
+
return autoFingerprint.mint(payload || {});
|
|
3005
|
+
},
|
|
3006
|
+
settings,
|
|
3007
|
+
});
|
|
3008
|
+
|
|
3009
|
+
registerAutoIpc(ipcMain, createAutoDispatch({ autoDrain, autoMint }));
|
|
3010
|
+
|
|
3011
|
+
// Arm whatever the user left switched on — and nothing else. Both start()
|
|
3012
|
+
// calls are reads of a persisted, default-off setting: on a default install
|
|
3013
|
+
// the drain timer is not created at all and the mint has no pending turns, so
|
|
3014
|
+
// the queue still waits for a click and no session is ever sealed by itself.
|
|
3015
|
+
autoDrain.start();
|
|
3016
|
+
autoMint.start();
|
|
2782
3017
|
|
|
2783
3018
|
// ---------------------------------------------------------------------
|
|
2784
3019
|
// Global quick-launcher (D? quick launcher): a frameless, always-on-top,
|
|
@@ -2927,7 +3162,17 @@ function bootstrap() {
|
|
|
2927
3162
|
// A held global shortcut outlives this app if not released — every quit
|
|
2928
3163
|
// path (explicit quit, window-all-closed on non-mac, OS shutdown) must
|
|
2929
3164
|
// free it.
|
|
3165
|
+
// The presence beacon (aegis1 /api/v1/heartbeat), held here so the quit path
|
|
3166
|
+
// can stop it. `null` until `app.whenReady` starts it: the beacon needs the
|
|
3167
|
+
// resolved account key, and a `let` (not a `const`) because a quit can
|
|
3168
|
+
// precede a successful start.
|
|
3169
|
+
let presenceBeacon = null;
|
|
3170
|
+
|
|
2930
3171
|
app.on('will-quit', () => {
|
|
3172
|
+
// Beacon timers are unref'd, so this is not what keeps the process alive —
|
|
3173
|
+
// it is what stops a beat from being sent while Electron tears the process
|
|
3174
|
+
// down under it.
|
|
3175
|
+
if (presenceBeacon && typeof presenceBeacon.stop === 'function') presenceBeacon.stop();
|
|
2931
3176
|
globalShortcut.unregisterAll();
|
|
2932
3177
|
// The update manager's retry/interval timers are unref'd so they never
|
|
2933
3178
|
// hold the process open, but an explicit quit should still cancel them
|
|
@@ -3089,6 +3334,57 @@ function bootstrap() {
|
|
|
3089
3334
|
if (result.key) settings.setAegisKey(result.key);
|
|
3090
3335
|
}
|
|
3091
3336
|
}
|
|
3337
|
+
// ── presence beacon ───────────────────────────────────────────────────
|
|
3338
|
+
//
|
|
3339
|
+
// aegis1's `/api/v1/heartbeat` (`heartbeat_v1`), the client half of the
|
|
3340
|
+
// admin panel's presence card: which app, which build, which OS, how many
|
|
3341
|
+
// machines, and whether a drain is running. Started HERE because it needs
|
|
3342
|
+
// the account key and this is the first point where the key is final —
|
|
3343
|
+
// resolved from env/the shared store, or auto-provisioned just above — so
|
|
3344
|
+
// the very first beat carries the account the app is actually signed in
|
|
3345
|
+
// with rather than a stale one. Stopped in `will-quit`.
|
|
3346
|
+
//
|
|
3347
|
+
// `install` is the machine fingerprint (client/device-fingerprint.js), the
|
|
3348
|
+
// SAME identity the device-registration path uses: the panel's "how many
|
|
3349
|
+
// machines" is only true if every beat from one laptop carries one id.
|
|
3350
|
+
//
|
|
3351
|
+
// A keyless app gets an inert store from presence.js — no timer, no
|
|
3352
|
+
// request — so an offline or signed-out launch is silent rather than a 401
|
|
3353
|
+
// loop. A beacon failure must never block the window from opening.
|
|
3354
|
+
try {
|
|
3355
|
+
if (presenceBeacon && typeof presenceBeacon.stop === 'function') presenceBeacon.stop();
|
|
3356
|
+
presenceBeacon = sharedPresence.createPresence({
|
|
3357
|
+
apiKey: aegis.apiKey,
|
|
3358
|
+
// 'desktop' is one of the names aegis1's PRESENCE_APPS keeps; anything
|
|
3359
|
+
// else it does not recognise is stored as 'other' with a 200 and no
|
|
3360
|
+
// error anywhere. See client/presence-constants.js.
|
|
3361
|
+
app: 'desktop',
|
|
3362
|
+
version: app.getVersion(),
|
|
3363
|
+
platform: process.platform,
|
|
3364
|
+
install: sharedFingerprint.machineFingerprint(),
|
|
3365
|
+
// Read at SEND time: the panel wants the queue as it is now, not the
|
|
3366
|
+
// depth it happened to have at launch. Same queue file the CLI drains
|
|
3367
|
+
// (`desktop/lib/local/queue.js` — see registerQueueIpc below).
|
|
3368
|
+
queueDepth: () => {
|
|
3369
|
+
try {
|
|
3370
|
+
const items = queueModule.readQueue(queueModule.queuePath());
|
|
3371
|
+
return items.filter((t) => t && t.status === 'pending').length;
|
|
3372
|
+
} catch {
|
|
3373
|
+
return 0;
|
|
3374
|
+
}
|
|
3375
|
+
},
|
|
3376
|
+
// The desktop card's drain is renderer-driven today; there is no
|
|
3377
|
+
// always-on worker in main to report, so this stays false rather than
|
|
3378
|
+
// claiming a drain that is not running.
|
|
3379
|
+
autonomous: false,
|
|
3380
|
+
log: (level, message) => {
|
|
3381
|
+
if (level !== 'debug') console.warn(message);
|
|
3382
|
+
},
|
|
3383
|
+
});
|
|
3384
|
+
presenceBeacon.start();
|
|
3385
|
+
} catch (err) {
|
|
3386
|
+
console.warn('presence: beacon not started', err && err.message);
|
|
3387
|
+
}
|
|
3092
3388
|
// Apply whatever was last saved (or the default): ship the shortcut only
|
|
3093
3389
|
// when app.isPackaged || the settings flag is on — see
|
|
3094
3390
|
// shouldEnableGlobalShortcut — so a plain `electron .` dev run never
|
|
@@ -3196,4 +3492,7 @@ module.exports = {
|
|
|
3196
3492
|
queueEngineWithDeniedApprovals,
|
|
3197
3493
|
createQueueDispatch,
|
|
3198
3494
|
registerQueueIpc,
|
|
3495
|
+
AUTO_PREFIX,
|
|
3496
|
+
createAutoDispatch,
|
|
3497
|
+
registerAutoIpc,
|
|
3199
3498
|
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegis-desktop",
|
|
3
3
|
"productName": "AEGIS Desktop",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.13",
|
|
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",
|
|
8
8
|
"email": "nborneklint@gmail.com"
|
|
9
9
|
},
|
|
10
|
-
"license": "
|
|
10
|
+
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"main": "main.js",
|
|
12
12
|
"homepage": "https://aegiscloud.org",
|
|
13
13
|
"bin": {
|
package/preload.js
CHANGED
|
@@ -18,6 +18,7 @@ const MODEL_PREFIX = 'model:';
|
|
|
18
18
|
const SYNC_PREFIX = 'sync:';
|
|
19
19
|
const QUICK_PREFIX = 'quick:';
|
|
20
20
|
const QUEUE_PREFIX = 'queue:';
|
|
21
|
+
const AUTO_PREFIX = 'auto:';
|
|
21
22
|
const QUEUE_PROGRESS_CHANNEL = `${QUEUE_PREFIX}progress`;
|
|
22
23
|
const CHAT_DELTA_CHANNEL = `${IPC_PREFIX}chatDelta`;
|
|
23
24
|
const UPDATE_STATUS_CHANNEL = `${IPC_PREFIX}updateStatus`;
|
|
@@ -63,6 +64,13 @@ function invokeQueue(name, payload) {
|
|
|
63
64
|
);
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
function invokeAuto(name, payload) {
|
|
68
|
+
return ipcRenderer.invoke(
|
|
69
|
+
AUTO_PREFIX + name,
|
|
70
|
+
payload === undefined ? undefined : payload
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
66
74
|
/**
|
|
67
75
|
* Build the chatDelta listener for one streaming request.
|
|
68
76
|
*
|
|
@@ -162,6 +170,26 @@ const api = {
|
|
|
162
170
|
// isSafeExternalUrl for the http/https-only allowlist.
|
|
163
171
|
openExternal: (url) => invoke('openExternal', { url }),
|
|
164
172
|
|
|
173
|
+
// φ(α) — the authorship key and sealed session records (main.js
|
|
174
|
+
// createIpcDispatch -> lib/fingerprint.js). Every call resolves
|
|
175
|
+
// `{ ok, … }`/`{ ok: false, error }` rather than throwing, so the UI shows the
|
|
176
|
+
// actual reason (e.g. "this key file is readable by other users") instead of
|
|
177
|
+
// Electron's opaque "Error invoking remote method". The key itself is never
|
|
178
|
+
// returned in any of these payloads — only its derived 12-hex fingerprint.
|
|
179
|
+
fingerprintStatus: () => invoke('fingerprintStatus'),
|
|
180
|
+
fingerprintKeygen: (payload) => invoke('fingerprintKeygen', payload || {}),
|
|
181
|
+
fingerprintMint: (payload) => invoke('fingerprintMint', payload || {}),
|
|
182
|
+
fingerprintVerify: (payload) => invoke('fingerprintVerify', payload || {}),
|
|
183
|
+
fingerprintBackup: (payload) => invoke('fingerprintBackup', payload || {}),
|
|
184
|
+
// The vault: publish a sealed record (default gdrive:AEGIS-AGPL/records),
|
|
185
|
+
// check the published copy against the link, list it, audit it for key
|
|
186
|
+
// material. `publish` is also a flag on fingerprintMint (`{ publish: true }`),
|
|
187
|
+
// which seals and travels in one call. No handler takes or returns the key.
|
|
188
|
+
fingerprintPublish: (payload) => invoke('fingerprintPublish', payload || {}),
|
|
189
|
+
fingerprintAnchor: (payload) => invoke('fingerprintAnchor', payload || {}),
|
|
190
|
+
fingerprintVault: () => invoke('fingerprintVault'),
|
|
191
|
+
fingerprintVaultAudit: () => invoke('fingerprintVaultAudit'),
|
|
192
|
+
|
|
165
193
|
// Tool-call approval toggle (Settings → "Confirm before running tools"):
|
|
166
194
|
// main.js createConfirmModeDispatch persists it in the settings store's
|
|
167
195
|
// reserved `__confirmMode` namespace, and the engine's gate
|
|
@@ -384,10 +412,16 @@ const quickLauncher = {
|
|
|
384
412
|
// task's own tool loop runs in — and main.js refuses it unless it is an
|
|
385
413
|
// absolute path that already exists; this file never joins, reads or writes
|
|
386
414
|
// a path itself.
|
|
387
|
-
// 2. No drain that starts by itself
|
|
388
|
-
// the queue card's two
|
|
389
|
-
//
|
|
390
|
-
//
|
|
415
|
+
// 2. No drain that starts by itself UNLESS the persisted switch is armed.
|
|
416
|
+
// `drain`/`proceed` above are invoked only from the queue card's two
|
|
417
|
+
// buttons; the timer that can call one without a click lives in
|
|
418
|
+
// desktop/lib/auto.js and is armed by exactly one thing — the
|
|
419
|
+
// `aegis.auto.setDrain({ enabled: true })` call the card's toggle makes
|
|
420
|
+
// below, which writes settings.js's reserved `__autoDrain` namespace. It
|
|
421
|
+
// is OFF in a default install, it is re-read on every tick, and switching
|
|
422
|
+
// it off stops the very next one. Draining spends money on a real model in
|
|
423
|
+
// a real checkout, so that switch is persisted and visible rather than
|
|
424
|
+
// implied — but it is also not hidden from this file's users any more.
|
|
391
425
|
const queue = {
|
|
392
426
|
// Queue one task. `payload` is { task, cwd, model, effort, workers, maxRounds,
|
|
393
427
|
// commit }; main.js range-checks every field and answers
|
|
@@ -427,6 +461,51 @@ const queue = {
|
|
|
427
461
|
},
|
|
428
462
|
};
|
|
429
463
|
|
|
464
|
+
/**
|
|
465
|
+
* The two opt-in automations (desktop/lib/auto.js via main.js
|
|
466
|
+
* registerAutoIpc): the auto-drain timer over the queue, and the seal-on-quiet
|
|
467
|
+
* timer over the current session.
|
|
468
|
+
*
|
|
469
|
+
* Same discipline as the queue surface above: every field is named and coerced
|
|
470
|
+
* HERE, so the renderer cannot smuggle an extra key into a settings namespace
|
|
471
|
+
* that main.js reserved (`__autoDrain` / `__autoMint`), and no path, secret or
|
|
472
|
+
* free-form string crosses. In particular there is NO field for an AI secret:
|
|
473
|
+
* the AI half of a sealed record is attested from `$AEGIS_AI_SECRET` in the
|
|
474
|
+
* app's own scope (client/attest.js), so no key can be handed in from the DOM
|
|
475
|
+
* even by a mistake in main.js.
|
|
476
|
+
*
|
|
477
|
+
* Both switches are OFF until one of these calls says otherwise, and the only
|
|
478
|
+
* caller is a toggle the user clicked — see the queue section's note 2 above.
|
|
479
|
+
*/
|
|
480
|
+
const auto = {
|
|
481
|
+
// { ok, drain: { enabled, intervalMs, armed, busy, ticks, drains, last, lastSkip }, mint: { … } }
|
|
482
|
+
status: () => invokeAuto('autoStatus'),
|
|
483
|
+
// Persist the drain switch and re-arm to match. `intervalMs` is clamped in
|
|
484
|
+
// settings.js (15s–30min); the renderer sends what the user picked and does
|
|
485
|
+
// not decide the floor.
|
|
486
|
+
setDrain: (patch) => {
|
|
487
|
+
const p = patch || {};
|
|
488
|
+
const out = { enabled: Boolean(p.enabled) };
|
|
489
|
+
if (p.intervalMs != null && Number.isFinite(Number(p.intervalMs))) {
|
|
490
|
+
out.intervalMs = Number(p.intervalMs);
|
|
491
|
+
}
|
|
492
|
+
return invokeAuto('autoSetDrain', out);
|
|
493
|
+
},
|
|
494
|
+
// Persist the auto-seal switch. `publish` is independent of `enabled`: a
|
|
495
|
+
// failed publish never unseals anything, so the two are separate answers.
|
|
496
|
+
setMint: (patch) => {
|
|
497
|
+
const p = patch || {};
|
|
498
|
+
return invokeAuto('autoSetMint', {
|
|
499
|
+
enabled: Boolean(p.enabled),
|
|
500
|
+
publish: Boolean(p.publish),
|
|
501
|
+
});
|
|
502
|
+
},
|
|
503
|
+
// Seal the open session now, through the same automation the timer uses — so
|
|
504
|
+
// the pending-turn counter is reset by the same call and a session is never
|
|
505
|
+
// sealed twice.
|
|
506
|
+
sealNow: (publish) => invokeAuto('autoSealNow', { publish: Boolean(publish) }),
|
|
507
|
+
};
|
|
508
|
+
|
|
430
509
|
/**
|
|
431
510
|
* Task ids are the queue file's positive integers (queue.js nextId). A retry or
|
|
432
511
|
* a remove with anything else is answered locally instead of being sent as a
|
|
@@ -470,3 +549,4 @@ contextBridge.exposeInMainWorld('models', Object.freeze(models));
|
|
|
470
549
|
contextBridge.exposeInMainWorld('sync', Object.freeze(sync));
|
|
471
550
|
contextBridge.exposeInMainWorld('quickLauncher', Object.freeze(quickLauncher));
|
|
472
551
|
contextBridge.exposeInMainWorld('queue', Object.freeze(queue));
|
|
552
|
+
contextBridge.exposeInMainWorld('auto', Object.freeze(auto));
|