aegis-desktop 1.0.13 → 1.0.14
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/google-connect.js +68 -10
- package/lib/local/engine.js +74 -1
- package/lib/local/entitlement.js +7 -1
- package/main.js +261 -122
- package/package.json +1 -4
- package/preload.js +13 -5
- package/renderer/app.js +102 -2
- package/renderer/fingerprint.js +17 -1
- package/vendor/aegis.js +128 -3
- package/vendor/drive-connect.js +28 -3
- package/vendor/google-signin.js +12 -4
- package/vendor/model-discovery.js +43 -0
package/lib/google-connect.js
CHANGED
|
@@ -25,10 +25,15 @@
|
|
|
25
25
|
* account purely by Google identity, with no notion of "the account
|
|
26
26
|
* this machine's API key already belongs to" — so connecting Drive
|
|
27
27
|
* before signing in attaches the tokens to a DIFFERENT account than the
|
|
28
|
-
* one this machine will poll, and the poll just times out.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* one this machine will poll, and the poll just times out.
|
|
29
|
+
*
|
|
30
|
+
* "Signed in with Google" is NOT "has an API key". Every install
|
|
31
|
+
* auto-provisions an anonymous device key, so a key always exists and an
|
|
32
|
+
* `!apiKey` guard never fires. The only honest source for the real
|
|
33
|
+
* question is the server's `google_linked` (users.google_id), carried on
|
|
34
|
+
* `/api/drive/token` and surfaced as `status().googleLinked` — so
|
|
35
|
+
* `connectDrive` refuses on that, and the renderer disables the button
|
|
36
|
+
* from the same flag.
|
|
32
37
|
*
|
|
33
38
|
* Electron-free and fully injected — same shape as `account-key.js` next to
|
|
34
39
|
* it — so the whole flow runs under a test with a stub `aegis` client, a
|
|
@@ -50,6 +55,10 @@ function createGoogleConnect({
|
|
|
50
55
|
let connectInFlight = null;
|
|
51
56
|
let lastSyncAt = null;
|
|
52
57
|
let lastSyncError = null;
|
|
58
|
+
// Did a Google round-trip complete in THIS process? That is first-hand
|
|
59
|
+
// evidence of a linked account and does not depend on the server reporting
|
|
60
|
+
// it — which older servers do not (see `status`).
|
|
61
|
+
let signedInViaGoogle = false;
|
|
53
62
|
|
|
54
63
|
/** Sign this machine in with Google, replacing whatever key it had. */
|
|
55
64
|
function signInWithGoogle() {
|
|
@@ -88,6 +97,10 @@ function createGoogleConnect({
|
|
|
88
97
|
/* best-effort — the account key itself is already saved */
|
|
89
98
|
}
|
|
90
99
|
}
|
|
100
|
+
// Before the notification, which is what makes the renderer re-read
|
|
101
|
+
// `status()` — otherwise that refresh races this assignment and
|
|
102
|
+
// reports the sign-in as not having happened.
|
|
103
|
+
signedInViaGoogle = true;
|
|
91
104
|
if (typeof onAccountChange === 'function') onAccountChange();
|
|
92
105
|
return { ok: true, opened, url: session.url, username: result.username || null };
|
|
93
106
|
} finally {
|
|
@@ -99,16 +112,41 @@ function createGoogleConnect({
|
|
|
99
112
|
}
|
|
100
113
|
|
|
101
114
|
/** Open `/connect/drive` and wait for this account to show connected. */
|
|
102
|
-
function connectDrive() {
|
|
115
|
+
async function connectDrive() {
|
|
103
116
|
if (!aegis.apiKey) {
|
|
104
|
-
return
|
|
117
|
+
return {
|
|
105
118
|
ok: false,
|
|
106
119
|
error: 'sign in with Google first — this device has no AEGIS account key yet',
|
|
107
|
-
}
|
|
120
|
+
};
|
|
108
121
|
}
|
|
109
122
|
if (!connectInFlight) {
|
|
110
123
|
connectInFlight = (async () => {
|
|
111
124
|
try {
|
|
125
|
+
// The key exists but may be an anonymous device key. Ask the server
|
|
126
|
+
// whether this account is actually tied to a Google identity before
|
|
127
|
+
// opening a consent screen that would attach the tokens elsewhere.
|
|
128
|
+
// Inside the in-flight promise so a double click cannot open two
|
|
129
|
+
// consent tabs. If the status call fails, fall through: a transport
|
|
130
|
+
// blip must not be reported as "not signed in with Google".
|
|
131
|
+
let linked = true;
|
|
132
|
+
try {
|
|
133
|
+
const st = await driveConnect.driveStatus(aegis);
|
|
134
|
+
// Refuse only on a definite "not linked". A server that predates
|
|
135
|
+
// `google_linked` answers the same shape as an unlinked account,
|
|
136
|
+
// and refusing on that would make a real Google sign-in unable to
|
|
137
|
+
// ever connect Drive — the exact dead end this guard exists to
|
|
138
|
+
// prevent, only inverted.
|
|
139
|
+
linked = !(st.googleLinkedKnown && !st.googleLinked);
|
|
140
|
+
} catch {
|
|
141
|
+
/* unknown — let the flow try rather than misreport the reason */
|
|
142
|
+
}
|
|
143
|
+
if (!linked) {
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
error: 'sign in with Google first — this account is not linked to a Google identity yet, '
|
|
147
|
+
+ 'and connecting Drive now would attach it to a different account',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
112
150
|
const url = driveConnect.driveConnectUrl(aegis.apiBase);
|
|
113
151
|
const opened = Boolean(openUrl(url));
|
|
114
152
|
try {
|
|
@@ -125,11 +163,31 @@ function createGoogleConnect({
|
|
|
125
163
|
return connectInFlight;
|
|
126
164
|
}
|
|
127
165
|
|
|
128
|
-
/**
|
|
166
|
+
/**
|
|
167
|
+
* A one-shot read: signed in with Google, and if so, is Drive connected.
|
|
168
|
+
*
|
|
169
|
+
* `signedIn` is the server's `google_linked`, not "has an API key" — see
|
|
170
|
+
* the header. The old `apiKey ? true` form reported every anonymous
|
|
171
|
+
* auto-provisioned device as signed in, which both mislabelled the card
|
|
172
|
+
* ("signed in · Drive not connected yet" on a machine that never touched
|
|
173
|
+
* Google) and enabled Connect Drive ahead of the sign-in it depends on.
|
|
174
|
+
*/
|
|
129
175
|
async function status() {
|
|
130
|
-
if (!aegis.apiKey)
|
|
176
|
+
if (!aegis.apiKey) {
|
|
177
|
+
return { signedIn: false, googleLinked: false, googleLinkedKnown: true, connected: false, email: null };
|
|
178
|
+
}
|
|
131
179
|
const s = await driveConnect.driveStatus(aegis);
|
|
132
|
-
|
|
180
|
+
// A completed round-trip outranks a server that cannot answer: without
|
|
181
|
+
// this, signing in against a server that predates `google_linked` leaves
|
|
182
|
+
// the card reading "not signed in" forever, because the sign-in that just
|
|
183
|
+
// succeeded is the one thing the flag was supposed to prove.
|
|
184
|
+
const signedIn = s.googleLinked === true || signedInViaGoogle;
|
|
185
|
+
return {
|
|
186
|
+
signedIn,
|
|
187
|
+
...s,
|
|
188
|
+
googleLinked: s.googleLinked === true || signedIn,
|
|
189
|
+
googleLinkedKnown: s.googleLinkedKnown || signedIn,
|
|
190
|
+
};
|
|
133
191
|
}
|
|
134
192
|
|
|
135
193
|
/** Pull this account's AEGIS cloud memory and back it up to Drive. */
|
package/lib/local/engine.js
CHANGED
|
@@ -745,6 +745,7 @@ const { offerableCatalog } = requireSharedBrain();
|
|
|
745
745
|
const { withByokAdditions } = requireSharedByokCatalog();
|
|
746
746
|
const {
|
|
747
747
|
withUserModels,
|
|
748
|
+
unmatchedUserProviders,
|
|
748
749
|
load: loadUserModels,
|
|
749
750
|
addUserModel,
|
|
750
751
|
removeUserModel,
|
|
@@ -1671,6 +1672,16 @@ function createLocalEngine({
|
|
|
1671
1672
|
// it via `fee.require_balance` below.
|
|
1672
1673
|
let providers = [];
|
|
1673
1674
|
let fee = null;
|
|
1675
|
+
// Did the CATALOG itself fail to arrive, as opposed to arriving empty?
|
|
1676
|
+
// The two are different states and the picker must not confuse them: a
|
|
1677
|
+
// 5xx / offline relay used to fall into the bare `catch` below and hand
|
|
1678
|
+
// back `providers: []`, which reads on screen exactly like an account
|
|
1679
|
+
// with nothing configured — while the user's provider row and their
|
|
1680
|
+
// `models.json` declaration sat intact on disk. Reported instead, the
|
|
1681
|
+
// same way the 'local' branch reports `listed: false` for a tag list
|
|
1682
|
+
// that did not arrive.
|
|
1683
|
+
let providersFailed = false;
|
|
1684
|
+
let providersError = null;
|
|
1674
1685
|
try {
|
|
1675
1686
|
const data = await aegis.byokProviders();
|
|
1676
1687
|
// The server's catalog, plus the ids it does not name but the vendor
|
|
@@ -1688,8 +1699,16 @@ function createLocalEngine({
|
|
|
1688
1699
|
// Absent until the server publishes one, and the UI must then say
|
|
1689
1700
|
// nothing rather than show a guess.
|
|
1690
1701
|
fee = (data && data.fee) || null;
|
|
1691
|
-
} catch {
|
|
1702
|
+
} catch (err) {
|
|
1703
|
+
// Keep the empty list (nothing to offer) but SAY why, so the renderer
|
|
1704
|
+
// can distinguish "catalog unreachable" from "catalog said none". The
|
|
1705
|
+
// message is carried rather than a boolean alone because the two cases
|
|
1706
|
+
// need different copy: offline wants a retry, a server error wants the
|
|
1707
|
+
// status. `err.message` is the transport's own text, already shaped
|
|
1708
|
+
// elsewhere in this file.
|
|
1692
1709
|
providers = [];
|
|
1710
|
+
providersFailed = true;
|
|
1711
|
+
providersError = (err && err.message) || String(err);
|
|
1693
1712
|
}
|
|
1694
1713
|
const models = [];
|
|
1695
1714
|
for (const p of providers) {
|
|
@@ -1725,6 +1744,18 @@ function createLocalEngine({
|
|
|
1725
1744
|
needsProviderKey: models.length > 0 && !models.some((m) => m.configured),
|
|
1726
1745
|
needsAegisKey: !aegis.apiKey,
|
|
1727
1746
|
fee,
|
|
1747
|
+
// True only when the catalog did not arrive — never merely because it
|
|
1748
|
+
// arrived with no providers. `withUserModels` never fabricates a
|
|
1749
|
+
// provider, so this list is the same either side of the fold above.
|
|
1750
|
+
providersFailed,
|
|
1751
|
+
providersError,
|
|
1752
|
+
// Declarations in `models.json` naming a provider the catalog does not
|
|
1753
|
+
// list — a typo, or a provider renamed/dropped server-side. `withUserModels`
|
|
1754
|
+
// deliberately does NOT invent a row for them (it would 401 on its first
|
|
1755
|
+
// turn), but silence is the other wrong answer: the model stays in the
|
|
1756
|
+
// file and the user is entitled to know the picker cannot show it.
|
|
1757
|
+
// Carried as the ids alone; the renderer names them.
|
|
1758
|
+
unmatchedProviders: unmatchedUserProviders(providers),
|
|
1728
1759
|
};
|
|
1729
1760
|
}
|
|
1730
1761
|
if (cls === 'local') {
|
|
@@ -2444,6 +2475,32 @@ function createLocalEngine({
|
|
|
2444
2475
|
const refusal = localT.remoteRefusal(cfg.baseURL || localT.DEFAULT_BASE);
|
|
2445
2476
|
if (refusal) throw refusal;
|
|
2446
2477
|
}
|
|
2478
|
+
// `custom` is the mirror of `local`: both are rows whose payload is an
|
|
2479
|
+
// ADDRESS rather than a credential, and both are unusable without it.
|
|
2480
|
+
// They differ in what absence MEANS. A blank `local` row falls back to
|
|
2481
|
+
// DEFAULT_BASE (the stock Ollama port) because there is exactly one
|
|
2482
|
+
// sensible address to guess. A blank `custom` row has no default at all —
|
|
2483
|
+
// the catalog descriptor pins none, so the server refuses the call with a
|
|
2484
|
+
// bare 400 that says nothing about which half of the setup is missing.
|
|
2485
|
+
//
|
|
2486
|
+
// Caught here, before any byte goes out, so the message can name the
|
|
2487
|
+
// command that fixes it. Reachable in practice: `/byok-key custom <key>`
|
|
2488
|
+
// stores a key with no address, and the CLI explicitly trusts an
|
|
2489
|
+
// unreachable catalog rather than refusing the id — so a user who typed
|
|
2490
|
+
// the key first and the endpoint never has a row that looks configured.
|
|
2491
|
+
// Keyed on the provider, not the class: every other byok provider's host
|
|
2492
|
+
// is a server-side constant and needs no address from us.
|
|
2493
|
+
if (cls === 'byok' && byokParts.provider === 'custom' && !(cfg.baseURL || '').trim()) {
|
|
2494
|
+
const err = new Error(
|
|
2495
|
+
'No endpoint saved for the "custom" provider — its key is on this machine but the address ' +
|
|
2496
|
+
'it dials is not, and no default exists for a vendor AEGIS does not know. Fix: ' +
|
|
2497
|
+
'/model-add custom <https-base-url> <api-key> <model-id> (writes both halves), or the ' +
|
|
2498
|
+
'endpoint field in Settings › Provider (custom).'
|
|
2499
|
+
);
|
|
2500
|
+
err.status = 400;
|
|
2501
|
+
err.code = 'custom_endpoint_missing';
|
|
2502
|
+
throw err;
|
|
2503
|
+
}
|
|
2447
2504
|
const base = {
|
|
2448
2505
|
cls, model, mode: payload && payload.mode, maxTokens, statedMaxTokens, autonomous, sessionId, signal, onDelta, cfg, apiKey, toolChoice,
|
|
2449
2506
|
// The turn's working directory, computed above as `turnCwd` — `dispatch`
|
|
@@ -3024,6 +3081,22 @@ function createLocalEngine({
|
|
|
3024
3081
|
});
|
|
3025
3082
|
}
|
|
3026
3083
|
}
|
|
3084
|
+
} catch (e) {
|
|
3085
|
+
// Stamp the stop at the one place it is known for certain.
|
|
3086
|
+
//
|
|
3087
|
+
// `cancel(sessionId)` aborts THIS turn's controller, and the in-flight
|
|
3088
|
+
// transport unwinds as a rejection. The host has to tell that rejection
|
|
3089
|
+
// apart from a genuine provider/transport failure before it can stop
|
|
3090
|
+
// printing Ctrl-X as an application crash — and it used to guess from the
|
|
3091
|
+
// message text, which is exactly how a real failure ("connection aborted
|
|
3092
|
+
// by peer", "upstream aborted the connection") gets relabelled as a
|
|
3093
|
+
// deliberate stop and leaves the user with a silently empty answer. The
|
|
3094
|
+
// message is not evidence. This is: the signal reads aborted only because
|
|
3095
|
+
// the user asked for it, so it cannot be confused with anything upstream
|
|
3096
|
+
// did. Attached to the error object itself so it survives any rethrow,
|
|
3097
|
+
// and read by `isAbortShapedError` in desktop/main.js.
|
|
3098
|
+
if (signal.aborted && e && typeof e === 'object') e.aegisCancelled = true;
|
|
3099
|
+
throw e;
|
|
3027
3100
|
} finally {
|
|
3028
3101
|
if (shell) shell.dispose();
|
|
3029
3102
|
controllers.delete(sessionId);
|
package/lib/local/entitlement.js
CHANGED
|
@@ -203,9 +203,15 @@ function byokFeeLine(fee) {
|
|
|
203
203
|
// default-off gate after it. Joined with ' — ' only when there IS a second
|
|
204
204
|
// clause: a sentence-ending dash with nothing after it ("…provider's bill —.")
|
|
205
205
|
// is what the first live run of this line actually printed.
|
|
206
|
+
//
|
|
207
|
+
// This rate is the deployment's published RATE, not a quote for the reader
|
|
208
|
+
// specifically — an Unlimited subscriber is waived from this fee entirely
|
|
209
|
+
// (billing_service.is_unlimited on aegis1), so the number above is what a
|
|
210
|
+
// Free-tier BYOK caller pays, not what everyone pays.
|
|
206
211
|
const price = `Bring your own key: relayed through AEGIS, handled at `
|
|
207
212
|
+ `${ratePerMillion(inRate)} in / ${ratePerMillion(outRate)} out, `
|
|
208
|
-
+ `on top of your provider's bill
|
|
213
|
+
+ `on top of your provider's bill — no fee at all on Unlimited, `
|
|
214
|
+
+ `and no fee at all during the free 1-month Unlimited trial`;
|
|
209
215
|
let clause = '';
|
|
210
216
|
if (f.require_account === true) {
|
|
211
217
|
clause = 'and needs an AEGIS account key (X-AEGIS-Key) for the fee to be billed';
|