aegis-desktop 0.8.29 → 0.8.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/local/autonomous.js +105 -38
- package/lib/local/engine.js +13 -0
- package/lib/local/entitlement.js +99 -16
- package/lib/local/queue.js +20 -16
- package/lib/settings.js +48 -14
- package/main.js +26 -17
- package/package.json +1 -1
- package/preload.js +7 -2
- package/renderer/app.js +152 -4
- package/renderer/index.html +18 -0
- package/vendor/aegis.js +10 -4
- package/vendor/auto-provision.js +12 -5
- package/vendor/prefs.js +32 -2
package/lib/local/autonomous.js
CHANGED
|
@@ -97,48 +97,100 @@ function writtenPath(tool) {
|
|
|
97
97
|
const DEFAULT_MODEL = 'nexus-brain';
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
|
-
* THE QUEUE RUNS ON
|
|
100
|
+
* THE QUEUE RUNS ON WHATEVER CLASS THE TASK NAMES — aegis, byok, or local —
|
|
101
|
+
* the same three classes engine.js's interactive turns serve (engine.js's
|
|
102
|
+
* exported `CLASSES`). This module does not `require('./engine.js')` to read
|
|
103
|
+
* that list itself (same reason queue.js stays engine-free): duplicating the
|
|
104
|
+
* three names here keeps both modules testable with no Electron, no network,
|
|
105
|
+
* and no model.
|
|
101
106
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
107
|
+
* This used to be Aegis Cloud only, because runTask sent every turn out with
|
|
108
|
+
* a hardcoded `class: 'aegis'` — a byok or local model id would have gone out
|
|
109
|
+
* under the wrong transport and failed at the server, or worse, silently
|
|
110
|
+
* picked up the wrong provider's key. Now `class` travels with the task
|
|
111
|
+
* (`item.class`, defaulted to 'aegis' for every task that predates this field
|
|
112
|
+
* — the CLI, a hand-written queue file, the old desktop card), and runTask
|
|
113
|
+
* forwards it verbatim instead of overwriting it.
|
|
109
114
|
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
115
|
+
* Each class validates differently, because each has a different notion of
|
|
116
|
+
* "no model stated is fine":
|
|
117
|
+
* - 'aegis' has a real default (the pooled brain), so a blank model is
|
|
118
|
+
* accepted and resolveModel fills it in. The accept-list is the
|
|
119
|
+
* pooled-brain tier family — the one entry engine.js's offerableCatalog
|
|
120
|
+
* offers for the Aegis Cloud class, plus the `-smart`/`-neo` tier
|
|
121
|
+
* spellings the server still serves as aliases of it (mirrors
|
|
122
|
+
* selectBrainEntry() there rather than re-deriving "anything starting
|
|
123
|
+
* with nexus-": `nexus-fast` is not a tier the catalog has ever served).
|
|
124
|
+
* - 'byok' and 'local' have no such default: a blank byok model cannot be
|
|
125
|
+
* "whichever provider the pool picks" (there is no pool), and a blank
|
|
126
|
+
* local model cannot be "whichever model is pulled" when the answer
|
|
127
|
+
* depends on what the user's own daemon happens to have. Both REQUIRE an
|
|
128
|
+
* explicit model, refused by name instead of silently resolving to
|
|
129
|
+
* something nobody chose.
|
|
117
130
|
*/
|
|
131
|
+
const MODEL_CLASSES = Object.freeze(['aegis', 'byok', 'local']);
|
|
132
|
+
|
|
118
133
|
const AEGIS_MODEL_RE = /^(?:nexus|aegis)-brain(?:-(?:smart|neo))?$/;
|
|
119
134
|
const AEGIS_MODEL_IDS = Object.freeze(['nexus-brain', 'aegis-brain']);
|
|
120
135
|
|
|
121
|
-
/** True when `id` names an AEGIS Cloud pooled-brain tier
|
|
136
|
+
/** True when `id` names an AEGIS Cloud pooled-brain tier. */
|
|
122
137
|
function isAegisModel(id) {
|
|
123
138
|
return AEGIS_MODEL_RE.test(String(id == null ? '' : id).trim());
|
|
124
139
|
}
|
|
125
140
|
|
|
126
141
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
142
|
+
* Normalize a task's stated class to one this module (and engine.js) knows.
|
|
143
|
+
* Blank or absent — every task queued before this field existed — defaults to
|
|
144
|
+
* 'aegis', the only class the queue ever ran before today.
|
|
145
|
+
*/
|
|
146
|
+
function normalizeClass(cls) {
|
|
147
|
+
const stated = String(cls == null ? '' : cls).trim().toLowerCase();
|
|
148
|
+
return stated || 'aegis';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Whether `id` is shaped "<provider>:<model>" — the only form engine.js's
|
|
153
|
+
* byok class accepts (splitByokModel). Not re-deriving the live provider list
|
|
154
|
+
* here (that is a catalog call, `listModels('byok')`, this module has no
|
|
155
|
+
* business caching) — just enough of a shape check that a malformed id is
|
|
156
|
+
* refused at the door instead of failing splitByokModel at the server with no
|
|
157
|
+
* context.
|
|
158
|
+
*/
|
|
159
|
+
function looksLikeByokModel(id) {
|
|
160
|
+
const s = String(id == null ? '' : id).trim();
|
|
161
|
+
const i = s.indexOf(':');
|
|
162
|
+
return i > 0 && i < s.length - 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Why a STATED (model, class) pair cannot be queued, or '' when it can.
|
|
129
167
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* otherwise: `class: 'aegis'` would be sent with an id the server does not
|
|
133
|
-
* serve, and the task would come back as an opaque error minutes later.
|
|
168
|
+
* `cls` defaults to 'aegis' when omitted, so every existing caller of
|
|
169
|
+
* `modelRefusal(id)` keeps its old behaviour verbatim.
|
|
134
170
|
*/
|
|
135
|
-
function modelRefusal(id) {
|
|
171
|
+
function modelRefusal(id, cls) {
|
|
136
172
|
const stated = String(id == null ? '' : id).trim();
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
`
|
|
140
|
-
|
|
141
|
-
)
|
|
173
|
+
const normalized = normalizeClass(cls);
|
|
174
|
+
if (!MODEL_CLASSES.includes(normalized)) {
|
|
175
|
+
return `"${cls}" is not a model class the queue serves (${MODEL_CLASSES.join('/')})`;
|
|
176
|
+
}
|
|
177
|
+
if (normalized === 'aegis') {
|
|
178
|
+
if (!stated || isAegisModel(stated)) return '';
|
|
179
|
+
return (
|
|
180
|
+
`the autonomous queue's Aegis Cloud class runs pooled-brain models only (${DEFAULT_MODEL}, ` +
|
|
181
|
+
`${AEGIS_MODEL_IDS.join('/')} aliases); "${stated}" is not one`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
if (normalized === 'byok') {
|
|
185
|
+
if (!stated) return 'a byok task needs a model, as "<provider>:<model>" (pick one from /models)';
|
|
186
|
+
if (!looksLikeByokModel(stated)) {
|
|
187
|
+
return `"${stated}" is not a byok model id — it must be "<provider>:<model>" (pick one from /models)`;
|
|
188
|
+
}
|
|
189
|
+
return '';
|
|
190
|
+
}
|
|
191
|
+
// normalized === 'local'
|
|
192
|
+
if (!stated) return 'a local task needs a model — the tag of whatever is pulled into the local daemon';
|
|
193
|
+
return '';
|
|
142
194
|
}
|
|
143
195
|
|
|
144
196
|
/** Phrase match for "work autonomously" in a prompt or a queued task. */
|
|
@@ -239,10 +291,14 @@ function withRoundHorizon(rounds, env, fn) {
|
|
|
239
291
|
* task over it would break drains for a reason that is not the task's
|
|
240
292
|
* fault; a fallback to the pooled brain keeps the drain honest and on-cloud.
|
|
241
293
|
*/
|
|
242
|
-
function resolveModel({ model, env } = {}) {
|
|
243
|
-
const e = env || process.env;
|
|
294
|
+
function resolveModel({ model, env, cls } = {}) {
|
|
244
295
|
const picked = String(model || '').trim();
|
|
296
|
+
// byok/local have no cloud default to fall back to — modelRefusal already
|
|
297
|
+
// guaranteed a non-empty, shaped model at queue time, so the task's own pick
|
|
298
|
+
// is the only answer for either class.
|
|
299
|
+
if (normalizeClass(cls) !== 'aegis') return picked;
|
|
245
300
|
if (picked) return picked;
|
|
301
|
+
const e = env || process.env;
|
|
246
302
|
const fromEnv = String(e.AEGIS_AUTONOMOUS_MODEL || e.AEGIS_MODEL || '').trim();
|
|
247
303
|
return isAegisModel(fromEnv) ? fromEnv : DEFAULT_MODEL;
|
|
248
304
|
}
|
|
@@ -255,7 +311,8 @@ function resolveModel({ model, env } = {}) {
|
|
|
255
311
|
* it: the queue falls back to the pool, and the one place that says so is this
|
|
256
312
|
* string, which runTask emits as a note and the desktop card shows.
|
|
257
313
|
*/
|
|
258
|
-
function ignoredEnvModel({ model, env } = {}) {
|
|
314
|
+
function ignoredEnvModel({ model, env, cls } = {}) {
|
|
315
|
+
if (normalizeClass(cls) !== 'aegis') return ''; // AEGIS_AUTONOMOUS_MODEL is an Aegis-only pin
|
|
259
316
|
if (String(model || '').trim()) return ''; // the task's own pick is what counts
|
|
260
317
|
const e = env || process.env;
|
|
261
318
|
const fromEnv = String(e.AEGIS_AUTONOMOUS_MODEL || e.AEGIS_MODEL || '').trim();
|
|
@@ -462,16 +519,23 @@ function createQueueWorker({
|
|
|
462
519
|
// pool does not serve: billed work if it happens to be a per-provider
|
|
463
520
|
// spelling, an opaque server error otherwise. Failing here names the model
|
|
464
521
|
// and the allowed ones, and costs nothing.
|
|
465
|
-
const
|
|
522
|
+
const cls = normalizeClass(item.class);
|
|
523
|
+
const refusal = modelRefusal(item.model, cls);
|
|
466
524
|
if (refusal) {
|
|
467
|
-
const failed = { ok: false, error: refusal, model: item.model, ms: 0 };
|
|
525
|
+
const failed = { ok: false, error: refusal, model: item.model, class: cls, ms: 0 };
|
|
468
526
|
emit({ type: 'finish', taskId: item.id, ok: false, result: failed });
|
|
469
527
|
return failed;
|
|
470
528
|
}
|
|
471
|
-
const model = resolveModel({ model: item.model, env });
|
|
472
|
-
const ignored = ignoredEnvModel({ model: item.model, env });
|
|
529
|
+
const model = resolveModel({ model: item.model, env, cls });
|
|
530
|
+
const ignored = ignoredEnvModel({ model: item.model, env, cls });
|
|
473
531
|
if (ignored) emit({ type: 'note', taskId: item.id, note: ignored });
|
|
474
|
-
|
|
532
|
+
// The pooled-brain fan-out is an Aegis Cloud mechanism (aegis1
|
|
533
|
+
// services/pool_brain.py) — engine.js itself only honours `autonomous`
|
|
534
|
+
// under `cls === 'aegis'` (engine.js:1524), so a byok/local task asking for
|
|
535
|
+
// it is forced to a plain single pass HERE rather than relying on that
|
|
536
|
+
// silent no-op, which would otherwise size the task's effort/round
|
|
537
|
+
// reporting as if a fan-out it never got had been granted.
|
|
538
|
+
const fanoutAsked = cls === 'aegis' && resolveFanout(item, env);
|
|
475
539
|
// The clamp. `canFanOut()` is the entitlement answer (true everywhere
|
|
476
540
|
// except a non-Pro desktop session), and it can only ever take a fan-out
|
|
477
541
|
// AWAY: a task the session is not entitled to fan out runs as one plain
|
|
@@ -526,14 +590,14 @@ function createQueueWorker({
|
|
|
526
590
|
const wantCommit = commit === undefined ? Boolean(item.commit) : Boolean(commit);
|
|
527
591
|
const before = wantCommit ? safe(() => git.gitStatusSnapshot(cwd), null) : null;
|
|
528
592
|
|
|
529
|
-
emit({ type: 'start', taskId: item.id, model, cwd, rounds, fanout, effort });
|
|
593
|
+
emit({ type: 'start', taskId: item.id, model, class: cls, cwd, rounds, fanout, effort });
|
|
530
594
|
const started = now();
|
|
531
595
|
let result;
|
|
532
596
|
try {
|
|
533
597
|
const res = await withRoundHorizon(rounds, env, () =>
|
|
534
598
|
engine.chat(
|
|
535
599
|
{
|
|
536
|
-
class:
|
|
600
|
+
class: cls,
|
|
537
601
|
model,
|
|
538
602
|
prompt: taskPrompt(item, { carry, rounds }),
|
|
539
603
|
// The pooled-brain fan-out ("fan out this turn" in the GUI chat
|
|
@@ -831,8 +895,11 @@ module.exports = {
|
|
|
831
895
|
DEFAULT_MODEL,
|
|
832
896
|
DEFAULT_ROUNDS,
|
|
833
897
|
MAX_ROUND_STOPS,
|
|
898
|
+
MODEL_CLASSES,
|
|
834
899
|
AEGIS_MODEL_IDS,
|
|
835
900
|
isAegisModel,
|
|
901
|
+
normalizeClass,
|
|
902
|
+
looksLikeByokModel,
|
|
836
903
|
modelRefusal,
|
|
837
904
|
WRITE_TOOLS,
|
|
838
905
|
writtenPath,
|
package/lib/local/engine.js
CHANGED
|
@@ -968,6 +968,19 @@ function createLocalEngine({
|
|
|
968
968
|
// reverse its verdict. An approval that could waive Λ2 would be the exact
|
|
969
969
|
// capture K3 describes. To change what is permitted, the covenant text
|
|
970
970
|
// itself has to be re-sealed — which no runtime input can do.
|
|
971
|
+
//
|
|
972
|
+
// DO NOT MOVE THIS BELOW A SHORT-CIRCUIT. It sits above every `return` of
|
|
973
|
+
// the confirmation gate on purpose (ON8/D8 rule 2): `gatedExecuteTool`
|
|
974
|
+
// short-circuits on `!confirmModeEnabled()`, on a remembered session
|
|
975
|
+
// "allow", and on a `/permissions` skip — and since ON8 the confirm-mode
|
|
976
|
+
// preference is the SHARED store's, i.e. the same value the terminal face
|
|
977
|
+
// reads, and `--dangerously-skip-permissions` can arrive in that store as
|
|
978
|
+
// well. A refactor that hoists this below any of those three would make
|
|
979
|
+
// the covenant waivable by a setting a user can write, which is the one
|
|
980
|
+
// thing D8 rule 2 forbids outright. The frozen classes
|
|
981
|
+
// (`write`/`shell`/`network`/`offDevice`, always `'ask'`; see
|
|
982
|
+
// lib/avatar/level.js `LEVEL_INDEPENDENT_APPROVALS`) have the same floor
|
|
983
|
+
// and are asserted beside this one by one/test/permissions.test.mjs.
|
|
971
984
|
const cv = covenant.preActionAssert({ tool: name, args: args || {} });
|
|
972
985
|
if (!cv.allowed) {
|
|
973
986
|
// Non-silent (never a quiet no-op) and bound for Λ4 accountability.
|
package/lib/local/entitlement.js
CHANGED
|
@@ -28,17 +28,37 @@
|
|
|
28
28
|
* THE SIGNAL (see the report / the module's `probe`): main injects a probe that
|
|
29
29
|
* calls `aegis.verifyApiKey()` — aegis1 `/api/verify-api-key`, which its own
|
|
30
30
|
* docstring describes as "Verify a shared api_key and return plan/email/
|
|
31
|
-
* memory_token". The plan field is the account's
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* no new
|
|
31
|
+
* memory_token". The plan field is the account's plan, and aegis1 owns the
|
|
32
|
+
* vocabulary for it (`services/billing_service.py`: `TIERS = ("free", "pro",
|
|
33
|
+
* "unlimited")`, `PAID_PLANS = ("pro", "unlimited")`, plus the legacy aliases
|
|
34
|
+
* that normalize onto one of them). This module MIRRORS that vocabulary rather
|
|
35
|
+
* than inventing one; there is no new billing anything here, no new endpoint,
|
|
36
|
+
* and no write to the server. The probe prefers the server's `plan_effective`
|
|
37
|
+
* (its own `is_pro()` verdict, trial included) over the stored `plan` string
|
|
38
|
+
* when it is present, because a welcome-trial account is a real Unlimited
|
|
39
|
+
* entitlement that `users.plan` does not record — see `classifyProbe`.
|
|
40
|
+
*
|
|
41
|
+
* THE BUG THIS VOCABULARY COST (fixed here). `PAID_PLANS` read `("pro",)`,
|
|
42
|
+
* mirroring the retired €2 line and MISSING `unlimited` — the €9/mo tier that
|
|
43
|
+
* is the only one sold. So `normalizePlan('unlimited')` fell through to `free`,
|
|
44
|
+
* and a paying Unlimited subscriber with a spent-out bank was handed the
|
|
45
|
+
* free-tier boundary: the fan-out refused, the cloud drain clamped to one local
|
|
46
|
+
* worker, and a "top up your balance" prompt — for an account whose plan is
|
|
47
|
+
* precisely what pays for the drain. aegis1's own `LEGACY_PLAN_ALIASES` carries
|
|
48
|
+
* the matching warning from the other side ("Deliberately NOT an alias:
|
|
49
|
+
* `unlimited` is a live tier you can subscribe to, so it must resolve through
|
|
50
|
+
* TIERS"), and its `test_tier_vocabulary` pins `PAID_PLANS` at two entries.
|
|
51
|
+
* Two implementations of "is this account paid" had already drifted once; this
|
|
52
|
+
* was the drift.
|
|
36
53
|
*
|
|
37
54
|
* PHASE 6 — THE SECOND SIGNAL, THE BALANCE. The plan is only half of how this
|
|
38
55
|
* product charges. aegis1's actual paywall is the Token Bank running dry: the
|
|
39
56
|
* balance is prepaid EUR the OPERATOR credits by hand, and `app.py` states the
|
|
40
57
|
* consequence plainly — "Pool-key users with token bank balance skip legacy
|
|
41
|
-
* credit checks." So entitlement here is `
|
|
58
|
+
* credit checks." So entitlement here is `paid_plan || balance > 0` — and the
|
|
59
|
+
* `paid_plan` half is what carries an Unlimited subscriber whose bank is empty;
|
|
60
|
+
* the bank is an OVERAGE lever, never the thing that decides whether a paying
|
|
61
|
+
* account may use the feature it pays for. When the
|
|
42
62
|
* bank reads zero the wall names the fix that exists (a top-up, campaign
|
|
43
63
|
* `topup_bank`) instead of a plan checkout the user may already own. Nothing in
|
|
44
64
|
* this module grants a balance, expires one, or auto-charges: the operator
|
|
@@ -64,8 +84,33 @@
|
|
|
64
84
|
/** The free-tier fan-out ceiling: one worker per task, i.e. a plain turn. */
|
|
65
85
|
const FREE_MAX_WORKERS = 1;
|
|
66
86
|
|
|
67
|
-
/**
|
|
68
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Aegis1's paid tiers. MIRROR of services/billing_service.py
|
|
89
|
+
* `PAID_PLANS = ("pro", "unlimited")` — pinned literal on purpose, because this
|
|
90
|
+
* is the line that has to move whenever the server sells another tier.
|
|
91
|
+
*
|
|
92
|
+
* `unlimited` (€9/mo) is the tier ON SALE and the one a paying customer almost
|
|
93
|
+
* certainly owns; `pro` is the grandfathered €2 line, kept because its
|
|
94
|
+
* subscribers keep their entitlements. Reading only `pro` was not a narrower
|
|
95
|
+
* version of this gate, it was a gate pointing at the wrong tier: every
|
|
96
|
+
* Unlimited account fell to the free boundary. `starter` stays out — aegis1
|
|
97
|
+
* itself calls that one free.
|
|
98
|
+
*/
|
|
99
|
+
const PAID_PLANS = Object.freeze(['pro', 'unlimited']);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* What each paid tier is CALLED, so a surface that names the account's plan
|
|
103
|
+
* names the one the server granted. The CLI has read the tier off the server's
|
|
104
|
+
* plan string since 6443a60 (`cli/src/entitlement.js::paymentView`) after it
|
|
105
|
+
* told a €9 subscriber they owned the retired €2 plan; this is the same value,
|
|
106
|
+
* resolved once in the shared module both hosts read.
|
|
107
|
+
*/
|
|
108
|
+
const TIER_NAMES = Object.freeze({ pro: 'Pro', unlimited: 'Unlimited' });
|
|
109
|
+
|
|
110
|
+
/** The display name for a plan string, or null when it is not a paid tier. */
|
|
111
|
+
function tierName(plan) {
|
|
112
|
+
return TIER_NAMES[normalizePlan(plan)] || null;
|
|
113
|
+
}
|
|
69
114
|
|
|
70
115
|
/**
|
|
71
116
|
* MIRROR of services/billing_service.py `LEGACY_PLAN_ALIASES`: every stored
|
|
@@ -251,11 +296,20 @@ function promptUrlFor(host) {
|
|
|
251
296
|
* card on the next refresh and long enough that a drain never probes per task. */
|
|
252
297
|
const DEFAULT_TTL_MS = 60 * 1000;
|
|
253
298
|
|
|
254
|
-
/** Collapse any stored/alias plan string onto a tier, exactly as aegis1 does.
|
|
299
|
+
/** Collapse any stored/alias plan string onto a tier, exactly as aegis1 does.
|
|
300
|
+
*
|
|
301
|
+
* Aegis1 checks its live `TIERS` BEFORE the alias table (`normalize_plan`), so
|
|
302
|
+
* an alias can never shadow a tier it also names — and `unlimited` is a live
|
|
303
|
+
* tier. It is therefore returned as itself and never collapsed onto `pro`: the
|
|
304
|
+
* two grant different entitlements server-side (the monthly Unlimited
|
|
305
|
+
* allowance and the BYOK fee waiver are `is_unlimited()`-gated), and telling a
|
|
306
|
+
* €9 customer they own the retired €2 plan is the mistake commit 6443a60
|
|
307
|
+
* exists to record.
|
|
308
|
+
*/
|
|
255
309
|
function normalizePlan(plan) {
|
|
256
310
|
const raw = String(plan == null ? '' : plan).trim().toLowerCase();
|
|
257
311
|
if (!raw) return 'free';
|
|
258
|
-
if (PAID_PLANS.includes(raw)) return
|
|
312
|
+
if (PAID_PLANS.includes(raw)) return raw;
|
|
259
313
|
const aliased = LEGACY_PLAN_ALIASES[raw];
|
|
260
314
|
return aliased || 'free';
|
|
261
315
|
}
|
|
@@ -265,9 +319,9 @@ function normalizePlan(plan) {
|
|
|
265
319
|
* drift into two different instructions. */
|
|
266
320
|
const ASK_EXAMPLE = 'Do X autonomously';
|
|
267
321
|
|
|
268
|
-
/** True only for a tier aegis1 itself would call paid. */
|
|
322
|
+
/** True only for a tier aegis1 itself would call paid (`pro` or `unlimited`). */
|
|
269
323
|
function isPaidPlan(plan) {
|
|
270
|
-
return normalizePlan(plan)
|
|
324
|
+
return normalizePlan(plan) !== 'free';
|
|
271
325
|
}
|
|
272
326
|
|
|
273
327
|
/**
|
|
@@ -367,7 +421,19 @@ function classifyProbe(result, error) {
|
|
|
367
421
|
const body = result && typeof result === 'object' ? result : {};
|
|
368
422
|
const bal = balanceOf(body);
|
|
369
423
|
if (body.valid === false) return { source: 'no-key', plan: 'free', degraded: false, ...bal };
|
|
370
|
-
|
|
424
|
+
// WHICH plan field. `plan_effective` is aegis1's own entitlement verdict —
|
|
425
|
+
// the same predicate its `is_pro()` gates pooled calls on, so it counts the
|
|
426
|
+
// 30-day welcome trial that never writes `users.plan` (see app.py's
|
|
427
|
+
// register_device: the trial lives in `subscriptions`). The stored `plan`
|
|
428
|
+
// column cannot express that account, so an installing user read as `free`
|
|
429
|
+
// here and only the €12 signup credit kept the gate open — spend it, or fail
|
|
430
|
+
// to receive it, and a real Unlimited account was refused the feature. The
|
|
431
|
+
// stored string is still the fallback: an older server that predates the
|
|
432
|
+
// field must keep working, and it is the same value when nothing is on trial.
|
|
433
|
+
const rawPlan = body.plan_effective != null && String(body.plan_effective).trim()
|
|
434
|
+
? body.plan_effective
|
|
435
|
+
: body.plan;
|
|
436
|
+
const stated = String(rawPlan == null ? '' : rawPlan).trim();
|
|
371
437
|
if (!stated) {
|
|
372
438
|
// A key that verified but carried no plan: aegis1's own default for such an
|
|
373
439
|
// account is `free` (the column default), and a missing field is not proof
|
|
@@ -411,6 +477,8 @@ function createEntitlement({
|
|
|
411
477
|
balanceZero: false,
|
|
412
478
|
wall: 'plan',
|
|
413
479
|
plan: 'free',
|
|
480
|
+
tier: null,
|
|
481
|
+
tierName: null,
|
|
414
482
|
planRaw: null,
|
|
415
483
|
maxWorkers: FREE_MAX_WORKERS,
|
|
416
484
|
source: 'unresolved',
|
|
@@ -424,12 +492,20 @@ function createEntitlement({
|
|
|
424
492
|
let inflight = null;
|
|
425
493
|
|
|
426
494
|
function fromProbe({ source, plan, degraded, planRaw, balanceKnown = false, balanceMicros = null }) {
|
|
427
|
-
|
|
495
|
+
// The tier itself, not a paid/not-paid bit: `unlimited` and `pro` are both
|
|
496
|
+
// paid and must stay distinguishable, because the surfaces that name the
|
|
497
|
+
// account's plan (the desktop's queue boundary, the CLI's `/billing`) name
|
|
498
|
+
// the one the server granted. `pro` below keeps its original meaning — "the
|
|
499
|
+
// PLAN is paid for", as opposed to a free plan carried by a funded bank.
|
|
500
|
+
const tier = normalizePlan(plan);
|
|
501
|
+
const pro = tier !== 'free';
|
|
428
502
|
const banked = balanceKnown && Number.isFinite(balanceMicros) && balanceMicros > 0;
|
|
429
503
|
// TWO independent ways to be paid for: a paid plan, or a funded bank. The
|
|
430
504
|
// operator controls the second by hand (aegis1 admin `add_funds`), which is
|
|
431
505
|
// the lever this wall is built around — nothing here grants, times out or
|
|
432
|
-
// auto-charges; it only reads the number the operator set.
|
|
506
|
+
// auto-charges; it only reads the number the operator set. The FIRST way is
|
|
507
|
+
// the one a paying subscriber holds, and it does not weaken when their bank
|
|
508
|
+
// runs dry: the balance is overage, the subscription is the entitlement.
|
|
433
509
|
const entitled = pro || banked;
|
|
434
510
|
// The bank was READABLE and EMPTY — the operator's own signal that this
|
|
435
511
|
// account is now paying, not the absence of a plan.
|
|
@@ -447,7 +523,12 @@ function createEntitlement({
|
|
|
447
523
|
balanceMicros: balanceKnown ? balanceMicros : null,
|
|
448
524
|
balanceZero,
|
|
449
525
|
wall,
|
|
450
|
-
|
|
526
|
+
// The tier a paid PLAN grants, else `free`. A funded bank on a free plan
|
|
527
|
+
// is still `free` here — the bank is not a tier, and naming one would put
|
|
528
|
+
// a plan on the account the server never granted.
|
|
529
|
+
plan: pro ? tier : 'free',
|
|
530
|
+
tier: pro ? tier : null,
|
|
531
|
+
tierName: pro ? (TIER_NAMES[tier] || null) : null,
|
|
451
532
|
planRaw: planRaw == null ? null : String(planRaw),
|
|
452
533
|
// A paid session has no ceiling of its own here: main.js still clamps the
|
|
453
534
|
// renderer's number to QUEUE_MAX_WORKERS, which stays the only upper bound.
|
|
@@ -543,6 +624,8 @@ module.exports = {
|
|
|
543
624
|
createEntitlement,
|
|
544
625
|
normalizePlan,
|
|
545
626
|
isPaidPlan,
|
|
627
|
+
tierName,
|
|
628
|
+
TIER_NAMES,
|
|
546
629
|
classifyProbe,
|
|
547
630
|
fanoutRefusal,
|
|
548
631
|
balanceRefusal,
|
package/lib/local/queue.js
CHANGED
|
@@ -176,34 +176,38 @@ function upsert(items, entry) {
|
|
|
176
176
|
|
|
177
177
|
/**
|
|
178
178
|
* Queue a task. `cwd` is where the work happens (the worker runs the turn
|
|
179
|
-
* with it as the tool loop's working directory), `model`
|
|
180
|
-
*
|
|
181
|
-
*
|
|
179
|
+
* with it as the tool loop's working directory), `class` + `model` are the
|
|
180
|
+
* same pair engine.js's interactive turns use ('aegis'/'byok'/'local' and the
|
|
181
|
+
* model id that class expects), and `commit` asks the worker to commit what
|
|
182
|
+
* the task changed (scoped — see autonomous.js).
|
|
182
183
|
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
184
|
+
* REFUSED AT THE DOOR, not three stages into a drain. `class` defaults to
|
|
185
|
+
* 'aegis' (every task queued before this field existed), and the (model,
|
|
186
|
+
* class) pair has to pass autonomous.js's own accept-list — the CLI
|
|
187
|
+
* (`aegiscode autonomous add --model`), the desktop card and a hand-written
|
|
188
|
+
* queue file are all held to the same rule, so a bad row fails here with a
|
|
189
|
+
* reason instead of coming back as an opaque server error after being picked
|
|
190
|
+
* up. `model: null` stays legal for the 'aegis' class only: that is "no
|
|
191
|
+
* pick", and autonomous.resolveModel fills in the pooled brain. 'byok' and
|
|
192
|
+
* 'local' have no such default and are refused if blank — see
|
|
193
|
+
* autonomous.modelRefusal. Required lazily: queue.js is a dependency of
|
|
194
|
+
* autonomous.js, and this check must not turn that into a load-order cycle.
|
|
192
195
|
*/
|
|
193
196
|
function addTask(env, opts = {}) {
|
|
194
197
|
const task = String(opts.task == null ? '' : opts.task).trim();
|
|
195
198
|
if (!task) throw new Error('queue: a task needs text');
|
|
199
|
+
const autonomousMod = require('./autonomous.js');
|
|
200
|
+
const cls = autonomousMod.normalizeClass(opts.class);
|
|
196
201
|
const statedModel = String(opts.model == null ? '' : opts.model).trim();
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (refusal) throw new Error(`queue: ${refusal}`);
|
|
200
|
-
}
|
|
202
|
+
const refusal = autonomousMod.modelRefusal(statedModel, cls);
|
|
203
|
+
if (refusal) throw new Error(`queue: ${refusal}`);
|
|
201
204
|
const items = loadQueue(env);
|
|
202
205
|
const now = opts.now || Date.now();
|
|
203
206
|
const entry = {
|
|
204
207
|
id: nextId(items),
|
|
205
208
|
task,
|
|
206
209
|
cwd: opts.cwd || process.cwd(),
|
|
210
|
+
class: cls,
|
|
207
211
|
model: statedModel || null,
|
|
208
212
|
effort: opts.effort || null,
|
|
209
213
|
workers: Number.isInteger(opts.workers) ? opts.workers : null,
|
package/lib/settings.js
CHANGED
|
@@ -270,6 +270,15 @@ function createSettingsStore({ dir, safeStorage, prefsDir, env } = {}) {
|
|
|
270
270
|
try {
|
|
271
271
|
return prefs.adopt({ dir: sharedDir, legacyDir: dir });
|
|
272
272
|
} catch (err) {
|
|
273
|
+
// Best-effort, but NOT silent (AEGISONE ON8). Swallowing this throw is how
|
|
274
|
+
// `prefs.adopt()`'s refusal to write its own ledger went unnoticed — the
|
|
275
|
+
// migration failed for every install that had something to adopt, and the
|
|
276
|
+
// store reported a healthy `adopted: false`. A failed migration is a
|
|
277
|
+
// preference a user set that this launch did not honor, so it is reported
|
|
278
|
+
// where a developer will see it; a store must still open either way.
|
|
279
|
+
if (process.env.AEGISCODE_DEBUG || process.env.AEGIS_HIST_DEBUG) {
|
|
280
|
+
process.stderr.write(`aegis-desktop: shared-preference adoption failed: ${err.message}\n`);
|
|
281
|
+
}
|
|
273
282
|
return { adopted: false, keys: [], reason: `adoption failed: ${err.message}` };
|
|
274
283
|
}
|
|
275
284
|
})();
|
|
@@ -525,26 +534,50 @@ function createSettingsStore({ dir, safeStorage, prefsDir, env } = {}) {
|
|
|
525
534
|
return quickLauncherConfig();
|
|
526
535
|
}
|
|
527
536
|
|
|
528
|
-
// --- Confirm mode:
|
|
529
|
-
// "Confirm before running tools" — the
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
//
|
|
537
|
+
// --- Confirm mode: SHARED preference (AEGISONE ON8, decision D8 rule 1) --
|
|
538
|
+
// "Confirm before running tools" — the switch for the renderer's tool-call
|
|
539
|
+
// approval gate (desktop/lib/local/engine.js gatedExecuteTool). ON (default,
|
|
540
|
+
// and the behaviour shipped so far) means every mutating tool call — exec /
|
|
541
|
+
// writeFile / editFile — is previewed and approved by the user first. OFF
|
|
542
|
+
// means the engine runs them straight through, like a tool the session
|
|
543
|
+
// already allowed. Nothing secret is stored here, so no encryption.
|
|
544
|
+
//
|
|
545
|
+
// This preference MOVED to the shared store (`confirmMode`) because D8 rule 1
|
|
546
|
+
// is "one posture, two faces": a rule set in the window has to be the rule the
|
|
547
|
+
// shell honors. The pre-ON8 `__confirmMode` namespace in `<userData>` is
|
|
548
|
+
// adopted once (client/prefs.js `extractNonSecretHalf`) and still read as a
|
|
549
|
+
// fallback, never written — a second writer is the split-brain this replaced.
|
|
550
|
+
// The gate's polarity is unchanged: ON when unset, in both places, so an
|
|
551
|
+
// install that never touched the switch keeps the gate exactly as it was.
|
|
552
|
+
|
|
553
|
+
function confirmModeState() {
|
|
554
|
+
const shared = loadShared();
|
|
555
|
+
if (typeof shared.confirmMode === 'boolean') {
|
|
556
|
+
return { enabled: shared.confirmMode, source: 'config' };
|
|
557
|
+
}
|
|
558
|
+
const cfg = load()[CONFIRM_MODE_NAMESPACE] || {};
|
|
559
|
+
if (cfg.enabled === undefined) return { enabled: true, source: 'default' };
|
|
560
|
+
return { enabled: Boolean(cfg.enabled), source: 'config' };
|
|
561
|
+
}
|
|
535
562
|
|
|
536
563
|
function getConfirmMode() {
|
|
537
|
-
|
|
538
|
-
// Unset === true: an existing install that never touched the toggle keeps
|
|
539
|
-
// the gate exactly as it was.
|
|
540
|
-
return cfg.enabled === undefined ? true : Boolean(cfg.enabled);
|
|
564
|
+
return confirmModeState().enabled;
|
|
541
565
|
}
|
|
542
566
|
|
|
543
567
|
function setConfirmMode(enabled) {
|
|
568
|
+
prefs.set({ confirmMode: Boolean(enabled) }, { dir: sharedDir });
|
|
569
|
+
// One-way: retire the legacy copy in the same breath, so the posture exists
|
|
570
|
+
// in exactly one place from here on (the same retirement the memory-persist
|
|
571
|
+
// preference below does).
|
|
544
572
|
const data = load();
|
|
545
|
-
data[CONFIRM_MODE_NAMESPACE]
|
|
546
|
-
|
|
547
|
-
|
|
573
|
+
if (data[CONFIRM_MODE_NAMESPACE] !== undefined) {
|
|
574
|
+
delete data[CONFIRM_MODE_NAMESPACE];
|
|
575
|
+
save(data);
|
|
576
|
+
}
|
|
577
|
+
// The boolean, not the state record: main.js's IPC reply is `{ enabled }`
|
|
578
|
+
// and it wraps the return value itself, so widening this would have put an
|
|
579
|
+
// object inside an object on the wire.
|
|
580
|
+
return confirmModeState().enabled;
|
|
548
581
|
}
|
|
549
582
|
|
|
550
583
|
// --- Telemetry: reserved namespace, plain preference, DEFAULT OFF --------
|
|
@@ -771,6 +804,7 @@ function createSettingsStore({ dir, safeStorage, prefsDir, env } = {}) {
|
|
|
771
804
|
setQuickLauncherConfig,
|
|
772
805
|
getConfirmMode,
|
|
773
806
|
setConfirmMode,
|
|
807
|
+
confirmModeState,
|
|
774
808
|
getTelemetryOptIn,
|
|
775
809
|
setTelemetryOptIn,
|
|
776
810
|
getMemoryPersist,
|
package/main.js
CHANGED
|
@@ -1972,8 +1972,10 @@ function createQueueDispatch({
|
|
|
1972
1972
|
draining,
|
|
1973
1973
|
stopping: stopRequested,
|
|
1974
1974
|
defaultCwd: process.cwd(),
|
|
1975
|
-
// What a task with no model of its own
|
|
1976
|
-
//
|
|
1975
|
+
// What a task with no model of its own, on the Aegis Cloud class, actually
|
|
1976
|
+
// runs on — a pooled tier id. byok and local tasks always carry an
|
|
1977
|
+
// explicit model (see autonomous.js modelRefusal), so there is no
|
|
1978
|
+
// equivalent default to report for those classes. The card prints this
|
|
1977
1979
|
// because "which model is this drain spending on" was previously
|
|
1978
1980
|
// unanswerable from the UI.
|
|
1979
1981
|
defaultModel: autonomous.resolveModel({ env }),
|
|
@@ -2008,19 +2010,20 @@ function createQueueDispatch({
|
|
|
2008
2010
|
if (!path.isAbsolute(cwd)) return { error: 'the working directory must be an absolute path' };
|
|
2009
2011
|
if (!isExistingDir(cwd)) return { error: `not a directory: ${cwd}` };
|
|
2010
2012
|
|
|
2013
|
+
const cls = autonomous.normalizeClass(p.class);
|
|
2011
2014
|
const model = String(p.model == null ? '' : p.model).trim().slice(0, 120) || null;
|
|
2012
|
-
//
|
|
2013
|
-
//
|
|
2014
|
-
//
|
|
2015
|
-
//
|
|
2016
|
-
|
|
2017
|
-
const modelRefusal = model ? autonomous.modelRefusal(model) : '';
|
|
2015
|
+
// Refused HERE so the card gets the reason at the moment of queueing
|
|
2016
|
+
// instead of a red row later. queue.addTask enforces the same (model,
|
|
2017
|
+
// class) rule for the CLI and for hand-written queue files — see
|
|
2018
|
+
// autonomous.js modelRefusal for what each class requires.
|
|
2019
|
+
const modelRefusal = autonomous.modelRefusal(model, cls);
|
|
2018
2020
|
if (modelRefusal) return { error: modelRefusal };
|
|
2019
2021
|
const effortRaw = String(p.effort == null ? '' : p.effort).trim().toLowerCase();
|
|
2020
2022
|
return {
|
|
2021
2023
|
opts: {
|
|
2022
2024
|
task,
|
|
2023
2025
|
cwd,
|
|
2026
|
+
class: cls,
|
|
2024
2027
|
model,
|
|
2025
2028
|
effort: QUEUE_EFFORTS.has(effortRaw) ? effortRaw : null,
|
|
2026
2029
|
workers: clampInt(p.workers, 1, QUEUE_MAX_WORKERS),
|
|
@@ -2029,7 +2032,12 @@ function createQueueDispatch({
|
|
|
2029
2032
|
// it costs about workers+1 full reasoning calls on one task, so the
|
|
2030
2033
|
// card has to ask for it per task. queue.addTask stores the inverse
|
|
2031
2034
|
// (`singlePass`), which is also the field a hand-written queue file
|
|
2032
|
-
// or the CLI uses.
|
|
2035
|
+
// or the CLI uses. Fan-out is an Aegis Cloud mechanism only — a
|
|
2036
|
+
// byok/local task that somehow carried `fanout: true` is NOT honoured
|
|
2037
|
+
// as a fan-out here (autonomous.js's runTask forces it off for any
|
|
2038
|
+
// non-aegis class regardless of this flag), but it is still recorded
|
|
2039
|
+
// as the task's own ask rather than silently rewritten, matching
|
|
2040
|
+
// resolveFanout's "a wrong pick is reported, not rewritten" rule.
|
|
2033
2041
|
singlePass: !(p.fanout === true || p.autonomous === true),
|
|
2034
2042
|
commit: Boolean(p.commit),
|
|
2035
2043
|
source: 'desktop',
|
|
@@ -3538,14 +3546,15 @@ function bootstrap() {
|
|
|
3538
3546
|
// Nothing configured anywhere (env, this app's encrypted copy, or the
|
|
3539
3547
|
// shared store): a fresh install. Auto-provision a free device key
|
|
3540
3548
|
// from aegis1 — same shared helper mcp/server.js and the CLI use, so
|
|
3541
|
-
// the desktop app works with no manual sign-in step.
|
|
3542
|
-
//
|
|
3543
|
-
//
|
|
3544
|
-
//
|
|
3545
|
-
//
|
|
3546
|
-
//
|
|
3547
|
-
//
|
|
3548
|
-
// launch tries
|
|
3549
|
+
// the desktop app works with no manual sign-in step. A brand-new
|
|
3550
|
+
// fingerprint is granted aegis1's 30-day welcome-trial `unlimited`
|
|
3551
|
+
// entitlement (token-bank credit + a subscriptions row) in the same
|
|
3552
|
+
// register-device call that mints the key — see client/
|
|
3553
|
+
// auto-provision.js's header for the aegis1 commit that made this a
|
|
3554
|
+
// real grant rather than a badge. Failure (offline, aegis1
|
|
3555
|
+
// unreachable) falls through exactly as before: the renderer's
|
|
3556
|
+
// normal "not signed in" state handles it, and the next launch tries
|
|
3557
|
+
// again.
|
|
3549
3558
|
const result = await autoProvision.autoProvisionApiKey(aegis, credentials);
|
|
3550
3559
|
if (result.key) settings.setAegisKey(result.key);
|
|
3551
3560
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegis-desktop",
|
|
3
3
|
"productName": "AEGIS Desktop",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.32",
|
|
5
5
|
"description": "Thin Electron host for AEGIS — a local chat UI over the shared client/aegis.js transport. Ships transport + UI only; engine logic stays server-side.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "AEGIS Code",
|
package/preload.js
CHANGED
|
@@ -423,8 +423,8 @@ const quickLauncher = {
|
|
|
423
423
|
// a real checkout, so that switch is persisted and visible rather than
|
|
424
424
|
// implied — but it is also not hidden from this file's users any more.
|
|
425
425
|
const queue = {
|
|
426
|
-
// Queue one task. `payload` is { task, cwd, model, effort, workers,
|
|
427
|
-
// commit }; main.js range-checks every field and answers
|
|
426
|
+
// Queue one task. `payload` is { task, cwd, class, model, effort, workers,
|
|
427
|
+
// maxRounds, commit }; main.js range-checks every field and answers
|
|
428
428
|
// { ok, item, items, pending, running, runs, … } or { ok: false, reason }.
|
|
429
429
|
enqueue: (payload) => invokeQueue('enqueue', queueTaskFields(payload)),
|
|
430
430
|
// Full state: { items, pending, running, draining, runs, defaultCwd, … }.
|
|
@@ -531,6 +531,11 @@ function queueTaskFields(payload) {
|
|
|
531
531
|
return {
|
|
532
532
|
task: String(p.task == null ? '' : p.task),
|
|
533
533
|
cwd: String(p.cwd == null ? '' : p.cwd),
|
|
534
|
+
// 'aegis' | 'byok' | 'local' — same three classes the main composer uses
|
|
535
|
+
// (window.models.listClasses()). main.js defaults a blank/missing value to
|
|
536
|
+
// 'aegis' (autonomous.normalizeClass), so an older renderer build that
|
|
537
|
+
// never sends this field keeps behaving exactly as it did before.
|
|
538
|
+
class: p.class == null ? null : String(p.class),
|
|
534
539
|
model: p.model == null ? null : String(p.model),
|
|
535
540
|
effort: p.effort == null ? null : String(p.effort),
|
|
536
541
|
workers: p.workers == null ? null : Number(p.workers),
|
package/renderer/app.js
CHANGED
|
@@ -145,6 +145,10 @@ const ELEMENT_IDS = {
|
|
|
145
145
|
send: 'send',
|
|
146
146
|
exploreToggle: 'explore-toggle',
|
|
147
147
|
queueTask: 'queue-task',
|
|
148
|
+
queueClassSelect: 'queue-class-select',
|
|
149
|
+
queueModelSelect: 'queue-model-select',
|
|
150
|
+
queueModelInput: 'queue-model-input',
|
|
151
|
+
queueModelHint: 'queue-model-hint',
|
|
148
152
|
queueCwd: 'queue-cwd',
|
|
149
153
|
queueCommit: 'queue-commit',
|
|
150
154
|
queueEnqueue: 'queue-enqueue',
|
|
@@ -186,6 +190,11 @@ for (const [prop, id] of Object.entries(ELEMENT_IDS)) {
|
|
|
186
190
|
}
|
|
187
191
|
|
|
188
192
|
const CLASS_KEY = 'aegis.class';
|
|
193
|
+
// The queue card's own class pick, kept separate from the chat composer's
|
|
194
|
+
// (CLASS_KEY) — a queued task's backend and the interactive chat's are
|
|
195
|
+
// independent choices, so picking "local" for one must not silently flip
|
|
196
|
+
// the other the next time either picker loads.
|
|
197
|
+
const QUEUE_CLASS_KEY = 'aegis.queueClass';
|
|
189
198
|
const AUTONOMOUS_KEY = 'aegis.autonomous';
|
|
190
199
|
// The budget rung. Applies to every class now that the Max tokens dropdown is
|
|
191
200
|
// gone, so it is no longer stored under the autonomous-mode namespace; the old
|
|
@@ -1463,6 +1472,8 @@ async function enqueueQueueTask() {
|
|
|
1463
1472
|
const res = await queueApi.enqueue({
|
|
1464
1473
|
task,
|
|
1465
1474
|
cwd: els.queueCwd ? els.queueCwd.value.trim() : '',
|
|
1475
|
+
class: els.queueClassSelect ? els.queueClassSelect.value : undefined,
|
|
1476
|
+
model: queueSelectedModel(),
|
|
1466
1477
|
commit: els.queueCommit ? els.queueCommit.checked : false,
|
|
1467
1478
|
});
|
|
1468
1479
|
if (res && res.ok) {
|
|
@@ -1525,9 +1536,14 @@ function renderQueueEntitlement(state) {
|
|
|
1525
1536
|
if (!resolved) els.queueBoundary.textContent = '';
|
|
1526
1537
|
else if (entitled) {
|
|
1527
1538
|
// Say which of the two ways this account is paid for, so a funded bank
|
|
1528
|
-
// does not read as a Pro plan somebody did not buy
|
|
1539
|
+
// does not read as a Pro plan somebody did not buy — and name WHICH tier
|
|
1540
|
+
// the plan is: `ent.tierName` is resolved in main from the plan string the
|
|
1541
|
+
// server stated (shared module's TIER_NAMES), so a €9 Unlimited subscriber
|
|
1542
|
+
// is not told they own the retired €2 plan. That mislabelling is the same
|
|
1543
|
+
// class CLI commit 6443a60 fixed in `/billing`; the literal is only a
|
|
1544
|
+
// fallback for a snapshot from a host that predates the field.
|
|
1529
1545
|
els.queueBoundary.textContent = ent.pro
|
|
1530
|
-
? 'Pro — the cloud drain and the fan-out are unlocked
|
|
1546
|
+
? `${ent.tierName || 'Pro'} — the cloud drain and the fan-out are unlocked.`
|
|
1531
1547
|
: 'Balance on your account — the cloud drain and the fan-out are unlocked.';
|
|
1532
1548
|
} else {
|
|
1533
1549
|
// The refusal names the Pro value AND keeps the free tier's real reach
|
|
@@ -1702,11 +1718,17 @@ function formatAutoInterval(ms) {
|
|
|
1702
1718
|
return Number.isInteger(min) ? `every ${min} min` : `every ${min.toFixed(1)} min`;
|
|
1703
1719
|
}
|
|
1704
1720
|
|
|
1705
|
-
/**
|
|
1721
|
+
/**
|
|
1722
|
+
* Paint the auto-drain half of main's answer (`auto.status()`). The mint
|
|
1723
|
+
* (auto-seal) half is NOT painted here: that control lives on the φ(α) panel,
|
|
1724
|
+
* not the queue card, and desktop/renderer/fingerprint.js's own mount() reads
|
|
1725
|
+
* and repaints it independently (loadAutoMint() -> renderAutoMint(), a
|
|
1726
|
+
* closure local to that module, not a global this file could reach even if
|
|
1727
|
+
* it wanted to).
|
|
1728
|
+
*/
|
|
1706
1729
|
function renderAutoStatus(payload) {
|
|
1707
1730
|
if (!payload || typeof payload !== 'object') return;
|
|
1708
1731
|
if (payload.drain) renderAutoDrain(payload.drain);
|
|
1709
|
-
if (typeof renderAutoMint === 'function' && payload.mint) renderAutoMint(payload.mint);
|
|
1710
1732
|
}
|
|
1711
1733
|
|
|
1712
1734
|
/**
|
|
@@ -3585,6 +3607,125 @@ async function loadModels(cls) {
|
|
|
3585
3607
|
}
|
|
3586
3608
|
}
|
|
3587
3609
|
|
|
3610
|
+
// --------------------------------------------------------- queue model class
|
|
3611
|
+
//
|
|
3612
|
+
// The queue card's own class/model picker, independent of the chat
|
|
3613
|
+
// composer's (QUEUE_CLASS_KEY vs CLASS_KEY — see the constant above): a
|
|
3614
|
+
// queued task can run on a different backend than whatever the composer is
|
|
3615
|
+
// currently pointed at. Deliberately a trimmed copy of loadClasses/loadModels
|
|
3616
|
+
// rather than a shared helper — the composer's version carries budget-ceiling
|
|
3617
|
+
// display and the autonomous-toggle visibility it owns, neither of which
|
|
3618
|
+
// belongs to a task that has not run yet.
|
|
3619
|
+
|
|
3620
|
+
/** Mirrors currentModelId() for the queue's own picker. */
|
|
3621
|
+
function queueSelectedModel() {
|
|
3622
|
+
if (els.queueModelInput && !els.queueModelInput.hidden) {
|
|
3623
|
+
const typed = els.queueModelInput.value.trim();
|
|
3624
|
+
if (typed) return typed;
|
|
3625
|
+
}
|
|
3626
|
+
return els.queueModelSelect ? els.queueModelSelect.value : '';
|
|
3627
|
+
}
|
|
3628
|
+
|
|
3629
|
+
/** Fan-out is an Aegis Cloud mechanism only (autonomous.js runTask forces it
|
|
3630
|
+
* off for any other class) — hide the control instead of leaving a checkbox
|
|
3631
|
+
* on screen that would silently do nothing, same reasoning as the composer's
|
|
3632
|
+
* autonomousToggleWrap (AUTONOMOUS_CLASS). */
|
|
3633
|
+
function updateQueueFanoutVisibility(cls) {
|
|
3634
|
+
const wrap = els.queueFanout && els.queueFanout.closest('label');
|
|
3635
|
+
if (wrap) wrap.hidden = cls !== 'aegis';
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
async function loadQueueModels(cls) {
|
|
3639
|
+
if (!els.queueModelSelect) return;
|
|
3640
|
+
updateQueueFanoutVisibility(cls);
|
|
3641
|
+
|
|
3642
|
+
els.queueModelSelect.hidden = false;
|
|
3643
|
+
els.queueModelSelect.disabled = false;
|
|
3644
|
+
if (els.queueModelInput) {
|
|
3645
|
+
els.queueModelInput.hidden = true;
|
|
3646
|
+
els.queueModelInput.value = '';
|
|
3647
|
+
}
|
|
3648
|
+
if (els.queueModelHint) els.queueModelHint.textContent = '';
|
|
3649
|
+
|
|
3650
|
+
els.queueModelSelect.innerHTML = '';
|
|
3651
|
+
if (cls === 'aegis') {
|
|
3652
|
+
// Only this class has a real default to fall back to (resolveModel fills
|
|
3653
|
+
// in the pooled brain) — byok/local tasks must name a model, so neither
|
|
3654
|
+
// gets this option.
|
|
3655
|
+
const auto = document.createElement('option');
|
|
3656
|
+
auto.value = '';
|
|
3657
|
+
auto.textContent = 'server default (auto)';
|
|
3658
|
+
els.queueModelSelect.appendChild(auto);
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
try {
|
|
3662
|
+
const data = await models.listModels(cls);
|
|
3663
|
+
const list = Array.isArray(data && data.models) ? data.models : [];
|
|
3664
|
+
const needsKey = Boolean(data && data.needsKey);
|
|
3665
|
+
const needsDaemon = Boolean(data && data.needsDaemon);
|
|
3666
|
+
const listed = !(data && data.listed === false);
|
|
3667
|
+
if (els.queueModelInput) {
|
|
3668
|
+
els.queueModelInput.hidden = !(cls === 'local' && (needsDaemon || !listed));
|
|
3669
|
+
}
|
|
3670
|
+
for (const m of list) {
|
|
3671
|
+
const opt = document.createElement('option');
|
|
3672
|
+
opt.value = m.id;
|
|
3673
|
+
opt.textContent = m.label || m.id;
|
|
3674
|
+
els.queueModelSelect.appendChild(opt);
|
|
3675
|
+
}
|
|
3676
|
+
if (!els.queueModelHint) return;
|
|
3677
|
+
if (needsKey) {
|
|
3678
|
+
els.queueModelHint.textContent =
|
|
3679
|
+
'Connect this class in the Model card above (API key / provider key), then Refresh.';
|
|
3680
|
+
} else if (needsDaemon) {
|
|
3681
|
+
els.queueModelHint.textContent =
|
|
3682
|
+
`No local model server at ${(data && data.baseURL) || 'the configured address'} — ` +
|
|
3683
|
+
'start it, then reopen this picker.';
|
|
3684
|
+
} else if (cls === 'local' && !listed) {
|
|
3685
|
+
els.queueModelHint.textContent =
|
|
3686
|
+
'That address answered but listed no models — type the model tag above.';
|
|
3687
|
+
} else if (!list.length) {
|
|
3688
|
+
els.queueModelHint.textContent =
|
|
3689
|
+
cls === 'local'
|
|
3690
|
+
? 'No models on this server yet — pull one first (ollama pull llama3.2).'
|
|
3691
|
+
: cls === 'aegis'
|
|
3692
|
+
? "No models to pin — this runs the pooled route on the server's own default."
|
|
3693
|
+
: 'No models listed.';
|
|
3694
|
+
} else if (cls === 'byok' && data && data.needsProviderKey) {
|
|
3695
|
+
els.queueModelHint.textContent =
|
|
3696
|
+
'A provider key is required before a queued task on this model can run — add one in the Model card above.';
|
|
3697
|
+
}
|
|
3698
|
+
} catch (err) {
|
|
3699
|
+
if (els.queueModelHint) {
|
|
3700
|
+
els.queueModelHint.textContent = `listModels failed: ${err && err.message ? err.message : err}`;
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3705
|
+
async function loadQueueClasses() {
|
|
3706
|
+
if (!els.queueClassSelect) return;
|
|
3707
|
+
let classOptions = [];
|
|
3708
|
+
try {
|
|
3709
|
+
classOptions = (await models.listClasses()) || [];
|
|
3710
|
+
} catch (err) {
|
|
3711
|
+
classOptions = [];
|
|
3712
|
+
if (els.queueModelHint) {
|
|
3713
|
+
els.queueModelHint.textContent = `listClasses failed: ${err && err.message ? err.message : err}`;
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
els.queueClassSelect.innerHTML = '';
|
|
3717
|
+
for (const c of classOptions) {
|
|
3718
|
+
const opt = document.createElement('option');
|
|
3719
|
+
opt.value = c.class;
|
|
3720
|
+
opt.textContent = c.configured ? c.label : `${c.label} (not configured)`;
|
|
3721
|
+
els.queueClassSelect.appendChild(opt);
|
|
3722
|
+
}
|
|
3723
|
+
const saved = localStorage.getItem(QUEUE_CLASS_KEY);
|
|
3724
|
+
if (saved && classOptions.some((c) => c.class === saved)) {
|
|
3725
|
+
els.queueClassSelect.value = saved;
|
|
3726
|
+
}
|
|
3727
|
+
await loadQueueModels(els.queueClassSelect.value);
|
|
3728
|
+
}
|
|
3588
3729
|
|
|
3589
3730
|
// -------------------------------------------------------------- settings pane
|
|
3590
3731
|
|
|
@@ -4756,10 +4897,17 @@ async function init() {
|
|
|
4756
4897
|
// A worker's frames are a status line, never transcript entries (that
|
|
4757
4898
|
// separation is main.js's own progress channel, not aegis:chatDelta).
|
|
4758
4899
|
if (typeof queueApi.onProgress === 'function') queueApi.onProgress(renderQueueProgress);
|
|
4900
|
+
if (els.queueClassSelect) {
|
|
4901
|
+
els.queueClassSelect.addEventListener('change', () => {
|
|
4902
|
+
localStorage.setItem(QUEUE_CLASS_KEY, els.queueClassSelect.value);
|
|
4903
|
+
loadQueueModels(els.queueClassSelect.value);
|
|
4904
|
+
});
|
|
4905
|
+
}
|
|
4759
4906
|
// Paint the card once, from state that already exists on disk. A list, not a
|
|
4760
4907
|
// drain: queueing a task in another window (or from the CLI) and opening
|
|
4761
4908
|
// this one must show that task, and must not run it.
|
|
4762
4909
|
await loadQueueState();
|
|
4910
|
+
await loadQueueClasses();
|
|
4763
4911
|
}
|
|
4764
4912
|
|
|
4765
4913
|
// ── the two automation switches ──────────────────────────────────────────
|
package/renderer/index.html
CHANGED
|
@@ -277,6 +277,24 @@
|
|
|
277
277
|
rows="2"
|
|
278
278
|
placeholder="Queue a task to run unattended…"
|
|
279
279
|
></textarea>
|
|
280
|
+
<!-- Which backend this task runs on — the same three classes
|
|
281
|
+
(window.models.listClasses()) and the same per-class model list
|
|
282
|
+
(listModels(cls)) the chat composer's #class-select/#model-select
|
|
283
|
+
use. Aegis Cloud can leave the model on "server default"; byok and
|
|
284
|
+
local tasks must name one (autonomous.js modelRefusal refuses a
|
|
285
|
+
blank at the door for those two). -->
|
|
286
|
+
<div class="api-key-row">
|
|
287
|
+
<select id="queue-class-select" title="Which backend this task runs on."></select>
|
|
288
|
+
<select id="queue-model-select" title="The model for the picked class."></select>
|
|
289
|
+
</div>
|
|
290
|
+
<input
|
|
291
|
+
type="text"
|
|
292
|
+
id="queue-model-input"
|
|
293
|
+
placeholder="model tag — e.g. llama3.2:3b"
|
|
294
|
+
autocomplete="off"
|
|
295
|
+
hidden
|
|
296
|
+
/>
|
|
297
|
+
<p class="hint" id="queue-model-hint"></p>
|
|
280
298
|
<div class="api-key-row">
|
|
281
299
|
<input
|
|
282
300
|
type="text"
|
package/vendor/aegis.js
CHANGED
|
@@ -841,10 +841,16 @@ function createClient(opts = {}) {
|
|
|
841
841
|
* Ask aegis1 for a free device key when the caller has none configured yet
|
|
842
842
|
* (`POST /api/v1/auth/register-device`). Idempotent server-side on
|
|
843
843
|
* `fingerprint` — the same fingerprint always returns the same account's
|
|
844
|
-
* key rather than minting a new one.
|
|
845
|
-
*
|
|
846
|
-
*
|
|
847
|
-
*
|
|
844
|
+
* key rather than minting a new one. A BRAND-NEW fingerprint (first call
|
|
845
|
+
* ever for this machine) is additionally granted aegis1's 30-day
|
|
846
|
+
* welcome-trial `unlimited` entitlement in the SAME request that mints the
|
|
847
|
+
* key — a token-bank credit of UNLIMITED_CLOUD_ALLOWANCE_MICROS plus a
|
|
848
|
+
* time-boxed `subscriptions` row (aegis1 commit 4372830), so a fresh
|
|
849
|
+
* install can use pooled cloud models immediately instead of starting at a
|
|
850
|
+
* zero balance. Re-registering an already-known fingerprint does NOT
|
|
851
|
+
* re-grant it; the pool balance gate still applies once the allowance is
|
|
852
|
+
* spent or the 30 days lapse. Sends no API key of its own — there isn't
|
|
853
|
+
* one yet.
|
|
848
854
|
*/
|
|
849
855
|
async function registerDevice(fingerprint, opts) {
|
|
850
856
|
return apiPost('/api/v1/auth/register-device', { fingerprint }, undefined, opts);
|
package/vendor/auto-provision.js
CHANGED
|
@@ -12,11 +12,18 @@
|
|
|
12
12
|
* device-fingerprint.js) so a wiped credential store still resolves back to
|
|
13
13
|
* the same account rather than minting a new, unfunded one.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* As of aegis1 commit 4372830, a brand-new fingerprint (first call ever for
|
|
16
|
+
* this machine) is granted the 30-day welcome-trial `unlimited` entitlement
|
|
17
|
+
* server-side — a token-bank credit (UNLIMITED_CLOUD_ALLOWANCE_MICROS) and a
|
|
18
|
+
* time-boxed `unlimited` row in `subscriptions` — in the SAME request that
|
|
19
|
+
* mints the key, not a separate step this client has to ask for. is_pro()'s
|
|
20
|
+
* pool gate and the BYOK fee waiver both see that grant immediately. This
|
|
21
|
+
* comment previously said the account "starts at ZERO balance" and "only
|
|
22
|
+
* unblocks BYOK" — that described pre-4372830 behavior and was stale; see
|
|
23
|
+
* services/billing_service.py's is_pro()/is_unlimited() docstrings on the
|
|
24
|
+
* aegis1 side for the current contract. A re-registration of an already-known
|
|
25
|
+
* fingerprint (the idempotent path) does NOT re-grant the trial — it only
|
|
26
|
+
* happens once per machine, ever.
|
|
20
27
|
*/
|
|
21
28
|
const { machineFingerprint } = require('./device-fingerprint.js');
|
|
22
29
|
|
package/vendor/prefs.js
CHANGED
|
@@ -69,6 +69,11 @@ const SHARED_PREF_KEYS = Object.freeze([
|
|
|
69
69
|
'effort',
|
|
70
70
|
'vim',
|
|
71
71
|
'memoryPersist',
|
|
72
|
+
// ON8 / D8 rule 1: the confirmation gate is a POSTURE, not a surface. The
|
|
73
|
+
// window's "confirm before running tools" switch and the shell's rule set
|
|
74
|
+
// answer the same question ("may this call run unattended?"), so the switch
|
|
75
|
+
// lives beside the rules that gate answers, in the one store both hosts read.
|
|
76
|
+
'confirmMode',
|
|
72
77
|
]);
|
|
73
78
|
|
|
74
79
|
/**
|
|
@@ -79,6 +84,15 @@ const SHARED_PREF_KEYS = Object.freeze([
|
|
|
79
84
|
*/
|
|
80
85
|
const LEGACY_MEMORY_PERSIST_NAMESPACE = '__memoryPersist';
|
|
81
86
|
|
|
87
|
+
/**
|
|
88
|
+
* The second legacy namespace this store retires (ON8): the window's
|
|
89
|
+
* `__confirmMode: { enabled }` becomes the shared `confirmMode` boolean. Same
|
|
90
|
+
* rule, same reason as memory-persist above — and it is the one that closes
|
|
91
|
+
* D8's rule 1 in the direction that used to be impossible: a posture set in
|
|
92
|
+
* the window is now the posture the shell reads.
|
|
93
|
+
*/
|
|
94
|
+
const LEGACY_CONFIRM_MODE_NAMESPACE = '__confirmMode';
|
|
95
|
+
|
|
82
96
|
/**
|
|
83
97
|
* Key names that mean "secret" wherever they appear in the tree. Deliberately
|
|
84
98
|
* conservative: `keyMask`-style *previews* never reach this file (they are not
|
|
@@ -233,7 +247,7 @@ function remove(key, opts) {
|
|
|
233
247
|
/**
|
|
234
248
|
* The non-secret half of a legacy `<userData>/settings.json`, in the shared
|
|
235
249
|
* store's spellings. Only namespaces THIS store owns are extracted: the other
|
|
236
|
-
* app-level namespaces (`
|
|
250
|
+
* app-level namespaces (`__avatar`, `__telemetry`,
|
|
237
251
|
* `__autoDrain`, `__autoMint`, `__quickLauncher`) stay host-local until their
|
|
238
252
|
* own phase moves them, and the provider rows are never here at all because a
|
|
239
253
|
* provider row is where a key lives.
|
|
@@ -244,6 +258,13 @@ function extractNonSecretHalf(legacy) {
|
|
|
244
258
|
if (src && typeof src === 'object' && typeof src.enabled === 'boolean') {
|
|
245
259
|
out.memoryPersist = src.enabled;
|
|
246
260
|
}
|
|
261
|
+
// ON8: the confirmation gate adopts the same way. `enabled: true` means the
|
|
262
|
+
// gate is UP, which is the shared `confirmMode: true`; the shared key keeps
|
|
263
|
+
// the window's own polarity so an adopting install cannot flip its gate.
|
|
264
|
+
const gate = (legacy || {})[LEGACY_CONFIRM_MODE_NAMESPACE];
|
|
265
|
+
if (gate && typeof gate === 'object' && typeof gate.enabled === 'boolean') {
|
|
266
|
+
out.confirmMode = gate.enabled;
|
|
267
|
+
}
|
|
247
268
|
return out;
|
|
248
269
|
}
|
|
249
270
|
|
|
@@ -290,10 +311,19 @@ function adopt(opts) {
|
|
|
290
311
|
}
|
|
291
312
|
const patch = {};
|
|
292
313
|
for (const k of keys) patch[k] = half[k];
|
|
314
|
+
// `names`, NOT `keys` (AEGISONE ON8): this ledger is written through `write()`
|
|
315
|
+
// below, so it is scanned like everything else — and `SECRET_KEY_RE` matches
|
|
316
|
+
// `key`/`keys` by design, because a preference named `keys` IS how key material
|
|
317
|
+
// would smuggle itself into a file the desktop and the shell both read. Spelled
|
|
318
|
+
// `keys`, this field made `assertNoSecrets` refuse the store's own write, so
|
|
319
|
+
// EVERY adoption with something to adopt threw — the whole pre-D5 migration was
|
|
320
|
+
// dead, silently (settings.js's best-effort catch turned the throw into "no
|
|
321
|
+
// adoption" and no one was told). The scanner is the control here, not the bug;
|
|
322
|
+
// a field name is free to change.
|
|
293
323
|
patch.adopted = {
|
|
294
324
|
from: legacyFile,
|
|
295
325
|
at: new Date().toISOString(),
|
|
296
|
-
|
|
326
|
+
names: keys.slice().sort(),
|
|
297
327
|
};
|
|
298
328
|
write({ ...current, ...patch }, o);
|
|
299
329
|
return { adopted: true, keys: keys.slice().sort(), from: legacyFile };
|