aegis-desktop 1.0.13 → 1.0.15
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/SOURCE-OFFER.txt +9 -0
- package/bin/aegis.js +1 -32
- package/lib/account-key.js +1 -56
- package/lib/auto.js +1 -430
- package/lib/avatar/companion.js +1 -698
- package/lib/avatar/cosmetics.js +1 -532
- package/lib/avatar/events.js +1 -344
- package/lib/avatar/holder-turn.js +1 -350
- package/lib/avatar/holders.js +1 -612
- package/lib/avatar/identity.js +1 -641
- package/lib/avatar/level.js +1 -368
- package/lib/avatar/mirror.js +1 -524
- package/lib/avatar/persona.js +1 -388
- package/lib/avatar/profile.js +1 -977
- package/lib/avatar/register.js +1 -307
- package/lib/avatar/store.js +1 -472
- package/lib/avatar/tiers.js +1 -38
- package/lib/avatar/transfer.js +1 -415
- package/lib/avatar/turn.js +1 -216
- package/lib/avatar/voice.js +1 -328
- package/lib/avatar/xp.js +1 -404
- package/lib/deep-link.js +1 -120
- package/lib/fingerprint.js +2 -347
- package/lib/google-connect.js +1 -169
- package/lib/local/agents.js +1 -105
- package/lib/local/autonomous.js +1 -1045
- package/lib/local/claude-pro.js +1 -993
- package/lib/local/context.js +1 -95
- package/lib/local/engine.js +1 -3228
- package/lib/local/entitlement.js +1 -654
- package/lib/local/git-scope.js +1 -228
- package/lib/local/local.js +1 -2135
- package/lib/local/prompt.js +1 -275
- package/lib/local/queue.js +1 -560
- package/lib/local/session-rounds.js +1 -208
- package/lib/local/shell.js +1 -680
- package/lib/local/telemetry.js +1 -470
- package/lib/local/tools.js +1 -1238
- package/lib/local/turn-guard.js +1 -300
- package/lib/local/worktree-lock.js +1 -235
- package/lib/provider-key-mirror.js +1 -252
- package/lib/quick-launcher.js +1 -84
- package/lib/settings.js +1 -890
- package/lib/sync/memory-queue.js +1 -60
- package/lib/sync/persist-gate.js +1 -148
- package/lib/sync/sessions.js +1 -106
- package/lib/window-state.js +1 -66
- package/main.js +3 -4390
- package/package.json +3 -5
- package/preload.js +3 -628
- package/renderer/app.js +1 -6560
- package/renderer/avatar/assemble.js +1 -335
- package/renderer/avatar/avatar.js +1 -380
- package/renderer/avatar/cards.js +1 -141
- package/renderer/avatar/hud.js +1 -206
- package/renderer/avatar/machine.js +1 -213
- package/renderer/avatar/pane.js +1 -439
- package/renderer/avatar/parts.js +1 -437
- package/renderer/budget.js +1 -168
- package/renderer/companion.js +1 -223
- package/renderer/diff.js +1 -359
- package/renderer/diffview.js +1 -336
- package/renderer/fingerprint.js +3 -720
- package/renderer/markdown.js +1 -168
- package/renderer/provider-toolbar.js +1 -134
- package/renderer/providers.js +1 -259
- package/renderer/quick.js +1 -191
- package/renderer/stream-policy.js +1 -306
- package/renderer/transcript-view.js +1 -323
- package/renderer/usage.js +1 -1239
- package/vendor/aegis.js +1 -2054
- package/vendor/attest.js +1 -442
- package/vendor/authorship-key.js +1 -410
- package/vendor/auto-provision.js +1 -134
- package/vendor/brain-catalog.js +1 -268
- package/vendor/byok-catalog.js +1 -230
- package/vendor/credentials.js +1 -494
- package/vendor/device-fingerprint.js +1 -141
- package/vendor/drive-connect.js +1 -85
- package/vendor/drive-upload.js +1 -174
- package/vendor/env-file.js +1 -1273
- package/vendor/fingerprint.js +1 -1064
- package/vendor/foreign-memory.js +1 -669
- package/vendor/google-signin.js +1 -125
- package/vendor/hooks.js +1 -430
- package/vendor/model-discovery.js +1 -330
- package/vendor/platform.js +1 -937
- package/vendor/prefs.js +1 -351
- package/vendor/presence-constants.js +1 -107
- package/vendor/presence.js +1 -438
- package/vendor/record-vault.js +3 -1055
- package/vendor/session-store.js +1 -548
- package/vendor/topup.js +1 -297
- package/vendor/touch.js +1 -458
- package/vendor/update.js +1 -704
- package/vendor/user-models.js +1 -381
package/lib/settings.js
CHANGED
|
@@ -1,892 +1,3 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
2
|
// Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
|
|
3
|
-
|
|
4
|
-
// Conveying a modified copy, or running one as a network service, owes the
|
|
5
|
-
// corresponding source under this licence (§5/§13) — see LICENSING.md.
|
|
6
|
-
'use strict';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* settings.js — provider-config store for the desktop host (plan P1 §5.1).
|
|
10
|
-
* Holds base URLs and provider keys in the main process only. Keys are
|
|
11
|
-
* encrypted at rest via Electron safeStorage when available, otherwise stored
|
|
12
|
-
* base64 (best-effort; never real security) — and only masked previews ever
|
|
13
|
-
* cross the IPC bridge.
|
|
14
|
-
*
|
|
15
|
-
* The in-app AEGIS key lives in its own reserved namespace (AEGIS_KEY_NAMESPACE)
|
|
16
|
-
* with dedicated accessors, so the provider CRUD surface — and the Settings
|
|
17
|
-
* pane built on it — can never list it as a provider or remove it.
|
|
18
|
-
*
|
|
19
|
-
* Pure Node + injectable storage dir / safeStorage so it unit-tests without
|
|
20
|
-
* an Electron binary.
|
|
21
|
-
*
|
|
22
|
-
* AEGISONE D5 splits this store in two, along the line the repo already drew
|
|
23
|
-
* for sessions and credentials:
|
|
24
|
-
*
|
|
25
|
-
* <userData>/settings.json HOST-LOCAL — provider rows and the AEGIS key
|
|
26
|
-
* (`safeStorage` blobs a plain Node process
|
|
27
|
-
* cannot decrypt on Linux), plus machine/UI
|
|
28
|
-
* state. `SETTINGS_FILE`, below.
|
|
29
|
-
* $AEGISCODE_HOME/preferences.json SHARED, NON-SECRET — the preferences both
|
|
30
|
-
* faces must agree about. Owned by
|
|
31
|
-
* `client/prefs.js`; `sharedDir` (an
|
|
32
|
-
* injectable seam, defaulting to
|
|
33
|
-
* `credentials.aegisHome()`) locates it.
|
|
34
|
-
*
|
|
35
|
-
* So `getSharedPref`/`setSharedPref` and the memory-persist preference read and
|
|
36
|
-
* write the SHARED file, a legacy `<userData>` `__memoryPersist` namespace is
|
|
37
|
-
* adopted once and then only read as a fallback, and everything key-bearing
|
|
38
|
-
* stays here in `<userData>`. The shared store refuses key material outright
|
|
39
|
-
* (`client/prefs.js` `assertNoSecrets`), which is what keeps the split honest.
|
|
40
|
-
*/
|
|
41
|
-
|
|
42
|
-
const fs = require('node:fs');
|
|
43
|
-
const path = require('node:path');
|
|
44
|
-
const { execFileSync } = require('node:child_process');
|
|
45
|
-
|
|
46
|
-
// The shared, non-secret preference store (AEGISONE D5) and the data-dir
|
|
47
|
-
// helper, resolved the same two ways lib/sync/sessions.js resolves the other
|
|
48
|
-
// cross-host modules: in-repo `<repo>/client/*.js`, packaged
|
|
49
|
-
// `desktop/vendor/*.js`. A missing module is a loud error rather than a second
|
|
50
|
-
// implementation — the drift this arrangement exists to prevent.
|
|
51
|
-
const prefs = (() => {
|
|
52
|
-
try {
|
|
53
|
-
return require('../../client/prefs.js');
|
|
54
|
-
} catch {
|
|
55
|
-
return require('../vendor/prefs.js');
|
|
56
|
-
}
|
|
57
|
-
})();
|
|
58
|
-
|
|
59
|
-
const credentials = (() => {
|
|
60
|
-
try {
|
|
61
|
-
return require('../../client/credentials.js');
|
|
62
|
-
} catch {
|
|
63
|
-
return require('../vendor/credentials.js');
|
|
64
|
-
}
|
|
65
|
-
})();
|
|
66
|
-
|
|
67
|
-
// The local-model transport. Required for ONE function: `remoteRefusal`, the
|
|
68
|
-
// fail-closed check that keeps a non-local base URL out of the `local` row. The
|
|
69
|
-
// module owns that policy so this store, the engine and the CLI cannot drift
|
|
70
|
-
// about what "local" means — the failure mode a second implementation would
|
|
71
|
-
// produce is one host refusing an unpaid turn while another served it.
|
|
72
|
-
const localTransport = require('./local/local.js');
|
|
73
|
-
|
|
74
|
-
/** The provider row the `local` class reads its base URL from. Not a reserved
|
|
75
|
-
* namespace: it is a legitimate config row, and `list()` reporting it is
|
|
76
|
-
* harmless because the byok rows are selected by their `byok:` prefix. */
|
|
77
|
-
const LOCAL_NAMESPACE = 'local';
|
|
78
|
-
|
|
79
|
-
const SETTINGS_FILE = 'settings.json';
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Reserved namespace for the in-app AEGIS API key. It deliberately does NOT
|
|
83
|
-
* look like a provider id: the provider-config surface (get/set/remove/list,
|
|
84
|
-
* and therefore the Settings pane and the model: IPC) must never be able to
|
|
85
|
-
* list it as a provider or delete it via "Remove" — that coupling is defect
|
|
86
|
-
* #1 of the AEGIS-key wiring review (settings.set('aegis', …) used the same
|
|
87
|
-
* namespace as user provider configs). Use the dedicated AEGIS accessors on
|
|
88
|
-
* the store (setAegisKey / aegisRawKey / aegisKey) to touch it.
|
|
89
|
-
*/
|
|
90
|
-
const AEGIS_KEY_NAMESPACE = '__aegis';
|
|
91
|
-
|
|
92
|
-
/** Pre-fix builds persisted the AEGIS key as a plain provider named 'aegis';
|
|
93
|
-
* migrateLegacyAegisKey() relocates it (and list() hides it meanwhile). */
|
|
94
|
-
const LEGACY_AEGIS_NAMESPACE = 'aegis';
|
|
95
|
-
|
|
96
|
-
/** Reserved namespace for the global quick-launcher shortcut config
|
|
97
|
-
* ({ enabled, shortcut }) — app-level settings, not a provider, so it must
|
|
98
|
-
* stay out of the provider CRUD surface for the same reason the AEGIS key
|
|
99
|
-
* does (see AEGIS_KEY_NAMESPACE above). */
|
|
100
|
-
const QUICK_LAUNCHER_NAMESPACE = '__quickLauncher';
|
|
101
|
-
|
|
102
|
-
/** Default global shortcut: rare enough on both major platforms to avoid
|
|
103
|
-
* fighting existing app/OS bindings, memorable enough to type once. */
|
|
104
|
-
const DEFAULT_QUICK_LAUNCHER_SHORTCUT = 'CmdOrCtrl+Shift+Space';
|
|
105
|
-
|
|
106
|
-
/** Reserved namespace for where `fingerprint publish`/the vault card sends
|
|
107
|
-
* sealed records: `{ backend }`, one of VAULT_BACKEND_VALUES — app-level, not
|
|
108
|
-
* a provider, for the same reason the quick launcher isn't. Default is
|
|
109
|
-
* `aegiscloud` (unset reads as the default, same convention as confirm mode)
|
|
110
|
-
* because it is the only backend that exists on a fresh install — see
|
|
111
|
-
* client/record-vault.js's own DEFAULT_BACKEND. `drive` asks the fingerprint
|
|
112
|
-
* service to talk to rclone's `gdrive` remote instead (the user's own Drive,
|
|
113
|
-
* configured by them); `off` means publish/anchor/vault/audit refuse with a
|
|
114
|
-
* named reason rather than silently reaching either backend. */
|
|
115
|
-
const VAULT_BACKEND_NAMESPACE = '__vaultBackend';
|
|
116
|
-
const DEFAULT_VAULT_BACKEND = 'aegiscloud';
|
|
117
|
-
const VAULT_BACKEND_VALUES = Object.freeze(['aegiscloud', 'drive', 'off']);
|
|
118
|
-
|
|
119
|
-
/** Reserved namespace for the tool-call approval ("confirm mode") preference:
|
|
120
|
-
* `{ enabled }` — app-level, not a provider, so it stays out of the provider
|
|
121
|
-
* CRUD surface for the same reason the AEGIS key does. Default is ON
|
|
122
|
-
* (settings.getConfirmMode() === true when unset), which is the historical
|
|
123
|
-
* behaviour: exec/writeFile/editFile always asked for approval.
|
|
124
|
-
* NOTE: like the AEGIS key and the quick launcher, this lives in its own
|
|
125
|
-
* top-level namespace — never inside a provider's `cfg[provider]` object. */
|
|
126
|
-
const CONFIRM_MODE_NAMESPACE = '__confirmMode';
|
|
127
|
-
|
|
128
|
-
/** Reserved namespace for the persisting-memory preference: `{ enabled }` —
|
|
129
|
-
* app-level, not a provider. This is the desktop's counterpart to the CLI's
|
|
130
|
-
* `memoryPersist` config key, and the gate `lib/sync/persist-gate.js` reads
|
|
131
|
-
* before any automatic cloud push. Default is ON (an absent namespace means
|
|
132
|
-
* persistence is on, which is the shipped decision); an explicit `false` is
|
|
133
|
-
* what turns it off. Nothing secret lives here, so no encryption. */
|
|
134
|
-
const MEMORY_PERSIST_NAMESPACE = '__memoryPersist';
|
|
135
|
-
|
|
136
|
-
/** Reserved namespace for the avatar/companion preferences:
|
|
137
|
-
* `{ enabled, showLevel, motion }` — app-level, not a provider, so it stays out
|
|
138
|
-
* of the provider CRUD surface for the same reason the AEGIS key does. The
|
|
139
|
-
* persona itself is deliberately NOT stored here: it is a document, not a
|
|
140
|
-
* preference, and it lives at `<userData>/avatar/persona.json`, read and
|
|
141
|
-
* written by main through lib/avatar/store.js so a hand-edited or imported
|
|
142
|
-
* persona is validated on the way in.
|
|
143
|
-
* Defaults are the shipped behaviour: the avatar is ON, the level readout is
|
|
144
|
-
* shown, and motion follows the OS `prefers-reduced-motion` setting. */
|
|
145
|
-
const AVATAR_NAMESPACE = '__avatar';
|
|
146
|
-
|
|
147
|
-
/** Reserved namespace for the funnel-instrumentation opt-in: `{ enabled }` —
|
|
148
|
-
* app-level, not a provider. DEFAULT OFF (an absent namespace means nothing is
|
|
149
|
-
* recorded), which is the opposite of every other preference in this file and
|
|
150
|
-
* is the whole point: telemetry is opt-in, not opt-out. Read by
|
|
151
|
-
* main.js telemetryEnabled() through getTelemetryOptIn(). */
|
|
152
|
-
const TELEMETRY_NAMESPACE = '__telemetry';
|
|
153
|
-
|
|
154
|
-
/** Reserved namespace for the auto-drain switch:
|
|
155
|
-
* `{ enabled, intervalMs }` — app-level, not a provider.
|
|
156
|
-
*
|
|
157
|
-
* DEFAULT ON (user request: the queue must work with no click and nothing on
|
|
158
|
-
* screen). An absent namespace means "drain in the background"; only an
|
|
159
|
-
* explicit `enabled: false` disarms it, and that is what the settings store
|
|
160
|
-
* writes when someone turns the worker off from the CLI or by hand.
|
|
161
|
-
*
|
|
162
|
-
* The guards are what make that default defensible, and they live in
|
|
163
|
-
* `lib/auto.js`, not here: a tick skips when the queue is empty (so an idle
|
|
164
|
-
* install costs one JSON parse and no lock I/O), when another host holds the
|
|
165
|
-
* drain lock, when a drain is already in flight, and when the switch was
|
|
166
|
-
* turned off since the last tick — the switch is re-read on EVERY tick, so
|
|
167
|
-
* disarming stops the very next one. Nothing secret lives here, so no
|
|
168
|
-
* encryption — same shape as `__confirmMode`/`__memoryPersist` above. */
|
|
169
|
-
const AUTO_DRAIN_NAMESPACE = '__autoDrain';
|
|
170
|
-
|
|
171
|
-
/** Reserved namespace for the auto-mint (seal-on-quiet) switch:
|
|
172
|
-
* `{ enabled, publish }` — app-level, not a provider.
|
|
173
|
-
*
|
|
174
|
-
* `enabled` is DEFAULT ON, same as auto-drain: a session seals itself once it
|
|
175
|
-
* has been quiet, with nothing on screen asking for it. Only an explicit
|
|
176
|
-
* `enabled: false` disarms it.
|
|
177
|
-
*
|
|
178
|
-
* `publish` stays a separate, DEFAULT OFF switch: sealing writes a local
|
|
179
|
-
* record, whereas publishing sends those bytes to the vault over `rclone` and
|
|
180
|
-
* writes an anchor beside them. An automatic network copy is never implied by
|
|
181
|
-
* an automatic seal.
|
|
182
|
-
*
|
|
183
|
-
* The AI half of the record resolves through the shared rungs in
|
|
184
|
-
* client/attest.js (`$AEGIS_AI_SECRET`/`$AEGIS_AI_MODEL` in the app scope) —
|
|
185
|
-
* this namespace deliberately does NOT hold a credential, because a settings
|
|
186
|
-
* file that can carry a provider key is a second place to leak one. */
|
|
187
|
-
const AUTO_MINT_NAMESPACE = '__autoMint';
|
|
188
|
-
|
|
189
|
-
/** How often an armed auto-drain looks at the queue. One minute is the
|
|
190
|
-
* compromise the two failure modes set: a shorter interval makes the desktop
|
|
191
|
-
* a busy-waiter over a file several hosts write (each tick stats the queue and
|
|
192
|
-
* the lock), and a longer one means a task queued in the card sits visibly
|
|
193
|
-
* idle. The worker does the rest of the waiting itself. */
|
|
194
|
-
const AUTO_DRAIN_INTERVAL_DEFAULT_MS = 60_000;
|
|
195
|
-
|
|
196
|
-
/** Floor for `intervalMs`. Below this the timer is a poll loop, not a drain
|
|
197
|
-
* cadence: nothing about a queue whose tasks take minutes to run changes
|
|
198
|
-
* fifteen times a second, and the tick does file I/O. */
|
|
199
|
-
const AUTO_DRAIN_INTERVAL_MIN_MS = 15_000;
|
|
200
|
-
|
|
201
|
-
/** Ceiling for `intervalMs` — half an hour. A hand-edited settings file asking
|
|
202
|
-
* for a day is a typo, not a preference. */
|
|
203
|
-
const AUTO_DRAIN_INTERVAL_MAX_MS = 30 * 60_000;
|
|
204
|
-
|
|
205
|
-
/** Reserved namespaces: the provider-config surface must never see or mutate
|
|
206
|
-
* any of these. The two automation switches are here for the SAME reason the
|
|
207
|
-
* others are — `set()`/`remove()`/`list()` operate on provider rows, and a
|
|
208
|
-
* preference parked in that namespace would be listed as a provider and
|
|
209
|
-
* deletable with "Remove". */
|
|
210
|
-
const RESERVED_NAMESPACES = Object.freeze([
|
|
211
|
-
AEGIS_KEY_NAMESPACE,
|
|
212
|
-
LEGACY_AEGIS_NAMESPACE,
|
|
213
|
-
QUICK_LAUNCHER_NAMESPACE,
|
|
214
|
-
VAULT_BACKEND_NAMESPACE,
|
|
215
|
-
CONFIRM_MODE_NAMESPACE,
|
|
216
|
-
MEMORY_PERSIST_NAMESPACE,
|
|
217
|
-
AVATAR_NAMESPACE,
|
|
218
|
-
TELEMETRY_NAMESPACE,
|
|
219
|
-
AUTO_DRAIN_NAMESPACE,
|
|
220
|
-
AUTO_MINT_NAMESPACE,
|
|
221
|
-
]);
|
|
222
|
-
|
|
223
|
-
/** Motion modes the renderer understands (`system` = follow the OS). */
|
|
224
|
-
const AVATAR_MOTION_MODES = Object.freeze(['system', 'full', 'minimal']);
|
|
225
|
-
|
|
226
|
-
/** True for the AEGIS-key namespace(s) — provider CRUD must refuse these. */
|
|
227
|
-
function isReservedNamespace(provider) {
|
|
228
|
-
return RESERVED_NAMESPACES.includes(provider);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/** Same masking shape as desktop/main.js so previews are consistent. */
|
|
232
|
-
function maskKey(key) {
|
|
233
|
-
if (!key) return null;
|
|
234
|
-
if (key.length <= 10) return 'configured';
|
|
235
|
-
return `${key.slice(0, 9)}\u2026${key.slice(-4)}`;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const IS_WIN32 = process.platform === 'win32';
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Restrict the settings file to the current user. POSIX enforces 0600 at the
|
|
242
|
-
* kernel; Windows ignores POSIX modes entirely, so an equivalent ACL is applied
|
|
243
|
-
* via icacls (inheritance removed, only the current user granted full control).
|
|
244
|
-
* A failure is reported — never silently swallowed — because a credential file
|
|
245
|
-
* that did not get restricted must not look like it did.
|
|
246
|
-
*/
|
|
247
|
-
function restrictFile(file) {
|
|
248
|
-
if (IS_WIN32) {
|
|
249
|
-
const user = process.env.USERNAME || process.env.USER || process.env.USERDOMAIN;
|
|
250
|
-
if (!user) {
|
|
251
|
-
console.error(
|
|
252
|
-
`[settings] cannot restrict ${file}: no current user to grant (USERNAME unset)`
|
|
253
|
-
);
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
try {
|
|
257
|
-
execFileSync('icacls', [file, '/inheritance:r', '/grant:r', `${user}:(F)`], {
|
|
258
|
-
stdio: 'ignore',
|
|
259
|
-
});
|
|
260
|
-
} catch (err) {
|
|
261
|
-
console.error(`[settings] failed to restrict ACL on ${file}: ${err.message}`);
|
|
262
|
-
}
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
try {
|
|
266
|
-
fs.chmodSync(file, 0o600);
|
|
267
|
-
} catch (err) {
|
|
268
|
-
console.error(`[settings] failed to chmod ${file} to 0600: ${err.message}`);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function createSettingsStore({ dir, safeStorage, prefsDir, env } = {}) {
|
|
273
|
-
if (!dir) throw new Error('createSettingsStore requires a storage dir');
|
|
274
|
-
const file = path.join(dir, SETTINGS_FILE);
|
|
275
|
-
|
|
276
|
-
// --- the shared, non-secret preference store (AEGISONE D5) --------------
|
|
277
|
-
//
|
|
278
|
-
// `prefsDir` is the injectable seam (tests point it at a temp dir); the
|
|
279
|
-
// default is the same `$AEGISCODE_HOME` / `~/.aegiscode` directory the
|
|
280
|
-
// session store and the credential store already use, so an install has one
|
|
281
|
-
// answer to "where does this host keep its data".
|
|
282
|
-
const sharedDir = (() => {
|
|
283
|
-
const explicit = prefsDir === undefined || prefsDir === null ? '' : String(prefsDir).trim();
|
|
284
|
-
return explicit ? path.resolve(explicit) : credentials.aegisHome(env);
|
|
285
|
-
})();
|
|
286
|
-
|
|
287
|
-
// One-way, once, idempotent adoption of a pre-D5 `<userData>/settings.json`
|
|
288
|
-
// non-secret half (D5, step 2): an existing user's preference survives the
|
|
289
|
-
// upgrade. Best-effort — a store must still open when the legacy file is
|
|
290
|
-
// unreadable or the shared dir is unwritable.
|
|
291
|
-
const adoption = (() => {
|
|
292
|
-
try {
|
|
293
|
-
return prefs.adopt({ dir: sharedDir, legacyDir: dir });
|
|
294
|
-
} catch (err) {
|
|
295
|
-
// Best-effort, but NOT silent (AEGISONE ON8). Swallowing this throw is how
|
|
296
|
-
// `prefs.adopt()`'s refusal to write its own ledger went unnoticed — the
|
|
297
|
-
// migration failed for every install that had something to adopt, and the
|
|
298
|
-
// store reported a healthy `adopted: false`. A failed migration is a
|
|
299
|
-
// preference a user set that this launch did not honor, so it is reported
|
|
300
|
-
// where a developer will see it; a store must still open either way.
|
|
301
|
-
if (process.env.AEGISCODE_DEBUG || process.env.AEGIS_HIST_DEBUG) {
|
|
302
|
-
process.stderr.write(`aegis-desktop: shared-preference adoption failed: ${err.message}\n`);
|
|
303
|
-
}
|
|
304
|
-
return { adopted: false, keys: [], reason: `adoption failed: ${err.message}` };
|
|
305
|
-
}
|
|
306
|
-
})();
|
|
307
|
-
|
|
308
|
-
/** The shared preference file this store reads and writes (never a secret). */
|
|
309
|
-
function sharedFile() {
|
|
310
|
-
return prefs.prefsFile({ dir: sharedDir });
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function loadShared() {
|
|
314
|
-
try {
|
|
315
|
-
return prefs.load({ dir: sharedDir });
|
|
316
|
-
} catch {
|
|
317
|
-
return {};
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* One shared preference, as the CLI's `config.js` stores it. Restricted to
|
|
323
|
-
* `SHARED_PREF_KEYS` — the window may read exactly the preferences the shell
|
|
324
|
-
* owns, not reach into arbitrary state in a file another host reads.
|
|
325
|
-
*/
|
|
326
|
-
function getSharedPref(key) {
|
|
327
|
-
if (!prefs.SHARED_PREF_KEYS.includes(key)) {
|
|
328
|
-
throw new Error(`"${key}" is not a shared preference (see client/prefs.js SHARED_PREF_KEYS)`);
|
|
329
|
-
}
|
|
330
|
-
const value = loadShared()[key];
|
|
331
|
-
return value === undefined ? null : value;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/** Write one shared preference. Key material is refused by the store itself. */
|
|
335
|
-
function setSharedPref(key, value) {
|
|
336
|
-
if (!prefs.SHARED_PREF_KEYS.includes(key)) {
|
|
337
|
-
throw new Error(`"${key}" is not a shared preference (see client/prefs.js SHARED_PREF_KEYS)`);
|
|
338
|
-
}
|
|
339
|
-
prefs.set({ [key]: value }, { dir: sharedDir });
|
|
340
|
-
return getSharedPref(key);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const safeAvailable = () =>
|
|
344
|
-
Boolean(
|
|
345
|
-
safeStorage &&
|
|
346
|
-
typeof safeStorage.isEncryptionAvailable === 'function' &&
|
|
347
|
-
safeStorage.isEncryptionAvailable()
|
|
348
|
-
);
|
|
349
|
-
|
|
350
|
-
function encrypt(value) {
|
|
351
|
-
if (value == null) return null;
|
|
352
|
-
if (safeAvailable()) {
|
|
353
|
-
return safeStorage.encryptString(String(value)).toString('base64');
|
|
354
|
-
}
|
|
355
|
-
return Buffer.from(String(value), 'utf8').toString('base64');
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function decrypt(value) {
|
|
359
|
-
if (!value) return null;
|
|
360
|
-
try {
|
|
361
|
-
const buf = Buffer.from(String(value), 'base64');
|
|
362
|
-
if (safeAvailable()) return safeStorage.decryptString(buf);
|
|
363
|
-
return buf.toString('utf8');
|
|
364
|
-
} catch {
|
|
365
|
-
return null;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function load() {
|
|
370
|
-
try {
|
|
371
|
-
return JSON.parse(fs.readFileSync(file, 'utf8')) || {};
|
|
372
|
-
} catch {
|
|
373
|
-
return {};
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function save(data) {
|
|
378
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
379
|
-
const tmp = `${file}.tmp-${process.pid}`;
|
|
380
|
-
// 0600, written before the rename so the file is never briefly readable:
|
|
381
|
-
// this store holds provider keys, and both hosts are now able to write it
|
|
382
|
-
// (the CLI uses the same file, without Electron's safeStorage, so at-rest
|
|
383
|
-
// base64 is all the protection there is — file mode is the real control).
|
|
384
|
-
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
385
|
-
fs.renameSync(tmp, file);
|
|
386
|
-
restrictFile(file);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/** Provider CRUD must never touch a reserved (AEGIS-key) namespace. */
|
|
390
|
-
function assertNotReserved(provider) {
|
|
391
|
-
if (isReservedNamespace(provider)) {
|
|
392
|
-
throw new Error(
|
|
393
|
-
`reserved namespace "${provider}" is not a provider config — ` +
|
|
394
|
-
'use setAegisKey()/aegisRawKey() for the AEGIS key'
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function get(provider) {
|
|
400
|
-
const cfg = (load()[provider]) || {};
|
|
401
|
-
const key = decrypt(cfg.key);
|
|
402
|
-
return {
|
|
403
|
-
provider,
|
|
404
|
-
baseURL: cfg.baseURL || '',
|
|
405
|
-
configured: Boolean(key),
|
|
406
|
-
keyMask: maskKey(key),
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Write a provider row.
|
|
412
|
-
*
|
|
413
|
-
* The endpoint policy lives at this seam for exactly one namespace: `local`,
|
|
414
|
-
* the row holding the local model server's base URL. It is refused HERE so
|
|
415
|
-
* the unusable configuration cannot be created — a row that could never
|
|
416
|
-
* legally be dialed should not be persistable, because it would sit on disk
|
|
417
|
-
* looking like a working setup. The engine re-checks before every call as
|
|
418
|
-
* well (a file hand-edited after it was written cannot be spent either), so
|
|
419
|
-
* there are two gates for two failure modes.
|
|
420
|
-
*
|
|
421
|
-
* Every other row is a `byok:<provider>`, and those stay policy-free: they are
|
|
422
|
-
* remote by definition and reach their provider through AEGIS's relay, and the
|
|
423
|
-
* relay is what bills the turn — so a remote address there is correct rather
|
|
424
|
-
* than a misconfiguration. `baseURL` stays in the record shape for back-compat
|
|
425
|
-
* with rows already on disk.
|
|
426
|
-
*/
|
|
427
|
-
function set(provider, { baseURL, key } = {}) {
|
|
428
|
-
assertNotReserved(provider);
|
|
429
|
-
let nextBaseURL = baseURL;
|
|
430
|
-
if (provider === LOCAL_NAMESPACE && baseURL) {
|
|
431
|
-
const refusal = localTransport.remoteRefusal(baseURL);
|
|
432
|
-
if (refusal) throw refusal;
|
|
433
|
-
// Persist the DIALABLE spelling, not the typed one: `localhost:11434` is
|
|
434
|
-
// how a user writes the address, and `http://localhost:11434` is what the
|
|
435
|
-
// probe and the chat call must dial (local.js `normalizeBase`, whose
|
|
436
|
-
// header explains why a scheme-less string is the common case rather than
|
|
437
|
-
// an edge one). Storing the raw form left every consumer to normalize for
|
|
438
|
-
// itself — and a consumer that forgot read the URL as a remote host.
|
|
439
|
-
nextBaseURL = localTransport.normalizeBase(baseURL);
|
|
440
|
-
}
|
|
441
|
-
const data = load();
|
|
442
|
-
const cfg = data[provider] || {};
|
|
443
|
-
if (nextBaseURL !== undefined) cfg.baseURL = nextBaseURL;
|
|
444
|
-
if (key !== undefined) cfg.key = key ? encrypt(key) : null;
|
|
445
|
-
data[provider] = cfg;
|
|
446
|
-
save(data);
|
|
447
|
-
return get(provider);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function rawKey(provider) {
|
|
451
|
-
const cfg = load()[provider] || {};
|
|
452
|
-
return decrypt(cfg.key);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function remove(provider) {
|
|
456
|
-
assertNotReserved(provider);
|
|
457
|
-
const data = load();
|
|
458
|
-
delete data[provider];
|
|
459
|
-
save(data);
|
|
460
|
-
return { ok: true };
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/** Every provider config, never the AEGIS key (defect #1). */
|
|
464
|
-
function list() {
|
|
465
|
-
const data = load();
|
|
466
|
-
return Object.keys(data)
|
|
467
|
-
.filter((p) => !isReservedNamespace(p))
|
|
468
|
-
.map((p) => get(p));
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// --- AEGIS key: separate namespace, separate accessors -------------------
|
|
472
|
-
|
|
473
|
-
function aegisKey() {
|
|
474
|
-
const cfg = load()[AEGIS_KEY_NAMESPACE] || {};
|
|
475
|
-
const key = decrypt(cfg.key);
|
|
476
|
-
return { configured: Boolean(key), keyMask: maskKey(key) };
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/** Main-process-only: the decrypted AEGIS key. Never crosses IPC. */
|
|
480
|
-
function aegisRawKey() {
|
|
481
|
-
const cfg = load()[AEGIS_KEY_NAMESPACE] || {};
|
|
482
|
-
return decrypt(cfg.key);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* When this host last wrote its own AEGIS key (ISO string, or null).
|
|
487
|
-
*
|
|
488
|
-
* The shared credential file records the same thing, and startup compares the
|
|
489
|
-
* two so the most recently saved key wins. Without a stamp the app cannot tell
|
|
490
|
-
* "I saved this last week" from "the user ran `aegiscode login` a minute ago
|
|
491
|
-
* with a rotated key", and would silently keep presenting the stale one.
|
|
492
|
-
*/
|
|
493
|
-
function aegisKeySavedAt() {
|
|
494
|
-
const cfg = load()[AEGIS_KEY_NAMESPACE] || {};
|
|
495
|
-
return cfg.savedAt || null;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function setAegisKey(key) {
|
|
499
|
-
const data = load();
|
|
500
|
-
data[AEGIS_KEY_NAMESPACE] = {
|
|
501
|
-
...(data[AEGIS_KEY_NAMESPACE] || {}),
|
|
502
|
-
key: key ? encrypt(key) : null,
|
|
503
|
-
savedAt: key ? new Date().toISOString() : null,
|
|
504
|
-
};
|
|
505
|
-
save(data);
|
|
506
|
-
return aegisKey();
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/**
|
|
510
|
-
* Relocate a pre-fix `settings['aegis'].key` into the reserved namespace and
|
|
511
|
-
* drop the pseudo-provider entry. Moved at ciphertext level (no decrypt /
|
|
512
|
-
* re-encrypt), so it is safe to run before Electron's app 'ready' — when
|
|
513
|
-
* safeStorage is not usable yet. Idempotent; never throws.
|
|
514
|
-
*/
|
|
515
|
-
function migrateLegacyAegisKey() {
|
|
516
|
-
try {
|
|
517
|
-
const data = load();
|
|
518
|
-
if (!data[LEGACY_AEGIS_NAMESPACE]) return { migrated: false };
|
|
519
|
-
const legacy = data[LEGACY_AEGIS_NAMESPACE] || {};
|
|
520
|
-
const current = data[AEGIS_KEY_NAMESPACE] || {};
|
|
521
|
-
const next = { ...data };
|
|
522
|
-
if (legacy.key && !current.key) {
|
|
523
|
-
next[AEGIS_KEY_NAMESPACE] = { ...current, key: legacy.key };
|
|
524
|
-
}
|
|
525
|
-
delete next[LEGACY_AEGIS_NAMESPACE];
|
|
526
|
-
save(next);
|
|
527
|
-
return { migrated: true };
|
|
528
|
-
} catch {
|
|
529
|
-
return { migrated: false };
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// --- Quick launcher: reserved namespace, plain (unencrypted) config ------
|
|
534
|
-
// No secret lives here — just a bool and a shortcut string — so unlike the
|
|
535
|
-
// AEGIS key above there is nothing to encrypt/decrypt.
|
|
536
|
-
|
|
537
|
-
function quickLauncherConfig() {
|
|
538
|
-
const cfg = load()[QUICK_LAUNCHER_NAMESPACE] || {};
|
|
539
|
-
const shortcut =
|
|
540
|
-
typeof cfg.shortcut === 'string' && cfg.shortcut.trim()
|
|
541
|
-
? cfg.shortcut.trim()
|
|
542
|
-
: DEFAULT_QUICK_LAUNCHER_SHORTCUT;
|
|
543
|
-
return { enabled: Boolean(cfg.enabled), shortcut };
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function setQuickLauncherConfig({ enabled, shortcut } = {}) {
|
|
547
|
-
const data = load();
|
|
548
|
-
data[QUICK_LAUNCHER_NAMESPACE] = {
|
|
549
|
-
enabled: Boolean(enabled),
|
|
550
|
-
shortcut:
|
|
551
|
-
typeof shortcut === 'string' && shortcut.trim()
|
|
552
|
-
? shortcut.trim()
|
|
553
|
-
: DEFAULT_QUICK_LAUNCHER_SHORTCUT,
|
|
554
|
-
};
|
|
555
|
-
save(data);
|
|
556
|
-
return quickLauncherConfig();
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// --- Vault backend: reserved namespace, plain (unencrypted) preference ---
|
|
560
|
-
// No secret lives here either — just which of three named destinations a
|
|
561
|
-
// publish goes to — so like the quick launcher there is nothing to encrypt.
|
|
562
|
-
|
|
563
|
-
function vaultBackendConfig() {
|
|
564
|
-
const cfg = load()[VAULT_BACKEND_NAMESPACE] || {};
|
|
565
|
-
const backend = VAULT_BACKEND_VALUES.includes(cfg.backend) ? cfg.backend : DEFAULT_VAULT_BACKEND;
|
|
566
|
-
return { backend };
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function setVaultBackendConfig(backend) {
|
|
570
|
-
if (!VAULT_BACKEND_VALUES.includes(backend)) return vaultBackendConfig();
|
|
571
|
-
const data = load();
|
|
572
|
-
data[VAULT_BACKEND_NAMESPACE] = { backend };
|
|
573
|
-
save(data);
|
|
574
|
-
return vaultBackendConfig();
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// --- Confirm mode: SHARED preference (AEGISONE ON8, decision D8 rule 1) --
|
|
578
|
-
// "Confirm before running tools" — the switch for the renderer's tool-call
|
|
579
|
-
// approval gate (desktop/lib/local/engine.js gatedExecuteTool). ON (default,
|
|
580
|
-
// and the behaviour shipped so far) means every mutating tool call — exec /
|
|
581
|
-
// writeFile / editFile — is previewed and approved by the user first. OFF
|
|
582
|
-
// means the engine runs them straight through, like a tool the session
|
|
583
|
-
// already allowed. Nothing secret is stored here, so no encryption.
|
|
584
|
-
//
|
|
585
|
-
// This preference MOVED to the shared store (`confirmMode`) because D8 rule 1
|
|
586
|
-
// is "one posture, two faces": a rule set in the window has to be the rule the
|
|
587
|
-
// shell honors. The pre-ON8 `__confirmMode` namespace in `<userData>` is
|
|
588
|
-
// adopted once (client/prefs.js `extractNonSecretHalf`) and still read as a
|
|
589
|
-
// fallback, never written — a second writer is the split-brain this replaced.
|
|
590
|
-
// The gate's polarity is unchanged: ON when unset, in both places, so an
|
|
591
|
-
// install that never touched the switch keeps the gate exactly as it was.
|
|
592
|
-
|
|
593
|
-
function confirmModeState() {
|
|
594
|
-
const shared = loadShared();
|
|
595
|
-
if (typeof shared.confirmMode === 'boolean') {
|
|
596
|
-
return { enabled: shared.confirmMode, source: 'config' };
|
|
597
|
-
}
|
|
598
|
-
const cfg = load()[CONFIRM_MODE_NAMESPACE] || {};
|
|
599
|
-
if (cfg.enabled === undefined) return { enabled: true, source: 'default' };
|
|
600
|
-
return { enabled: Boolean(cfg.enabled), source: 'config' };
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function getConfirmMode() {
|
|
604
|
-
return confirmModeState().enabled;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
function setConfirmMode(enabled) {
|
|
608
|
-
prefs.set({ confirmMode: Boolean(enabled) }, { dir: sharedDir });
|
|
609
|
-
// One-way: retire the legacy copy in the same breath, so the posture exists
|
|
610
|
-
// in exactly one place from here on (the same retirement the memory-persist
|
|
611
|
-
// preference below does).
|
|
612
|
-
const data = load();
|
|
613
|
-
if (data[CONFIRM_MODE_NAMESPACE] !== undefined) {
|
|
614
|
-
delete data[CONFIRM_MODE_NAMESPACE];
|
|
615
|
-
save(data);
|
|
616
|
-
}
|
|
617
|
-
// The boolean, not the state record: main.js's IPC reply is `{ enabled }`
|
|
618
|
-
// and it wraps the return value itself, so widening this would have put an
|
|
619
|
-
// object inside an object on the wire.
|
|
620
|
-
return confirmModeState().enabled;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
// --- Telemetry: reserved namespace, plain preference, DEFAULT OFF --------
|
|
624
|
-
// The opt-in flag for the funnel instrumentation (Phase 2 of
|
|
625
|
-
// docs/upgrade-conversion-plan.md; the store itself is
|
|
626
|
-
// lib/local/telemetry.js). Unlike confirm mode, the absent case is FALSE:
|
|
627
|
-
// nothing is recorded until someone says so, either here or through
|
|
628
|
-
// `AEGIS_TELEMETRY=1` (main.js telemetryEnabled() reads both, env first).
|
|
629
|
-
// The flag only says "may this install record its own two prompt events to
|
|
630
|
-
// its own userData"; the records stay anonymous either way — see the header
|
|
631
|
-
// of lib/local/telemetry.js for what a record may contain.
|
|
632
|
-
|
|
633
|
-
function getTelemetryOptIn() {
|
|
634
|
-
const cfg = load()[TELEMETRY_NAMESPACE] || {};
|
|
635
|
-
return cfg.enabled === true;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
function setTelemetryOptIn(enabled) {
|
|
639
|
-
const data = load();
|
|
640
|
-
data[TELEMETRY_NAMESPACE] = { enabled: Boolean(enabled) };
|
|
641
|
-
save(data);
|
|
642
|
-
return getTelemetryOptIn();
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// --- Persisting memory: SHARED preference (AEGISONE D5) -----------------
|
|
646
|
-
// The desktop's half of "persisting memory for every account": whether a
|
|
647
|
-
// finished turn is pushed to cloud memory automatically. ON when unset —
|
|
648
|
-
// an install that never touched the toggle behaves like cli/, whose
|
|
649
|
-
// `memoryPersist` also defaults to on. `memoryPersistState()` reports the
|
|
650
|
-
// SOURCE as well as the value so "off" and "never set" stay distinguishable.
|
|
651
|
-
//
|
|
652
|
-
// This preference is the seam D5 closes: it lives in the SHARED store under
|
|
653
|
-
// the shell's own key (`memoryPersist`), so `/cloud memory off` in the
|
|
654
|
-
// terminal and the Settings toggle in the window are one value, not two that
|
|
655
|
-
// can disagree. The pre-D5 `__memoryPersist` namespace in `<userData>` is
|
|
656
|
-
// adopted once (above) and still read as a fallback, never written — a second
|
|
657
|
-
// writer is exactly the split-brain this replaced. lib/sync/persist-gate.js
|
|
658
|
-
// reads the same shared key straight off disk, in the main process, and the
|
|
659
|
-
// two must agree on the default.
|
|
660
|
-
|
|
661
|
-
function getMemoryPersist() {
|
|
662
|
-
return memoryPersistState().enabled;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
/** `{ enabled, source }` — mirrors cli/src/cloudsync.js memoryPersistState(). */
|
|
666
|
-
function memoryPersistState() {
|
|
667
|
-
const shared = loadShared();
|
|
668
|
-
if (typeof shared.memoryPersist === 'boolean') {
|
|
669
|
-
return { enabled: shared.memoryPersist, source: 'config' };
|
|
670
|
-
}
|
|
671
|
-
// Pre-D5 install, or one whose adoption had nothing to move: the legacy
|
|
672
|
-
// namespace still answers until the first write retires it.
|
|
673
|
-
const cfg = load()[MEMORY_PERSIST_NAMESPACE] || {};
|
|
674
|
-
if (cfg.enabled === undefined) return { enabled: true, source: 'default' };
|
|
675
|
-
return { enabled: Boolean(cfg.enabled), source: 'config' };
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
function setMemoryPersist(enabled) {
|
|
679
|
-
prefs.set({ memoryPersist: Boolean(enabled) }, { dir: sharedDir });
|
|
680
|
-
// One-way: drop the legacy copy in the same breath so the preference exists
|
|
681
|
-
// in exactly one place from here on (the same retirement
|
|
682
|
-
// cli/src/config.js's setMemoryPersist() does for the pre-flip `cloudSync`).
|
|
683
|
-
const data = load();
|
|
684
|
-
if (data[MEMORY_PERSIST_NAMESPACE] !== undefined) {
|
|
685
|
-
delete data[MEMORY_PERSIST_NAMESPACE];
|
|
686
|
-
save(data);
|
|
687
|
-
}
|
|
688
|
-
return memoryPersistState();
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// --- Avatar: reserved namespace, plain preferences ------------
|
|
692
|
-
// The companion's on/off switch, its level readout, and its motion mode.
|
|
693
|
-
// Nothing secret lives here, so no encryption — same reasoning as the quick
|
|
694
|
-
// launcher and memory-persist namespaces above.
|
|
695
|
-
//
|
|
696
|
-
// Deliberately narrow: `setAvatar()` stores only these three known keys, so a
|
|
697
|
-
// renderer (or a hand-edited settings.json) cannot park arbitrary state — a
|
|
698
|
-
// persona document, a capability override — in a namespace main reads back.
|
|
699
|
-
// The persona is a separate, validated file (lib/avatar/store.js) for exactly
|
|
700
|
-
// that reason.
|
|
701
|
-
|
|
702
|
-
function getAvatar() {
|
|
703
|
-
const cfg = load()[AVATAR_NAMESPACE] || {};
|
|
704
|
-
return {
|
|
705
|
-
// Unset === true: the companion ships on; an explicit `false` is the only
|
|
706
|
-
// thing that turns it off (and it must survive a reload).
|
|
707
|
-
enabled: cfg.enabled === undefined ? true : Boolean(cfg.enabled),
|
|
708
|
-
showLevel: cfg.showLevel === undefined ? true : Boolean(cfg.showLevel),
|
|
709
|
-
motion: AVATAR_MOTION_MODES.includes(cfg.motion) ? cfg.motion : 'system',
|
|
710
|
-
};
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
/** `{ ...preferences, source }` — mirrors memoryPersistState() so "off" and
|
|
714
|
-
* "never set" stay distinguishable in the UI and in tests. */
|
|
715
|
-
function avatarState() {
|
|
716
|
-
const cfg = load()[AVATAR_NAMESPACE] || {};
|
|
717
|
-
return {
|
|
718
|
-
...getAvatar(),
|
|
719
|
-
source: cfg.enabled === undefined ? 'default' : 'config',
|
|
720
|
-
};
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
/** Partial merge of the three known keys. Unknown keys are dropped, not saved. */
|
|
724
|
-
function setAvatar(patch = {}) {
|
|
725
|
-
const current = getAvatar();
|
|
726
|
-
const p = patch && typeof patch === 'object' ? patch : {};
|
|
727
|
-
const next = {
|
|
728
|
-
enabled: p.enabled === undefined ? current.enabled : Boolean(p.enabled),
|
|
729
|
-
showLevel: p.showLevel === undefined ? current.showLevel : Boolean(p.showLevel),
|
|
730
|
-
motion: AVATAR_MOTION_MODES.includes(p.motion) ? p.motion : current.motion,
|
|
731
|
-
};
|
|
732
|
-
const data = load();
|
|
733
|
-
data[AVATAR_NAMESPACE] = next;
|
|
734
|
-
save(data);
|
|
735
|
-
return avatarState();
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
// --- Auto-drain / auto-mint: reserved namespaces, DEFAULT OFF ------------
|
|
739
|
-
// The two opt-in automations (lib/auto.js). Both live in their own top-level
|
|
740
|
-
// namespace, both are booted OFF, and both keep the "off" and "never set"
|
|
741
|
-
// cases distinguishable through a `source` field — the same shape
|
|
742
|
-
// memoryPersistState()/avatarState() use above, for the same reason: a UI
|
|
743
|
-
// that cannot tell "you turned it off" from "this build never had it" shows
|
|
744
|
-
// the wrong sentence after an upgrade.
|
|
745
|
-
//
|
|
746
|
-
// `intervalMs` is clamped rather than trusted: it comes back through IPC from
|
|
747
|
-
// a renderer and off disk from a hand-edited file, and a timer is the one
|
|
748
|
-
// place where a hostile or fat-fingered number costs real CPU.
|
|
749
|
-
|
|
750
|
-
/** A usable tick interval, or the default. Out-of-range reads as unset. */
|
|
751
|
-
function clampAutoDrainInterval(value) {
|
|
752
|
-
const n = Number(value);
|
|
753
|
-
if (!Number.isFinite(n)) return AUTO_DRAIN_INTERVAL_DEFAULT_MS;
|
|
754
|
-
const ms = Math.round(n);
|
|
755
|
-
if (ms < AUTO_DRAIN_INTERVAL_MIN_MS || ms > AUTO_DRAIN_INTERVAL_MAX_MS) {
|
|
756
|
-
return AUTO_DRAIN_INTERVAL_DEFAULT_MS;
|
|
757
|
-
}
|
|
758
|
-
return ms;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
function getAutoDrain() {
|
|
762
|
-
const cfg = load()[AUTO_DRAIN_NAMESPACE] || {};
|
|
763
|
-
return {
|
|
764
|
-
// Unset === TRUE. The queue drains in the background unless someone
|
|
765
|
-
// switched it off in as many words; `lib/auto.js` re-reads this on every
|
|
766
|
-
// tick, so that off switch stops the very next one.
|
|
767
|
-
enabled: cfg.enabled !== false,
|
|
768
|
-
intervalMs: clampAutoDrainInterval(cfg.intervalMs),
|
|
769
|
-
};
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
function autoDrainState() {
|
|
773
|
-
const cfg = load()[AUTO_DRAIN_NAMESPACE] || {};
|
|
774
|
-
return {
|
|
775
|
-
...getAutoDrain(),
|
|
776
|
-
source: cfg.enabled === undefined ? 'default' : 'config',
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
function setAutoDrain(patch = {}) {
|
|
781
|
-
const current = getAutoDrain();
|
|
782
|
-
const p = patch && typeof patch === 'object' ? patch : {};
|
|
783
|
-
const data = load();
|
|
784
|
-
data[AUTO_DRAIN_NAMESPACE] = {
|
|
785
|
-
enabled: p.enabled === undefined ? current.enabled : Boolean(p.enabled),
|
|
786
|
-
intervalMs:
|
|
787
|
-
p.intervalMs === undefined
|
|
788
|
-
? current.intervalMs
|
|
789
|
-
: clampAutoDrainInterval(p.intervalMs),
|
|
790
|
-
};
|
|
791
|
-
save(data);
|
|
792
|
-
return autoDrainState();
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
function getAutoMint() {
|
|
796
|
-
const cfg = load()[AUTO_MINT_NAMESPACE] || {};
|
|
797
|
-
return {
|
|
798
|
-
// Unset === TRUE (background sealing). `publish` below stays opt-in.
|
|
799
|
-
enabled: cfg.enabled !== false,
|
|
800
|
-
// Independent of `enabled` on purpose: an automatic seal must not drag
|
|
801
|
-
// an automatic upload along with it.
|
|
802
|
-
publish: cfg.publish === true,
|
|
803
|
-
};
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
function autoMintState() {
|
|
807
|
-
const cfg = load()[AUTO_MINT_NAMESPACE] || {};
|
|
808
|
-
return {
|
|
809
|
-
...getAutoMint(),
|
|
810
|
-
source: cfg.enabled === undefined ? 'default' : 'config',
|
|
811
|
-
};
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
function setAutoMint(patch = {}) {
|
|
815
|
-
const current = getAutoMint();
|
|
816
|
-
const p = patch && typeof patch === 'object' ? patch : {};
|
|
817
|
-
const data = load();
|
|
818
|
-
data[AUTO_MINT_NAMESPACE] = {
|
|
819
|
-
enabled: p.enabled === undefined ? current.enabled : Boolean(p.enabled),
|
|
820
|
-
publish: p.publish === undefined ? current.publish : Boolean(p.publish),
|
|
821
|
-
};
|
|
822
|
-
save(data);
|
|
823
|
-
return autoMintState();
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
return {
|
|
827
|
-
file,
|
|
828
|
-
sharedDir,
|
|
829
|
-
adoption,
|
|
830
|
-
sharedFile,
|
|
831
|
-
getSharedPref,
|
|
832
|
-
setSharedPref,
|
|
833
|
-
get,
|
|
834
|
-
set,
|
|
835
|
-
rawKey,
|
|
836
|
-
remove,
|
|
837
|
-
list,
|
|
838
|
-
maskKey,
|
|
839
|
-
aegisKey,
|
|
840
|
-
aegisRawKey,
|
|
841
|
-
aegisKeySavedAt,
|
|
842
|
-
setAegisKey,
|
|
843
|
-
migrateLegacyAegisKey,
|
|
844
|
-
quickLauncherConfig,
|
|
845
|
-
setQuickLauncherConfig,
|
|
846
|
-
vaultBackendConfig,
|
|
847
|
-
setVaultBackendConfig,
|
|
848
|
-
getConfirmMode,
|
|
849
|
-
setConfirmMode,
|
|
850
|
-
confirmModeState,
|
|
851
|
-
getTelemetryOptIn,
|
|
852
|
-
setTelemetryOptIn,
|
|
853
|
-
getMemoryPersist,
|
|
854
|
-
memoryPersistState,
|
|
855
|
-
setMemoryPersist,
|
|
856
|
-
getAvatar,
|
|
857
|
-
avatarState,
|
|
858
|
-
setAvatar,
|
|
859
|
-
getAutoDrain,
|
|
860
|
-
autoDrainState,
|
|
861
|
-
setAutoDrain,
|
|
862
|
-
getAutoMint,
|
|
863
|
-
autoMintState,
|
|
864
|
-
setAutoMint,
|
|
865
|
-
clampAutoDrainInterval,
|
|
866
|
-
};
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
module.exports = {
|
|
870
|
-
SETTINGS_FILE,
|
|
871
|
-
AEGIS_KEY_NAMESPACE,
|
|
872
|
-
LEGACY_AEGIS_NAMESPACE,
|
|
873
|
-
QUICK_LAUNCHER_NAMESPACE,
|
|
874
|
-
VAULT_BACKEND_NAMESPACE,
|
|
875
|
-
CONFIRM_MODE_NAMESPACE,
|
|
876
|
-
MEMORY_PERSIST_NAMESPACE,
|
|
877
|
-
AVATAR_NAMESPACE,
|
|
878
|
-
TELEMETRY_NAMESPACE,
|
|
879
|
-
AUTO_DRAIN_NAMESPACE,
|
|
880
|
-
AUTO_MINT_NAMESPACE,
|
|
881
|
-
AUTO_DRAIN_INTERVAL_DEFAULT_MS,
|
|
882
|
-
AUTO_DRAIN_INTERVAL_MIN_MS,
|
|
883
|
-
AUTO_DRAIN_INTERVAL_MAX_MS,
|
|
884
|
-
AVATAR_MOTION_MODES,
|
|
885
|
-
DEFAULT_QUICK_LAUNCHER_SHORTCUT,
|
|
886
|
-
DEFAULT_VAULT_BACKEND,
|
|
887
|
-
VAULT_BACKEND_VALUES,
|
|
888
|
-
RESERVED_NAMESPACES,
|
|
889
|
-
isReservedNamespace,
|
|
890
|
-
maskKey,
|
|
891
|
-
createSettingsStore,
|
|
892
|
-
};
|
|
3
|
+
"use strict";const fs=require("node:fs"),path=require("node:path"),{execFileSync:execFileSync}=require("node:child_process"),prefs=(()=>{try{return require("../../client/prefs.js")}catch{return require("../vendor/prefs.js")}})(),credentials=(()=>{try{return require("../../client/credentials.js")}catch{return require("../vendor/credentials.js")}})(),localTransport=require("./local/local.js"),LOCAL_NAMESPACE="local",SETTINGS_FILE="settings.json",AEGIS_KEY_NAMESPACE="__aegis",LEGACY_AEGIS_NAMESPACE="aegis",QUICK_LAUNCHER_NAMESPACE="__quickLauncher",DEFAULT_QUICK_LAUNCHER_SHORTCUT="CmdOrCtrl+Shift+Space",VAULT_BACKEND_NAMESPACE="__vaultBackend",DEFAULT_VAULT_BACKEND="aegiscloud",VAULT_BACKEND_VALUES=Object.freeze(["aegiscloud","drive","off"]),CONFIRM_MODE_NAMESPACE="__confirmMode",MEMORY_PERSIST_NAMESPACE="__memoryPersist",AVATAR_NAMESPACE="__avatar",TELEMETRY_NAMESPACE="__telemetry",AUTO_DRAIN_NAMESPACE="__autoDrain",AUTO_MINT_NAMESPACE="__autoMint",AUTO_DRAIN_INTERVAL_DEFAULT_MS=6e4,AUTO_DRAIN_INTERVAL_MIN_MS=15e3,AUTO_DRAIN_INTERVAL_MAX_MS=18e5,RESERVED_NAMESPACES=Object.freeze(["__aegis","aegis","__quickLauncher","__vaultBackend","__confirmMode","__memoryPersist","__avatar","__telemetry","__autoDrain","__autoMint"]),AVATAR_MOTION_MODES=Object.freeze(["system","full","minimal"]);function isReservedNamespace(e){return RESERVED_NAMESPACES.includes(e)}function maskKey(e){return e?e.length<=10?"configured":`${e.slice(0,9)}…${e.slice(-4)}`:null}const IS_WIN32="win32"===process.platform;function restrictFile(e){if(IS_WIN32){const t=process.env.USERNAME||process.env.USER||process.env.USERDOMAIN;if(!t)return void console.error(`[settings] cannot restrict ${e}: no current user to grant (USERNAME unset)`);try{execFileSync("icacls",[e,"/inheritance:r","/grant:r",`${t}:(F)`],{stdio:"ignore"})}catch(t){console.error(`[settings] failed to restrict ACL on ${e}: ${t.message}`)}}else try{fs.chmodSync(e,384)}catch(t){console.error(`[settings] failed to chmod ${e} to 0600: ${t.message}`)}}function createSettingsStore({dir:e,safeStorage:t,prefsDir:n,env:r}={}){if(!e)throw new Error("createSettingsStore requires a storage dir");const o=path.join(e,SETTINGS_FILE),i=(()=>{const e=null==n?"":String(n).trim();return e?path.resolve(e):credentials.aegisHome(r)})(),s=(()=>{try{return prefs.adopt({dir:i,legacyDir:e})}catch(e){return(process.env.AEGISCODE_DEBUG||process.env.AEGIS_HIST_DEBUG)&&process.stderr.write(`aegis-desktop: shared-preference adoption failed: ${e.message}\n`),{adopted:!1,keys:[],reason:`adoption failed: ${e.message}`}}})();function a(){try{return prefs.load({dir:i})}catch{return{}}}function c(e){if(!prefs.SHARED_PREF_KEYS.includes(e))throw new Error(`"${e}" is not a shared preference (see client/prefs.js SHARED_PREF_KEYS)`);const t=a()[e];return void 0===t?null:t}const u=()=>Boolean(t&&"function"==typeof t.isEncryptionAvailable&&t.isEncryptionAvailable());function _(e){return null==e?null:u()?t.encryptString(String(e)).toString("base64"):Buffer.from(String(e),"utf8").toString("base64")}function l(e){if(!e)return null;try{const n=Buffer.from(String(e),"base64");return u()?t.decryptString(n):n.toString("utf8")}catch{return null}}function d(){try{return JSON.parse(fs.readFileSync(o,"utf8"))||{}}catch{return{}}}function A(e){fs.mkdirSync(path.dirname(o),{recursive:!0});const t=`${o}.tmp-${process.pid}`;fs.writeFileSync(t,JSON.stringify(e,null,2),{mode:384}),fs.renameSync(t,o),restrictFile(o)}function f(e){if(isReservedNamespace(e))throw new Error(`reserved namespace "${e}" is not a provider config — use setAegisKey()/aegisRawKey() for the AEGIS key`)}function E(e){const t=d()[e]||{},n=l(t.key);return{provider:e,baseURL:t.baseURL||"",configured:Boolean(n),keyMask:maskKey(n)}}function S(){const e=l((d().__aegis||{}).key);return{configured:Boolean(e),keyMask:maskKey(e)}}function m(){const e=d().__quickLauncher||{},t="string"==typeof e.shortcut&&e.shortcut.trim()?e.shortcut.trim():"CmdOrCtrl+Shift+Space";return{enabled:Boolean(e.enabled),shortcut:t}}function M(){const e=d().__vaultBackend||{};return{backend:VAULT_BACKEND_VALUES.includes(e.backend)?e.backend:"aegiscloud"}}function g(){const e=a();if("boolean"==typeof e.confirmMode)return{enabled:e.confirmMode,source:"config"};const t=d().__confirmMode||{};return void 0===t.enabled?{enabled:!0,source:"default"}:{enabled:Boolean(t.enabled),source:"config"}}function y(){return!0===(d().__telemetry||{}).enabled}function N(){const e=a();if("boolean"==typeof e.memoryPersist)return{enabled:e.memoryPersist,source:"config"};const t=d().__memoryPersist||{};return void 0===t.enabled?{enabled:!0,source:"default"}:{enabled:Boolean(t.enabled),source:"config"}}function p(){const e=d().__avatar||{};return{enabled:void 0===e.enabled||Boolean(e.enabled),showLevel:void 0===e.showLevel||Boolean(e.showLevel),motion:AVATAR_MOTION_MODES.includes(e.motion)?e.motion:"system"}}function v(){const e=d().__avatar||{};return{...p(),source:void 0===e.enabled?"default":"config"}}function b(e){const t=Number(e);if(!Number.isFinite(t))return 6e4;const n=Math.round(t);return n<15e3||n>18e5?6e4:n}function T(){const e=d().__autoDrain||{};return{enabled:!1!==e.enabled,intervalMs:b(e.intervalMs)}}function R(){const e=d().__autoDrain||{};return{...T(),source:void 0===e.enabled?"default":"config"}}function C(){const e=d().__autoMint||{};return{enabled:!1!==e.enabled,publish:!0===e.publish}}function h(){const e=d().__autoMint||{};return{...C(),source:void 0===e.enabled?"default":"config"}}return{file:o,sharedDir:i,adoption:s,sharedFile:function(){return prefs.prefsFile({dir:i})},getSharedPref:c,setSharedPref:function(e,t){if(!prefs.SHARED_PREF_KEYS.includes(e))throw new Error(`"${e}" is not a shared preference (see client/prefs.js SHARED_PREF_KEYS)`);return prefs.set({[e]:t},{dir:i}),c(e)},get:E,set:function(e,{baseURL:t,key:n}={}){f(e);let r=t;if("local"===e&&t){const e=localTransport.remoteRefusal(t);if(e)throw e;r=localTransport.normalizeBase(t)}const o=d(),i=o[e]||{};return void 0!==r&&(i.baseURL=r),void 0!==n&&(i.key=n?_(n):null),o[e]=i,A(o),E(e)},rawKey:function(e){return l((d()[e]||{}).key)},remove:function(e){f(e);const t=d();return delete t[e],A(t),{ok:!0}},list:function(){const e=d();return Object.keys(e).filter((e=>!isReservedNamespace(e))).map((e=>E(e)))},maskKey:maskKey,aegisKey:S,aegisRawKey:function(){return l((d().__aegis||{}).key)},aegisKeySavedAt:function(){return(d().__aegis||{}).savedAt||null},setAegisKey:function(e){const t=d();return t.__aegis={...t.__aegis||{},key:e?_(e):null,savedAt:e?(new Date).toISOString():null},A(t),S()},migrateLegacyAegisKey:function(){try{const e=d();if(!e.aegis)return{migrated:!1};const t=e.aegis||{},n=e.__aegis||{},r={...e};return t.key&&!n.key&&(r.__aegis={...n,key:t.key}),delete r.aegis,A(r),{migrated:!0}}catch{return{migrated:!1}}},quickLauncherConfig:m,setQuickLauncherConfig:function({enabled:e,shortcut:t}={}){const n=d();return n.__quickLauncher={enabled:Boolean(e),shortcut:"string"==typeof t&&t.trim()?t.trim():"CmdOrCtrl+Shift+Space"},A(n),m()},vaultBackendConfig:M,setVaultBackendConfig:function(e){if(!VAULT_BACKEND_VALUES.includes(e))return M();const t=d();return t.__vaultBackend={backend:e},A(t),M()},getConfirmMode:function(){return g().enabled},setConfirmMode:function(e){prefs.set({confirmMode:Boolean(e)},{dir:i});const t=d();return void 0!==t.__confirmMode&&(delete t.__confirmMode,A(t)),g().enabled},confirmModeState:g,getTelemetryOptIn:y,setTelemetryOptIn:function(e){const t=d();return t.__telemetry={enabled:Boolean(e)},A(t),y()},getMemoryPersist:function(){return N().enabled},memoryPersistState:N,setMemoryPersist:function(e){prefs.set({memoryPersist:Boolean(e)},{dir:i});const t=d();return void 0!==t.__memoryPersist&&(delete t.__memoryPersist,A(t)),N()},getAvatar:p,avatarState:v,setAvatar:function(e={}){const t=p(),n=e&&"object"==typeof e?e:{},r={enabled:void 0===n.enabled?t.enabled:Boolean(n.enabled),showLevel:void 0===n.showLevel?t.showLevel:Boolean(n.showLevel),motion:AVATAR_MOTION_MODES.includes(n.motion)?n.motion:t.motion},o=d();return o.__avatar=r,A(o),v()},getAutoDrain:T,autoDrainState:R,setAutoDrain:function(e={}){const t=T(),n=e&&"object"==typeof e?e:{},r=d();return r.__autoDrain={enabled:void 0===n.enabled?t.enabled:Boolean(n.enabled),intervalMs:void 0===n.intervalMs?t.intervalMs:b(n.intervalMs)},A(r),R()},getAutoMint:C,autoMintState:h,setAutoMint:function(e={}){const t=C(),n=e&&"object"==typeof e?e:{},r=d();return r.__autoMint={enabled:void 0===n.enabled?t.enabled:Boolean(n.enabled),publish:void 0===n.publish?t.publish:Boolean(n.publish)},A(r),h()},clampAutoDrainInterval:b}}module.exports={SETTINGS_FILE:SETTINGS_FILE,AEGIS_KEY_NAMESPACE:"__aegis",LEGACY_AEGIS_NAMESPACE:"aegis",QUICK_LAUNCHER_NAMESPACE:"__quickLauncher",VAULT_BACKEND_NAMESPACE:"__vaultBackend",CONFIRM_MODE_NAMESPACE:"__confirmMode",MEMORY_PERSIST_NAMESPACE:"__memoryPersist",AVATAR_NAMESPACE:"__avatar",TELEMETRY_NAMESPACE:"__telemetry",AUTO_DRAIN_NAMESPACE:"__autoDrain",AUTO_MINT_NAMESPACE:"__autoMint",AUTO_DRAIN_INTERVAL_DEFAULT_MS:6e4,AUTO_DRAIN_INTERVAL_MIN_MS:15e3,AUTO_DRAIN_INTERVAL_MAX_MS:18e5,AVATAR_MOTION_MODES:AVATAR_MOTION_MODES,DEFAULT_QUICK_LAUNCHER_SHORTCUT:"CmdOrCtrl+Shift+Space",DEFAULT_VAULT_BACKEND:"aegiscloud",VAULT_BACKEND_VALUES:VAULT_BACKEND_VALUES,RESERVED_NAMESPACES:RESERVED_NAMESPACES,isReservedNamespace:isReservedNamespace,maskKey:maskKey,createSettingsStore:createSettingsStore};
|