@cortexkit/antigravity-auth-core 3.0.5 → 3.0.7
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/dist/account-manager.d.ts +15 -23
- package/dist/account-manager.d.ts.map +1 -1
- package/dist/account-manager.js +18 -32
- package/dist/account-manager.js.map +1 -1
- package/dist/account-migration.d.ts +1 -1
- package/dist/account-migration.d.ts.map +1 -1
- package/dist/account-migration.js +228 -53
- package/dist/account-migration.js.map +1 -1
- package/dist/account-repository-types.d.ts +1 -1
- package/dist/account-selector.d.ts +13 -27
- package/dist/account-selector.d.ts.map +1 -1
- package/dist/account-selector.js +38 -145
- package/dist/account-selector.js.map +1 -1
- package/dist/account-types.d.ts +1 -3
- package/dist/account-types.d.ts.map +1 -1
- package/dist/agy-transport.d.ts.map +1 -1
- package/dist/agy-transport.js +2 -0
- package/dist/agy-transport.js.map +1 -1
- package/dist/antigravity/oauth.d.ts +5 -1
- package/dist/antigravity/oauth.d.ts.map +1 -1
- package/dist/antigravity/oauth.js +43 -9
- package/dist/antigravity/oauth.js.map +1 -1
- package/dist/antigravity-command-menu.d.ts +4 -8
- package/dist/antigravity-command-menu.d.ts.map +1 -1
- package/dist/antigravity-command-menu.js +14 -37
- package/dist/antigravity-command-menu.js.map +1 -1
- package/dist/common-auth-embedded/claustrum/consumer.d.ts +44 -4
- package/dist/common-auth-embedded/claustrum/consumer.js +134 -2
- package/dist/common-auth-embedded/claustrum/custody.d.ts +33 -1
- package/dist/common-auth-embedded/claustrum/custody.js +17 -0
- package/dist/common-auth-embedded/claustrum/enrollment.d.ts +6 -1
- package/dist/common-auth-embedded/claustrum/enrollment.js +6 -1
- package/dist/common-auth-embedded/claustrum/index.d.ts +3 -2
- package/dist/common-auth-embedded/claustrum/index.js +5 -3
- package/dist/common-auth-embedded/claustrum/insula.d.ts +69 -0
- package/dist/common-auth-embedded/claustrum/insula.js +152 -0
- package/dist/common-auth-embedded/claustrum/roster.d.ts +13 -0
- package/dist/common-auth-embedded/claustrum/roster.js +33 -3
- package/dist/common-auth-embedded/fs/refresh-file-lock.d.ts +8 -2
- package/dist/common-auth-embedded/fs/refresh-file-lock.js +26 -12
- package/dist/common-auth-embedded/pi-slot/lock.js +7 -3
- package/dist/common-auth-embedded/quota/codec.d.ts +2 -0
- package/dist/common-auth-embedded/quota/codec.js +2 -1
- package/dist/common-auth-embedded/quota/format.d.ts +40 -3
- package/dist/common-auth-embedded/quota/format.js +196 -43
- package/dist/common-auth-embedded/quota/index.d.ts +4 -4
- package/dist/common-auth-embedded/quota/index.js +2 -2
- package/dist/common-auth-embedded/quota/map.d.ts +11 -0
- package/dist/common-auth-embedded/quota/map.js +36 -1
- package/dist/common-auth-embedded/quota/merge.d.ts +3 -1
- package/dist/common-auth-embedded/quota/merge.js +21 -5
- package/dist/common-auth-embedded/quota/projection.d.ts +2 -1
- package/dist/common-auth-embedded/quota/projection.js +3 -1
- package/dist/common-auth-embedded/source-output.json +117 -101
- package/dist/common-auth-embedded/store/pool.d.ts +1 -1
- package/dist/common-auth-embedded/store/pool.js +0 -1
- package/dist/common-auth-embedded/store/refresh-lock.d.ts +0 -1
- package/dist/common-auth-embedded/store/refresh-lock.js +0 -1
- package/dist/common-auth-embedded/store/schema.d.ts +2 -0
- package/dist/common-auth-embedded/store/schema.js +3 -2
- package/dist/common-auth-runtime.d.ts +2 -1
- package/dist/common-auth-runtime.d.ts.map +1 -1
- package/dist/common-auth-runtime.js +2 -1
- package/dist/common-auth-runtime.js.map +1 -1
- package/dist/constants.d.ts +1 -26
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +1 -37
- package/dist/constants.js.map +1 -1
- package/dist/credential-redaction.d.ts +8 -4
- package/dist/credential-redaction.d.ts.map +1 -1
- package/dist/credential-redaction.js +55 -28
- package/dist/credential-redaction.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/model-registry.d.ts +0 -1
- package/dist/model-registry.d.ts.map +1 -1
- package/dist/model-registry.js +3 -60
- package/dist/model-registry.js.map +1 -1
- package/dist/pi-paths.d.ts +14 -0
- package/dist/pi-paths.d.ts.map +1 -0
- package/dist/pi-paths.js +27 -0
- package/dist/pi-paths.js.map +1 -0
- package/dist/pre-response-deadline.d.ts +41 -0
- package/dist/pre-response-deadline.d.ts.map +1 -0
- package/dist/pre-response-deadline.js +150 -0
- package/dist/pre-response-deadline.js.map +1 -0
- package/dist/quota-manager.d.ts +7 -32
- package/dist/quota-manager.d.ts.map +1 -1
- package/dist/quota-manager.js +27 -76
- package/dist/quota-manager.js.map +1 -1
- package/dist/quota-types.d.ts +2 -11
- package/dist/quota-types.d.ts.map +1 -1
- package/dist/transform/index.d.ts +1 -1
- package/dist/transform/index.d.ts.map +1 -1
- package/dist/transform/index.js +1 -1
- package/dist/transform/index.js.map +1 -1
- package/dist/transform/model-resolver.d.ts +4 -28
- package/dist/transform/model-resolver.d.ts.map +1 -1
- package/dist/transform/model-resolver.js +33 -130
- package/dist/transform/model-resolver.js.map +1 -1
- package/dist/transform/types.d.ts +0 -5
- package/dist/transform/types.d.ts.map +1 -1
- package/dist/vault-account-source.d.ts +24 -3
- package/dist/vault-account-source.d.ts.map +1 -1
- package/dist/vault-account-source.js +39 -12
- package/dist/vault-account-source.js.map +1 -1
- package/dist/vault-folder-refusal.d.ts +32 -0
- package/dist/vault-folder-refusal.d.ts.map +1 -0
- package/dist/vault-folder-refusal.js +68 -0
- package/dist/vault-folder-refusal.js.map +1 -0
- package/dist/vault-grant-reach.d.ts +23 -0
- package/dist/vault-grant-reach.d.ts.map +1 -0
- package/dist/vault-grant-reach.js +15 -0
- package/dist/vault-grant-reach.js.map +1 -0
- package/dist/vault-provider-state.d.ts +1 -0
- package/dist/vault-provider-state.d.ts.map +1 -1
- package/dist/vault-provider-state.js +6 -0
- package/dist/vault-provider-state.js.map +1 -1
- package/package.json +3 -2
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createLogger } from '../logger/index.js';
|
|
2
2
|
import { combineAbortSignals } from '../util/any-signal.js';
|
|
3
3
|
import { ClaustrumScopedCustody, decideScopedRetryAfter401, } from './custody.js';
|
|
4
|
+
import { readClaustrumEnrollmentToken } from './enrollment.js';
|
|
4
5
|
import { ClaustrumConsumerError } from './errors.js';
|
|
5
|
-
import {
|
|
6
|
+
import { acquireInsulaQuotaSource } from './insula.js';
|
|
7
|
+
import { acceptVaultRoute, declineVaultRoute, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, vaultRoutingRows, } from './roster.js';
|
|
6
8
|
/**
|
|
7
9
|
* One plugin host's view of its vault accounts. It turns vault credentials
|
|
8
10
|
* into rows for `/routing`, authorizes each physical send with the vault, and
|
|
@@ -19,8 +21,14 @@ export class ClaustrumConsumer {
|
|
|
19
21
|
#connecting;
|
|
20
22
|
#refreshing;
|
|
21
23
|
#roster;
|
|
24
|
+
/**
|
|
25
|
+
* The grants named by this instance's latest discovery, with the view that
|
|
26
|
+
* discovery committed. Memory only: the roster file never carries it.
|
|
27
|
+
*/
|
|
28
|
+
#grantReach;
|
|
22
29
|
#timer;
|
|
23
30
|
#started = false;
|
|
31
|
+
#insula;
|
|
24
32
|
constructor(options) {
|
|
25
33
|
this.#options = options;
|
|
26
34
|
this.#logger = options.logger ?? createLogger('claustrum');
|
|
@@ -84,6 +92,9 @@ export class ClaustrumConsumer {
|
|
|
84
92
|
now: this.#options.now,
|
|
85
93
|
logger: this.#logger,
|
|
86
94
|
});
|
|
95
|
+
if (this.#options.insula && client.insula) {
|
|
96
|
+
this.#insula = acquireInsulaQuotaSource(client.insula, this.#options.tokenPath);
|
|
97
|
+
}
|
|
87
98
|
return this.#custody;
|
|
88
99
|
})
|
|
89
100
|
.finally(() => {
|
|
@@ -96,6 +107,21 @@ export class ClaustrumConsumer {
|
|
|
96
107
|
snapshot() {
|
|
97
108
|
return this.#roster;
|
|
98
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* What this consumer is granted, from its latest discovery, without any I/O.
|
|
112
|
+
* It explains an empty roster: `grants === 0` means no grant at all, while
|
|
113
|
+
* `grants > 0` means a grant that reaches no credential (`tuples` names its
|
|
114
|
+
* selector). Undefined means unknown, never zero: the vault or client is too
|
|
115
|
+
* old to send it, nothing was discovered yet, or the roster now in memory is
|
|
116
|
+
* a different view than the one this instance discovered (a peer process
|
|
117
|
+
* held the discovery lease and its newer roster was served instead).
|
|
118
|
+
*/
|
|
119
|
+
grantReach() {
|
|
120
|
+
const latest = this.#grantReach;
|
|
121
|
+
return latest && this.#roster?.view === latest.view
|
|
122
|
+
? latest.reach
|
|
123
|
+
: undefined;
|
|
124
|
+
}
|
|
99
125
|
/** Rows for `/routing`, from the last committed roster. */
|
|
100
126
|
routingRows() {
|
|
101
127
|
return vaultRoutingRows(this.#roster);
|
|
@@ -107,14 +133,25 @@ export class ClaustrumConsumer {
|
|
|
107
133
|
this.#refreshing = (async () => {
|
|
108
134
|
if (!(await this.#active())) {
|
|
109
135
|
this.#roster = undefined;
|
|
136
|
+
this.#grantReach = undefined;
|
|
110
137
|
this.#custody?.close();
|
|
111
138
|
this.#custody = undefined;
|
|
139
|
+
this.#insula?.release();
|
|
140
|
+
this.#insula = undefined;
|
|
112
141
|
return undefined;
|
|
113
142
|
}
|
|
114
143
|
const custody = await this.#getCustody();
|
|
144
|
+
// Capture the inventory so its grant fields can be kept beside the roster;
|
|
145
|
+
// the roster file itself never stores them.
|
|
146
|
+
let discovered;
|
|
115
147
|
const roster = await refreshVaultRoster({
|
|
116
148
|
path: this.#options.rosterPath,
|
|
117
|
-
custody
|
|
149
|
+
custody: {
|
|
150
|
+
discover: async (signal) => {
|
|
151
|
+
discovered = await custody.discover(signal);
|
|
152
|
+
return discovered;
|
|
153
|
+
},
|
|
154
|
+
},
|
|
118
155
|
isActive: () => this.#active(),
|
|
119
156
|
signal: this.#shutdown.signal,
|
|
120
157
|
projection: () => ({
|
|
@@ -127,6 +164,13 @@ export class ClaustrumConsumer {
|
|
|
127
164
|
this.#assertOpen();
|
|
128
165
|
const changed = roster?.view !== this.#roster?.view;
|
|
129
166
|
this.#roster = roster;
|
|
167
|
+
// Each discovery replaces the previous value, including with unknown. A
|
|
168
|
+
// refresh that served a peer's roster discovered nothing and leaves it;
|
|
169
|
+
// grantReach() hides it once the roster's view no longer matches.
|
|
170
|
+
if (discovered && roster?.view !== undefined)
|
|
171
|
+
this.#grantReach = discovered.grantReach
|
|
172
|
+
? { view: roster.view, reach: discovered.grantReach }
|
|
173
|
+
: undefined;
|
|
130
174
|
// Notify on view changes only: subscribers rewrite shared state on each
|
|
131
175
|
// notification, and the poll runs every few seconds.
|
|
132
176
|
if (roster && changed)
|
|
@@ -145,6 +189,7 @@ export class ClaustrumConsumer {
|
|
|
145
189
|
const tick = async () => {
|
|
146
190
|
try {
|
|
147
191
|
await this.refresh();
|
|
192
|
+
await this.refreshInsulaQuota();
|
|
148
193
|
}
|
|
149
194
|
catch (error) {
|
|
150
195
|
if (!this.#shutdown.signal.aborted)
|
|
@@ -163,6 +208,72 @@ export class ClaustrumConsumer {
|
|
|
163
208
|
};
|
|
164
209
|
void tick();
|
|
165
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Fetch on a background tick, at most once per minute across projects with
|
|
213
|
+
* the same daemon and enrollment token path. Admission must use the cache
|
|
214
|
+
* instead, so an optional quota daemon never delays a provider request.
|
|
215
|
+
*/
|
|
216
|
+
async refreshInsulaQuota() {
|
|
217
|
+
if (!this.#options.insula ||
|
|
218
|
+
this.#shutdown.signal.aborted ||
|
|
219
|
+
!(await this.#active()))
|
|
220
|
+
return;
|
|
221
|
+
try {
|
|
222
|
+
await readClaustrumEnrollmentToken(this.#options.tokenPath);
|
|
223
|
+
await this.#getCustody();
|
|
224
|
+
await this.#insula?.source.refresh((this.#options.now ?? Date.now)());
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// An optional source must never prevent the plugin's own quota poll.
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Cache-only, for unknown-quota admission with a served receipt. Undefined
|
|
232
|
+
* means poll the provider normally. Only get_scoped's asserted account_id
|
|
233
|
+
* joins insula; parsed identities and display metadata cannot substitute.
|
|
234
|
+
* Background polling should use cachedInsulaQuotaForRoute instead.
|
|
235
|
+
*/
|
|
236
|
+
async cachedInsulaQuota(receipt) {
|
|
237
|
+
if (!this.#options.insula ||
|
|
238
|
+
this.#shutdown.signal.aborted ||
|
|
239
|
+
!(await this.#active()))
|
|
240
|
+
return undefined;
|
|
241
|
+
try {
|
|
242
|
+
await readClaustrumEnrollmentToken(this.#options.tokenPath);
|
|
243
|
+
return this.#normalizeInsulaQuota(receipt.assertedAccountIdentity);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Background cache lookup without fetching a credential. Re-read the roster
|
|
251
|
+
* so a peer's account replacement invalidates the old route immediately.
|
|
252
|
+
* A route without a known provider account cannot join an insula entry.
|
|
253
|
+
*/
|
|
254
|
+
async cachedInsulaQuotaForRoute(routeId) {
|
|
255
|
+
if (!this.#options.insula ||
|
|
256
|
+
this.#shutdown.signal.aborted ||
|
|
257
|
+
!(await this.#active()))
|
|
258
|
+
return undefined;
|
|
259
|
+
try {
|
|
260
|
+
await readClaustrumEnrollmentToken(this.#options.tokenPath);
|
|
261
|
+
const roster = await readVaultRoster(this.#options.rosterPath);
|
|
262
|
+
const row = roster?.rows.find((entry) => entry.routeId === routeId);
|
|
263
|
+
return this.#normalizeInsulaQuota(row?.accountIdentity);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
#normalizeInsulaQuota(accountIdentity) {
|
|
270
|
+
if (!this.#options.insula || !accountIdentity)
|
|
271
|
+
return undefined;
|
|
272
|
+
const current = this.#insula?.source.current(this.#options.insula.provider, accountIdentity, (this.#options.now ?? Date.now)());
|
|
273
|
+
return current
|
|
274
|
+
? this.#options.insula.normalize(current.entry, current.context)
|
|
275
|
+
: undefined;
|
|
276
|
+
}
|
|
166
277
|
/**
|
|
167
278
|
* Authorize one physical send on a vault route. The roster file is re-read
|
|
168
279
|
* first, so a decline committed by another process applies before the next
|
|
@@ -285,6 +396,24 @@ export class ClaustrumConsumer {
|
|
|
285
396
|
this.#roster = await readVaultRoster(this.#options.rosterPath);
|
|
286
397
|
return kept;
|
|
287
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* Store a background observation that belongs to an account rather than to
|
|
401
|
+
* a served credential (an insula reading from `cachedInsulaQuotaForRoute`).
|
|
402
|
+
* It lands only while the route is still bound to that known account, so no
|
|
403
|
+
* credential fetch is needed and a replaced account's reading is dropped.
|
|
404
|
+
* The in-memory roster is reloaded from the file, without a vault call.
|
|
405
|
+
*/
|
|
406
|
+
async recordAccountQuota(routeId, accountIdentity, observation) {
|
|
407
|
+
this.#assertOpen();
|
|
408
|
+
const kept = await recordVaultAccountQuota(this.#options.rosterPath, {
|
|
409
|
+
routeId,
|
|
410
|
+
accountIdentity,
|
|
411
|
+
observation,
|
|
412
|
+
});
|
|
413
|
+
if (kept)
|
|
414
|
+
this.#roster = await readVaultRoster(this.#options.rosterPath);
|
|
415
|
+
return kept;
|
|
416
|
+
}
|
|
288
417
|
close() {
|
|
289
418
|
if (this.#shutdown.signal.aborted)
|
|
290
419
|
return;
|
|
@@ -293,6 +422,9 @@ export class ClaustrumConsumer {
|
|
|
293
422
|
(this.#options.clearTimeoutImpl ?? clearTimeout)(this.#timer);
|
|
294
423
|
this.#timer = undefined;
|
|
295
424
|
this.#custody?.close();
|
|
425
|
+
this.#insula?.release();
|
|
426
|
+
this.#insula = undefined;
|
|
296
427
|
this.#roster = undefined;
|
|
428
|
+
this.#grantReach = undefined;
|
|
297
429
|
}
|
|
298
430
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { type ClaustrumClient, type ClaustrumReporterSource, type EnrollmentTokenFile } from '@cortexkit/claustrum-client';
|
|
2
2
|
import { type ClaustrumLogger } from './errors.js';
|
|
3
|
-
|
|
3
|
+
import type { InsulaUsageTransport } from './insula.js';
|
|
4
|
+
export type ClaustrumScopedClient = Pick<ClaustrumClient, 'listScoped' | 'getScoped' | 'reportAuthFailureScoped' | 'close'> & {
|
|
5
|
+
insula?: InsulaUsageTransport;
|
|
6
|
+
};
|
|
4
7
|
/** The two credential types a plugin's family can hold; routing differs by type. */
|
|
5
8
|
export type VaultCredentialType = 'oauth' | 'api_key';
|
|
6
9
|
/**
|
|
@@ -41,6 +44,30 @@ export interface SkippedVaultRecord {
|
|
|
41
44
|
readonly credentialId?: string;
|
|
42
45
|
readonly reason: SkippedVaultReason;
|
|
43
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* One grant this consumer's enrollment holds, as the vault's list reply names
|
|
49
|
+
* it: for example `{ selectorKind: 'category', selector: 'openai-native',
|
|
50
|
+
* operation: 'read' }`. `selectorKind` is `exact` (the selector is a credential
|
|
51
|
+
* id) or `category` (a bare category name); both open strings.
|
|
52
|
+
*/
|
|
53
|
+
export interface VaultGrantTuple {
|
|
54
|
+
readonly selectorKind: string;
|
|
55
|
+
readonly selector: string;
|
|
56
|
+
readonly operation: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* What this consumer is granted, independent of what those grants currently
|
|
60
|
+
* reach. It is what tells two empty listings apart: no rows with `grants === 0`
|
|
61
|
+
* means no grant at all (the fix is `ck auth grant`), while no rows with
|
|
62
|
+
* `grants > 0` means a grant that reaches no credential, usually logins that
|
|
63
|
+
* lack the category (`ck auth reclassify --from-registry`); `tuples` names the
|
|
64
|
+
* selector that matched nothing. Never written to the roster file.
|
|
65
|
+
*/
|
|
66
|
+
export interface VaultGrantReach {
|
|
67
|
+
/** How many grants the consumer holds, deposit grants excluded. */
|
|
68
|
+
readonly grants: number;
|
|
69
|
+
readonly tuples: readonly VaultGrantTuple[];
|
|
70
|
+
}
|
|
44
71
|
export interface VaultInventory {
|
|
45
72
|
/**
|
|
46
73
|
* The vault's change cursor: a digest over exactly what this consumer can see
|
|
@@ -50,6 +77,11 @@ export interface VaultInventory {
|
|
|
50
77
|
readonly view: string;
|
|
51
78
|
readonly credentials: readonly VaultCredential[];
|
|
52
79
|
readonly skipped: readonly SkippedVaultRecord[];
|
|
80
|
+
/**
|
|
81
|
+
* The consumer's grants, as the vault's list reply named them. Undefined means unknown
|
|
82
|
+
* (the vault or client predates the fields), never zero grants.
|
|
83
|
+
*/
|
|
84
|
+
readonly grantReach?: VaultGrantReach;
|
|
53
85
|
}
|
|
54
86
|
export interface ClaustrumScopedIdentity {
|
|
55
87
|
readonly credentialId: string;
|
|
@@ -83,6 +83,21 @@ function credentialTypeOf(row, family) {
|
|
|
83
83
|
return row.refreshAdapter === undefined ? 'api_key' : undefined;
|
|
84
84
|
return undefined;
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* The grants named in the vault's list reply, or undefined when it names none.
|
|
88
|
+
* Both fields must be present: a reply carrying only one of them is treated as
|
|
89
|
+
* unknown rather than completed with a guess. claustrum-client already refuses
|
|
90
|
+
* a malformed value, so nothing here validates it again.
|
|
91
|
+
*/
|
|
92
|
+
function grantReachOf(listed) {
|
|
93
|
+
const { grants, grantTuples } = listed;
|
|
94
|
+
if (grants === undefined || grantTuples === undefined)
|
|
95
|
+
return undefined;
|
|
96
|
+
return Object.freeze({
|
|
97
|
+
grants,
|
|
98
|
+
tuples: Object.freeze(grantTuples.map(({ selectorKind, selector, operation }) => Object.freeze({ selectorKind, selector, operation }))),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
86
101
|
/**
|
|
87
102
|
* Reads this consumer's vault credentials (list, fetch, 401 report), with the
|
|
88
103
|
* enrollment token as authorization. Used by every host of a plugin. There is
|
|
@@ -217,10 +232,12 @@ export class ClaustrumScopedCustody {
|
|
|
217
232
|
}
|
|
218
233
|
for (const record of skipped)
|
|
219
234
|
this.#logger.warn('skipped malformed vault record', record);
|
|
235
|
+
const grantReach = grantReachOf(inventory);
|
|
220
236
|
return {
|
|
221
237
|
view: inventory.view,
|
|
222
238
|
credentials: Object.freeze(credentials),
|
|
223
239
|
skipped: Object.freeze(skipped),
|
|
240
|
+
...(grantReach && { grantReach }),
|
|
224
241
|
};
|
|
225
242
|
}
|
|
226
243
|
/**
|
|
@@ -103,7 +103,12 @@ export interface EnrollmentAncestorOptions {
|
|
|
103
103
|
*/
|
|
104
104
|
export declare function refuseWritableAncestor(parent: string, options?: EnrollmentAncestorOptions): Promise<void>;
|
|
105
105
|
export declare function readClaustrumEnrollmentStatus(paths: ClaustrumEnrollmentPaths, proposedName: string): Promise<ClaustrumEnrollmentStatus>;
|
|
106
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* Resolve the host's account mode from its enrollment files. It never changes
|
|
108
|
+
* their contents or the host's mode. On Windows, reading them may create a
|
|
109
|
+
* missing private state folder and tighten an enrollment file's permissions to
|
|
110
|
+
* owner-only, as every private-file read there does.
|
|
111
|
+
*/
|
|
107
112
|
export declare function enrollmentAuthority(paths: ClaustrumEnrollmentPaths, proposedName: string): Promise<'vault' | 'local' | 'unreadable'>;
|
|
108
113
|
/**
|
|
109
114
|
* Read fresh bearer material for one scoped operation; never publish it.
|
|
@@ -419,7 +419,12 @@ export async function readClaustrumEnrollmentStatus(paths, proposedName) {
|
|
|
419
419
|
}
|
|
420
420
|
return statusFromState(state);
|
|
421
421
|
}
|
|
422
|
-
/**
|
|
422
|
+
/**
|
|
423
|
+
* Resolve the host's account mode from its enrollment files. It never changes
|
|
424
|
+
* their contents or the host's mode. On Windows, reading them may create a
|
|
425
|
+
* missing private state folder and tighten an enrollment file's permissions to
|
|
426
|
+
* owner-only, as every private-file read there does.
|
|
427
|
+
*/
|
|
423
428
|
export async function enrollmentAuthority(paths, proposedName) {
|
|
424
429
|
let status;
|
|
425
430
|
try {
|
|
@@ -2,12 +2,13 @@ import { type ClaustrumClientOptions } from '@cortexkit/claustrum-client';
|
|
|
2
2
|
import type { ClaustrumScopedClient } from './custody.js';
|
|
3
3
|
export { ClaustrumCredentialError, type ClaustrumReporterSource, type EnrollmentTokenFile, } from '@cortexkit/claustrum-client';
|
|
4
4
|
export { ClaustrumConsumer, type ClaustrumConsumerOptions, type SendOptions, } from './consumer.js';
|
|
5
|
-
export { type AccountIdentitySource, type ClaustrumFamily, type ClaustrumScopedAttempt, type ClaustrumScopedClient, ClaustrumScopedCustody, type ClaustrumScopedIdentity, decideScopedRetryAfter401, type IdentityParser, isScopedCredentialRotation, type ScopedRetryReason, SERVING_MARGIN_MS, type SkippedVaultReason, type SkippedVaultRecord, type VaultCredential, type VaultCredentialType, type VaultInventory, } from './custody.js';
|
|
5
|
+
export { type AccountIdentitySource, type ClaustrumFamily, type ClaustrumScopedAttempt, type ClaustrumScopedClient, ClaustrumScopedCustody, type ClaustrumScopedIdentity, decideScopedRetryAfter401, type IdentityParser, isScopedCredentialRotation, type ScopedRetryReason, SERVING_MARGIN_MS, type SkippedVaultReason, type SkippedVaultRecord, type VaultCredential, type VaultCredentialType, type VaultGrantReach, type VaultGrantTuple, type VaultInventory, } from './custody.js';
|
|
6
6
|
export { type ClaustrumEnrollmentClient, type ClaustrumEnrollmentConnection, type ClaustrumEnrollmentDisconnectResult, ClaustrumEnrollmentManager, type ClaustrumEnrollmentPaths, type ClaustrumEnrollmentResetResult, type ClaustrumEnrollmentStatus, classifyEnrollmentError, connectClaustrumEnrollmentClient, disconnectClaustrumEnrollment, type EnrollmentDisposition, enrollmentAuthority, enrollmentName, getClaustrumEnrollmentPaths, hostEnrollmentPaths, RETRYABLE_ENROLLMENT_CODES, readClaustrumEnrollmentStatus, readClaustrumEnrollmentToken, resetClaustrumEnrollmentState, TERMINAL_ENROLLMENT_CODES, } from './enrollment.js';
|
|
7
7
|
export { ClaustrumConsumerError, type ClaustrumConsumerFailureKind, type ClaustrumLogger, } from './errors.js';
|
|
8
8
|
export { assertHostSlotMatchesMode, assertNotCustodyPlaceholder, CUSTODY_PLACEHOLDER_PREFIX, classifyHostSlot, custodyPlaceholder, custodyPlaceholderKey, type HostSlotContent, isCustodyPlaceholder, isCustodyPlaceholderValue, } from './host-slot.js';
|
|
9
|
+
export type { InsulaQuotaContext, InsulaQuotaOptions, InsulaUsageEntry, InsulaUsageTransport, InsulaUsageWindow, } from './insula.js';
|
|
9
10
|
export { acceptAccount, type DeclinedAccount, declineAccount, isDeclined, } from './interlock.js';
|
|
10
|
-
export { type AccountMapper, acceptVaultRoute, DEFAULT_ROUTE_PREFIX, declineVaultRoute, type MutateVaultRosterOptions, mutateVaultRoster, type ProjectionOptions, projectVaultRoster, type QuotaReceipt, readVaultRoster, recordVaultQuota, refreshVaultRoster, resolveVaultPrimary, type VaultPrimary, type VaultPrimaryBinding, type VaultPrimaryUnavailableReason, type VaultRosterFile, type VaultRosterLockContext, type VaultRosterRow, vaultRoutingRows, } from './roster.js';
|
|
11
|
+
export { type AccountMapper, acceptVaultRoute, DEFAULT_ROUTE_PREFIX, declineVaultRoute, type MutateVaultRosterOptions, mutateVaultRoster, type ProjectionOptions, projectVaultRoster, type QuotaReceipt, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, resolveVaultPrimary, type VaultPrimary, type VaultPrimaryBinding, type VaultPrimaryUnavailableReason, type VaultRosterFile, type VaultRosterLockContext, type VaultRosterRow, vaultRoutingRows, } from './roster.js';
|
|
11
12
|
/**
|
|
12
13
|
* Connect the client that lists and fetches this consumer's vault credentials
|
|
13
14
|
* on the request path. `connectionFile` is required: this library
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// declined-account interlock and the host-slot guard. Needs the optional
|
|
5
5
|
// peer `@cortexkit/claustrum-client`.
|
|
6
6
|
import { ClaustrumClient, } from '@cortexkit/claustrum-client';
|
|
7
|
+
import { insulaUsageTransport } from './insula.js';
|
|
7
8
|
export { ClaustrumCredentialError, } from '@cortexkit/claustrum-client';
|
|
8
9
|
export { ClaustrumConsumer, } from './consumer.js';
|
|
9
10
|
export { ClaustrumScopedCustody, decideScopedRetryAfter401, isScopedCredentialRotation, SERVING_MARGIN_MS, } from './custody.js';
|
|
@@ -11,12 +12,13 @@ export { ClaustrumEnrollmentManager, classifyEnrollmentError, connectClaustrumEn
|
|
|
11
12
|
export { ClaustrumConsumerError, } from './errors.js';
|
|
12
13
|
export { assertHostSlotMatchesMode, assertNotCustodyPlaceholder, CUSTODY_PLACEHOLDER_PREFIX, classifyHostSlot, custodyPlaceholder, custodyPlaceholderKey, isCustodyPlaceholder, isCustodyPlaceholderValue, } from './host-slot.js';
|
|
13
14
|
export { acceptAccount, declineAccount, isDeclined, } from './interlock.js';
|
|
14
|
-
export { acceptVaultRoute, DEFAULT_ROUTE_PREFIX, declineVaultRoute, mutateVaultRoster, projectVaultRoster, readVaultRoster, recordVaultQuota, refreshVaultRoster, resolveVaultPrimary, vaultRoutingRows, } from './roster.js';
|
|
15
|
+
export { acceptVaultRoute, DEFAULT_ROUTE_PREFIX, declineVaultRoute, mutateVaultRoster, projectVaultRoster, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, resolveVaultPrimary, vaultRoutingRows, } from './roster.js';
|
|
15
16
|
/**
|
|
16
17
|
* Connect the client that lists and fetches this consumer's vault credentials
|
|
17
18
|
* on the request path. `connectionFile` is required: this library
|
|
18
19
|
* reads no environment, so the plugin resolves the vault's connection file.
|
|
19
20
|
*/
|
|
20
|
-
export function connectClaustrumScopedClient(options) {
|
|
21
|
-
|
|
21
|
+
export async function connectClaustrumScopedClient(options) {
|
|
22
|
+
const client = await ClaustrumClient.connect(options);
|
|
23
|
+
return Object.assign(client, { insula: insulaUsageTransport(options) });
|
|
22
24
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ClaustrumClientOptions } from '@cortexkit/claustrum-client';
|
|
2
|
+
/** Insula's window fields are open: plugins choose which limits they understand. */
|
|
3
|
+
export interface InsulaUsageWindow {
|
|
4
|
+
usedPercent: number;
|
|
5
|
+
rawUsedPercent?: number;
|
|
6
|
+
resetsAt?: string;
|
|
7
|
+
windowMinutes?: number;
|
|
8
|
+
windowKind?: string;
|
|
9
|
+
[field: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
export interface InsulaUsageEntry {
|
|
12
|
+
provider: string;
|
|
13
|
+
account?: string;
|
|
14
|
+
fetchedAt?: string;
|
|
15
|
+
source?: string;
|
|
16
|
+
stale?: unknown;
|
|
17
|
+
errorClass?: unknown;
|
|
18
|
+
usage?: {
|
|
19
|
+
primary?: InsulaUsageWindow;
|
|
20
|
+
secondary?: InsulaUsageWindow;
|
|
21
|
+
tertiary?: InsulaUsageWindow;
|
|
22
|
+
extraRateWindows?: Array<{
|
|
23
|
+
id?: string;
|
|
24
|
+
title?: string;
|
|
25
|
+
window?: InsulaUsageWindow;
|
|
26
|
+
}>;
|
|
27
|
+
[field: string]: unknown;
|
|
28
|
+
};
|
|
29
|
+
[field: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
export interface InsulaQuotaContext {
|
|
32
|
+
/** Milliseconds since the epoch, parsed from the producer's fetchedAt. */
|
|
33
|
+
fetchedAt: number;
|
|
34
|
+
source?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface InsulaQuotaOptions<Q> {
|
|
37
|
+
provider: 'codex' | 'claude' | 'antigravity';
|
|
38
|
+
/**
|
|
39
|
+
* Set every converted window's reading time to context.fetchedAt, not poll
|
|
40
|
+
* time. Return undefined for entries the plugin cannot use. Antigravity can
|
|
41
|
+
* receive its own sidebar reading back from insula: accept it only from the
|
|
42
|
+
* vault or when fetchedAt is newer than the plugin's last reading.
|
|
43
|
+
*/
|
|
44
|
+
normalize: (entry: InsulaUsageEntry, context: InsulaQuotaContext) => Q | undefined;
|
|
45
|
+
}
|
|
46
|
+
/** Optional transport for custom scoped clients; getUsage must not retry refusals. */
|
|
47
|
+
export interface InsulaUsageTransport {
|
|
48
|
+
connectionFile: string;
|
|
49
|
+
getUsage(): Promise<unknown>;
|
|
50
|
+
}
|
|
51
|
+
/** Constructing this transport opens no connection; getUsage opens one per call. */
|
|
52
|
+
export declare function insulaUsageTransport(options: ClaustrumClientOptions & {
|
|
53
|
+
connectionFile: string;
|
|
54
|
+
}): InsulaUsageTransport;
|
|
55
|
+
export declare class InsulaQuotaSource {
|
|
56
|
+
#private;
|
|
57
|
+
private readonly transport;
|
|
58
|
+
private readonly key;
|
|
59
|
+
constructor(transport: InsulaUsageTransport, key: string);
|
|
60
|
+
refresh(now: number): Promise<void>;
|
|
61
|
+
current(provider: string, accountId: string, now: number): {
|
|
62
|
+
entry: InsulaUsageEntry;
|
|
63
|
+
context: InsulaQuotaContext;
|
|
64
|
+
} | undefined;
|
|
65
|
+
}
|
|
66
|
+
export declare function acquireInsulaQuotaSource(transport: InsulaUsageTransport, tokenPath: string): {
|
|
67
|
+
source: InsulaQuotaSource;
|
|
68
|
+
release(): void;
|
|
69
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { storeIdentity } from '@cortexkit/claustrum-client';
|
|
3
|
+
// A static import: claustrum-client already loads subc-client whenever
|
|
4
|
+
// /claustrum is imported, so loading it lazily saves nothing, and a dynamic
|
|
5
|
+
// import makes bundlers emit a separate chunk that a running host loses when
|
|
6
|
+
// its dist folder is rebuilt underneath it.
|
|
7
|
+
import { SubcClient } from '@cortexkit/subc-client';
|
|
8
|
+
/** Constructing this transport opens no connection; getUsage opens one per call. */
|
|
9
|
+
export function insulaUsageTransport(options) {
|
|
10
|
+
const identity = options.identity ??
|
|
11
|
+
storeIdentity(options.projectRoot ?? process.cwd(), options.storagePath ?? process.cwd());
|
|
12
|
+
return {
|
|
13
|
+
connectionFile: options.connectionFile,
|
|
14
|
+
async getUsage() {
|
|
15
|
+
let client;
|
|
16
|
+
let expired = false;
|
|
17
|
+
let timer;
|
|
18
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
19
|
+
timer = setTimeout(() => reject(new Error('insula usage timeout')), 3_000);
|
|
20
|
+
timer.unref?.();
|
|
21
|
+
});
|
|
22
|
+
const request = (async () => {
|
|
23
|
+
client = options.connector
|
|
24
|
+
? await options.connector({
|
|
25
|
+
connectionFile: options.connectionFile,
|
|
26
|
+
handshakeTimeoutMs: Math.min(options.handshakeTimeoutMs ?? 3_000, 3_000),
|
|
27
|
+
})
|
|
28
|
+
: await SubcClient.connect({
|
|
29
|
+
connectionFile: options.connectionFile,
|
|
30
|
+
handshakeTimeoutMs: Math.min(options.handshakeTimeoutMs ?? 3_000, 3_000),
|
|
31
|
+
});
|
|
32
|
+
// A slow connector may resolve after the whole-call deadline. It still
|
|
33
|
+
// owns a socket, which must not survive or open a route after fallback.
|
|
34
|
+
if (expired) {
|
|
35
|
+
client.close();
|
|
36
|
+
throw new Error('insula usage timeout');
|
|
37
|
+
}
|
|
38
|
+
// Managed call() retries warming routes. A single routeOpen/request
|
|
39
|
+
// instead lets the plugin fall back as soon as the daemon refuses.
|
|
40
|
+
const route = await client.routeOpen({ kind: 'management_surface', module_id: 'insula' }, identity);
|
|
41
|
+
return await client.request(route, { method: 'usage.get', params: {} });
|
|
42
|
+
})();
|
|
43
|
+
try {
|
|
44
|
+
return await Promise.race([request, timeout]);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
expired = true;
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
client?.close();
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const MINUTE = 60_000;
|
|
55
|
+
const MAX_AGE = 4 * MINUTE;
|
|
56
|
+
const sources = new Map();
|
|
57
|
+
// Keep only the minute budget after the last owner closes, so project churn
|
|
58
|
+
// cannot reset the bound. Expired budgets are pruned on the next tick.
|
|
59
|
+
const attempts = new Map();
|
|
60
|
+
function record(value) {
|
|
61
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
62
|
+
}
|
|
63
|
+
export class InsulaQuotaSource {
|
|
64
|
+
transport;
|
|
65
|
+
key;
|
|
66
|
+
#entries = [];
|
|
67
|
+
#pending;
|
|
68
|
+
constructor(transport, key) {
|
|
69
|
+
this.transport = transport;
|
|
70
|
+
this.key = key;
|
|
71
|
+
}
|
|
72
|
+
refresh(now) {
|
|
73
|
+
if (this.#pending)
|
|
74
|
+
return this.#pending;
|
|
75
|
+
for (const [key, time] of attempts) {
|
|
76
|
+
if (now - time >= MINUTE)
|
|
77
|
+
attempts.delete(key);
|
|
78
|
+
}
|
|
79
|
+
const lastAttempt = attempts.get(this.key);
|
|
80
|
+
if (lastAttempt !== undefined && now - lastAttempt < MINUTE)
|
|
81
|
+
return Promise.resolve();
|
|
82
|
+
// Reserve the minute before I/O, so concurrent projects cannot each poll.
|
|
83
|
+
attempts.set(this.key, now);
|
|
84
|
+
this.#pending = (async () => {
|
|
85
|
+
try {
|
|
86
|
+
const reply = await this.transport.getUsage();
|
|
87
|
+
if (!record(reply) ||
|
|
88
|
+
!Array.isArray(reply.result) ||
|
|
89
|
+
reply.error !== undefined)
|
|
90
|
+
throw new Error('unavailable insula usage');
|
|
91
|
+
// Each response is a snapshot, not a delta. Missing accounts fall back;
|
|
92
|
+
// neither pending nor the entry count asserts provider completeness.
|
|
93
|
+
this.#entries = reply.result.filter((entry) => record(entry) && typeof entry.provider === 'string');
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Refusal is absence, even if the previous reading was still young.
|
|
97
|
+
this.#entries = [];
|
|
98
|
+
}
|
|
99
|
+
})().finally(() => {
|
|
100
|
+
this.#pending = undefined;
|
|
101
|
+
});
|
|
102
|
+
return this.#pending;
|
|
103
|
+
}
|
|
104
|
+
current(provider, accountId, now) {
|
|
105
|
+
for (const entry of this.#entries) {
|
|
106
|
+
if (entry.provider !== provider ||
|
|
107
|
+
typeof entry.account !== 'string' ||
|
|
108
|
+
entry.account.trim() !== accountId)
|
|
109
|
+
continue;
|
|
110
|
+
if (entry.stale !== undefined ||
|
|
111
|
+
entry.errorClass !== undefined ||
|
|
112
|
+
!record(entry.usage))
|
|
113
|
+
continue;
|
|
114
|
+
const fetchedAt = typeof entry.fetchedAt === 'string'
|
|
115
|
+
? Date.parse(entry.fetchedAt)
|
|
116
|
+
: Number.NaN;
|
|
117
|
+
if (!Number.isFinite(fetchedAt) || now - fetchedAt >= MAX_AGE)
|
|
118
|
+
continue;
|
|
119
|
+
return {
|
|
120
|
+
entry,
|
|
121
|
+
context: {
|
|
122
|
+
fetchedAt,
|
|
123
|
+
...(typeof entry.source === 'string' && { source: entry.source }),
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export function acquireInsulaQuotaSource(transport, tokenPath) {
|
|
131
|
+
const key = JSON.stringify([
|
|
132
|
+
resolve(transport.connectionFile),
|
|
133
|
+
resolve(tokenPath),
|
|
134
|
+
]);
|
|
135
|
+
let shared = sources.get(key);
|
|
136
|
+
if (!shared) {
|
|
137
|
+
shared = { source: new InsulaQuotaSource(transport, key), refs: 0 };
|
|
138
|
+
sources.set(key, shared);
|
|
139
|
+
}
|
|
140
|
+
shared.refs++;
|
|
141
|
+
let released = false;
|
|
142
|
+
return {
|
|
143
|
+
source: shared.source,
|
|
144
|
+
release() {
|
|
145
|
+
if (released)
|
|
146
|
+
return;
|
|
147
|
+
released = true;
|
|
148
|
+
if (--shared.refs === 0)
|
|
149
|
+
sources.delete(key);
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -133,6 +133,19 @@ export declare function recordVaultQuota(path: string, input: QuotaReceipt & {
|
|
|
133
133
|
routeId: string;
|
|
134
134
|
observation: QuotaObservation;
|
|
135
135
|
}): Promise<boolean>;
|
|
136
|
+
/**
|
|
137
|
+
* Merge a quota observation that belongs to an account rather than to a
|
|
138
|
+
* served credential, such as a background reading from insula. The write
|
|
139
|
+
* lands only while the row is still bound to that exact known account, so a
|
|
140
|
+
* reading taken before a replacement never reaches the successor. Unlike
|
|
141
|
+
* `recordVaultQuota` it never binds an account: a row without a known account
|
|
142
|
+
* is refused. Returns whether the observation was kept.
|
|
143
|
+
*/
|
|
144
|
+
export declare function recordVaultAccountQuota(path: string, input: {
|
|
145
|
+
routeId: string;
|
|
146
|
+
accountIdentity: string;
|
|
147
|
+
observation: QuotaObservation;
|
|
148
|
+
}): Promise<boolean>;
|
|
136
149
|
/**
|
|
137
150
|
* Discover and commit the roster. The lease covers the list call and the
|
|
138
151
|
* commit, not just the write: an opaque view cannot tell a delayed old reply
|