@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.
Files changed (121) hide show
  1. package/dist/account-manager.d.ts +15 -23
  2. package/dist/account-manager.d.ts.map +1 -1
  3. package/dist/account-manager.js +18 -32
  4. package/dist/account-manager.js.map +1 -1
  5. package/dist/account-migration.d.ts +1 -1
  6. package/dist/account-migration.d.ts.map +1 -1
  7. package/dist/account-migration.js +228 -53
  8. package/dist/account-migration.js.map +1 -1
  9. package/dist/account-repository-types.d.ts +1 -1
  10. package/dist/account-selector.d.ts +13 -27
  11. package/dist/account-selector.d.ts.map +1 -1
  12. package/dist/account-selector.js +38 -145
  13. package/dist/account-selector.js.map +1 -1
  14. package/dist/account-types.d.ts +1 -3
  15. package/dist/account-types.d.ts.map +1 -1
  16. package/dist/agy-transport.d.ts.map +1 -1
  17. package/dist/agy-transport.js +2 -0
  18. package/dist/agy-transport.js.map +1 -1
  19. package/dist/antigravity/oauth.d.ts +5 -1
  20. package/dist/antigravity/oauth.d.ts.map +1 -1
  21. package/dist/antigravity/oauth.js +43 -9
  22. package/dist/antigravity/oauth.js.map +1 -1
  23. package/dist/antigravity-command-menu.d.ts +4 -8
  24. package/dist/antigravity-command-menu.d.ts.map +1 -1
  25. package/dist/antigravity-command-menu.js +14 -37
  26. package/dist/antigravity-command-menu.js.map +1 -1
  27. package/dist/common-auth-embedded/claustrum/consumer.d.ts +44 -4
  28. package/dist/common-auth-embedded/claustrum/consumer.js +134 -2
  29. package/dist/common-auth-embedded/claustrum/custody.d.ts +33 -1
  30. package/dist/common-auth-embedded/claustrum/custody.js +17 -0
  31. package/dist/common-auth-embedded/claustrum/enrollment.d.ts +6 -1
  32. package/dist/common-auth-embedded/claustrum/enrollment.js +6 -1
  33. package/dist/common-auth-embedded/claustrum/index.d.ts +3 -2
  34. package/dist/common-auth-embedded/claustrum/index.js +5 -3
  35. package/dist/common-auth-embedded/claustrum/insula.d.ts +69 -0
  36. package/dist/common-auth-embedded/claustrum/insula.js +152 -0
  37. package/dist/common-auth-embedded/claustrum/roster.d.ts +13 -0
  38. package/dist/common-auth-embedded/claustrum/roster.js +33 -3
  39. package/dist/common-auth-embedded/fs/refresh-file-lock.d.ts +8 -2
  40. package/dist/common-auth-embedded/fs/refresh-file-lock.js +26 -12
  41. package/dist/common-auth-embedded/pi-slot/lock.js +7 -3
  42. package/dist/common-auth-embedded/quota/codec.d.ts +2 -0
  43. package/dist/common-auth-embedded/quota/codec.js +2 -1
  44. package/dist/common-auth-embedded/quota/format.d.ts +40 -3
  45. package/dist/common-auth-embedded/quota/format.js +196 -43
  46. package/dist/common-auth-embedded/quota/index.d.ts +4 -4
  47. package/dist/common-auth-embedded/quota/index.js +2 -2
  48. package/dist/common-auth-embedded/quota/map.d.ts +11 -0
  49. package/dist/common-auth-embedded/quota/map.js +36 -1
  50. package/dist/common-auth-embedded/quota/merge.d.ts +3 -1
  51. package/dist/common-auth-embedded/quota/merge.js +21 -5
  52. package/dist/common-auth-embedded/quota/projection.d.ts +2 -1
  53. package/dist/common-auth-embedded/quota/projection.js +3 -1
  54. package/dist/common-auth-embedded/source-output.json +117 -101
  55. package/dist/common-auth-embedded/store/pool.d.ts +1 -1
  56. package/dist/common-auth-embedded/store/pool.js +0 -1
  57. package/dist/common-auth-embedded/store/refresh-lock.d.ts +0 -1
  58. package/dist/common-auth-embedded/store/refresh-lock.js +0 -1
  59. package/dist/common-auth-embedded/store/schema.d.ts +2 -0
  60. package/dist/common-auth-embedded/store/schema.js +3 -2
  61. package/dist/common-auth-runtime.d.ts +2 -1
  62. package/dist/common-auth-runtime.d.ts.map +1 -1
  63. package/dist/common-auth-runtime.js +2 -1
  64. package/dist/common-auth-runtime.js.map +1 -1
  65. package/dist/constants.d.ts +1 -26
  66. package/dist/constants.d.ts.map +1 -1
  67. package/dist/constants.js +1 -37
  68. package/dist/constants.js.map +1 -1
  69. package/dist/credential-redaction.d.ts +8 -4
  70. package/dist/credential-redaction.d.ts.map +1 -1
  71. package/dist/credential-redaction.js +55 -28
  72. package/dist/credential-redaction.js.map +1 -1
  73. package/dist/index.d.ts +5 -1
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +5 -1
  76. package/dist/index.js.map +1 -1
  77. package/dist/model-registry.d.ts +0 -1
  78. package/dist/model-registry.d.ts.map +1 -1
  79. package/dist/model-registry.js +3 -60
  80. package/dist/model-registry.js.map +1 -1
  81. package/dist/pi-paths.d.ts +14 -0
  82. package/dist/pi-paths.d.ts.map +1 -0
  83. package/dist/pi-paths.js +27 -0
  84. package/dist/pi-paths.js.map +1 -0
  85. package/dist/pre-response-deadline.d.ts +41 -0
  86. package/dist/pre-response-deadline.d.ts.map +1 -0
  87. package/dist/pre-response-deadline.js +150 -0
  88. package/dist/pre-response-deadline.js.map +1 -0
  89. package/dist/quota-manager.d.ts +7 -32
  90. package/dist/quota-manager.d.ts.map +1 -1
  91. package/dist/quota-manager.js +27 -76
  92. package/dist/quota-manager.js.map +1 -1
  93. package/dist/quota-types.d.ts +2 -11
  94. package/dist/quota-types.d.ts.map +1 -1
  95. package/dist/transform/index.d.ts +1 -1
  96. package/dist/transform/index.d.ts.map +1 -1
  97. package/dist/transform/index.js +1 -1
  98. package/dist/transform/index.js.map +1 -1
  99. package/dist/transform/model-resolver.d.ts +4 -28
  100. package/dist/transform/model-resolver.d.ts.map +1 -1
  101. package/dist/transform/model-resolver.js +33 -130
  102. package/dist/transform/model-resolver.js.map +1 -1
  103. package/dist/transform/types.d.ts +0 -5
  104. package/dist/transform/types.d.ts.map +1 -1
  105. package/dist/vault-account-source.d.ts +24 -3
  106. package/dist/vault-account-source.d.ts.map +1 -1
  107. package/dist/vault-account-source.js +39 -12
  108. package/dist/vault-account-source.js.map +1 -1
  109. package/dist/vault-folder-refusal.d.ts +32 -0
  110. package/dist/vault-folder-refusal.d.ts.map +1 -0
  111. package/dist/vault-folder-refusal.js +68 -0
  112. package/dist/vault-folder-refusal.js.map +1 -0
  113. package/dist/vault-grant-reach.d.ts +23 -0
  114. package/dist/vault-grant-reach.d.ts.map +1 -0
  115. package/dist/vault-grant-reach.js +15 -0
  116. package/dist/vault-grant-reach.js.map +1 -0
  117. package/dist/vault-provider-state.d.ts +1 -0
  118. package/dist/vault-provider-state.d.ts.map +1 -1
  119. package/dist/vault-provider-state.js +6 -0
  120. package/dist/vault-provider-state.js.map +1 -1
  121. 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 { acceptVaultRoute, declineVaultRoute, readVaultRoster, recordVaultQuota, refreshVaultRoster, vaultRoutingRows, } from './roster.js';
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
- export type ClaustrumScopedClient = Pick<ClaustrumClient, 'listScoped' | 'getScoped' | 'reportAuthFailureScoped' | 'close'>;
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
- /** Resolve the host's account mode from its enrollment files without writing. */
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
- /** Resolve the host's account mode from its enrollment files without writing. */
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
- return ClaustrumClient.connect(options);
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