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.
Files changed (96) hide show
  1. package/SOURCE-OFFER.txt +9 -0
  2. package/bin/aegis.js +1 -32
  3. package/lib/account-key.js +1 -56
  4. package/lib/auto.js +1 -430
  5. package/lib/avatar/companion.js +1 -698
  6. package/lib/avatar/cosmetics.js +1 -532
  7. package/lib/avatar/events.js +1 -344
  8. package/lib/avatar/holder-turn.js +1 -350
  9. package/lib/avatar/holders.js +1 -612
  10. package/lib/avatar/identity.js +1 -641
  11. package/lib/avatar/level.js +1 -368
  12. package/lib/avatar/mirror.js +1 -524
  13. package/lib/avatar/persona.js +1 -388
  14. package/lib/avatar/profile.js +1 -977
  15. package/lib/avatar/register.js +1 -307
  16. package/lib/avatar/store.js +1 -472
  17. package/lib/avatar/tiers.js +1 -38
  18. package/lib/avatar/transfer.js +1 -415
  19. package/lib/avatar/turn.js +1 -216
  20. package/lib/avatar/voice.js +1 -328
  21. package/lib/avatar/xp.js +1 -404
  22. package/lib/deep-link.js +1 -120
  23. package/lib/fingerprint.js +2 -347
  24. package/lib/google-connect.js +1 -169
  25. package/lib/local/agents.js +1 -105
  26. package/lib/local/autonomous.js +1 -1045
  27. package/lib/local/claude-pro.js +1 -993
  28. package/lib/local/context.js +1 -95
  29. package/lib/local/engine.js +1 -3228
  30. package/lib/local/entitlement.js +1 -654
  31. package/lib/local/git-scope.js +1 -228
  32. package/lib/local/local.js +1 -2135
  33. package/lib/local/prompt.js +1 -275
  34. package/lib/local/queue.js +1 -560
  35. package/lib/local/session-rounds.js +1 -208
  36. package/lib/local/shell.js +1 -680
  37. package/lib/local/telemetry.js +1 -470
  38. package/lib/local/tools.js +1 -1238
  39. package/lib/local/turn-guard.js +1 -300
  40. package/lib/local/worktree-lock.js +1 -235
  41. package/lib/provider-key-mirror.js +1 -252
  42. package/lib/quick-launcher.js +1 -84
  43. package/lib/settings.js +1 -890
  44. package/lib/sync/memory-queue.js +1 -60
  45. package/lib/sync/persist-gate.js +1 -148
  46. package/lib/sync/sessions.js +1 -106
  47. package/lib/window-state.js +1 -66
  48. package/main.js +3 -4390
  49. package/package.json +3 -5
  50. package/preload.js +3 -628
  51. package/renderer/app.js +1 -6560
  52. package/renderer/avatar/assemble.js +1 -335
  53. package/renderer/avatar/avatar.js +1 -380
  54. package/renderer/avatar/cards.js +1 -141
  55. package/renderer/avatar/hud.js +1 -206
  56. package/renderer/avatar/machine.js +1 -213
  57. package/renderer/avatar/pane.js +1 -439
  58. package/renderer/avatar/parts.js +1 -437
  59. package/renderer/budget.js +1 -168
  60. package/renderer/companion.js +1 -223
  61. package/renderer/diff.js +1 -359
  62. package/renderer/diffview.js +1 -336
  63. package/renderer/fingerprint.js +3 -720
  64. package/renderer/markdown.js +1 -168
  65. package/renderer/provider-toolbar.js +1 -134
  66. package/renderer/providers.js +1 -259
  67. package/renderer/quick.js +1 -191
  68. package/renderer/stream-policy.js +1 -306
  69. package/renderer/transcript-view.js +1 -323
  70. package/renderer/usage.js +1 -1239
  71. package/vendor/aegis.js +1 -2054
  72. package/vendor/attest.js +1 -442
  73. package/vendor/authorship-key.js +1 -410
  74. package/vendor/auto-provision.js +1 -134
  75. package/vendor/brain-catalog.js +1 -268
  76. package/vendor/byok-catalog.js +1 -230
  77. package/vendor/credentials.js +1 -494
  78. package/vendor/device-fingerprint.js +1 -141
  79. package/vendor/drive-connect.js +1 -85
  80. package/vendor/drive-upload.js +1 -174
  81. package/vendor/env-file.js +1 -1273
  82. package/vendor/fingerprint.js +1 -1064
  83. package/vendor/foreign-memory.js +1 -669
  84. package/vendor/google-signin.js +1 -125
  85. package/vendor/hooks.js +1 -430
  86. package/vendor/model-discovery.js +1 -330
  87. package/vendor/platform.js +1 -937
  88. package/vendor/prefs.js +1 -351
  89. package/vendor/presence-constants.js +1 -107
  90. package/vendor/presence.js +1 -438
  91. package/vendor/record-vault.js +3 -1055
  92. package/vendor/session-store.js +1 -548
  93. package/vendor/topup.js +1 -297
  94. package/vendor/touch.js +1 -458
  95. package/vendor/update.js +1 -704
  96. 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
- // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
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};