dsh-keys-manager 0.2.1 → 0.2.3

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/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AccountAttemptView, StateView } from './domain.js';
1
+ import type { AccountAttemptView, KeyTestResult, StateView } from './domain.js';
2
2
  import type { CredentialsLike, GenerateOptionsLike, HostContext, KeySelection, RequestErrorAction, AgentRequestPayload, RequestErrorPayload, SettingsLike, StreamChunkLike } from './host-types.js';
3
3
  import { KeysManagerStore } from './store.js';
4
4
  export declare const name = "keys-manager";
@@ -6,6 +6,15 @@ export declare const inject: string[];
6
6
  export declare const provide: string[];
7
7
  /** The credential-record scope pi-ai stores its per-provider grants under (llm-pi-ai/src/auth.ts:29). */
8
8
  export declare const OAUTH_GRANT_RECORD_SCOPE = "llm-pi-ai";
9
+ /**
10
+ * The credential-record scope the llm-omp transports package owns: one
11
+ * `llm-omp-transports/<route>` grant per transport route (its
12
+ * `CURSOR_CREDENTIAL_KEY` is `llm-omp-transports/cursor`). Keys Manager reads
13
+ * those records to list/verify an account and deletes one on removal; it
14
+ * never writes one and never projects one into `llm-pi-ai/<provider>`, so a
15
+ * transport route keeps serving from its own adapter-owned credential.
16
+ */
17
+ export declare const TRANSPORT_CREDENTIAL_SCOPE = "llm-omp-transports";
9
18
  /**
10
19
  * Stable failure code for a harvest whose state flush rejected AND whose
11
20
  * credential-side rollback then failed to write.
@@ -46,17 +55,147 @@ export declare class KeysManagerService {
46
55
  private readonly credentialWrappers;
47
56
  private brokerOwners;
48
57
  private readonly bindingCache;
58
+ /**
59
+ * Providers whose managed credentials are being removed right now. While a
60
+ * provider is unmanaged only the unbind direction of
61
+ * {@link syncBrokerBinding} may run: a demotion inside the deletion loop
62
+ * (an `oauth` row iterated before a surviving `api-key` row) must never
63
+ * re-point the released profile at the broker.
64
+ */
65
+ private readonly releasingProviders;
49
66
  private lastBindingPruneAt;
50
67
  private readonly selectionQueues;
51
68
  private queue;
52
69
  private closing;
70
+ /** The one shutdown this manager runs; a later close() joins it (round 38). */
71
+ private closePromise;
72
+ /**
73
+ * The pre-close restoration is running (PR review round 37). Only inside it
74
+ * — set when {@link restoreBorrowedWindows} starts and cleared in its
75
+ * `finally`, the cleanup deadline included — does the module-private
76
+ * {@link PRE_CLOSE_RESTORE} capability let a write past shutdown. A
77
+ * restoration that outlives its deadline resumes after this went down and
78
+ * is refused like any other continuation.
79
+ */
80
+ private preCloseRestorePhase;
81
+ /**
82
+ * Shutdown has begun (PR review round 34). Set on `close()`'s first line,
83
+ * before the pre-close restoration runs, so a test arriving while that
84
+ * restoration is putting the outstanding borrows back is refused instead of
85
+ * starting a second window the restoration would not cover. Never cleared:
86
+ * the instance is closing.
87
+ */
88
+ private shutdownRequested;
89
+ /**
90
+ * Providers whose in-flight row test borrows the route's credential
91
+ * surface (QC B4: an OAuth row test's projection, and the broker reference
92
+ * it may temporarily release). While a provider is borrowed the binding
93
+ * authority must not derive a real binding from the borrowed world: a
94
+ * mutation landing inside the window (`addKey`, `deleteKey`, an account
95
+ * removal) would otherwise persist an unbind decided about a state that is
96
+ * not the route's. {@link syncBrokerBinding} consults this exactly like
97
+ * {@link releasingProviders}; the test's own window-end sync passes
98
+ * `borrow: true` so the restored state is written back in one step.
99
+ */
100
+ private readonly testBindingBorrows;
101
+ /**
102
+ * Providers with a borrowing row test in flight, mapped to that test's own
103
+ * promise. The promise is the isolation seam: an ordinary request for the
104
+ * provider waits on it (bounded, see {@link settleProviderTest}) before
105
+ * resolving a credential, and reports its own outcome if the test fails.
106
+ * Only OAuth-row tests borrow (QC N7), so only they register here.
107
+ */
108
+ private readonly testsInFlight;
109
+ /**
110
+ * Every in-flight row test, by provider — the single-concurrent-test lock
111
+ * for both row kinds. Separate from {@link testsInFlight} because an
112
+ * api-key test borrows nothing and must not isolate ordinary requests.
113
+ */
114
+ private readonly testsActive;
115
+ /**
116
+ * Every row test window that has not been released yet, by provider (PR
117
+ * review round 33). Shutdown restores each window's borrowed projection
118
+ * before `closing` refuses the in-window cleanup, so the map outlives
119
+ * {@link testsActive} for a window whose request never settles.
120
+ */
121
+ private readonly testWindows;
122
+ /**
123
+ * Upper bound on one ordinary request's wait for an in-flight row test
124
+ * (QC B3). Injectable so tests can exercise the bound quickly.
125
+ */
126
+ private isolationWaitMs;
127
+ /**
128
+ * Providers whose abandoned test window is still being cleaned up (QC fix
129
+ * wave 2). The fence stays until a cleanup completed: a new test would
130
+ * borrow a projection that is not back yet, and an ordinary request must not
131
+ * be served by a borrowed grant or an unbound route.
132
+ */
133
+ private readonly cleaningTests;
134
+ /**
135
+ * Nesting depth of an in-flight removal per provider (PR review round 12).
136
+ * {@link settleTestBeforeRemoval} can only wait for a test that already
137
+ * exists, so a test starting after that check would read the grant and the
138
+ * projection and then re-project an account the removal deleted. {@link
139
+ * testKey} refuses while this marker is up. A depth, because `deleteKey`
140
+ * releases through `releaseLocked` inside its own removal.
141
+ */
142
+ private readonly removalsInProgress;
143
+ /**
144
+ * Generation of the newest cleanup attempt per provider (PR review round 2).
145
+ * An attempt that outlives its deadline keeps writing while a later attempt
146
+ * for the same window starts. Only the newest generation may release
147
+ * provider-wide state — the fence, the borrow marker, the in-flight maps —
148
+ * so a superseded attempt can never clear isolation a newer window owns.
149
+ */
150
+ private readonly cleanupAttempts;
151
+ /**
152
+ * The abandoned window currently fencing each provider (QC fix wave 3,
153
+ * W-002), so a later test attempt can retry its cleanup. Without this the
154
+ * fence could only ever clear from the abandoned promise's settlement, and a
155
+ * promise that settled while the seam was still wedged left the provider
156
+ * isolated forever.
157
+ */
158
+ private readonly abandonedWindows;
159
+ /** Hard bound on one abandoned window's cleanup. Injectable for tests. */
160
+ private testCleanupDeadlineMs;
161
+ /** Hard bound on the shutdown drain of the grant chains. Injectable for tests. */
162
+ private grantDrainDeadlineMs;
163
+ /** Providers whose isolation-deadline warning already fired (warn once). */
164
+ private readonly isolationWaitWarned;
53
165
  /** Interactive sign-in attempts, by attempt id. */
54
166
  private readonly loginAttempts;
55
167
  /** The provider → attempt id index for the single-attempt rule. */
56
168
  private readonly loginsInFlight;
57
169
  private loginSequence;
170
+ /** The route → attempt id index for the single transport sign-in rule. */
171
+ private readonly transportInFlight;
172
+ /**
173
+ * Per-route removal epoch. A removal bumps it before withdrawing the live
174
+ * attempt, so that attempt's settle path can tell the account is gone and
175
+ * drop a grant its commit had already started writing.
176
+ */
177
+ private readonly transportRemovals;
178
+ /**
179
+ * Transport-account rows, hydrated from the `llm-omp-transports/<route>`
180
+ * records. Kept out of the pi-ai pool document on purpose: the rows are a
181
+ * read-only projection of another package's records, rebuilt by
182
+ * {@link refreshTransportAccounts} and rendered by `buildState`.
183
+ */
184
+ private transportRows;
185
+ /** Tail of the serialized transport read chain. */
186
+ private transportRefresh;
58
187
  /** The account row whose grant payload currently IS the projection, per provider. */
59
188
  private readonly activeAccounts;
189
+ /**
190
+ * Providers whose live projection EXISTS but no managed row owns it (PR
191
+ * review round 3). `activeAccounts` alone cannot tell "no projection" from
192
+ * "a projection this process does not manage": an orphan or foreign grant
193
+ * (hydration found no matching row) still serves ahead of any broker
194
+ * reference, so the route must not be treated as the key pool's. Set by
195
+ * hydration and by the reconciler when a record is present but unattributed,
196
+ * cleared when the record is deleted or becomes owned.
197
+ */
198
+ private readonly foreignProjections;
60
199
  /**
61
200
  * Account rows committed to removal (C6-1/I1 removal-aware oracle), marked
62
201
  * by {@link removeOAuthGrant} before its first awaited delete. A multi-row
@@ -101,12 +240,15 @@ export declare class KeysManagerService {
101
240
  * order is preserved so the older observation can never commit after the
102
241
  * newer one and leave the row on a stale generation.
103
242
  *
104
- * Round-8 finding I-000106: activation joins the same chain. The only
105
- * writer of `row.oauth.grantRef` is {@link reconcileGrantRecord}, which runs
106
- * outside the mutation queue, so activation's record read and its projection
107
- * write must be mutually exclusive with that writer — otherwise a rotation
108
- * landing between them leaves the projection on a generation the row no
109
- * longer names.
243
+ * Round-8 finding I-000106 assumed activation would join this chain. It does
244
+ * not: activation runs its compare-and-write inside the mutation queue turn,
245
+ * and the only writer of `row.oauth.grantRef` is {@link reconcileGrantRecord},
246
+ * which runs outside that queue. The fence is therefore a generation re-check
247
+ * on both sides of the write — before it (the row must still name the
248
+ * generation that was read) and after it (a rotation commit can land inside
249
+ * the seam's compare-and-write await, which only reads the projection key).
250
+ * Chain members are the echo reconciler, the key-test borrow and adoption
251
+ * classification.
110
252
  */
111
253
  private readonly grantReconciliations;
112
254
  /**
@@ -157,6 +299,16 @@ export declare class KeysManagerService {
157
299
  * to the requested provider's own unresolved read.
158
300
  */
159
301
  private readonly hydratedProviders;
302
+ /**
303
+ * Providers whose hydration ownership read FAILED (PR review round 8). A
304
+ * failed read knows nothing about the route, so it is not the "no projection"
305
+ * answer a successful empty read is: treating it as one let the startup
306
+ * repair read an empty owner/foreign map as proof that the pool serves and
307
+ * bind the broker over a persisted OAuth projection it could not read.
308
+ * Readiness therefore stays false for these providers and the repair skips
309
+ * them; only a restart can retry the read.
310
+ */
311
+ private readonly ownershipReadFailures;
160
312
  /**
161
313
  * One resolver set per provider still being waited on (round-10 should-fix
162
314
  * SF2). A wait registers exactly one entry here and removes it when the wait
@@ -168,13 +320,43 @@ export declare class KeysManagerService {
168
320
  private readonly hydrationWaiters;
169
321
  /**
170
322
  * True once the load-time hydration read is over (round-10 SF1). From then
171
- * on no provider's read can land, so attribution is known for every provider
172
- * — including one adopted after the read, whose OAuth rows did not exist
173
- * when the projection was looked for.
323
+ * on no hydration read can land. It is NOT evidence that a given provider's
324
+ * projection was read (PR review round 17): a provider adopted after the
325
+ * snapshot was never in it, so readiness is decided per provider
326
+ * ({@link projectionOwnershipKnown}) and adoption reads its own projection.
174
327
  */
175
328
  private hydrationSettled;
329
+ /**
330
+ * Credential-store generation of the ownership hydration (PR review round
331
+ * 27). A remounted credentials service starts a new pass while the previous
332
+ * store's reads can still be in flight; their answers describe records the
333
+ * new backend does not hold, so the epoch gates every write and settle of
334
+ * the pass that started it.
335
+ */
336
+ private hydrationEpoch;
337
+ /**
338
+ * Whether a credential store was ever attached (PR review round 27). The
339
+ * first attach is the startup mount and keeps its behaviour; every later one
340
+ * replaces the credential backend, so the ownership classification loaded
341
+ * from the previous one is stale and the read restarts from unknown.
342
+ */
343
+ private credentialsMounted;
176
344
  /** Providers whose bounded-wait warning already fired (warn once). */
177
345
  private readonly hydrationWaitWarned;
346
+ /**
347
+ * Stream attempts each provider served without an attributed account
348
+ * (`I-000120`, D8): the ownership read had not landed, or it had failed.
349
+ * Counted at the one managed serving site (`wrapStream`), never in the
350
+ * hydration pass — a failed read serves no request, it only makes later
351
+ * attempts unattributed. Two earlier branches are outside this definition and
352
+ * stay uncounted: a provider this manager does not manage (no ownership is
353
+ * ever asked for) and the `mismatch` profile shape, which hands the request
354
+ * to pi-ai untouched precisely because this manager's own binding cannot
355
+ * describe what serves it. Cleared only when a later read decides the route's
356
+ * attribution; process-local, never persisted and never on the warnings
357
+ * channel.
358
+ */
359
+ private readonly unattributedRequests;
178
360
  /** Channels whose lost-subscription warning already fired (warn once). */
179
361
  private readonly subscriptionLostWarnings;
180
362
  /** Disposer for the `authorization/settled` subscription. */
@@ -193,7 +375,17 @@ export declare class KeysManagerService {
193
375
  * after a restart. An orphan projection — no matching row, e.g. a foreign
194
376
  * or stale grant — is left untouched: the manager never adopts a grant it
195
377
  * does not own, and out-of-band updates keep surfacing the native-
196
- * management warning.
378
+ * management warning. A live record that is not a managed grant at all (a
379
+ * native api-key, an unrecognized payload) is foreign for the same reason
380
+ * (PR review round 4): it serves the route, so the startup repair must not
381
+ * bind the broker over it.
382
+ *
383
+ * EVERY managed provider's projection is read (PR review round 13), not
384
+ * only those with OAuth rows: a key-only pool can be overridden by a
385
+ * native grant persisted for the same route, and a skipped read left the
386
+ * startup repair free to bind the broker over it. The reads run
387
+ * concurrently so one provider's stalled read cannot delay another
388
+ * provider's own readiness (round 9).
197
389
  *
198
390
  * Returns its own completion (round-8 finding I-000108), and the returned
199
391
  * promise is already caught, so a failed hydration degrades to "no owner"
@@ -202,11 +394,31 @@ export declare class KeysManagerService {
202
394
  * each request waits on its own provider's landing instead.
203
395
  */
204
396
  private hydrateProjectionOwnership;
397
+ /**
398
+ * Drop every ownership fact loaded from a previous credential store (PR
399
+ * review round 27). The classification is per backend: a record that existed
400
+ * in the old store says nothing about the remounted one, and a route whose
401
+ * old store held a managed projection would otherwise stay attributed to
402
+ * that account and off its surviving key pool. The epoch bump discards a read
403
+ * still in flight from the replaced store instead of letting its answer land
404
+ * over the new one; ownership stays unknown until the new pass reads.
405
+ */
406
+ private invalidateProjectionOwnership;
205
407
  /**
206
408
  * Record that one provider's hydration read is over — owner or not (SF1) —
207
409
  * and wake the requests waiting on that provider alone (SF2).
208
410
  */
209
411
  private noteProviderHydrated;
412
+ /**
413
+ * An authoritative projection read answered for this provider, so a failed
414
+ * startup hydration no longer leaves ownership unknown (PR review round 13:
415
+ * the marker used to survive until a restart — no attribution, no key
416
+ * selection, and row tests refused with "ownership is still loading"). The
417
+ * binding re-derive is queued behind the reconciliation that proved the
418
+ * read: its attribution decides the route's serving shape, and the startup
419
+ * repair was skipped while that shape was unknown.
420
+ */
421
+ private noteProjectionReadSucceeded;
210
422
  /** Release every provider's readiness wait when hydration cannot proceed. */
211
423
  private settleHydrationWaits;
212
424
  /**
@@ -232,10 +444,10 @@ export declare class KeysManagerService {
232
444
  * Wait (bounded) until projection-ownership hydration can attribute this
233
445
  * request, and report whether it did.
234
446
  *
235
- * A provider without OAuth rows can never own a projection, so it never
236
- * waits — a stalled read for some other provider must not delay it
237
- * (round-9 should-fix: this used to gate every managed provider, API-key-
238
- * only ones included).
447
+ * Readiness is decided PER PROVIDER (round-10 should-fix SF1): every managed
448
+ * provider's own projection read lands independently (PR review round 13), so
449
+ * a provider whose read has landed needs no wait and a stalled read for
450
+ * another provider cannot delay this one.
239
451
  *
240
452
  * Returns false when the bound elapsed first. The caller then treats
241
453
  * attribution as unknown (no owner) and keeps key state untouched: a
@@ -243,15 +455,24 @@ export declare class KeysManagerService {
243
455
  * repeat the I-000108 corruption. The hydration promise is not cancelled,
244
456
  * so a late result still lands under the generation guard and later
245
457
  * requests are attributed normally.
246
- *
247
- * Readiness is decided PER PROVIDER (round-10 should-fix SF1). Hydration
248
- * writes owners progressively, so racing the one global promise would
249
- * discard an owner this provider already confirmed just because another
250
- * provider's read stalled. A provider whose own read has landed — with an
251
- * owner or with none — is ready without waiting, and the "unknown" fallback
252
- * is confined to the requested provider's own unresolved read.
253
458
  */
254
459
  private awaitProjectionOwnership;
460
+ /**
461
+ * Whether this provider's projection ownership can already be decided
462
+ * without waiting (round-10 SF1, made the single readiness rule by PR review
463
+ * round 3). Every managed provider's projection is read at hydration (a
464
+ * key-only pool can be overridden by a persisted native grant, PR review
465
+ * round 13), so the rule is the same for all of them: this provider's own
466
+ * read landed, or a claim already owns it.
467
+ *
468
+ * The process-wide settled flag is deliberately NOT part of this rule (PR
469
+ * review round 17): a provider adopted after the snapshot was never read by
470
+ * it, so "hydration is over" says nothing about THAT route's projection.
471
+ * Adoption reads it itself ({@link readAdoptionProjection}) and an extra
472
+ * provider's profile is created by its own call, so every managed provider
473
+ * still answers through its own marker.
474
+ */
475
+ private projectionOwnershipKnown;
255
476
  /**
256
477
  * Record that a newly committed grant became the provider's projection and
257
478
  * return that projection's generation (round-6 finding SF1).
@@ -266,6 +487,14 @@ export declare class KeysManagerService {
266
487
  * previous grant's.
267
488
  */
268
489
  private noteProjectionCommitted;
490
+ /**
491
+ * Whether a claim landed after `generation` was captured. A reconciliation
492
+ * reads its snapshot before its awaited record read and decides after it, so
493
+ * an activation (or login) that claimed the route in between owns the answer
494
+ * now: the stale body must not clear the claim, mark the route foreign, or
495
+ * re-point the binding. The hydration write uses the same guard.
496
+ */
497
+ private ownershipSuperseded;
269
498
  /**
270
499
  * Claim the projection owner for one account row, bumping the provider's
271
500
  * ownership generation so an in-flight hydration read of an older state
@@ -284,13 +513,69 @@ export declare class KeysManagerService {
284
513
  * the remaining attempt/step records after `ATTEMPT_TTL_MS`.
285
514
  */
286
515
  pruneTransient(now?: number): void;
516
+ /**
517
+ * Whether a continuation that resumes from an await must stop writing
518
+ * (PR review round 35). `shutdownRequested` is up for `close()`'s own
519
+ * pre-close restoration, which writes by design: it alone carries the
520
+ * module-private {@link PRE_CLOSE_RESTORE} capability (round 36), which is
521
+ * honoured only while that phase is running (round 37). Both facts are read
522
+ * here, at the caller's write decision, so a capability the phase left
523
+ * behind cannot pass a check made earlier.
524
+ */
525
+ private persistenceRefused;
287
526
  /** Stop accepting mutations, drain the queue, then persist the final snapshot. */
288
527
  close(): Promise<void>;
528
+ /** The body of the one shutdown this manager runs. */
529
+ private closeOnce;
530
+ /**
531
+ * Put every outstanding window's borrowed projection back before shutdown
532
+ * closes the store (PR review round 33).
533
+ *
534
+ * `close()` sets `closing`, which refuses {@link cleanupTestWindow} — the one
535
+ * identity-checked restoration — so a test whose request ignores the abort
536
+ * used to leave the tested account projected and the binding it released
537
+ * released, past unload and into the next startup. Each window's captured
538
+ * facts are restored through the same compare-by-payload path as the late
539
+ * reconcile, under one cleanup bound and the attempt fence (a superseded
540
+ * attempt writes nothing), and a window that released the binding re-syncs it
541
+ * against the restored serving source.
542
+ *
543
+ * The request itself is never waited for — an adapter can still rewrite the
544
+ * projection after its restore — so a restoration that cannot finish in the
545
+ * bound warns once and leaves the state to the next startup's hydration.
546
+ */
547
+ private restoreBorrowedWindows;
548
+ /**
549
+ * Drain the grant chains under a hard bound (PR review round 29).
550
+ *
551
+ * The work is not cancelled — the credential seam offers no cancellation and
552
+ * a wedged provider request may never settle — it is only stopped from
553
+ * holding shutdown: the chain past the deadline is abandoned, and its late
554
+ * write can only land in a store that is closing anyway.
555
+ */
556
+ private drainGrantReconciliations;
289
557
  /** Push a user-facing warning that rides the next state response. */
290
558
  private pushWarning;
291
559
  private takeWarnings;
560
+ /**
561
+ * Report declared headers whose stored value no longer matches what the
562
+ * environment supplies. The stored value is never replaced — the user may
563
+ * have set it deliberately — but the divergence must be visible, because
564
+ * otherwise the route keeps sending the stale value forever.
565
+ */
566
+ private warnHeaderDrift;
292
567
  /** Current settings service, if the composition mounted one. */
293
568
  private settingsService;
569
+ /**
570
+ * Mount the settings service — the host's `ctx.inject(['settings'])` seam.
571
+ *
572
+ * A mount that lands after construction retries the binding repair: the
573
+ * startup pass could not read a profile yet and would otherwise leave the
574
+ * route broken for the whole session.
575
+ */
576
+ attachSettings(settings: SettingsLike): void;
577
+ /** Unmount the settings service that a re-mount replaced. */
578
+ detachSettings(settings: SettingsLike): void;
294
579
  /** Current raw credential provider. */
295
580
  private credentialsRaw;
296
581
  /**
@@ -326,6 +611,23 @@ export declare class KeysManagerService {
326
611
  * Add account that would fail. The check is lazy — see {@link oauthFlowFor}.
327
612
  */
328
613
  private oauthFlowAvailable;
614
+ /**
615
+ * Resolve one transport route's sign-in flow.
616
+ *
617
+ * The transports package registers `llm-omp-transports/<route>` itself, so
618
+ * the live flow list is also this manager's inventory of transport routes:
619
+ * a route with no registered flow is not a transport account surface, and a
620
+ * route whose record exists but whose flow is gone is not listed at all
621
+ * (there would be nothing to sign in with).
622
+ *
623
+ * The method id is the flow's own (`methods[0].id`), never a literal
624
+ * `'oauth'`: the scope prefix is the whole cross-package contract, and the
625
+ * transports package may name a method that is not OAuth. A flow with no
626
+ * method cannot be begun and is not a sign-in surface.
627
+ */
628
+ private transportFlowFor;
629
+ /** Every registered transport flow, in seam order. */
630
+ private transportFlows;
329
631
  /**
330
632
  * Warn once per channel: a lost subscription silently disables a whole
331
633
  * feature (harvest never runs; rotations never sync), so the degradation
@@ -342,6 +644,14 @@ export declare class KeysManagerService {
342
644
  private watchAuthorizationSettled;
343
645
  /** One settled broadcast: finalize the matching attempt, harvest on success. */
344
646
  private onAuthorizationSettled;
647
+ /**
648
+ * The seam interaction one attempt hands to its flow.
649
+ *
650
+ * Shared by the pi-ai and transport sign-ins: both surfaces park notices and
651
+ * prompts on the same {@link LoginAttempt} and are polled through
652
+ * `accountAttempt`; only what happens after `authorized` differs.
653
+ */
654
+ private attemptInteraction;
345
655
  /**
346
656
  * Start one interactive OAuth sign-in. Returns immediately with the
347
657
  * attempt view; the attempt walks `pending → (prompt ⇄ pending) →
@@ -357,6 +667,65 @@ export declare class KeysManagerService {
357
667
  accountAttempt(attemptId: string): AccountAttemptView | undefined;
358
668
  /** Every attempt, newest first, for T3 surfaces. */
359
669
  listAccountAttempts(): AccountAttemptView[];
670
+ /**
671
+ * Re-read every `llm-omp-transports/<route>` grant into
672
+ * {@link transportRows}.
673
+ *
674
+ * Every caller gets a pass appended after the one already in flight, so an
675
+ * awaiting caller observes a read that started after its own write (a
676
+ * removal, a sign-in commit). Poll-only callers use
677
+ * {@link refreshTransportAccountsIfIdle} instead, so a slow store cannot
678
+ * accumulate one pass per page tick. A failed read leaves that route's
679
+ * previous row alone instead of claiming the account disappeared; the next
680
+ * pass retries.
681
+ */
682
+ refreshTransportAccounts(): Promise<void>;
683
+ /** Poll hint: start a pass only when none is in flight. */
684
+ private refreshTransportAccountsIfIdle;
685
+ /** One read pass over every registered transport route. */
686
+ private readTransportAccounts;
687
+ /**
688
+ * Remote: start one transport sign-in.
689
+ *
690
+ * The flow lives on the transports package's own key
691
+ * (`llm-omp-transports/<route>`), so the grant it commits stays that route's
692
+ * credential: nothing here harvests it into a pool row or a `llm-pi-ai`
693
+ * projection. The attempt is otherwise the same record the pi-ai dialog
694
+ * polls through `accountAttempt`, so the client reuses that dialog; an
695
+ * authorized outcome is terminal here (the flow has already committed).
696
+ */
697
+ beginTransportLogin(route: string): Promise<AccountAttemptView>;
698
+ /**
699
+ * Remote: confirm one transport route's stored grant.
700
+ *
701
+ * A transport route has exactly one grant record and its adapter resolves
702
+ * that record per request, so the stored grant already IS the route's live
703
+ * credential. Activation is therefore a state-level re-read: it writes
704
+ * nothing (in particular no `llm-pi-ai/<route>` projection, which the
705
+ * transport route never consults) and reports the current status so the UI
706
+ * can tell "active" from "expired" or "sign in again".
707
+ */
708
+ activateTransportAccount(route: string): Promise<StateView>;
709
+ /**
710
+ * Remote: drop one transport route's grant record.
711
+ *
712
+ * The record is the whole account — the transports package re-creates none —
713
+ * so deleting it leaves the route signed out until the next sign-in. A live
714
+ * sign-in is withdrawn first: left running, its commit would recreate the
715
+ * grant this removal just deleted. No manager state is written, hence no
716
+ * `run` queue and no state flush.
717
+ */
718
+ removeTransportAccount(route: string): Promise<StateView>;
719
+ /** The route's removal epoch; absent means never removed. */
720
+ private transportGenerationOf;
721
+ /**
722
+ * Drop a grant a withdrawn sign-in wrote after its account was removed.
723
+ *
724
+ * The seam refuses a commit once the attempt's signal aborts, but a commit
725
+ * that had already started can still land. The account was deleted, so that
726
+ * write is deleted too instead of being reported as a signed-in route.
727
+ */
728
+ private dropRemovedTransportGrant;
360
729
  /**
361
730
  * Detach the parked prompt. With a reason the flow's awaited promise is
362
731
  * rejected too — a parked prompt dropped un-rejected would leave the
@@ -386,6 +755,11 @@ export declare class KeysManagerService {
386
755
  * recorded at that same broadcast (R6-SF1): it names the projection whose
387
756
  * failures own a cooldown, so the stale-window reset below clears only a
388
757
  * window attributed to an OLDER projection.
758
+ *
759
+ * A harvest that finds the attempt already settled writes neither the
760
+ * plugin-scope copy nor a row, but the projection the seam committed for
761
+ * this attempt is still demoted (ownership-checked, I-000122): a
762
+ * failed/cancelled login must not keep serving through the route.
389
763
  */
390
764
  private harvestLogin;
391
765
  /**
@@ -460,6 +834,30 @@ export declare class KeysManagerService {
460
834
  * Resolves `ok: false` only when the compare/delete writes themselves
461
835
  * rejected; an ownership mismatch is the guard doing its job (the live
462
836
  * record belongs to someone else) and counts as a clean skip.
837
+ *
838
+ * Every manager-initiated projection deletion registers the one-shot
839
+ * {@link intentionalProjectionRemovals} marker before the delete, exactly
840
+ * as {@link decideAndDeleteProjection} does (I-000125). Unmarked, the
841
+ * removal's own `credentials/record-updated` echo reconciles as an
842
+ * external deletion: the route's previously active managed account is
843
+ * flagged `valueMissing`, its owner claim is dropped and a misleading
844
+ * "removed outside Keys Manager" warning is pushed — even though that
845
+ * row and its plugin-scope grant are untouched.
846
+ *
847
+ * The marker only silences that echo; the route's state is corrected
848
+ * explicitly. The removal leaves the route with no stored projection, so the
849
+ * provider's owner claim — whatever it named before this attempt's grant
850
+ * replaced the record — is untruthful: {@link oauthOwnerFor} keeps naming a
851
+ * row pi-ai no longer resolves, `wrapStream` skips key-pool selection, oauth
852
+ * failures are attributed to that row, and the client keeps "Set active"
853
+ * disabled — the account cannot be re-activated. The claim is therefore
854
+ * invalidated (I-000125), and ONLY the claim: the surviving row keeps its
855
+ * `grantRef` and its `valueMissing: false`, so it stays managed and
856
+ * re-activatable while the route falls back to its API-key pool.
857
+ *
858
+ * The marker is armed only while the provider is still managed: a released
859
+ * provider has no echo consumer, and a marker nothing consumes would stay
860
+ * armed for its whole TTL and mask a genuine out-of-band removal.
463
861
  */
464
862
  private revertHarvestProjection;
465
863
  /** Expire wedged sign-ins and drop long-settled attempt records. */
@@ -586,24 +984,315 @@ export declare class KeysManagerService {
586
984
  * Make one account's grant payload the route's stored credential
587
985
  * (D1 activation projection).
588
986
  *
589
- * Round-8 finding I-000106 (must-fix): the decide snapshot used to be read
590
- * before the seam's lock, and `reconcileGrantRecord` runs OUTSIDE the
591
- * mutation queue — so a rotation echo could repoint `row.oauth.grantRef` and
592
- * delete the record being projected between the read and the write, leaving
593
- * the route on an older generation than the row names. The whole
594
- * read → project → claim section therefore runs on the provider's grant
595
- * chain ({@link scheduleGrantReconciliation}) — the same chain the
596
- * reconciler bodies use. A rotation committed before this body is observed
597
- * in `row.oauth.grantRef`; one that arrives while the body runs is queued
598
- * behind it and reconciles the generation this body projected. Either way
599
- * the projection holds exactly the generation the row names.
600
- *
601
- * The seam's per-key lock still serializes the read-decide-replace steps of
602
- * `modifyRecord` (credentials/src/index.ts:236-250) against other writers.
603
- * The account whose payload lands becomes the pool's active account and is
604
- * cascade-selectable again.
987
+ * Plan iter-pool-activate-queue, "Spike outcome" — the contract this method
988
+ * implements. The slow half (read the row's grant, validate it, capture what
989
+ * the route serves now) runs outside the mutation queue, so a slow credential
990
+ * store does not delay unrelated mutations. The mutation queue turn owns every
991
+ * write:
992
+ *
993
+ * 1. admission checks in queue order, which produce this method's error
994
+ * surface (`Provider "…" is not managed`, `Account "…" not found on …`,
995
+ * `Key "…" is not an OAuth account`, `The credential store does not support
996
+ * records`) — plus a re-check that the row still names the generation the
997
+ * read used;
998
+ * 2. the projection write as a compare-and-write under the seam's per-key
999
+ * lock (credentials/src/index.ts:236-250): it lands only while the live
1000
+ * record still equals the captured baseline, "both absent" included. A
1001
+ * rotation, a removal or any foreign writer that replaced the projection
1002
+ * meanwhile makes the compare fail, so this call refuses with the retryable
1003
+ * `could not be projected` message and leaves the newer projection
1004
+ * untouched — there is nothing to delete, because nothing was written;
1005
+ * 3. the owner claim, the row writes and the returned `buildState`.
1006
+ *
1007
+ * There is deliberately no shutdown check after the write: `close()` awaits
1008
+ * this queue before it closes the store, so a turn that started in time always
1009
+ * finishes (measured in the spike that produced this contract). A check there
1010
+ * is what used to leave an orphan projection behind.
1011
+ *
1012
+ * The one write the turn cannot make durable by itself is a rejected state
1013
+ * flush; see {@link compensateActivationFlush} for that compensation.
605
1014
  */
606
1015
  activateAccount(provider: string, accountId: string): Promise<StateView>;
1016
+ /**
1017
+ * The slow half of an activation, outside the queue: read the row's grant
1018
+ * generation, validate it, and capture the projection the route serves now.
1019
+ */
1020
+ private prepareActivation;
1021
+ /** Whether the projection still holds the value this activation read. */
1022
+ private matchesActivationBaseline;
1023
+ /**
1024
+ * Write the projection only while the route still serves the baseline. The
1025
+ * seam's `modifyRecord` mutate body runs under the per-key lock, so this is
1026
+ * the one atomic primitive the activation has (I-000090: `deleteRecord` has
1027
+ * no expected-payload form). Returns the payload written, or `undefined` when
1028
+ * the compare failed and nothing was touched.
1029
+ */
1030
+ private compareAndWriteProjection;
1031
+ /**
1032
+ * Undo one activation whose state flush was rejected: the row this turn
1033
+ * cleared in memory, the owner claim it took, and the projection it wrote
1034
+ * (back to the captured baseline).
1035
+ *
1036
+ * The baseline decides the mechanism. A present baseline is restored with the
1037
+ * same compare-and-write the projection used; only an absent baseline has to
1038
+ * remove a record, and only while that record is still this activation's own
1039
+ * payload. A foreign writer's record is never deleted.
1040
+ */
1041
+ private compensateActivationFlush;
1042
+ /** Put the route's owner classification back to a captured snapshot. */
1043
+ private applyActivationClassification;
1044
+ /** Put the projection back to the present baseline, if it is still ours. */
1045
+ private restoreActivationProjection;
1046
+ /**
1047
+ * The delete authority's proof for an activation's own projection, as an
1048
+ * explicit three-valued answer (QC R2): only `matches` may authorize touching
1049
+ * the record. `unknown` is a store failure — a closed seam, an unreadable
1050
+ * record — and proves nothing, so it is never permission.
1051
+ */
1052
+ private activationProjectionProof;
1053
+ /**
1054
+ * Delete one projection an activation just proved it still owns, through the
1055
+ * same guarded shape the demotion path uses (MF2): the intentional-removal
1056
+ * marker is armed before the delete, so the deletion's own echo is read as
1057
+ * ours instead of as an out-of-band removal.
1058
+ */
1059
+ private deleteActivationProjection;
1060
+ /**
1061
+ * Remote: send one minimal request with the selected key or account row.
1062
+ *
1063
+ * Observation-only by construction: the request runs under a test-only
1064
+ * request scope, so the broker answers with the pinned credential and the
1065
+ * stream listener skips selection and every health/binding write. An OAuth
1066
+ * row's grant is projected only for the duration of the request; the
1067
+ * projection (and the in-memory owner) that served the route before is
1068
+ * restored even when the request fails, so a test never changes which
1069
+ * account the route is on.
1070
+ *
1071
+ * Every expected refusal is a structured `{ ok: false, error }` result
1072
+ * (M2): the inline row line is the only failure surface, so a refusal must
1073
+ * not also raise the page-level banner a thrown RPC error produces.
1074
+ */
1075
+ testKey(provider: string, keyId: string): Promise<KeyTestResult>;
1076
+ /**
1077
+ * Take over a test window that did not settle within the isolation deadline
1078
+ * (QC fix wave 2) or whose own cleanup could not restore the route (PR
1079
+ * review MF1).
1080
+ *
1081
+ * The fence is set synchronously, before the first cleanup await: the wedged
1082
+ * test may resume at any await from here, and it must observe the
1083
+ * abandonment before it starts a borrow or restores from its own stale
1084
+ * snapshot. The provider keeps its isolation entry until the cleanup
1085
+ * completed — see {@link settleAbandonedWindow}.
1086
+ */
1087
+ private abandonTestWindow;
1088
+ /**
1089
+ * Clean up one abandoned window and release its provider only on success.
1090
+ *
1091
+ * A cleanup that cannot finish needs the same untimed seam the wedged test is
1092
+ * parked in, so it fails exactly like the test did. Releasing the provider
1093
+ * then would serve ordinary traffic from the borrowed grant (or, on a
1094
+ * broker-bound profile, from a route whose binding was released and never
1095
+ * restored), so the isolation entry and the fence both stay. Two events can
1096
+ * retry the cleanup: the abandoned promise's settlement, and the next test
1097
+ * attempt ({@link recoverAbandonedWindow}, QC fix wave 3 W-002) — a promise
1098
+ * that settles while the seam is still wedged must not be the only path.
1099
+ */
1100
+ private settleAbandonedWindow;
1101
+ /** One later cleanup attempt for an abandoned window, once the seam answers again. */
1102
+ private retryAbandonedWindow;
1103
+ /**
1104
+ * One bounded cleanup attempt for a fenced provider (QC fix wave 3, W-002).
1105
+ *
1106
+ * Returns whether the provider is servable again. The caller's attempt is
1107
+ * what makes the fence self-clearing: `testKey` calls this before refusing,
1108
+ * so isolation no longer depends on the abandoned promise settling after the
1109
+ * seam recovered. A failed attempt keeps the fence and warns exactly once.
1110
+ */
1111
+ private recoverAbandonedWindow;
1112
+ /**
1113
+ * Run the window's cleanup under its hard deadline and drop the provider's
1114
+ * isolation only when it completed. Returns the reason it could not finish.
1115
+ *
1116
+ * PR review round 21: the request that borrowed the route is not part of the
1117
+ * cleanup, so a cleanup can finish while that request is still able to
1118
+ * refresh and rewrite the borrowed projection. Such a window is held
1119
+ * ({@link RowTestWindow.held}) — the fence and the isolation entries stay —
1120
+ * and only its settlement reconciles the late write and releases it.
1121
+ */
1122
+ private attemptAbandonedCleanup;
1123
+ /** Drop every isolation entry this window still owns, identity-checked. */
1124
+ private releaseAbandonedWindow;
1125
+ /**
1126
+ * Put the route back the way the window found it: the mutations this window
1127
+ * started, then the projection (compare-by-payload and rotation-aware, so
1128
+ * nothing a writer stored meanwhile is overwritten), then the binding — the
1129
+ * original finally's order, because the binding decision reads the restored
1130
+ * owner map — then the borrow marker. Returns the reason the cleanup could
1131
+ * not complete.
1132
+ *
1133
+ * `stillCurrent` is passed by abandoned-window attempts only: a superseded
1134
+ * attempt that outlived its deadline may finish its writes, but the
1135
+ * provider-wide borrow marker belongs to the newest attempt (PR review
1136
+ * round 2) — deleting it would let ordinary traffic reach a newer window's
1137
+ * borrowed projection.
1138
+ */
1139
+ private cleanupTestWindow;
1140
+ /**
1141
+ * Reconcile the projection once an abandoned window's request finally
1142
+ * settles (PR review round 13).
1143
+ *
1144
+ * The watchdog's cleanup can complete before the wedged request resumes: an
1145
+ * adapter that ignored the abort then refreshes and rewrites the borrowed
1146
+ * grant, and the abandoned window used to skip cleanup on settlement — so
1147
+ * the tested account stayed projected over the restored one and served
1148
+ * ordinary traffic. The shared compare-by-payload restore is idempotent and
1149
+ * only touches a projection this test left (the borrowed grant or a
1150
+ * rotation of the tested row): a foreign payload survives, and a newer
1151
+ * generation is written back to the tested row before the pre-test
1152
+ * projection is restored.
1153
+ *
1154
+ * Returns the failure, so a caller that releases the provider afterwards (PR
1155
+ * review round 21) can keep the fence instead. `ownsFence` is passed by the
1156
+ * settlement of a held window and by the cleanup that releases a window whose
1157
+ * settlement it deferred (PR review round 28): both already own the
1158
+ * provider's fence, which the default guards would read as a competing
1159
+ * cleanup and skip.
1160
+ */
1161
+ private reconcileLateWindowProjection;
1162
+ /**
1163
+ * Record one in-flight window mutation before it is awaited (QC fix wave 3,
1164
+ * W-001). The normalized promise never rejects: the mutation's own outcome
1165
+ * belongs to the code awaiting it, while the cleanup only needs to know when
1166
+ * the write is over.
1167
+ */
1168
+ private recordWindowMutation;
1169
+ /**
1170
+ * Await every mutation this window recorded, including one the continuation
1171
+ * of an earlier mutation records (a borrow that settles right before its
1172
+ * binding release starts). The microtask hop after each settling round lets
1173
+ * that continuation run before the cleanup judges the route clean. A
1174
+ * mutation the seam never answers is bounded by the caller's deadline, not
1175
+ * here.
1176
+ */
1177
+ private awaitWindowMutations;
1178
+ /**
1179
+ * Run one cleanup under its hard deadline. The work is not cancelled — the
1180
+ * credential seam offers no cancellation — it is only stopped from holding
1181
+ * the provider past the bound; a late result is ignored.
1182
+ */
1183
+ private withinCleanupDeadline;
1184
+ /**
1185
+ * Decide the route's serving shape and run one row test. Separate from
1186
+ * {@link testKey} so the concurrency lock is registered before the first
1187
+ * await.
1188
+ */
1189
+ private runRowTest;
1190
+ /**
1191
+ * The live provider profile one test must reason about, read once at entry
1192
+ * (QC B1/N6), or the reason it cannot be used. An unreadable settings
1193
+ * snapshot and a missing profile are distinct causes, so the row never
1194
+ * claims a model is missing when the settings service is merely absent.
1195
+ */
1196
+ private testRouteProfile;
1197
+ /** One request pinned to the row's API-key literal. */
1198
+ private runApiKeyRowTest;
1199
+ /** One request with the row's grant temporarily projected as the route's credential. */
1200
+ private runOAuthRowTest;
1201
+ /** The refusal an abandoned test reports instead of a stale success. */
1202
+ private abandonedTestResult;
1203
+ /** The refusal a chain reports when shutdown refuses its persistence. */
1204
+ private shutdownTestResult;
1205
+ /** The refusal a test reports when it arrives after shutdown began (round 34). */
1206
+ private shutdownEntryResult;
1207
+ /**
1208
+ * Mark one provider as being removed for the whole removal body (PR review
1209
+ * round 12). The marker is up before the body's first await — the removal's
1210
+ * own test wait included — so {@link testKey} can refuse a test that would
1211
+ * otherwise start inside the removal. Nesting is a depth because a
1212
+ * last-key `deleteKey` releases through `releaseLocked`.
1213
+ */
1214
+ private withRemovalInProgress;
1215
+ /**
1216
+ * Hold a row/account removal until the provider's running row test settles
1217
+ * (PR review round 9, R8-2).
1218
+ *
1219
+ * `testKey` captures its row, its credential and the route's serving shape
1220
+ * before the first await, so a removal landing inside that window can delete
1221
+ * the credential the test is about to send. For a borrowed OAuth account the
1222
+ * removal is worse than a stale read: it compares its decision against the
1223
+ * test's temporary projection, deletes it and demotes every row, and the
1224
+ * test's own restore then writes the removed account's grant back. The
1225
+ * removal therefore waits for the window instead of racing it. The wait is
1226
+ * bounded by {@link isolationWaitMs} — a test wedged in an untimed seam call
1227
+ * must not park the removal forever — and a provider whose abandoned window
1228
+ * still owns the route's records refuses the removal outright. A wait that
1229
+ * hits its own bound refuses too (PR review round 10, I-000179): proceeding
1230
+ * would delete the credential the parked test is about to send. The user can
1231
+ * retry once the abandoned window has been cleaned up.
1232
+ */
1233
+ private settleTestBeforeRemoval;
1234
+ /**
1235
+ * Put the projection back as it was before the test borrowed it, without
1236
+ * losing or overwriting anything that changed meanwhile.
1237
+ *
1238
+ * `late` marks the abandoned window's settlement, after the watchdog's
1239
+ * cleanup already released the provider: a removal no longer waits for this
1240
+ * test, so the pre-test projection's owning row may be gone by the time this
1241
+ * runs and must not be projected again (PR review round 19).
1242
+ *
1243
+ * The decision is compare-by-payload against the borrowed grant: only a
1244
+ * live record that still carries it — or a refresh-rotated version of it
1245
+ * that {@link matchOwningAccount} attributes to the tested row — is
1246
+ * replaced. A rotation is reconciled into the tested row first (the same
1247
+ * plugin-scope write-back {@link reconcileGrantRecord} performs), because
1248
+ * the provider may already have consumed the superseded refresh token. A
1249
+ * foreign or newer payload (another account, a native writer) is left
1250
+ * untouched, and a record removed during the window stays removed. In every
1251
+ * branch the owner map ends consistent with what the store actually holds.
1252
+ *
1253
+ * Returns a failure message when the restore or the rotation write-back
1254
+ * did not land, so the caller reports `{ ok: false }` instead of a false
1255
+ * pass.
1256
+ */
1257
+ private restoreProjection;
1258
+ /**
1259
+ * Point the owner map at the OAuth row the stored record identifies.
1260
+ *
1261
+ * The argument is the live record, not its grant payload (PR review round 8):
1262
+ * a payload-only signature cannot tell a removed projection from a PRESENT
1263
+ * record this pool cannot interpret (a native api-key, an unrecognized
1264
+ * payload), so a surviving native record reached this method as `undefined`,
1265
+ * cleared the foreign marker, and `keyPoolServes` then let the window-end
1266
+ * sync bind the broker over it. Only an absent record is a confirmed removal
1267
+ * (I-000167); a present record no row owns keeps the route foreign (round 3).
1268
+ */
1269
+ private reconcileProjectionOwner;
1270
+ /**
1271
+ * Persist one authoritative rotated grant payload for a row: write the
1272
+ * plugin-scope record for the rotated generation, then hand the commit to
1273
+ * {@link commitGrantGeneration} — the same tail {@link reconcileGrantRecord}
1274
+ * performs. The caller runs inside the provider's grant chain and owns the
1275
+ * decision, including the echo-specific generation guards, so only the
1276
+ * write-back semantics live here.
1277
+ */
1278
+ private writeBackRotation;
1279
+ /**
1280
+ * The single owner of `row.oauth.grantRef` semantics (QC N1): point the row
1281
+ * at the record holding its rotated generation, merge the refresh-derived
1282
+ * identity fields, and remove the superseded record. Both the echo
1283
+ * reconciler and the connection test's rotation write-back commit through
1284
+ * here, so the ref scheme, the identity merge and the cleanup policy have
1285
+ * exactly one implementation.
1286
+ *
1287
+ * The record write stays with the caller: the reconciler must revalidate the
1288
+ * row between that write and this commit (C6-1), so only the tail is shared.
1289
+ */
1290
+ private commitGrantGeneration;
1291
+ /**
1292
+ * Send the minimal request under the given scope and map its terminal
1293
+ * chunk. A thrown error or the 20-second abort is reported, never swallowed.
1294
+ */
1295
+ private runTestRequest;
607
1296
  /**
608
1297
  * Demote the cascade to the key pool by deleting the projection record:
609
1298
  * pi-ai's per-request resolution prefers the stored credential and only
@@ -724,22 +1413,131 @@ export declare class KeysManagerService {
724
1413
  private describeSettings;
725
1414
  /** Find one live settings descriptor by entry id. */
726
1415
  private namespace;
727
- /** Apply one path-addressed settings write through the live revision. */
1416
+ /**
1417
+ * Apply one path-addressed settings write through the live revision.
1418
+ *
1419
+ * `stillWrites` runs after the descriptor re-read and immediately before the
1420
+ * mutation: a decision derived from an earlier snapshot can be stale by then
1421
+ * ({@link syncBrokerBinding}'s bind gate, PR review round 25). A declining
1422
+ * guard skips the write and reports it.
1423
+ */
728
1424
  private mutateSettings;
729
1425
  /** Prune expired bindings at most once a minute, without a restart. */
730
1426
  private pruneBindings;
1427
+ /**
1428
+ * Project one provider's unattributed-request count into its row. Additive
1429
+ * and optional (`I-000120`): a row with no count spreads nothing, so an
1430
+ * older snapshot and a healthy store render exactly as before.
1431
+ */
1432
+ private unattributedView;
731
1433
  /** Build the complete UI state. */
732
1434
  buildState(): StateView;
733
- /** Remote: complete manager state. */
1435
+ /**
1436
+ * Remote: complete manager state.
1437
+ *
1438
+ * The transport rows come from another package's records, and reading them
1439
+ * is async while this endpoint is synchronous, so every poll starts a
1440
+ * refresh for the next one (the client ticks every 30 s), unless one is
1441
+ * already in flight. The constructor warms the first read and record
1442
+ * updates refresh immediately, so the one-tick lag only shows on a cold
1443
+ * start.
1444
+ */
734
1445
  getState(): StateView;
735
1446
  /** Find one directory entry by route id. */
736
1447
  private directoryEntry;
1448
+ /**
1449
+ * Whether the key pool is the credential source that serves this route: no
1450
+ * OAuth projection owns it and at least one API-key row still holds a value.
1451
+ * This is the one arbiter for both the verdict below and the binding write
1452
+ * ({@link syncBrokerBinding}).
1453
+ */
1454
+ private keyPoolServes;
737
1455
  /**
738
1456
  * Classify the live binding. An unreadable settings snapshot is `unknown`,
739
1457
  * not a confirmed mismatch: transient settings failures must not disable
740
1458
  * key-pool recovery for a provider that is still managed.
1459
+ *
1460
+ * Only a value Keys Manager never wrote is a `mismatch`: the warning now
1461
+ * means "the user took the profile back", not "the OAuth route is broken".
1462
+ * An unbound profile is healthy while the key pool is not the serving
1463
+ * source (an OAuth projection serves, or no API-key row holds a value) —
1464
+ * that is the OAuth-only route's native shape.
741
1465
  */
742
1466
  private bindingState;
1467
+ /**
1468
+ * The single authority for a managed provider's pi-ai `apiKeyEnv`.
1469
+ *
1470
+ * pi-ai resolves a named credential synchronously and throws
1471
+ * `MISSING_CREDENTIAL` when the credential seam cannot answer it, so the
1472
+ * broker reference may only sit on a profile whose requests the key pool
1473
+ * actually serves ({@link keyPoolServes}). Every other shape must be
1474
+ * unbound, or pi-ai never reaches its own stored OAuth grant.
1475
+ *
1476
+ * Each direction has exactly one provably owned write:
1477
+ * - bind: a profile that names no credential, or — with `takeover`, passed
1478
+ * solely by {@link adoptProvider} — the pre-adoption reference adoption
1479
+ * recorded and deliberately replaces;
1480
+ * - unbind: only the broker reference Keys Manager itself wrote.
1481
+ *
1482
+ * Any other value is a hand-edit (or the pre-adoption reference release
1483
+ * restored): it is never touched, and the `mismatch` verdict stays the
1484
+ * signal that the user took the profile back.
1485
+ *
1486
+ * `write: false` is the startup repair: it only touches a profile that
1487
+ * already exists, so a first run never materializes one from nothing. The
1488
+ * release direction is single-sourced in {@link releasingProviders}: a
1489
+ * provider being unmanaged is never bound again, whatever the row order of
1490
+ * the pool it is deleting.
1491
+ *
1492
+ * A read-side problem or a refused write degrades to a warning: the
1493
+ * callers' committed change must not fail on a settings problem, the user
1494
+ * still sees that the route may resolve the wrong credential source, and
1495
+ * the next repair pass retries.
1496
+ *
1497
+ * Returns whether the profile ends in the shape the serving source justifies
1498
+ * (`true` also when no write was needed). The connection test's borrow is the
1499
+ * one caller that acts on it: a failed release must refuse the test instead
1500
+ * of sending a request the wrong credential source would answer.
1501
+ *
1502
+ * While a row test borrows the route (QC B4) no caller may persist a decision
1503
+ * derived from that borrowed world — an `addKey` landing inside the window
1504
+ * would otherwise unbind the profile for good. The test's own writes pass
1505
+ * `borrow: true`; the window-end sync then writes the restored state.
1506
+ */
1507
+ private syncBrokerBinding;
1508
+ /**
1509
+ * Repair every managed provider's binding through the one authority. The
1510
+ * startup pass and the settings-mount seam both run it, so a profile that
1511
+ * was unreadable when hydration settled is still healed in this process
1512
+ * instead of waiting for the next start.
1513
+ */
1514
+ private repairBrokerBindings;
1515
+ /**
1516
+ * One provider's repair pass (PR review round 42). A recorded adoption
1517
+ * takeover debt is settled here only when the authoritative readiness says
1518
+ * no projection serves the route — the case a restart leaves behind when the
1519
+ * foreign record disappeared while the process was down.
1520
+ */
1521
+ private repairBinding;
1522
+ /**
1523
+ * Re-derive a managed route's binding after its projection changed outside
1524
+ * this manager's account pool: a removal, a committed harvest that handed
1525
+ * the route back to its API-key pool (PR review round 2), or a foreign
1526
+ * projection taking a bound route over (round 4).
1527
+ *
1528
+ * The route's serving source is no longer the managed pool, or the pool is
1529
+ * the only source left: either way `syncBrokerBinding` derives the one
1530
+ * profile shape pi-ai's resolution order requires. A store refusal only
1531
+ * warns — these are compensation paths and must never fail the change that
1532
+ * triggered them; the next repair pass retries.
1533
+ */
1534
+ private syncBindingAfterProjectionChange;
1535
+ /**
1536
+ * A refused binding write must not fail the change that triggered it, but
1537
+ * it cannot stay in the log alone either: the route may resolve the wrong
1538
+ * credential source until the next repair.
1539
+ */
1540
+ private bindingWriteRefused;
743
1541
  /** Candidate credential refs from which an existing key can be imported. */
744
1542
  private importRefs;
745
1543
  /** Create and store one key record without adding it to a provider. */
@@ -748,10 +1546,75 @@ export declare class KeysManagerService {
748
1546
  private findImport;
749
1547
  /** Remote: take over one existing provider route. */
750
1548
  adoptProvider(provider: string): Promise<StateView>;
751
- /** Discover models for a new extra provider, falling back to the seed catalog. */
1549
+ /**
1550
+ * Read and classify one provider's live projection at adoption (PR review
1551
+ * round 17) — the same read hydration does, for the one route hydration
1552
+ * cannot cover: a provider that did not exist when the snapshot was taken.
1553
+ *
1554
+ * An adoption owns no account row yet, so any live record is one no row of
1555
+ * this pool owns: a native api-key, an unrecognized payload, and a grant left
1556
+ * behind by a released account all serve the route ahead of the broker
1557
+ * reference, so the route is marked foreign and the pool is not bound over it
1558
+ * (PR review rounds 3–4). An absent record clears any stale marker: the pool
1559
+ * may serve. A failed read is not an absent projection (round 8): ownership
1560
+ * stays unknown, so nothing may be bound and no repair touches the route
1561
+ * until an authoritative read answers ({@link noteProjectionReadSucceeded}).
1562
+ *
1563
+ * Returns `unknown` for that failure; the adoption itself is kept — there is
1564
+ * no write to roll back, and only the binding is undecided.
1565
+ *
1566
+ * The classification runs inside the route's grant chain (PR review round
1567
+ * 22, Codex P1). A native write or deletion reconciled while the read is
1568
+ * outstanding is therefore either already visible to it or queued behind it:
1569
+ * a read whose snapshot a newer event overtook reports `superseded` and lands
1570
+ * nothing, so a stale absence can never bind the broker over a newer
1571
+ * projection state and a stale presence can never leave the pool unbound.
1572
+ */
1573
+ private readAdoptionProjection;
1574
+ /** Body of {@link readAdoptionProjection}: read, then land the classification. */
1575
+ private classifyAdoptionProjection;
1576
+ /**
1577
+ * Undo an adoption whose initial binding write was refused (PR review
1578
+ * round 2): drop the managed record and its broker entry, delete the
1579
+ * plugin-scope copy this call created, and put back a pre-adoption
1580
+ * reference that landed despite the refusal. The source credential and the
1581
+ * profile's other fields are never touched — adoption only read them.
1582
+ */
1583
+ private rollbackAdoption;
1584
+ /**
1585
+ * Discover models for a new extra provider, falling back to the seed catalog.
1586
+ * A route that declares a static header installs the seed catalog directly:
1587
+ * the discovery request has no header slot, so probing such an endpoint can
1588
+ * only fail (and would hold the serialized mutation queue for the probe's
1589
+ * whole timeout).
1590
+ */
752
1591
  private discoverModels;
753
- /** Remote: install and adopt one extra built-in provider. */
1592
+ /**
1593
+ * Remote: install and adopt one extra built-in provider.
1594
+ *
1595
+ * Deliberate exemption from the one-authority rule
1596
+ * ({@link syncBrokerBinding}): this call creates the profile it binds and
1597
+ * the key that serves it in the same mutation, so the write below is the
1598
+ * route's first binding rather than a takeover of a value the user owns.
1599
+ */
754
1600
  addExtraProvider(extraId: string, alias: string, value: string): Promise<StateView>;
1601
+ /**
1602
+ * Restore a managed extra route's declared static headers.
1603
+ *
1604
+ * A managed profile can lose its declared headers through a Models edit;
1605
+ * every managed add-key flow repairs them instead of leaving a route whose
1606
+ * required header is silently absent. Only a live profile with a healthy
1607
+ * binding is ours to repair: a deleted profile would gain a header-only
1608
+ * half-profile, and a replaced one is the state release refuses to touch. A
1609
+ * stored value for the same header always wins. The caller may pass the
1610
+ * headers it already resolved at its own entry (where a missing variable can
1611
+ * still refuse the add before any write); otherwise this method resolves
1612
+ * them. Either way a failure only warns, because the key has already been
1613
+ * added: a route whose headers cannot be resolved, and a settings write that
1614
+ * cannot be performed, both leave the repair pending for the next add so a
1615
+ * half-applied add is never turned into a rejected one.
1616
+ */
1617
+ private repairDeclaredHeaders;
755
1618
  /** Add one key; caller already owns the mutation queue. */
756
1619
  private addKeyLocked;
757
1620
  /** Remote: add one key to a managed provider. */
@@ -760,12 +1623,45 @@ export declare class KeysManagerService {
760
1623
  updateKey(provider: string, keyId: string, patch: unknown): Promise<StateView>;
761
1624
  /** Remote: delete one key, releasing the provider when it was the last one. */
762
1625
  deleteKey(provider: string, keyId: string): Promise<StateView>;
763
- /** Read one live profile for release-time cleanup decisions. */
1626
+ /** Read one live profile for repair and release-time cleanup decisions. */
764
1627
  private profileAt;
765
- /** Restore the exact live profile captured before release mutated it. */
1628
+ /**
1629
+ * Restore the exact live profile captured before release mutated it.
1630
+ *
1631
+ * Deliberate exemption from the one-authority rule: release is unmanaging,
1632
+ * so this write puts the pre-adoption document back instead of deriving a
1633
+ * binding from a key pool that is being deleted. Every other caller goes
1634
+ * through {@link syncBrokerBinding}.
1635
+ */
766
1636
  private restoreProfileSnapshot;
767
- /** Remove every managed credential, recording progress before a retry. */
1637
+ /**
1638
+ * Remove every managed credential, recording progress before a retry.
1639
+ *
1640
+ * The provider is flagged as releasing for the whole loop: a demotion
1641
+ * inside it (an `oauth` row iterated before a surviving `api-key` row) must
1642
+ * not re-bind the profile, or the reference release just restored would be
1643
+ * overwritten and then stripped by the closing unbind. While unmanaging the
1644
+ * binding only ever goes one way — off.
1645
+ */
768
1646
  private deleteManagedCredentials;
1647
+ /**
1648
+ * Drop any armed intentional-removal marker for a provider being released.
1649
+ *
1650
+ * The marker only exists to silence the record-updated echo of the manager's
1651
+ * own projection removal, and that echo is dropped outright once the provider
1652
+ * is unmanaged: {@link onGrantRecordUpdated} returns at event time and
1653
+ * {@link reconcileGrantRecord} at body time. A release can land between the
1654
+ * arm and that echo — the abort compensation arms inside the mutation queue
1655
+ * while its echo is still chained behind an in-flight body — leaving the
1656
+ * marker armed for its whole TTL with nothing left to consume it. A provider
1657
+ * re-adopted inside that window would then read a genuine out-of-band
1658
+ * removal as the manager's own: no warning, no `valueMissing`, no
1659
+ * stale-owner cleanup.
1660
+ *
1661
+ * Called only on the successful release path: a release that throws leaves
1662
+ * the provider managed, and its echo may still legitimately arrive.
1663
+ */
1664
+ private dropIntentionalRemoval;
769
1665
  /** Restore the provider profile and drop management; caller owns the queue. */
770
1666
  private releaseLocked;
771
1667
  /** Remote: stop managing a provider and restore its original key reference. */
@@ -832,6 +1728,38 @@ export declare class KeysManagerService {
832
1728
  * entry (row removed) resolves to null.
833
1729
  */
834
1730
  private oauthOwnerFor;
1731
+ /**
1732
+ * Keep an ordinary request out of the window where a row test borrows (or
1733
+ * is restoring) this provider's projection. The provider stream is built
1734
+ * only after the bounded wait, so neither branch can start a request inside
1735
+ * the borrowed window; a consumer that cancels during the wait constructs
1736
+ * nothing at all. A test-scope request returns from `streamListener` before
1737
+ * this wrapper, so a test never waits on its own guard.
1738
+ */
1739
+ private isolateFromTest;
1740
+ /**
1741
+ * Wait (bounded) for the in-flight borrowing test on this provider to
1742
+ * settle. The bound is {@link KEY_TEST_ISOLATION_WAIT_MS}, not the test's
1743
+ * own request timeout: the awaited promise also covers untimed
1744
+ * credential-store reads, so without this an ordinary request could park
1745
+ * here forever. A failed or timed-out test never fails the waiting request.
1746
+ *
1747
+ * QC fix wave 2: the one exception is a provider whose abandoned window is
1748
+ * still fenced. The wait is over, but the route's credential surface has not
1749
+ * been reconciled, so serving the request would answer it with the borrowed
1750
+ * grant (or, after a released binding, with no bound route at all). Such a
1751
+ * request is refused instead — the fence is what keeps cross-use impossible
1752
+ * until the cleanup completes.
1753
+ */
1754
+ private settleProviderTest;
1755
+ /** The refusal an ordinary request gets while its route is still unreconciled. */
1756
+ private testCleanupRefusal;
1757
+ /**
1758
+ * Wrap one connection-test stream: keep the pinned credential scope alive
1759
+ * for every chunk and do nothing else. No selection, no cursor, no health
1760
+ * writes — the request is pure observation.
1761
+ */
1762
+ private wrapTestStream;
835
1763
  /** Wrap one managed stream so credential resolution sees this key. */
836
1764
  private wrapStream;
837
1765
  /**