dsh-keys-manager 0.2.1 → 0.2.2

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
@@ -157,6 +296,16 @@ export declare class KeysManagerService {
157
296
  * to the requested provider's own unresolved read.
158
297
  */
159
298
  private readonly hydratedProviders;
299
+ /**
300
+ * Providers whose hydration ownership read FAILED (PR review round 8). A
301
+ * failed read knows nothing about the route, so it is not the "no projection"
302
+ * answer a successful empty read is: treating it as one let the startup
303
+ * repair read an empty owner/foreign map as proof that the pool serves and
304
+ * bind the broker over a persisted OAuth projection it could not read.
305
+ * Readiness therefore stays false for these providers and the repair skips
306
+ * them; only a restart can retry the read.
307
+ */
308
+ private readonly ownershipReadFailures;
160
309
  /**
161
310
  * One resolver set per provider still being waited on (round-10 should-fix
162
311
  * SF2). A wait registers exactly one entry here and removes it when the wait
@@ -168,11 +317,27 @@ export declare class KeysManagerService {
168
317
  private readonly hydrationWaiters;
169
318
  /**
170
319
  * 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.
320
+ * on no hydration read can land. It is NOT evidence that a given provider's
321
+ * projection was read (PR review round 17): a provider adopted after the
322
+ * snapshot was never in it, so readiness is decided per provider
323
+ * ({@link projectionOwnershipKnown}) and adoption reads its own projection.
174
324
  */
175
325
  private hydrationSettled;
326
+ /**
327
+ * Credential-store generation of the ownership hydration (PR review round
328
+ * 27). A remounted credentials service starts a new pass while the previous
329
+ * store's reads can still be in flight; their answers describe records the
330
+ * new backend does not hold, so the epoch gates every write and settle of
331
+ * the pass that started it.
332
+ */
333
+ private hydrationEpoch;
334
+ /**
335
+ * Whether a credential store was ever attached (PR review round 27). The
336
+ * first attach is the startup mount and keeps its behaviour; every later one
337
+ * replaces the credential backend, so the ownership classification loaded
338
+ * from the previous one is stale and the read restarts from unknown.
339
+ */
340
+ private credentialsMounted;
176
341
  /** Providers whose bounded-wait warning already fired (warn once). */
177
342
  private readonly hydrationWaitWarned;
178
343
  /** Channels whose lost-subscription warning already fired (warn once). */
@@ -193,7 +358,17 @@ export declare class KeysManagerService {
193
358
  * after a restart. An orphan projection — no matching row, e.g. a foreign
194
359
  * or stale grant — is left untouched: the manager never adopts a grant it
195
360
  * does not own, and out-of-band updates keep surfacing the native-
196
- * management warning.
361
+ * management warning. A live record that is not a managed grant at all (a
362
+ * native api-key, an unrecognized payload) is foreign for the same reason
363
+ * (PR review round 4): it serves the route, so the startup repair must not
364
+ * bind the broker over it.
365
+ *
366
+ * EVERY managed provider's projection is read (PR review round 13), not
367
+ * only those with OAuth rows: a key-only pool can be overridden by a
368
+ * native grant persisted for the same route, and a skipped read left the
369
+ * startup repair free to bind the broker over it. The reads run
370
+ * concurrently so one provider's stalled read cannot delay another
371
+ * provider's own readiness (round 9).
197
372
  *
198
373
  * Returns its own completion (round-8 finding I-000108), and the returned
199
374
  * promise is already caught, so a failed hydration degrades to "no owner"
@@ -202,11 +377,31 @@ export declare class KeysManagerService {
202
377
  * each request waits on its own provider's landing instead.
203
378
  */
204
379
  private hydrateProjectionOwnership;
380
+ /**
381
+ * Drop every ownership fact loaded from a previous credential store (PR
382
+ * review round 27). The classification is per backend: a record that existed
383
+ * in the old store says nothing about the remounted one, and a route whose
384
+ * old store held a managed projection would otherwise stay attributed to
385
+ * that account and off its surviving key pool. The epoch bump discards a read
386
+ * still in flight from the replaced store instead of letting its answer land
387
+ * over the new one; ownership stays unknown until the new pass reads.
388
+ */
389
+ private invalidateProjectionOwnership;
205
390
  /**
206
391
  * Record that one provider's hydration read is over — owner or not (SF1) —
207
392
  * and wake the requests waiting on that provider alone (SF2).
208
393
  */
209
394
  private noteProviderHydrated;
395
+ /**
396
+ * An authoritative projection read answered for this provider, so a failed
397
+ * startup hydration no longer leaves ownership unknown (PR review round 13:
398
+ * the marker used to survive until a restart — no attribution, no key
399
+ * selection, and row tests refused with "ownership is still loading"). The
400
+ * binding re-derive is queued behind the reconciliation that proved the
401
+ * read: its attribution decides the route's serving shape, and the startup
402
+ * repair was skipped while that shape was unknown.
403
+ */
404
+ private noteProjectionReadSucceeded;
210
405
  /** Release every provider's readiness wait when hydration cannot proceed. */
211
406
  private settleHydrationWaits;
212
407
  /**
@@ -232,10 +427,10 @@ export declare class KeysManagerService {
232
427
  * Wait (bounded) until projection-ownership hydration can attribute this
233
428
  * request, and report whether it did.
234
429
  *
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).
430
+ * Readiness is decided PER PROVIDER (round-10 should-fix SF1): every managed
431
+ * provider's own projection read lands independently (PR review round 13), so
432
+ * a provider whose read has landed needs no wait and a stalled read for
433
+ * another provider cannot delay this one.
239
434
  *
240
435
  * Returns false when the bound elapsed first. The caller then treats
241
436
  * attribution as unknown (no owner) and keeps key state untouched: a
@@ -243,15 +438,24 @@ export declare class KeysManagerService {
243
438
  * repeat the I-000108 corruption. The hydration promise is not cancelled,
244
439
  * so a late result still lands under the generation guard and later
245
440
  * 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
441
  */
254
442
  private awaitProjectionOwnership;
443
+ /**
444
+ * Whether this provider's projection ownership can already be decided
445
+ * without waiting (round-10 SF1, made the single readiness rule by PR review
446
+ * round 3). Every managed provider's projection is read at hydration (a
447
+ * key-only pool can be overridden by a persisted native grant, PR review
448
+ * round 13), so the rule is the same for all of them: this provider's own
449
+ * read landed, or a claim already owns it.
450
+ *
451
+ * The process-wide settled flag is deliberately NOT part of this rule (PR
452
+ * review round 17): a provider adopted after the snapshot was never read by
453
+ * it, so "hydration is over" says nothing about THAT route's projection.
454
+ * Adoption reads it itself ({@link readAdoptionProjection}) and an extra
455
+ * provider's profile is created by its own call, so every managed provider
456
+ * still answers through its own marker.
457
+ */
458
+ private projectionOwnershipKnown;
255
459
  /**
256
460
  * Record that a newly committed grant became the provider's projection and
257
461
  * return that projection's generation (round-6 finding SF1).
@@ -284,13 +488,69 @@ export declare class KeysManagerService {
284
488
  * the remaining attempt/step records after `ATTEMPT_TTL_MS`.
285
489
  */
286
490
  pruneTransient(now?: number): void;
491
+ /**
492
+ * Whether a continuation that resumes from an await must stop writing
493
+ * (PR review round 35). `shutdownRequested` is up for `close()`'s own
494
+ * pre-close restoration, which writes by design: it alone carries the
495
+ * module-private {@link PRE_CLOSE_RESTORE} capability (round 36), which is
496
+ * honoured only while that phase is running (round 37). Both facts are read
497
+ * here, at the caller's write decision, so a capability the phase left
498
+ * behind cannot pass a check made earlier.
499
+ */
500
+ private persistenceRefused;
287
501
  /** Stop accepting mutations, drain the queue, then persist the final snapshot. */
288
502
  close(): Promise<void>;
503
+ /** The body of the one shutdown this manager runs. */
504
+ private closeOnce;
505
+ /**
506
+ * Put every outstanding window's borrowed projection back before shutdown
507
+ * closes the store (PR review round 33).
508
+ *
509
+ * `close()` sets `closing`, which refuses {@link cleanupTestWindow} — the one
510
+ * identity-checked restoration — so a test whose request ignores the abort
511
+ * used to leave the tested account projected and the binding it released
512
+ * released, past unload and into the next startup. Each window's captured
513
+ * facts are restored through the same compare-by-payload path as the late
514
+ * reconcile, under one cleanup bound and the attempt fence (a superseded
515
+ * attempt writes nothing), and a window that released the binding re-syncs it
516
+ * against the restored serving source.
517
+ *
518
+ * The request itself is never waited for — an adapter can still rewrite the
519
+ * projection after its restore — so a restoration that cannot finish in the
520
+ * bound warns once and leaves the state to the next startup's hydration.
521
+ */
522
+ private restoreBorrowedWindows;
523
+ /**
524
+ * Drain the grant chains under a hard bound (PR review round 29).
525
+ *
526
+ * The work is not cancelled — the credential seam offers no cancellation and
527
+ * a wedged provider request may never settle — it is only stopped from
528
+ * holding shutdown: the chain past the deadline is abandoned, and its late
529
+ * write can only land in a store that is closing anyway.
530
+ */
531
+ private drainGrantReconciliations;
289
532
  /** Push a user-facing warning that rides the next state response. */
290
533
  private pushWarning;
291
534
  private takeWarnings;
535
+ /**
536
+ * Report declared headers whose stored value no longer matches what the
537
+ * environment supplies. The stored value is never replaced — the user may
538
+ * have set it deliberately — but the divergence must be visible, because
539
+ * otherwise the route keeps sending the stale value forever.
540
+ */
541
+ private warnHeaderDrift;
292
542
  /** Current settings service, if the composition mounted one. */
293
543
  private settingsService;
544
+ /**
545
+ * Mount the settings service — the host's `ctx.inject(['settings'])` seam.
546
+ *
547
+ * A mount that lands after construction retries the binding repair: the
548
+ * startup pass could not read a profile yet and would otherwise leave the
549
+ * route broken for the whole session.
550
+ */
551
+ attachSettings(settings: SettingsLike): void;
552
+ /** Unmount the settings service that a re-mount replaced. */
553
+ detachSettings(settings: SettingsLike): void;
294
554
  /** Current raw credential provider. */
295
555
  private credentialsRaw;
296
556
  /**
@@ -326,6 +586,23 @@ export declare class KeysManagerService {
326
586
  * Add account that would fail. The check is lazy — see {@link oauthFlowFor}.
327
587
  */
328
588
  private oauthFlowAvailable;
589
+ /**
590
+ * Resolve one transport route's sign-in flow.
591
+ *
592
+ * The transports package registers `llm-omp-transports/<route>` itself, so
593
+ * the live flow list is also this manager's inventory of transport routes:
594
+ * a route with no registered flow is not a transport account surface, and a
595
+ * route whose record exists but whose flow is gone is not listed at all
596
+ * (there would be nothing to sign in with).
597
+ *
598
+ * The method id is the flow's own (`methods[0].id`), never a literal
599
+ * `'oauth'`: the scope prefix is the whole cross-package contract, and the
600
+ * transports package may name a method that is not OAuth. A flow with no
601
+ * method cannot be begun and is not a sign-in surface.
602
+ */
603
+ private transportFlowFor;
604
+ /** Every registered transport flow, in seam order. */
605
+ private transportFlows;
329
606
  /**
330
607
  * Warn once per channel: a lost subscription silently disables a whole
331
608
  * feature (harvest never runs; rotations never sync), so the degradation
@@ -342,6 +619,14 @@ export declare class KeysManagerService {
342
619
  private watchAuthorizationSettled;
343
620
  /** One settled broadcast: finalize the matching attempt, harvest on success. */
344
621
  private onAuthorizationSettled;
622
+ /**
623
+ * The seam interaction one attempt hands to its flow.
624
+ *
625
+ * Shared by the pi-ai and transport sign-ins: both surfaces park notices and
626
+ * prompts on the same {@link LoginAttempt} and are polled through
627
+ * `accountAttempt`; only what happens after `authorized` differs.
628
+ */
629
+ private attemptInteraction;
345
630
  /**
346
631
  * Start one interactive OAuth sign-in. Returns immediately with the
347
632
  * attempt view; the attempt walks `pending → (prompt ⇄ pending) →
@@ -357,6 +642,65 @@ export declare class KeysManagerService {
357
642
  accountAttempt(attemptId: string): AccountAttemptView | undefined;
358
643
  /** Every attempt, newest first, for T3 surfaces. */
359
644
  listAccountAttempts(): AccountAttemptView[];
645
+ /**
646
+ * Re-read every `llm-omp-transports/<route>` grant into
647
+ * {@link transportRows}.
648
+ *
649
+ * Every caller gets a pass appended after the one already in flight, so an
650
+ * awaiting caller observes a read that started after its own write (a
651
+ * removal, a sign-in commit). Poll-only callers use
652
+ * {@link refreshTransportAccountsIfIdle} instead, so a slow store cannot
653
+ * accumulate one pass per page tick. A failed read leaves that route's
654
+ * previous row alone instead of claiming the account disappeared; the next
655
+ * pass retries.
656
+ */
657
+ refreshTransportAccounts(): Promise<void>;
658
+ /** Poll hint: start a pass only when none is in flight. */
659
+ private refreshTransportAccountsIfIdle;
660
+ /** One read pass over every registered transport route. */
661
+ private readTransportAccounts;
662
+ /**
663
+ * Remote: start one transport sign-in.
664
+ *
665
+ * The flow lives on the transports package's own key
666
+ * (`llm-omp-transports/<route>`), so the grant it commits stays that route's
667
+ * credential: nothing here harvests it into a pool row or a `llm-pi-ai`
668
+ * projection. The attempt is otherwise the same record the pi-ai dialog
669
+ * polls through `accountAttempt`, so the client reuses that dialog; an
670
+ * authorized outcome is terminal here (the flow has already committed).
671
+ */
672
+ beginTransportLogin(route: string): Promise<AccountAttemptView>;
673
+ /**
674
+ * Remote: confirm one transport route's stored grant.
675
+ *
676
+ * A transport route has exactly one grant record and its adapter resolves
677
+ * that record per request, so the stored grant already IS the route's live
678
+ * credential. Activation is therefore a state-level re-read: it writes
679
+ * nothing (in particular no `llm-pi-ai/<route>` projection, which the
680
+ * transport route never consults) and reports the current status so the UI
681
+ * can tell "active" from "expired" or "sign in again".
682
+ */
683
+ activateTransportAccount(route: string): Promise<StateView>;
684
+ /**
685
+ * Remote: drop one transport route's grant record.
686
+ *
687
+ * The record is the whole account — the transports package re-creates none —
688
+ * so deleting it leaves the route signed out until the next sign-in. A live
689
+ * sign-in is withdrawn first: left running, its commit would recreate the
690
+ * grant this removal just deleted. No manager state is written, hence no
691
+ * `run` queue and no state flush.
692
+ */
693
+ removeTransportAccount(route: string): Promise<StateView>;
694
+ /** The route's removal epoch; absent means never removed. */
695
+ private transportGenerationOf;
696
+ /**
697
+ * Drop a grant a withdrawn sign-in wrote after its account was removed.
698
+ *
699
+ * The seam refuses a commit once the attempt's signal aborts, but a commit
700
+ * that had already started can still land. The account was deleted, so that
701
+ * write is deleted too instead of being reported as a signed-in route.
702
+ */
703
+ private dropRemovedTransportGrant;
360
704
  /**
361
705
  * Detach the parked prompt. With a reason the flow's awaited promise is
362
706
  * rejected too — a parked prompt dropped un-rejected would leave the
@@ -386,6 +730,11 @@ export declare class KeysManagerService {
386
730
  * recorded at that same broadcast (R6-SF1): it names the projection whose
387
731
  * failures own a cooldown, so the stale-window reset below clears only a
388
732
  * window attributed to an OLDER projection.
733
+ *
734
+ * A harvest that finds the attempt already settled writes neither the
735
+ * plugin-scope copy nor a row, but the projection the seam committed for
736
+ * this attempt is still demoted (ownership-checked, I-000122): a
737
+ * failed/cancelled login must not keep serving through the route.
389
738
  */
390
739
  private harvestLogin;
391
740
  /**
@@ -460,6 +809,30 @@ export declare class KeysManagerService {
460
809
  * Resolves `ok: false` only when the compare/delete writes themselves
461
810
  * rejected; an ownership mismatch is the guard doing its job (the live
462
811
  * record belongs to someone else) and counts as a clean skip.
812
+ *
813
+ * Every manager-initiated projection deletion registers the one-shot
814
+ * {@link intentionalProjectionRemovals} marker before the delete, exactly
815
+ * as {@link decideAndDeleteProjection} does (I-000125). Unmarked, the
816
+ * removal's own `credentials/record-updated` echo reconciles as an
817
+ * external deletion: the route's previously active managed account is
818
+ * flagged `valueMissing`, its owner claim is dropped and a misleading
819
+ * "removed outside Keys Manager" warning is pushed — even though that
820
+ * row and its plugin-scope grant are untouched.
821
+ *
822
+ * The marker only silences that echo; the route's state is corrected
823
+ * explicitly. The removal leaves the route with no stored projection, so the
824
+ * provider's owner claim — whatever it named before this attempt's grant
825
+ * replaced the record — is untruthful: {@link oauthOwnerFor} keeps naming a
826
+ * row pi-ai no longer resolves, `wrapStream` skips key-pool selection, oauth
827
+ * failures are attributed to that row, and the client keeps "Set active"
828
+ * disabled — the account cannot be re-activated. The claim is therefore
829
+ * invalidated (I-000125), and ONLY the claim: the surviving row keeps its
830
+ * `grantRef` and its `valueMissing: false`, so it stays managed and
831
+ * re-activatable while the route falls back to its API-key pool.
832
+ *
833
+ * The marker is armed only while the provider is still managed: a released
834
+ * provider has no echo consumer, and a marker nothing consumes would stay
835
+ * armed for its whole TTL and mask a genuine out-of-band removal.
463
836
  */
464
837
  private revertHarvestProjection;
465
838
  /** Expire wedged sign-ins and drop long-settled attempt records. */
@@ -604,6 +977,242 @@ export declare class KeysManagerService {
604
977
  * cascade-selectable again.
605
978
  */
606
979
  activateAccount(provider: string, accountId: string): Promise<StateView>;
980
+ /**
981
+ * Remote: send one minimal request with the selected key or account row.
982
+ *
983
+ * Observation-only by construction: the request runs under a test-only
984
+ * request scope, so the broker answers with the pinned credential and the
985
+ * stream listener skips selection and every health/binding write. An OAuth
986
+ * row's grant is projected only for the duration of the request; the
987
+ * projection (and the in-memory owner) that served the route before is
988
+ * restored even when the request fails, so a test never changes which
989
+ * account the route is on.
990
+ *
991
+ * Every expected refusal is a structured `{ ok: false, error }` result
992
+ * (M2): the inline row line is the only failure surface, so a refusal must
993
+ * not also raise the page-level banner a thrown RPC error produces.
994
+ */
995
+ testKey(provider: string, keyId: string): Promise<KeyTestResult>;
996
+ /**
997
+ * Take over a test window that did not settle within the isolation deadline
998
+ * (QC fix wave 2) or whose own cleanup could not restore the route (PR
999
+ * review MF1).
1000
+ *
1001
+ * The fence is set synchronously, before the first cleanup await: the wedged
1002
+ * test may resume at any await from here, and it must observe the
1003
+ * abandonment before it starts a borrow or restores from its own stale
1004
+ * snapshot. The provider keeps its isolation entry until the cleanup
1005
+ * completed — see {@link settleAbandonedWindow}.
1006
+ */
1007
+ private abandonTestWindow;
1008
+ /**
1009
+ * Clean up one abandoned window and release its provider only on success.
1010
+ *
1011
+ * A cleanup that cannot finish needs the same untimed seam the wedged test is
1012
+ * parked in, so it fails exactly like the test did. Releasing the provider
1013
+ * then would serve ordinary traffic from the borrowed grant (or, on a
1014
+ * broker-bound profile, from a route whose binding was released and never
1015
+ * restored), so the isolation entry and the fence both stay. Two events can
1016
+ * retry the cleanup: the abandoned promise's settlement, and the next test
1017
+ * attempt ({@link recoverAbandonedWindow}, QC fix wave 3 W-002) — a promise
1018
+ * that settles while the seam is still wedged must not be the only path.
1019
+ */
1020
+ private settleAbandonedWindow;
1021
+ /** One later cleanup attempt for an abandoned window, once the seam answers again. */
1022
+ private retryAbandonedWindow;
1023
+ /**
1024
+ * One bounded cleanup attempt for a fenced provider (QC fix wave 3, W-002).
1025
+ *
1026
+ * Returns whether the provider is servable again. The caller's attempt is
1027
+ * what makes the fence self-clearing: `testKey` calls this before refusing,
1028
+ * so isolation no longer depends on the abandoned promise settling after the
1029
+ * seam recovered. A failed attempt keeps the fence and warns exactly once.
1030
+ */
1031
+ private recoverAbandonedWindow;
1032
+ /**
1033
+ * Run the window's cleanup under its hard deadline and drop the provider's
1034
+ * isolation only when it completed. Returns the reason it could not finish.
1035
+ *
1036
+ * PR review round 21: the request that borrowed the route is not part of the
1037
+ * cleanup, so a cleanup can finish while that request is still able to
1038
+ * refresh and rewrite the borrowed projection. Such a window is held
1039
+ * ({@link RowTestWindow.held}) — the fence and the isolation entries stay —
1040
+ * and only its settlement reconciles the late write and releases it.
1041
+ */
1042
+ private attemptAbandonedCleanup;
1043
+ /** Drop every isolation entry this window still owns, identity-checked. */
1044
+ private releaseAbandonedWindow;
1045
+ /**
1046
+ * Put the route back the way the window found it: the mutations this window
1047
+ * started, then the projection (compare-by-payload and rotation-aware, so
1048
+ * nothing a writer stored meanwhile is overwritten), then the binding — the
1049
+ * original finally's order, because the binding decision reads the restored
1050
+ * owner map — then the borrow marker. Returns the reason the cleanup could
1051
+ * not complete.
1052
+ *
1053
+ * `stillCurrent` is passed by abandoned-window attempts only: a superseded
1054
+ * attempt that outlived its deadline may finish its writes, but the
1055
+ * provider-wide borrow marker belongs to the newest attempt (PR review
1056
+ * round 2) — deleting it would let ordinary traffic reach a newer window's
1057
+ * borrowed projection.
1058
+ */
1059
+ private cleanupTestWindow;
1060
+ /**
1061
+ * Reconcile the projection once an abandoned window's request finally
1062
+ * settles (PR review round 13).
1063
+ *
1064
+ * The watchdog's cleanup can complete before the wedged request resumes: an
1065
+ * adapter that ignored the abort then refreshes and rewrites the borrowed
1066
+ * grant, and the abandoned window used to skip cleanup on settlement — so
1067
+ * the tested account stayed projected over the restored one and served
1068
+ * ordinary traffic. The shared compare-by-payload restore is idempotent and
1069
+ * only touches a projection this test left (the borrowed grant or a
1070
+ * rotation of the tested row): a foreign payload survives, and a newer
1071
+ * generation is written back to the tested row before the pre-test
1072
+ * projection is restored.
1073
+ *
1074
+ * Returns the failure, so a caller that releases the provider afterwards (PR
1075
+ * review round 21) can keep the fence instead. `ownsFence` is passed by the
1076
+ * settlement of a held window and by the cleanup that releases a window whose
1077
+ * settlement it deferred (PR review round 28): both already own the
1078
+ * provider's fence, which the default guards would read as a competing
1079
+ * cleanup and skip.
1080
+ */
1081
+ private reconcileLateWindowProjection;
1082
+ /**
1083
+ * Record one in-flight window mutation before it is awaited (QC fix wave 3,
1084
+ * W-001). The normalized promise never rejects: the mutation's own outcome
1085
+ * belongs to the code awaiting it, while the cleanup only needs to know when
1086
+ * the write is over.
1087
+ */
1088
+ private recordWindowMutation;
1089
+ /**
1090
+ * Await every mutation this window recorded, including one the continuation
1091
+ * of an earlier mutation records (a borrow that settles right before its
1092
+ * binding release starts). The microtask hop after each settling round lets
1093
+ * that continuation run before the cleanup judges the route clean. A
1094
+ * mutation the seam never answers is bounded by the caller's deadline, not
1095
+ * here.
1096
+ */
1097
+ private awaitWindowMutations;
1098
+ /**
1099
+ * Run one cleanup under its hard deadline. The work is not cancelled — the
1100
+ * credential seam offers no cancellation — it is only stopped from holding
1101
+ * the provider past the bound; a late result is ignored.
1102
+ */
1103
+ private withinCleanupDeadline;
1104
+ /**
1105
+ * Decide the route's serving shape and run one row test. Separate from
1106
+ * {@link testKey} so the concurrency lock is registered before the first
1107
+ * await.
1108
+ */
1109
+ private runRowTest;
1110
+ /**
1111
+ * The live provider profile one test must reason about, read once at entry
1112
+ * (QC B1/N6), or the reason it cannot be used. An unreadable settings
1113
+ * snapshot and a missing profile are distinct causes, so the row never
1114
+ * claims a model is missing when the settings service is merely absent.
1115
+ */
1116
+ private testRouteProfile;
1117
+ /** One request pinned to the row's API-key literal. */
1118
+ private runApiKeyRowTest;
1119
+ /** One request with the row's grant temporarily projected as the route's credential. */
1120
+ private runOAuthRowTest;
1121
+ /** The refusal an abandoned test reports instead of a stale success. */
1122
+ private abandonedTestResult;
1123
+ /** The refusal a chain reports when shutdown refuses its persistence. */
1124
+ private shutdownTestResult;
1125
+ /** The refusal a test reports when it arrives after shutdown began (round 34). */
1126
+ private shutdownEntryResult;
1127
+ /**
1128
+ * Mark one provider as being removed for the whole removal body (PR review
1129
+ * round 12). The marker is up before the body's first await — the removal's
1130
+ * own test wait included — so {@link testKey} can refuse a test that would
1131
+ * otherwise start inside the removal. Nesting is a depth because a
1132
+ * last-key `deleteKey` releases through `releaseLocked`.
1133
+ */
1134
+ private withRemovalInProgress;
1135
+ /**
1136
+ * Hold a row/account removal until the provider's running row test settles
1137
+ * (PR review round 9, R8-2).
1138
+ *
1139
+ * `testKey` captures its row, its credential and the route's serving shape
1140
+ * before the first await, so a removal landing inside that window can delete
1141
+ * the credential the test is about to send. For a borrowed OAuth account the
1142
+ * removal is worse than a stale read: it compares its decision against the
1143
+ * test's temporary projection, deletes it and demotes every row, and the
1144
+ * test's own restore then writes the removed account's grant back. The
1145
+ * removal therefore waits for the window instead of racing it. The wait is
1146
+ * bounded by {@link isolationWaitMs} — a test wedged in an untimed seam call
1147
+ * must not park the removal forever — and a provider whose abandoned window
1148
+ * still owns the route's records refuses the removal outright. A wait that
1149
+ * hits its own bound refuses too (PR review round 10, I-000179): proceeding
1150
+ * would delete the credential the parked test is about to send. The user can
1151
+ * retry once the abandoned window has been cleaned up.
1152
+ */
1153
+ private settleTestBeforeRemoval;
1154
+ /**
1155
+ * Put the projection back as it was before the test borrowed it, without
1156
+ * losing or overwriting anything that changed meanwhile.
1157
+ *
1158
+ * `late` marks the abandoned window's settlement, after the watchdog's
1159
+ * cleanup already released the provider: a removal no longer waits for this
1160
+ * test, so the pre-test projection's owning row may be gone by the time this
1161
+ * runs and must not be projected again (PR review round 19).
1162
+ *
1163
+ * The decision is compare-by-payload against the borrowed grant: only a
1164
+ * live record that still carries it — or a refresh-rotated version of it
1165
+ * that {@link matchOwningAccount} attributes to the tested row — is
1166
+ * replaced. A rotation is reconciled into the tested row first (the same
1167
+ * plugin-scope write-back {@link reconcileGrantRecord} performs), because
1168
+ * the provider may already have consumed the superseded refresh token. A
1169
+ * foreign or newer payload (another account, a native writer) is left
1170
+ * untouched, and a record removed during the window stays removed. In every
1171
+ * branch the owner map ends consistent with what the store actually holds.
1172
+ *
1173
+ * Returns a failure message when the restore or the rotation write-back
1174
+ * did not land, so the caller reports `{ ok: false }` instead of a false
1175
+ * pass.
1176
+ */
1177
+ private restoreProjection;
1178
+ /**
1179
+ * Point the owner map at the OAuth row the stored record identifies.
1180
+ *
1181
+ * The argument is the live record, not its grant payload (PR review round 8):
1182
+ * a payload-only signature cannot tell a removed projection from a PRESENT
1183
+ * record this pool cannot interpret (a native api-key, an unrecognized
1184
+ * payload), so a surviving native record reached this method as `undefined`,
1185
+ * cleared the foreign marker, and `keyPoolServes` then let the window-end
1186
+ * sync bind the broker over it. Only an absent record is a confirmed removal
1187
+ * (I-000167); a present record no row owns keeps the route foreign (round 3).
1188
+ */
1189
+ private reconcileProjectionOwner;
1190
+ /**
1191
+ * Persist one authoritative rotated grant payload for a row: write the
1192
+ * plugin-scope record for the rotated generation, then hand the commit to
1193
+ * {@link commitGrantGeneration} — the same tail {@link reconcileGrantRecord}
1194
+ * performs. The caller runs inside the provider's grant chain and owns the
1195
+ * decision, including the echo-specific generation guards, so only the
1196
+ * write-back semantics live here.
1197
+ */
1198
+ private writeBackRotation;
1199
+ /**
1200
+ * The single owner of `row.oauth.grantRef` semantics (QC N1): point the row
1201
+ * at the record holding its rotated generation, merge the refresh-derived
1202
+ * identity fields, and remove the superseded record. Both the echo
1203
+ * reconciler and the connection test's rotation write-back commit through
1204
+ * here, so the ref scheme, the identity merge and the cleanup policy have
1205
+ * exactly one implementation.
1206
+ *
1207
+ * The record write stays with the caller: the reconciler must revalidate the
1208
+ * row between that write and this commit (C6-1), so only the tail is shared.
1209
+ */
1210
+ private commitGrantGeneration;
1211
+ /**
1212
+ * Send the minimal request under the given scope and map its terminal
1213
+ * chunk. A thrown error or the 20-second abort is reported, never swallowed.
1214
+ */
1215
+ private runTestRequest;
607
1216
  /**
608
1217
  * Demote the cascade to the key pool by deleting the projection record:
609
1218
  * pi-ai's per-request resolution prefers the stored credential and only
@@ -724,22 +1333,125 @@ export declare class KeysManagerService {
724
1333
  private describeSettings;
725
1334
  /** Find one live settings descriptor by entry id. */
726
1335
  private namespace;
727
- /** Apply one path-addressed settings write through the live revision. */
1336
+ /**
1337
+ * Apply one path-addressed settings write through the live revision.
1338
+ *
1339
+ * `stillWrites` runs after the descriptor re-read and immediately before the
1340
+ * mutation: a decision derived from an earlier snapshot can be stale by then
1341
+ * ({@link syncBrokerBinding}'s bind gate, PR review round 25). A declining
1342
+ * guard skips the write and reports it.
1343
+ */
728
1344
  private mutateSettings;
729
1345
  /** Prune expired bindings at most once a minute, without a restart. */
730
1346
  private pruneBindings;
731
1347
  /** Build the complete UI state. */
732
1348
  buildState(): StateView;
733
- /** Remote: complete manager state. */
1349
+ /**
1350
+ * Remote: complete manager state.
1351
+ *
1352
+ * The transport rows come from another package's records, and reading them
1353
+ * is async while this endpoint is synchronous, so every poll starts a
1354
+ * refresh for the next one (the client ticks every 30 s), unless one is
1355
+ * already in flight. The constructor warms the first read and record
1356
+ * updates refresh immediately, so the one-tick lag only shows on a cold
1357
+ * start.
1358
+ */
734
1359
  getState(): StateView;
735
1360
  /** Find one directory entry by route id. */
736
1361
  private directoryEntry;
1362
+ /**
1363
+ * Whether the key pool is the credential source that serves this route: no
1364
+ * OAuth projection owns it and at least one API-key row still holds a value.
1365
+ * This is the one arbiter for both the verdict below and the binding write
1366
+ * ({@link syncBrokerBinding}).
1367
+ */
1368
+ private keyPoolServes;
737
1369
  /**
738
1370
  * Classify the live binding. An unreadable settings snapshot is `unknown`,
739
1371
  * not a confirmed mismatch: transient settings failures must not disable
740
1372
  * key-pool recovery for a provider that is still managed.
1373
+ *
1374
+ * Only a value Keys Manager never wrote is a `mismatch`: the warning now
1375
+ * means "the user took the profile back", not "the OAuth route is broken".
1376
+ * An unbound profile is healthy while the key pool is not the serving
1377
+ * source (an OAuth projection serves, or no API-key row holds a value) —
1378
+ * that is the OAuth-only route's native shape.
741
1379
  */
742
1380
  private bindingState;
1381
+ /**
1382
+ * The single authority for a managed provider's pi-ai `apiKeyEnv`.
1383
+ *
1384
+ * pi-ai resolves a named credential synchronously and throws
1385
+ * `MISSING_CREDENTIAL` when the credential seam cannot answer it, so the
1386
+ * broker reference may only sit on a profile whose requests the key pool
1387
+ * actually serves ({@link keyPoolServes}). Every other shape must be
1388
+ * unbound, or pi-ai never reaches its own stored OAuth grant.
1389
+ *
1390
+ * Each direction has exactly one provably owned write:
1391
+ * - bind: a profile that names no credential, or — with `takeover`, passed
1392
+ * solely by {@link adoptProvider} — the pre-adoption reference adoption
1393
+ * recorded and deliberately replaces;
1394
+ * - unbind: only the broker reference Keys Manager itself wrote.
1395
+ *
1396
+ * Any other value is a hand-edit (or the pre-adoption reference release
1397
+ * restored): it is never touched, and the `mismatch` verdict stays the
1398
+ * signal that the user took the profile back.
1399
+ *
1400
+ * `write: false` is the startup repair: it only touches a profile that
1401
+ * already exists, so a first run never materializes one from nothing. The
1402
+ * release direction is single-sourced in {@link releasingProviders}: a
1403
+ * provider being unmanaged is never bound again, whatever the row order of
1404
+ * the pool it is deleting.
1405
+ *
1406
+ * A read-side problem or a refused write degrades to a warning: the
1407
+ * callers' committed change must not fail on a settings problem, the user
1408
+ * still sees that the route may resolve the wrong credential source, and
1409
+ * the next repair pass retries.
1410
+ *
1411
+ * Returns whether the profile ends in the shape the serving source justifies
1412
+ * (`true` also when no write was needed). The connection test's borrow is the
1413
+ * one caller that acts on it: a failed release must refuse the test instead
1414
+ * of sending a request the wrong credential source would answer.
1415
+ *
1416
+ * While a row test borrows the route (QC B4) no caller may persist a decision
1417
+ * derived from that borrowed world — an `addKey` landing inside the window
1418
+ * would otherwise unbind the profile for good. The test's own writes pass
1419
+ * `borrow: true`; the window-end sync then writes the restored state.
1420
+ */
1421
+ private syncBrokerBinding;
1422
+ /**
1423
+ * Repair every managed provider's binding through the one authority. The
1424
+ * startup pass and the settings-mount seam both run it, so a profile that
1425
+ * was unreadable when hydration settled is still healed in this process
1426
+ * instead of waiting for the next start.
1427
+ */
1428
+ private repairBrokerBindings;
1429
+ /**
1430
+ * One provider's repair pass (PR review round 42). A recorded adoption
1431
+ * takeover debt is settled here only when the authoritative readiness says
1432
+ * no projection serves the route — the case a restart leaves behind when the
1433
+ * foreign record disappeared while the process was down.
1434
+ */
1435
+ private repairBinding;
1436
+ /**
1437
+ * Re-derive a managed route's binding after its projection changed outside
1438
+ * this manager's account pool: a removal, a committed harvest that handed
1439
+ * the route back to its API-key pool (PR review round 2), or a foreign
1440
+ * projection taking a bound route over (round 4).
1441
+ *
1442
+ * The route's serving source is no longer the managed pool, or the pool is
1443
+ * the only source left: either way `syncBrokerBinding` derives the one
1444
+ * profile shape pi-ai's resolution order requires. A store refusal only
1445
+ * warns — these are compensation paths and must never fail the change that
1446
+ * triggered them; the next repair pass retries.
1447
+ */
1448
+ private syncBindingAfterProjectionChange;
1449
+ /**
1450
+ * A refused binding write must not fail the change that triggered it, but
1451
+ * it cannot stay in the log alone either: the route may resolve the wrong
1452
+ * credential source until the next repair.
1453
+ */
1454
+ private bindingWriteRefused;
743
1455
  /** Candidate credential refs from which an existing key can be imported. */
744
1456
  private importRefs;
745
1457
  /** Create and store one key record without adding it to a provider. */
@@ -748,10 +1460,75 @@ export declare class KeysManagerService {
748
1460
  private findImport;
749
1461
  /** Remote: take over one existing provider route. */
750
1462
  adoptProvider(provider: string): Promise<StateView>;
751
- /** Discover models for a new extra provider, falling back to the seed catalog. */
1463
+ /**
1464
+ * Read and classify one provider's live projection at adoption (PR review
1465
+ * round 17) — the same read hydration does, for the one route hydration
1466
+ * cannot cover: a provider that did not exist when the snapshot was taken.
1467
+ *
1468
+ * An adoption owns no account row yet, so any live record is one no row of
1469
+ * this pool owns: a native api-key, an unrecognized payload, and a grant left
1470
+ * behind by a released account all serve the route ahead of the broker
1471
+ * reference, so the route is marked foreign and the pool is not bound over it
1472
+ * (PR review rounds 3–4). An absent record clears any stale marker: the pool
1473
+ * may serve. A failed read is not an absent projection (round 8): ownership
1474
+ * stays unknown, so nothing may be bound and no repair touches the route
1475
+ * until an authoritative read answers ({@link noteProjectionReadSucceeded}).
1476
+ *
1477
+ * Returns `unknown` for that failure; the adoption itself is kept — there is
1478
+ * no write to roll back, and only the binding is undecided.
1479
+ *
1480
+ * The classification runs inside the route's grant chain (PR review round
1481
+ * 22, Codex P1). A native write or deletion reconciled while the read is
1482
+ * outstanding is therefore either already visible to it or queued behind it:
1483
+ * a read whose snapshot a newer event overtook reports `superseded` and lands
1484
+ * nothing, so a stale absence can never bind the broker over a newer
1485
+ * projection state and a stale presence can never leave the pool unbound.
1486
+ */
1487
+ private readAdoptionProjection;
1488
+ /** Body of {@link readAdoptionProjection}: read, then land the classification. */
1489
+ private classifyAdoptionProjection;
1490
+ /**
1491
+ * Undo an adoption whose initial binding write was refused (PR review
1492
+ * round 2): drop the managed record and its broker entry, delete the
1493
+ * plugin-scope copy this call created, and put back a pre-adoption
1494
+ * reference that landed despite the refusal. The source credential and the
1495
+ * profile's other fields are never touched — adoption only read them.
1496
+ */
1497
+ private rollbackAdoption;
1498
+ /**
1499
+ * Discover models for a new extra provider, falling back to the seed catalog.
1500
+ * A route that declares a static header installs the seed catalog directly:
1501
+ * the discovery request has no header slot, so probing such an endpoint can
1502
+ * only fail (and would hold the serialized mutation queue for the probe's
1503
+ * whole timeout).
1504
+ */
752
1505
  private discoverModels;
753
- /** Remote: install and adopt one extra built-in provider. */
1506
+ /**
1507
+ * Remote: install and adopt one extra built-in provider.
1508
+ *
1509
+ * Deliberate exemption from the one-authority rule
1510
+ * ({@link syncBrokerBinding}): this call creates the profile it binds and
1511
+ * the key that serves it in the same mutation, so the write below is the
1512
+ * route's first binding rather than a takeover of a value the user owns.
1513
+ */
754
1514
  addExtraProvider(extraId: string, alias: string, value: string): Promise<StateView>;
1515
+ /**
1516
+ * Restore a managed extra route's declared static headers.
1517
+ *
1518
+ * A managed profile can lose its declared headers through a Models edit;
1519
+ * every managed add-key flow repairs them instead of leaving a route whose
1520
+ * required header is silently absent. Only a live profile with a healthy
1521
+ * binding is ours to repair: a deleted profile would gain a header-only
1522
+ * half-profile, and a replaced one is the state release refuses to touch. A
1523
+ * stored value for the same header always wins. The caller may pass the
1524
+ * headers it already resolved at its own entry (where a missing variable can
1525
+ * still refuse the add before any write); otherwise this method resolves
1526
+ * them. Either way a failure only warns, because the key has already been
1527
+ * added: a route whose headers cannot be resolved, and a settings write that
1528
+ * cannot be performed, both leave the repair pending for the next add so a
1529
+ * half-applied add is never turned into a rejected one.
1530
+ */
1531
+ private repairDeclaredHeaders;
755
1532
  /** Add one key; caller already owns the mutation queue. */
756
1533
  private addKeyLocked;
757
1534
  /** Remote: add one key to a managed provider. */
@@ -760,12 +1537,45 @@ export declare class KeysManagerService {
760
1537
  updateKey(provider: string, keyId: string, patch: unknown): Promise<StateView>;
761
1538
  /** Remote: delete one key, releasing the provider when it was the last one. */
762
1539
  deleteKey(provider: string, keyId: string): Promise<StateView>;
763
- /** Read one live profile for release-time cleanup decisions. */
1540
+ /** Read one live profile for repair and release-time cleanup decisions. */
764
1541
  private profileAt;
765
- /** Restore the exact live profile captured before release mutated it. */
1542
+ /**
1543
+ * Restore the exact live profile captured before release mutated it.
1544
+ *
1545
+ * Deliberate exemption from the one-authority rule: release is unmanaging,
1546
+ * so this write puts the pre-adoption document back instead of deriving a
1547
+ * binding from a key pool that is being deleted. Every other caller goes
1548
+ * through {@link syncBrokerBinding}.
1549
+ */
766
1550
  private restoreProfileSnapshot;
767
- /** Remove every managed credential, recording progress before a retry. */
1551
+ /**
1552
+ * Remove every managed credential, recording progress before a retry.
1553
+ *
1554
+ * The provider is flagged as releasing for the whole loop: a demotion
1555
+ * inside it (an `oauth` row iterated before a surviving `api-key` row) must
1556
+ * not re-bind the profile, or the reference release just restored would be
1557
+ * overwritten and then stripped by the closing unbind. While unmanaging the
1558
+ * binding only ever goes one way — off.
1559
+ */
768
1560
  private deleteManagedCredentials;
1561
+ /**
1562
+ * Drop any armed intentional-removal marker for a provider being released.
1563
+ *
1564
+ * The marker only exists to silence the record-updated echo of the manager's
1565
+ * own projection removal, and that echo is dropped outright once the provider
1566
+ * is unmanaged: {@link onGrantRecordUpdated} returns at event time and
1567
+ * {@link reconcileGrantRecord} at body time. A release can land between the
1568
+ * arm and that echo — the abort compensation arms inside the mutation queue
1569
+ * while its echo is still chained behind an in-flight body — leaving the
1570
+ * marker armed for its whole TTL with nothing left to consume it. A provider
1571
+ * re-adopted inside that window would then read a genuine out-of-band
1572
+ * removal as the manager's own: no warning, no `valueMissing`, no
1573
+ * stale-owner cleanup.
1574
+ *
1575
+ * Called only on the successful release path: a release that throws leaves
1576
+ * the provider managed, and its echo may still legitimately arrive.
1577
+ */
1578
+ private dropIntentionalRemoval;
769
1579
  /** Restore the provider profile and drop management; caller owns the queue. */
770
1580
  private releaseLocked;
771
1581
  /** Remote: stop managing a provider and restore its original key reference. */
@@ -832,6 +1642,38 @@ export declare class KeysManagerService {
832
1642
  * entry (row removed) resolves to null.
833
1643
  */
834
1644
  private oauthOwnerFor;
1645
+ /**
1646
+ * Keep an ordinary request out of the window where a row test borrows (or
1647
+ * is restoring) this provider's projection. The provider stream is built
1648
+ * only after the bounded wait, so neither branch can start a request inside
1649
+ * the borrowed window; a consumer that cancels during the wait constructs
1650
+ * nothing at all. A test-scope request returns from `streamListener` before
1651
+ * this wrapper, so a test never waits on its own guard.
1652
+ */
1653
+ private isolateFromTest;
1654
+ /**
1655
+ * Wait (bounded) for the in-flight borrowing test on this provider to
1656
+ * settle. The bound is {@link KEY_TEST_ISOLATION_WAIT_MS}, not the test's
1657
+ * own request timeout: the awaited promise also covers untimed
1658
+ * credential-store reads, so without this an ordinary request could park
1659
+ * here forever. A failed or timed-out test never fails the waiting request.
1660
+ *
1661
+ * QC fix wave 2: the one exception is a provider whose abandoned window is
1662
+ * still fenced. The wait is over, but the route's credential surface has not
1663
+ * been reconciled, so serving the request would answer it with the borrowed
1664
+ * grant (or, after a released binding, with no bound route at all). Such a
1665
+ * request is refused instead — the fence is what keeps cross-use impossible
1666
+ * until the cleanup completes.
1667
+ */
1668
+ private settleProviderTest;
1669
+ /** The refusal an ordinary request gets while its route is still unreconciled. */
1670
+ private testCleanupRefusal;
1671
+ /**
1672
+ * Wrap one connection-test stream: keep the pinned credential scope alive
1673
+ * for every chunk and do nothing else. No selection, no cursor, no health
1674
+ * writes — the request is pure observation.
1675
+ */
1676
+ private wrapTestStream;
835
1677
  /** Wrap one managed stream so credential resolution sees this key. */
836
1678
  private wrapStream;
837
1679
  /**