@omnicross/daemon 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,19 +1,25 @@
1
- import { OutboundApiServerConfig, Logger, ProviderConfigSource, TransformerService, Transformer, ResolvedTransformerChain, ApiServerSettingsStore, PricingStore, OutboundKeyDb as OutboundKeyDb$1, OutboundKeyDbRow, OutboundKeyPolicy } from '@omnicross/core';
1
+ import * as _omnicross_core from '@omnicross/core';
2
+ import { OutboundApiServerConfig, Logger, ProviderConfigSource, TransformerService, Transformer, ResolvedTransformerChain, ApiServerSettingsStore, PricingStore, AutomaticPricingSource, OutboundKeyDb, OutboundKeyDbRow, OutboundKeyPolicy, PricingEngine as PricingEngine$1 } from '@omnicross/core';
2
3
  import { ApiKeyPoolService } from '@omnicross/core/completion/ApiKeyPoolService';
3
- import { AccountProbeConfig, OutboundKeyDb, VoucherDb, KeySpendReader, OutboundApiServer } from '@omnicross/core/outbound-api';
4
+ import { AllowanceSchedulingConfig, AccountProbeConfig, OutboundKeyDb as OutboundKeyDb$1, VoucherDb, KeySpendReader, OutboundApiServer } from '@omnicross/core/outbound-api';
4
5
  import { ProviderProxy } from '@omnicross/core/provider-proxy';
5
6
  import { UsageRecorder, PricingEngine } from '@omnicross/core/usage';
6
7
  import { SubscriptionCredentialStore, FetchLike, SubscriptionProviderRegistry, SubscriptionAccountService } from '@omnicross/subscriptions';
7
- import { LoggingConfig, HealthReport } from '@omnicross/contracts/health-logging-types';
8
- import { ClaudeTokenConfig, CodexTokenConfig, GeminiTokenConfig, AccountTokensConfig, ProxyConfig, SubscriptionAccountSanitized, AccountClientIdentity } from '@omnicross/contracts/account-tokens-types';
8
+ import { AccountAllowanceSnapshot } from '@omnicross/contracts/account-allowance-types';
9
+ import { ClaudeTokenConfig, CodexTokenConfig, GeminiTokenConfig, AccountTokensConfig, ProxyConfig, SubscriptionAccountSanitized, AccountClientIdentity, SubscriptionAccountEntry } from '@omnicross/contracts/account-tokens-types';
10
+ import * as _omnicross_contracts_subscription_types from '@omnicross/contracts/subscription-types';
9
11
  import { OpenCodeGoTokenConfig, SubscriptionProviderId } from '@omnicross/contracts/subscription-types';
12
+ import { AccountAllowanceStore } from '@omnicross/core/pipeline/AccountAllowanceStore';
13
+ import { AllowanceSchedulingDecision } from '@omnicross/core/pipeline/AccountAllowanceScheduling';
14
+ import { SubscriptionIdentityStore } from '@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore';
15
+ import { LoggingConfig, HealthReport } from '@omnicross/contracts/health-logging-types';
10
16
  import { SubscriptionAccountHealth } from '@omnicross/core/pipeline/SubscriptionAccountHealth';
11
17
  import { fetchUpstream } from '@omnicross/core/pipeline/upstreamFetch';
12
18
  import { AuditRecord, AuditConfig } from '@omnicross/contracts/audit-types';
13
19
  import { BillingDeliveryStatus, BillingConfig, BillingEvent } from '@omnicross/contracts/billing-types';
14
20
  import http from 'node:http';
15
21
  import { LLMProvider, AgentDefaultModels, GlobalModelParameters } from '@omnicross/contracts/llm-config';
16
- import { PricingEntry, PricingEntryInput, PricingResolution } from '@omnicross/contracts/pricing-types';
22
+ import { PricingEntry, PricingEntryInput, PricingResolution, PricingSourceRefreshResult } from '@omnicross/contracts/pricing-types';
17
23
  import { WebhookConfig, WebhookEvent } from '@omnicross/contracts/webhook-types';
18
24
 
19
25
  /**
@@ -104,8 +110,17 @@ declare class SecretBox {
104
110
  * @module @omnicross/daemon/config
105
111
  */
106
112
 
107
- /** The wire formats the daemon's BYO providers can speak. */
108
- type DaemonApiFormat = 'openai' | 'anthropic' | 'gemini';
113
+ /**
114
+ * The wire formats the daemon's BYO providers can speak.
115
+ *
116
+ * Each value names EXACTLY ONE provider-slot format transformer (see
117
+ * `ConfigFileProviderConfigSource.FORMAT_TRANSFORMER`). `openai-response` used
118
+ * to be inexpressible here, so a Responses-wire upstream had to smuggle its
119
+ * format through `transformer.use[]` — which put TWO format transformers in
120
+ * reach of the provider slot. Naming it here keeps `use[]` purely a modifier
121
+ * list. `validateProvider` migrates the old shape on load.
122
+ */
123
+ type DaemonApiFormat = 'openai' | 'anthropic' | 'gemini' | 'openai-response';
109
124
  /**
110
125
  * One pool key on a provider row (design D1). Structurally compatible with
111
126
  * core's `ApiKeyEntry` (`@omnicross/contracts/llm-config`) — a hand-authored SUBSET: only
@@ -406,21 +421,54 @@ declare function loadConfig(path: string): DaemonConfig;
406
421
  declare function saveConfig(path: string, cfg: DaemonConfig): void;
407
422
 
408
423
  /**
409
- * external-cli-credentialsread-only access to the external CLI native
424
+ * account-multidaemon-side pure helpers for the subscription multi-account
425
+ * layout.
426
+ *
427
+ * Host-clean: no I/O, no encryption — callers persist through their own
428
+ * encrypted writers.
429
+ *
430
+ * Load-bearing invariant: the top-level per-provider block is ALWAYS a byte-equal
431
+ * mirror of the active account's `tokens`; every mutator re-derives it last.
432
+ *
433
+ * @module @omnicross/daemon/ports/account-multi
434
+ */
435
+
436
+ /** Provider id → owned contract field names. */
437
+ type DaemonProvider = 'claude' | 'codex' | 'gemini' | 'opencodego';
438
+ interface AccountMetadataPatch {
439
+ label?: string;
440
+ enabled?: boolean;
441
+ priority?: number;
442
+ group?: string | null;
443
+ tags?: string[];
444
+ }
445
+ interface AccountRef {
446
+ providerId: DaemonProvider;
447
+ accountId: string;
448
+ }
449
+ type AccountBatchMutation = {
450
+ action: 'enable' | 'disable';
451
+ } | {
452
+ action: 'set-group';
453
+ group: string | null;
454
+ } | {
455
+ action: 'delete';
456
+ };
457
+
458
+ /**
459
+ * external-cli-credentials read-only access to the external CLI native
410
460
  * credential stores (external-cli-sync).
411
461
  *
412
- * The daemon never WRITES these files (they belong to the CLIs); it only reads
413
- * them back to (a) recover from the rotating-refresh-token race — when e.g.
414
- * Claude Code refreshes `~/.claude/.credentials.json` it rotates OUR stored
415
- * refresh token out from under us, and the external file then holds the only
416
- * live credential — and (b) detect divergence for the account-list warning.
462
+ * The daemon never WRITES these files (they belong to the CLIs); it reads them
463
+ * only for the explicit admin availability check and copy-as-new-account import
464
+ * flow. Normal account reads and refreshes never call this reader.
417
465
  *
418
466
  * File shapes (mirrors the shapes the CLIs themselves write):
419
467
  * claude `~/.claude/.credentials.json`
420
- * `{ claudeAiOauth: { accessToken, refreshToken?, expiresAt(number ms),
468
+ * `{ claudeAiOauth: { accessToken, refreshToken, expiresAt(number ms),
421
469
  * scopes? } }`
422
470
  * codex `~/.codex/auth.json`
423
- * `{ tokens: { id_token?, access_token, refresh_token? } }` no explicit
471
+ * `{ tokens: { id_token, access_token, refresh_token } }` no explicit
424
472
  * expiry; the access token's JWT `exp` claim is the only expiry signal.
425
473
  *
426
474
  * Gemini is deliberately excluded: the gemini CLI's oauth store is not a
@@ -445,56 +493,9 @@ interface ExternalCliCredentials {
445
493
  /** claude only. */
446
494
  scopes?: string[];
447
495
  }
448
- /** Reader port injectable so tests never touch the real home directory. */
496
+ /** Reader port injectable so tests never touch the real home directory. */
449
497
  type ExternalCliReader = (provider: ExternalCliProvider) => ExternalCliCredentials | null;
450
498
 
451
- /**
452
- * external-cli-store — WRITE side of the external CLI native credential stores
453
- * (external-cli-sync, import + write-back).
454
- *
455
- * Counterpart of `external-cli-credentials` (the read side). The daemon only
456
- * ever writes a file it MANAGES: an explicit "import existing CLI login" puts
457
- * an `.omnicross-managed` marker (recording the owning account id) next to the
458
- * native store, and every subsequent successful refresh of THAT account writes
459
- * the rotated credential back into the file. Without the write-back, the
460
- * daemon's refresh would rotate the single-use refresh token and silently log
461
- * the user's bare CLI out — the write-back keeps both sides on the same live
462
- * credential.
463
- *
464
- * Safety properties:
465
- * - NEVER writes without a matching marker (an unmanaged / foreign-account
466
- * file is untouched);
467
- * - read-then-merge: unrelated top-level keys in the native file (e.g.
468
- * claude `email`, codex `OPENAI_API_KEY`) are preserved;
469
- * - one-time `.omnicross-backup` of the original file before the FIRST
470
- * overwrite (restorable by hand if the user wants the daemon out);
471
- * - atomic write (temp file → rename) so a crash never leaves a torn file.
472
- *
473
- * The envelope shapes mirror what the CLIs themselves write — the round-trip
474
- * test parses a written file back through `external-cli-credentials` to keep
475
- * the two sides from drifting.
476
- *
477
- * @module @omnicross/daemon/ports/external-cli-store
478
- */
479
-
480
- /** Token blocks the write-back accepts (the two external-store providers). */
481
- type ExternalWritableTokens = ClaudeTokenConfig | CodexTokenConfig;
482
- /**
483
- * Injectable port over the external store writes (tests use an in-memory fake;
484
- * the store wires `realExternalCliStore`).
485
- */
486
- interface ExternalCliStorePort {
487
- /** The owning account id recorded by the marker, or undefined when unmanaged. */
488
- readMarkerAccountId(provider: ExternalCliProvider): string | undefined;
489
- /** Record (or move) ownership of the provider's native store to an account. */
490
- writeMarker(provider: ExternalCliProvider, accountId: string): void;
491
- /**
492
- * Write the refreshed tokens back into the native store — ONLY when the
493
- * marker names `accountId`. Returns true when a write happened.
494
- */
495
- writeBack(provider: ExternalCliProvider, accountId: string, tokens: ExternalWritableTokens): boolean;
496
- }
497
-
498
499
  /**
499
500
  * JsonSubscriptionCredentialStore — the daemon's file-backed
500
501
  * `SubscriptionCredentialStore` port impl (design D1).
@@ -503,7 +504,7 @@ interface ExternalCliStorePort {
503
504
  * over a sibling `tokens.json` holding an `AccountTokensConfig`-shaped object
504
505
  * (`{ claude?, codex?, gemini?, opencodego?, updatedAt }`). Modeled on
505
506
  * `JsonOutboundKeyDb`: the constructor takes the path; reads are
506
- * `existsSync` `readFileSync` `JSON.parse`, tolerating a missing/corrupt
507
+ * `existsSync` `readFileSync` `JSON.parse`, tolerating a missing/corrupt
507
508
  * file by returning a minimal `{ updatedAt }` config (the strategies already
508
509
  * guard `?.accessToken`, so a partial/empty config never crashes dispatch).
509
510
  *
@@ -513,7 +514,7 @@ interface ExternalCliStorePort {
513
514
  * (strategies only consume + refresh, never log in).
514
515
  *
515
516
  * DAEMON-ONLY WRITE PATH (token-paste, design D1): `writeProviderTokens` /
516
- * `clearProvider` are CONCRETE-CLASS methods NOT part of the
517
+ * `clearProvider` are CONCRETE-CLASS methods NOT part of the
517
518
  * `SubscriptionCredentialStore` port. The registry / auth strategies / account
518
519
  * service never see them (they hold the port type), so a mutation can never leak
519
520
  * into the subscription block. Only the daemon admin API (which holds the
@@ -523,7 +524,7 @@ interface ExternalCliStorePort {
523
524
  *
524
525
  * AT-REST ENCRYPTION (secrets design D6/D7): the constructor takes a `SecretBox`.
525
526
  * `readConfig` decrypts the token-material fields on read (so every getter +
526
- * `getFullConfig` returns PLAINTEXT tokens the subscription bearer path is
527
+ * `getFullConfig` returns PLAINTEXT tokens the subscription bearer path is
527
528
  * byte-identical), and `persist` encrypts them before writing. Because EVERY
528
529
  * write funnels through `persist`, the OAuth-refresh writes below are encrypted
529
530
  * at-rest with NO extra work (the store API guarantees it). The "re-read on every
@@ -531,16 +532,16 @@ interface ExternalCliStorePort {
531
532
  *
532
533
  * REAL TOKEN REFRESH (oauth design D4): `refresh{Claude,Codex,Gemini}Token` mint
533
534
  * a new access token via the shared host-clean OAuth refresh functions
534
- * (`@omnicross/subscriptions/oauth`, injected `FetchLike` default global
535
+ * (`@omnicross/subscriptions/oauth`, injected `FetchLike` default global
535
536
  * `fetch`), then read-merge the refreshed fields into the provider block and
536
- * write back through `persist` (encrypted). Field-writes:
537
+ * write back through `persist` (encrypted). Field-writes:
537
538
  * claude/codex write access+refresh(+codex idToken)
538
539
  * +expiresAt+status:authorized+lastRefreshedAt; gemini writes ONLY access+
539
- * expiresAt (its refresh response omits refresh_token the OLD value is reused,
540
+ * expiresAt (its refresh response omits refresh_token the OLD value is reused,
540
541
  * never overwritten). On any failure the block is marked `status:'expired'` +
541
542
  * errorMessage and `false` is returned. When the block has NO refresh_token
542
543
  * (claude setup-token, manual token), it is an HONEST `false` BEFORE any upstream
543
- * call the block is not touched and no refresh_token is invented.
544
+ * call the block is not touched and no refresh_token is invented.
544
545
  *
545
546
  * @module @omnicross/daemon/ports/JsonSubscriptionCredentialStore
546
547
  */
@@ -548,7 +549,7 @@ interface ExternalCliStorePort {
548
549
  /**
549
550
  * The per-provider token block accepted by `writeProviderTokens`. Mirrors the
550
551
  * `AccountTokensConfig` per-provider field types (one of the four contract token
551
- * shapes), keyed by `SubscriptionProviderId` the daemon admin layer validates
552
+ * shapes), keyed by `SubscriptionProviderId` the daemon admin layer validates
552
553
  * the wire body to one of these before calling the writer.
553
554
  */
554
555
  type SubscriptionTokenBlock = ClaudeTokenConfig | CodexTokenConfig | GeminiTokenConfig | OpenCodeGoTokenConfig;
@@ -556,10 +557,8 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
556
557
  private readonly tokensPath;
557
558
  private readonly box;
558
559
  private readonly fetchImpl;
559
- /** Injectable external CLI native-store reader (external-cli-sync). */
560
+ /** Injectable, strictly read-only external CLI native-store reader. */
560
561
  private readonly externalCliReader;
561
- /** Injectable external CLI native-store WRITER (marker-gated write-back). */
562
- private readonly externalCliStore;
563
562
  /**
564
563
  * @param tokensPath on-disk `tokens.json` location.
565
564
  * @param box at-rest `SecretBox` (encrypt-on-write / decrypt-on-read).
@@ -569,23 +568,21 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
569
568
  * proxy-aware {@link fetchUpstream} that threads the
570
569
  * `{ providerId, accountId }` ctx (upstream-proxy M1) so a
571
570
  * per-account/per-provider proxy is honored on refresh exactly
572
- * as on relay refresh egresses from the SAME proxy IP as the
571
+ * as on relay refresh egresses from the SAME proxy IP as the
573
572
  * account's traffic. NOT used by any read/write path.
574
573
  */
575
574
  constructor(tokensPath: string, box: SecretBox, fetchImpl?: FetchLike | undefined,
576
- /** Injectable external CLI native-store reader (external-cli-sync). */
577
- externalCliReader?: ExternalCliReader,
578
- /** Injectable external CLI native-store WRITER (marker-gated write-back). */
579
- externalCliStore?: ExternalCliStorePort);
575
+ /** Injectable, strictly read-only external CLI native-store reader. */
576
+ externalCliReader?: ExternalCliReader);
580
577
  /**
581
578
  * The proxy-aware `FetchLike` for one refresh round-trip (upstream-proxy M1). A
582
579
  * TEST-injected `fetchImpl` is returned verbatim; otherwise the refresh routes
583
580
  * through {@link fetchUpstream} with the account's `{ providerId, accountId }`
584
- * ctx so the per-account/provider proxy applies. `@internal` also a test seam.
581
+ * ctx so the per-account/provider proxy applies. `@internal` also a test seam.
585
582
  */
586
583
  buildRefreshFetch(providerId: string, accountId?: string): FetchLike;
587
584
  /**
588
- * In-flight refresh coalescing (external-cli-sync). OAuth refresh tokens are
585
+ * In-flight refresh coalescing. OAuth refresh tokens are
589
586
  * SINGLE-USE: two concurrent refreshes of one account each spend the same
590
587
  * token and the loser bricks a healthy account. Every refresh entry point
591
588
  * (auth-strategy lazy refresh, 401 retry, background scheduler) funnels
@@ -594,11 +591,11 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
594
591
  private readonly inFlightRefreshes;
595
592
  private coalesce;
596
593
  /** Full parsed account-tokens config (or a minimal `{ updatedAt }` when the
597
- * file is absent/corrupt). This is the hot read the codex / gemini auth
594
+ * file is absent/corrupt). This is the hot read the codex / gemini auth
598
595
  * strategies pull `accessToken` / `expiresAt` / `status` from it. */
599
596
  getFullConfig(): Promise<AccountTokensConfig>;
600
597
  /** Current Claude OAuth access token, or `null` when none is stored. No inline
601
- * refresh here the lead-window / 401-retry refresh is driven by the
598
+ * refresh here the lead-window / 401-retry refresh is driven by the
602
599
  * subscription auth strategy, which calls `refreshClaudeToken` (now real). */
603
600
  getValidClaudeAccessToken(): Promise<string | null>;
604
601
  /** Current OpenCodeGo static API key, or `null` when none is stored. */
@@ -614,28 +611,25 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
614
611
  /**
615
612
  * DAEMON-ONLY sanitized accounts list (design D8, NOT on the port). Projects
616
613
  * each provider's accounts to the secret-free `SubscriptionAccountSanitized`
617
- * shape (id/label/status/expiresAt/hasAccessToken/isActive) NEVER a token.
614
+ * shape (id/label/status/expiresAt/hasAccessToken/isActive) NEVER a token.
618
615
  * Used by the admin accounts GET (secret-IN-never-OUT).
619
616
  */
620
617
  listSanitizedAccounts(): Promise<Record<string, SubscriptionAccountSanitized[]>>;
621
618
  /**
622
- * List-time credential-conflict warnings (external-cli-sync). Computed, not
623
- * persisted: (a) `duplicate-token` when two accounts of one provider share a
624
- * credential, (b) `external-divergent` when the external CLI native store has
625
- * rotated PAST the ACTIVE account (claude/codex only). A warning persisted by
626
- * a failed refresh (`external-not-rotated`) takes precedence — it is the most
627
- * actionable state.
619
+ * List-time managed-credential conflict warnings. Computed, not persisted:
620
+ * `duplicate-token` is projected when two accounts of one provider share a
621
+ * credential. This deliberately does not inspect either native CLI file.
628
622
  */
629
- private attachSyncWarnings;
623
+ private attachDuplicateWarnings;
630
624
  /** Read the external CLI store, never letting an fs/parse error escape. */
631
625
  private safeReadExternal;
632
626
  /**
633
627
  * Refresh the Claude OAuth access token (oauth design D4). HONEST `false` when
634
- * the block has no refresh_token (setup-token / manual) no upstream call, the
628
+ * the block has no refresh_token (setup-token / manual) no upstream call, the
635
629
  * block is untouched. Otherwise mint via the shared claude refresh flow and
636
630
  * write back access+refresh+expiresAt+status:authorized+lastRefreshedAt.
637
- * On failure status:expired +
638
- * errorMessage `false`.
631
+ * On failure status:expired +
632
+ * errorMessage `false`.
639
633
  */
640
634
  refreshClaudeToken(): Promise<boolean>;
641
635
  /**
@@ -653,11 +647,10 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
653
647
  */
654
648
  refreshGeminiToken(): Promise<boolean>;
655
649
  /**
656
- * Refresh a SPECIFIC account by id (background scheduler sweep,
657
- * external-cli-sync). Unlike the active-account refreshers it does NOT
658
- * attempt the external-import fallback the external CLI file's lineage can
659
- * only plausibly match the ACTIVE account. Coalesced per `provider:id`; on
660
- * failure flags ONLY that account `expired`.
650
+ * Refresh a SPECIFIC managed account by id (background scheduler sweep and
651
+ * account-pool resolution). It uses only that account's stored refresh
652
+ * token. Coalesced per `provider:id`; on failure flags ONLY that account
653
+ * `expired`.
661
654
  */
662
655
  refreshAccountById(provider: 'claude' | 'codex' | 'gemini', id: string): Promise<boolean>;
663
656
  /**
@@ -672,7 +665,7 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
672
665
  /**
673
666
  * Refresh a SPECIFIC account's OAuth token by id (design D6/D7). Delegates to
674
667
  * `refreshAccountById` (coalesced per `provider:id`); opencodego is a static key
675
- * `false` (no refresh affordance).
668
+ * `false` (no refresh affordance).
676
669
  */
677
670
  refreshAccountToken(providerId: SubscriptionProviderId, accountId: string): Promise<boolean>;
678
671
  /**
@@ -686,7 +679,7 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
686
679
  * (subscription-client-fingerprint #7, P2). Entry-metadata only (NON-secret
687
680
  * whitelisted fingerprint headers; the token mirror is untouched); a no-op for
688
681
  * an unknown id. Called by the identity store's persistence port on a first-seen
689
- * freeze / TTL refresh, so it stays infrequent. Never throws to the caller the
682
+ * freeze / TTL refresh, so it stays infrequent. Never throws to the caller the
690
683
  * store's port wrapper swallows a rejection so the relay hot path is unaffected.
691
684
  */
692
685
  setAccountIdentity(providerId: SubscriptionProviderId, accountId: string, identity: AccountClientIdentity): Promise<void>;
@@ -702,7 +695,7 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
702
695
  * DAEMON-ONLY set/clear per-account proxy (upstream-proxy, admin write, NOT on
703
696
  * the port). Passing `undefined` clears the override. Write-only password: when
704
697
  * the incoming structured proxy omits the password but the account already had
705
- * one, the current (decrypted) password is preserved editing host/port never
698
+ * one, the current (decrypted) password is preserved editing host/port never
706
699
  * wipes the secret. Persist re-encrypts `proxy.password` via the tokens SecretBox.
707
700
  */
708
701
  setAccountProxy(providerId: SubscriptionProviderId, accountId: string, proxy: ProxyConfig | undefined): Promise<{
@@ -719,44 +712,36 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
719
712
  }>;
720
713
  /** Dispatch one OAuth refresh round-trip to the provider's shared flow. */
721
714
  private refreshUpstream;
722
- /**
723
- * External-import fallback for a FAILED active-account refresh
724
- * (external-cli-sync). Reads the CLI native store; imports when the external
725
- * lineage ROTATED (different refresh token) or its access token is still
726
- * valid. When the imported access token is already expired it refreshes once
727
- * with the rotated refresh token. A `not-rotated` outcome persists the
728
- * `external-not-rotated` warning on the (about-to-be-expired) account so the
729
- * UI can tell "genuine revocation" apart from a plain refresh failure.
730
- */
731
- private tryExternalImport;
732
- /**
733
- * Marker-gated external write-back (external-cli-sync). After a successful
734
- * refresh of the account that OWNS the provider's native CLI store (imported
735
- * via `importExternalCliAccount`), push the rotated credential back into the
736
- * file — otherwise the daemon's refresh invalidates the single-use refresh
737
- * token and silently logs the bare CLI out. NON-FATAL: the internal store is
738
- * already persisted; a failed external write only leaves the file stale,
739
- * which the `external-divergent` warning surfaces.
740
- */
741
- private resyncExternal;
742
- /** Read the marker's owning account id, never letting an fs error escape. */
743
- private safeReadMarker;
715
+ /** Atomically patch one account's non-secret management metadata. */
716
+ patchAccountMetadata(providerId: SubscriptionProviderId, accountId: string, patch: AccountMetadataPatch): Promise<{
717
+ ok: boolean;
718
+ }>;
719
+ /** Validate every target, then persist one all-or-nothing batch mutation. */
720
+ batchManageAccounts(refs: AccountRef[], mutation: AccountBatchMutation): Promise<{
721
+ ok: true;
722
+ affected: number;
723
+ } | {
724
+ ok: false;
725
+ missing: AccountRef;
726
+ }>;
744
727
  /**
745
728
  * DAEMON-ONLY (admin import button): which providers have a usable external
746
- * CLI credential on THIS machine. Pure detection reads the native files,
729
+ * CLI credential on THIS machine. Pure detection reads the native files,
747
730
  * never mutates anything, never returns a token.
748
731
  */
749
732
  listExternalCliAvailability(): Promise<Record<ExternalCliProvider, boolean>>;
750
733
  /**
751
734
  * DAEMON-ONLY (admin import button): import the external CLI's current login
752
- * as a NEW account (+ activate), and take MANAGED ownership of the native
753
- * store (marker) so subsequent refreshes write back keeping the bare CLI
754
- * and the daemon on the same live credential instead of silently killing one
755
- * side's single-use refresh token.
735
+ * as a NEW account (+ activate). This is a COPY-ONLY import: Omnicross never
736
+ * claims, writes, moves, restores, or deletes the native CLI credential file
737
+ * or any legacy `.omnicross-managed` marker/backup beside it. Subsequent
738
+ * refreshes persist only Omnicross's encrypted token store.
756
739
  */
757
740
  importExternalCliAccount(provider: ExternalCliProvider, label?: string): Promise<{
758
741
  ok: true;
759
742
  id: string;
743
+ nativeCredentialMode: 'read-only';
744
+ refreshWritesNativeCredentials: false;
760
745
  } | {
761
746
  ok: false;
762
747
  reason: 'no-credential';
@@ -788,13 +773,13 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
788
773
  * `updatedAt`, and re-persist `tokens.json` as pretty JSON. Preserves every
789
774
  * OTHER provider's existing block (read-merge-write, not overwrite). Reuses the
790
775
  * tolerate-on-read base (`{ updatedAt: '' }` when the file is absent/corrupt),
791
- * so a first-ever write still produces a valid config. No cache the next read
776
+ * so a first-ever write still produces a valid config. No cache the next read
792
777
  * sees this write.
793
778
  */
794
779
  writeProviderTokens(providerId: SubscriptionProviderId, config: SubscriptionTokenBlock): Promise<void>;
795
780
  /**
796
781
  * DAEMON-ONLY login append (design D5, NOT on the port). Append a NEW account
797
- * (optional label) and set it active, then re-derive the mirror used by
782
+ * (optional label) and set it active, then re-derive the mirror used by
798
783
  * `omnicross login <provider> --label` to add an account instead of overwriting.
799
784
  */
800
785
  appendProviderAccount(providerId: SubscriptionProviderId, config: SubscriptionTokenBlock, label?: string): Promise<{
@@ -817,7 +802,7 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
817
802
  }>;
818
803
  /**
819
804
  * DAEMON-ONLY per-account rename (NOT on the port). Update one account's label;
820
- * rejects an unknown id. Label-only no token material is read or written
805
+ * rejects an unknown id. Label-only no token material is read or written
821
806
  * (the secret-free invariant holds).
822
807
  */
823
808
  renameAccount(providerId: SubscriptionProviderId, id: string, label: string): Promise<{
@@ -832,8 +817,8 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
832
817
  clearProvider(providerId: SubscriptionProviderId): Promise<void>;
833
818
  /** Write the merged config to disk as pretty JSON (mkdir parent if needed).
834
819
  * Encrypt-on-write: the token-material fields are encrypted (legacy plaintext
835
- * `enc:v1:`; already-`enc:`/`$ENV` untouched) before serializing, so any
836
- * write incl. child 4's future refresh writes lands encrypted. */
820
+ * `enc:v1:`; already-`enc:`/`$ENV` untouched) before serializing, so any
821
+ * write incl. child 4's future refresh writes lands encrypted. */
837
822
  private persist;
838
823
  /**
839
824
  * Read + parse `tokens.json`, tolerating a missing/corrupt file, then DECRYPT
@@ -841,11 +826,11 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
841
826
  * subscription bearer path is byte-identical).
842
827
  *
843
828
  * The fs-read + JSON-parse tolerance is INSIDE the try (a missing or corrupt
844
- * file empty `{ updatedAt: '' }`). The DECRYPT runs OUTSIDE the try, so a
829
+ * file empty `{ updatedAt: '' }`). The DECRYPT runs OUTSIDE the try, so a
845
830
  * wrong/missing master key or a tampered `enc:` envelope FAILS FAST with the
846
- * box's clear, secret-free error (secrets spec "错误密钥 / 篡改的解密失败 UX":
847
- * SHALL fail-fast, SHALL NOT 静默降级 — a swallowed decrypt would report "no
848
- * tokens" and silently send the WRONG bearer upstream 401). Mirrors
831
+ * box's clear, secret-free error (secrets spec "/ UX":
832
+ * SHALL fail-fast, SHALL NOT a swallowed decrypt would report "no
833
+ * tokens" and silently send the WRONG bearer upstream 401). Mirrors
849
834
  * `config.ts loadConfig`, which decrypts outside its parse try.
850
835
  */
851
836
  private readConfig;
@@ -891,6 +876,18 @@ interface SubscriptionTokenWriter {
891
876
  setAccountPriority(providerId: SubscriptionProviderId, id: string, priority: number): Promise<{
892
877
  ok: boolean;
893
878
  }>;
879
+ /** Patch non-secret account management metadata in one write. */
880
+ patchAccountMetadata(providerId: SubscriptionProviderId, id: string, patch: AccountMetadataPatch): Promise<{
881
+ ok: boolean;
882
+ }>;
883
+ /** All-or-nothing multi-provider account management mutation. */
884
+ batchManageAccounts(refs: AccountRef[], mutation: AccountBatchMutation): Promise<{
885
+ ok: true;
886
+ affected: number;
887
+ } | {
888
+ ok: false;
889
+ missing: AccountRef;
890
+ }>;
894
891
  /** Set (or CLEAR, with `undefined`) one account's per-account proxy override
895
892
  * (upstream-proxy). The `proxy.password` is a secret (encrypted at rest, masked
896
893
  * in the sanitized view). Rejects an unknown id. */
@@ -912,6 +909,8 @@ interface SubscriptionTokenWriter {
912
909
  importExternalCliAccount(providerId: 'claude' | 'codex', label?: string): Promise<{
913
910
  ok: true;
914
911
  id: string;
912
+ nativeCredentialMode: 'read-only';
913
+ refreshWritesNativeCredentials: false;
915
914
  } | {
916
915
  ok: false;
917
916
  reason: 'no-credential';
@@ -1084,6 +1083,127 @@ declare class CodexOAuthSessionStore {
1084
1083
  private sweep;
1085
1084
  }
1086
1085
 
1086
+ /**
1087
+ * Claude OAuth usage collector.
1088
+ *
1089
+ * Fetches one account's five-hour, seven-day, and seven-day Sonnet windows,
1090
+ * coalesces concurrent refreshes by account, and caches every result for five
1091
+ * minutes. Tokens and raw upstream payloads never leave this module.
1092
+ */
1093
+
1094
+ interface ClaudeAllowanceCredentialReader {
1095
+ getAccessTokenForAccount(providerId: 'claude', accountId: string): Promise<string | null>;
1096
+ refreshAccountToken(providerId: 'claude', accountId: string): Promise<boolean>;
1097
+ }
1098
+ type ClaudeAllowanceFetch = (url: string, init: RequestInit, accountId: string) => Promise<Response>;
1099
+ interface ClaudeAllowanceCollectOptions {
1100
+ force?: boolean;
1101
+ /**
1102
+ * Treat an otherwise valid cache entry as due when it will expire within this
1103
+ * window. The resident background scheduler uses this to refresh shortly
1104
+ * before expiry without bypassing the normal cache on every tick.
1105
+ */
1106
+ refreshAheadMs?: number;
1107
+ }
1108
+ declare class ClaudeAllowanceCollector {
1109
+ private readonly credentials;
1110
+ private readonly store;
1111
+ private readonly fetchImpl;
1112
+ private readonly identityStore;
1113
+ private readonly now;
1114
+ private readonly inFlight;
1115
+ constructor(credentials: ClaudeAllowanceCredentialReader, store?: AccountAllowanceStore, fetchImpl?: ClaudeAllowanceFetch, identityStore?: SubscriptionIdentityStore, now?: () => number);
1116
+ collectMany(accounts: readonly SubscriptionAccountEntry<ClaudeTokenConfig>[], options?: ClaudeAllowanceCollectOptions): Promise<AccountAllowanceSnapshot[]>;
1117
+ collect(account: SubscriptionAccountEntry<ClaudeTokenConfig>, options?: ClaudeAllowanceCollectOptions): Promise<AccountAllowanceSnapshot>;
1118
+ private isCacheValid;
1119
+ private fetchAccount;
1120
+ private request;
1121
+ private failureSnapshot;
1122
+ private unsupportedSnapshot;
1123
+ }
1124
+
1125
+ /** Secret-free account allowance query/refresh facade used by the admin API. */
1126
+
1127
+ interface AccountAllowanceCredentialReader extends ClaudeAllowanceCredentialReader {
1128
+ getFullConfig(): Promise<AccountTokensConfig>;
1129
+ }
1130
+ interface AccountAllowanceFilter {
1131
+ providerId?: SubscriptionProviderId;
1132
+ accountId?: string;
1133
+ }
1134
+ interface AccountAllowanceSchedulingStatus {
1135
+ config: AllowanceSchedulingConfig;
1136
+ history: AllowanceSchedulingDecision[];
1137
+ }
1138
+ declare class AccountAllowanceService {
1139
+ private readonly credentials;
1140
+ private readonly store;
1141
+ private readonly now;
1142
+ readonly claudeCollector: ClaudeAllowanceCollector;
1143
+ constructor(credentials: AccountAllowanceCredentialReader, store?: AccountAllowanceStore, collector?: ClaudeAllowanceCollector, now?: () => number);
1144
+ /**
1145
+ * Read all/filtered snapshots. Claude's five-minute cache is refreshed lazily;
1146
+ * Codex remains passive and reports not-observed until a real model response.
1147
+ */
1148
+ list(filter?: AccountAllowanceFilter): Promise<AccountAllowanceSnapshot[]>;
1149
+ /** Force-refresh Claude usage for one account or every stored Claude account. */
1150
+ refreshClaude(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
1151
+ /**
1152
+ * Keep Claude snapshots warm for allowance-aware routing. This deliberately
1153
+ * excludes Codex (whose quota is learned from real response headers) and
1154
+ * preserves the collector's cache + per-account in-flight coalescing.
1155
+ */
1156
+ maintainClaudeCache(refreshAheadMs: number): Promise<void>;
1157
+ /** Remove a cache row as soon as an account is deleted by the admin path. */
1158
+ removeAccountSnapshot(providerId: SubscriptionProviderId, accountId: string): void;
1159
+ /** Remove all allowance rows for a provider block that was deleted. */
1160
+ removeProviderSnapshots(providerId: SubscriptionProviderId): void;
1161
+ /** Secret-free policy diagnostics for the settings/accounts UI. */
1162
+ getSchedulingStatus(): AccountAllowanceSchedulingStatus;
1163
+ }
1164
+
1165
+ /**
1166
+ * Low-frequency, non-blocking cache maintenance for Claude allowance snapshots.
1167
+ *
1168
+ * The routing policy intentionally ignores stale quota data. Without a resident
1169
+ * UI poll, the five-minute Claude snapshot would therefore age out and silently
1170
+ * stop influencing account selection. This worker checks once a minute and asks
1171
+ * the existing collector to refresh entries that are close to expiry. The
1172
+ * collector remains the cache/coalescing authority, so a tick normally performs
1173
+ * no network I/O and can share an in-flight request with the admin UI.
1174
+ *
1175
+ * Zero-regression invariant: the worker does not arm a timer or perform an
1176
+ * initial sweep unless `server.allowanceScheduling.enabled` is true.
1177
+ */
1178
+
1179
+ interface ClaudeAllowanceCacheMaintainer {
1180
+ maintainClaudeCache(refreshAheadMs: number): Promise<void>;
1181
+ }
1182
+ declare class ClaudeAllowanceRefreshScheduler {
1183
+ private readonly service;
1184
+ private readonly logger;
1185
+ private readonly intervalMs;
1186
+ private readonly refreshAheadMs;
1187
+ private timer;
1188
+ private started;
1189
+ private enabled;
1190
+ private sweeping;
1191
+ constructor(service: ClaudeAllowanceCacheMaintainer, logger: Logger, intervalMs?: number, refreshAheadMs?: number);
1192
+ /**
1193
+ * Apply live server policy. Once started, enable/disable changes arm or disarm
1194
+ * immediately; the initial enabled sweep is fire-and-forget.
1195
+ */
1196
+ configure(config: AllowanceSchedulingConfig | undefined): void;
1197
+ /** Start the lifecycle. Disabled policy remains completely inert. */
1198
+ start(): void;
1199
+ /** Stop all future checks. Idempotent and safe during an in-flight refresh. */
1200
+ dispose(): void;
1201
+ /** One non-overlapping cache-maintenance pass. Exposed for focused tests. */
1202
+ sweep(): Promise<void>;
1203
+ private arm;
1204
+ private disarm;
1205
+ }
1206
+
1087
1207
  /**
1088
1208
  * ProbeStrategy — the per-provider two-tier probe plan
1089
1209
  * (subscription-account-probe #8, design D1).
@@ -1498,6 +1618,14 @@ declare class JsonApiServerSettingsStore implements ApiServerSettingsStore {
1498
1618
  declare class JsonPricingStore implements PricingStore {
1499
1619
  private readonly pricingPath;
1500
1620
  constructor(pricingPath: string);
1621
+ /**
1622
+ * Return whether the durable snapshot can actually serve at least one price.
1623
+ *
1624
+ * This intentionally checks the file itself instead of relying on refresh
1625
+ * metadata: a recent `lastSuccessAt` must not hide a deleted, truncated, or
1626
+ * otherwise unusable pricing table after a crash or manual file edit.
1627
+ */
1628
+ hasUsableSnapshot(): boolean;
1501
1629
  getAll(): Promise<PricingEntry[]>;
1502
1630
  /**
1503
1631
  * Insert or update one row keyed (providerId, modelId). `asUserEdit` stamps
@@ -1510,10 +1638,10 @@ declare class JsonPricingStore implements PricingStore {
1510
1638
  /**
1511
1639
  * Apply a batch fetched from a pricing source. Rows whose local copy is
1512
1640
  * user-edited are NOT applied — they come back as `{ current, incoming }`
1513
- * conflicts; everything else is upserted (source 'litellm'). ONE file write
1514
- * for the whole batch.
1641
+ * conflicts; everything else is upserted with the supplied automatic source.
1642
+ * ONE file write for the whole batch.
1515
1643
  */
1516
- bulkApplyFromSource(entries: PricingEntryInput[]): Promise<{
1644
+ bulkApplyFromSource(entries: PricingEntryInput[], source?: AutomaticPricingSource): Promise<{
1517
1645
  applied: PricingEntry[];
1518
1646
  conflicts: Array<{
1519
1647
  current: PricingEntry;
@@ -1539,6 +1667,98 @@ declare class JsonPricingStore implements PricingStore {
1539
1667
  /** Read the pricing rows, tolerating a missing/corrupt file (→ empty list). */
1540
1668
  private readRows;
1541
1669
  private writeRows;
1670
+ /** Isolated for deterministic failure testing; never removes the target. */
1671
+ private replaceFile;
1672
+ }
1673
+
1674
+ type IntegrationClientId = 'codex' | 'claude';
1675
+ type IntegrationStatusKind = 'not-installed' | 'enabled' | 'configuration-drift' | 'configuration-missing' | 'key-missing';
1676
+ interface IntegrationClientStatus {
1677
+ client: IntegrationClientId;
1678
+ status: IntegrationStatusKind;
1679
+ configPath: string;
1680
+ installedAt?: number;
1681
+ gatewayBaseUrl?: string;
1682
+ message?: string;
1683
+ }
1684
+ interface IntegrationChangePlan {
1685
+ client: IntegrationClientId;
1686
+ configPath: string;
1687
+ action: 'install' | 'none' | 'repair';
1688
+ canApply: boolean;
1689
+ /** Redacted logical fields only; never file contents or credential values. */
1690
+ changes: string[];
1691
+ warnings: string[];
1692
+ }
1693
+ interface IntegrationInstallRecord {
1694
+ client: IntegrationClientId;
1695
+ configPath: string;
1696
+ originalExisted: boolean;
1697
+ /** Encrypted by IntegrationStateStore before it reaches disk. */
1698
+ originalContent: string;
1699
+ originalHash: string;
1700
+ installedHash: string;
1701
+ installedAt: number;
1702
+ gatewayBaseUrl: string;
1703
+ /** Codex auth.json snapshot and installed hash; absent on legacy records. */
1704
+ credentialFile?: IntegrationManagedFileRecord;
1705
+ }
1706
+ interface IntegrationManagedFileRecord {
1707
+ path: string;
1708
+ originalExisted: boolean;
1709
+ /** Encrypted by IntegrationStateStore before it reaches disk. */
1710
+ originalContent: string;
1711
+ originalHash: string;
1712
+ installedHash: string;
1713
+ }
1714
+ interface IntegrationGatewayKeyRecord {
1715
+ id: string;
1716
+ /** Encrypted by IntegrationStateStore before it reaches disk. */
1717
+ secret: string;
1718
+ createdAt: number;
1719
+ }
1720
+ interface IntegrationState {
1721
+ version: 1;
1722
+ gatewayKey?: IntegrationGatewayKeyRecord;
1723
+ clients: Partial<Record<IntegrationClientId, IntegrationInstallRecord>>;
1724
+ }
1725
+
1726
+ /** Encrypted, Omnicross-owned state for reversible native CLI configuration. */
1727
+ declare class IntegrationStateStore {
1728
+ readonly path: string;
1729
+ private readonly box;
1730
+ constructor(path: string, box: SecretBox);
1731
+ load(): IntegrationState;
1732
+ save(state: IntegrationState): void;
1733
+ }
1734
+
1735
+ interface IntegrationManagerOptions {
1736
+ configPath: string;
1737
+ gatewayBaseUrl: string;
1738
+ keyDb: OutboundKeyDb;
1739
+ stateStore: IntegrationStateStore;
1740
+ homeDir?: string;
1741
+ }
1742
+ /** Coordinates a least-privilege gateway key with reversible native CLI config edits. */
1743
+ declare class IntegrationManager {
1744
+ private readonly options;
1745
+ private readonly homeDir;
1746
+ constructor(options: IntegrationManagerOptions);
1747
+ listStatus(): Promise<IntegrationClientStatus[]>;
1748
+ plan(client: IntegrationClientId, configPath?: string): Promise<IntegrationChangePlan>;
1749
+ install(client: IntegrationClientId, configPath?: string): Promise<IntegrationClientStatus>;
1750
+ repair(client: IntegrationClientId): Promise<IntegrationClientStatus>;
1751
+ remove(client: IntegrationClientId): Promise<IntegrationClientStatus>;
1752
+ rotateGatewayKey(): Promise<{
1753
+ keyId: string;
1754
+ }>;
1755
+ getGatewayToken(): Promise<string>;
1756
+ private ensureGatewayKey;
1757
+ private isKeyUsable;
1758
+ private statusFor;
1759
+ private defaultConfigPath;
1760
+ private codexAuthPathForConfig;
1761
+ private renderInstalled;
1542
1762
  }
1543
1763
 
1544
1764
  /**
@@ -1623,22 +1843,18 @@ interface MigrationCredentialStore extends SubscriptionAccountAppender {
1623
1843
  getFullConfig(): Promise<AccountTokensConfig>;
1624
1844
  }
1625
1845
 
1626
- /**
1627
- * adminApi — the daemon admin dashboard's management API router (`/admin/api/*`,
1628
- * JSON) (RT3, design D3/D4/D5).
1629
- *
1630
- * A small method+path router over the LIVE daemon handles + exported core fns.
1631
- * Resources: providers (CRUD + hot-reload), keys (CRUD + one-time plaintext),
1632
- * server config (live apply), accounts (read-only status), status, playground
1633
- * (same-origin proxy to `/v1/*`).
1634
- *
1635
- * SECRET SPINE (design D4 — the load-bearing invariant): secrets flow IN
1636
- * (POST/PUT) and NEVER OUT (GET). The provider mask (`maskProviderApiKey`) and
1637
- * the key DTO map (`toKeyInfo`) are the single places the masking is applied so
1638
- * the invariant lives in one spot.
1639
- *
1640
- * @module @omnicross/daemon/admin/adminApi
1641
- */
1846
+ /** Minimal, auth-gated admin API for secret-free account allowance snapshots. */
1847
+
1848
+ interface AccountAllowanceAdminReader {
1849
+ list(filter?: {
1850
+ providerId?: SubscriptionProviderId;
1851
+ accountId?: string;
1852
+ }): Promise<AccountAllowanceSnapshot[]>;
1853
+ refreshClaude(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
1854
+ removeAccountSnapshot?(providerId: SubscriptionProviderId, accountId: string): void;
1855
+ removeProviderSnapshots?(providerId: SubscriptionProviderId): void;
1856
+ getSchedulingStatus?(): AccountAllowanceSchedulingStatus;
1857
+ }
1642
1858
 
1643
1859
  /** Token-free subscription account list entry (passthrough from core's service). */
1644
1860
  interface AdminAccountsLister {
@@ -1666,7 +1882,7 @@ interface AdminApiDeps {
1666
1882
  /** Live provider catalog (hot-reload target). */
1667
1883
  readonly llmConfig: ConfigFileProviderConfigSource;
1668
1884
  /** Named outbound-key store. */
1669
- readonly keyDb: OutboundKeyDb;
1885
+ readonly keyDb: OutboundKeyDb$1;
1670
1886
  /**
1671
1887
  * OPTIONAL voucher (redemption-card) store (voucher-redemption #9). When wired,
1672
1888
  * the `/admin/api/voucher` surface can generate/list/revoke cards. Absent ⇒ the
@@ -1685,6 +1901,20 @@ interface AdminApiDeps {
1685
1901
  readonly outboundApiServer: OutboundApiServer;
1686
1902
  /** Subscription accounts (token-free `listAll`). */
1687
1903
  readonly subscriptionAccounts: AdminAccountsLister;
1904
+ /**
1905
+ * Secret-free upstream allowance facade. Optional for lightweight embedders;
1906
+ * the standalone daemon wires it and the route returns 501 when absent.
1907
+ */
1908
+ readonly accountAllowanceService?: AccountAllowanceAdminReader;
1909
+ /** Live Claude cache worker; hot-reconfigured with allowance scheduling. */
1910
+ readonly allowanceRefreshScheduler?: Pick<ClaudeAllowanceRefreshScheduler, 'configure'>;
1911
+ /** Optional secret-free account connection probe + rolling history surface. */
1912
+ readonly accountProbeService?: AccountProbeHistoryReader & {
1913
+ probeAccount(providerId: _omnicross_contracts_subscription_types.SubscriptionProviderId, accountId: string): Promise<{
1914
+ ok: boolean;
1915
+ marked: boolean;
1916
+ }>;
1917
+ };
1688
1918
  /**
1689
1919
  * Least-authority subscription-token WRITER (design D4) — ONLY the mutation
1690
1920
  * methods (`writeProviderTokens` / `clearProvider`), never a token-returning
@@ -1765,6 +1995,8 @@ interface AdminApiDeps {
1765
1995
  * actually runs.
1766
1996
  */
1767
1997
  readonly cliCommandRunner?: CommandRunner;
1998
+ /** Factory so each request observes the outbound server's current loopback port. */
1999
+ readonly integrationManagerFactory?: () => IntegrationManager;
1768
2000
  }
1769
2001
  /**
1770
2002
  * Dispatch one `/admin/api/*` request. `path` is the already-extracted pathname
@@ -1872,21 +2104,7 @@ declare class AdminServer {
1872
2104
  getStatus(): AdminServerStatus;
1873
2105
  }
1874
2106
 
1875
- /**
1876
- * JsonOutboundKeyDb — the daemon's file-backed `OutboundKeyDb` port impl
1877
- * (design D3).
1878
- *
1879
- * Durable storage for named outbound API keys, backed by a json file (a sibling
1880
- * of `config.json`, e.g. `keys.json`) holding an `OutboundKeyDbRow[]`. This port
1881
- * provides ONLY storage — it never generates secrets nor hashes. Core's
1882
- * `createNamedKey(db, name)` calls `outboundApiKeysCreate` with the sha256
1883
- * `keyHash` + display `keyPrefix` and returns the one-time plaintext; the hot
1884
- * auth path uses core's `hashKey(presented)` + `outboundApiKeysGetByHash`.
1885
- *
1886
- * @module @omnicross/daemon/ports/JsonOutboundKeyDb
1887
- */
1888
-
1889
- declare class JsonOutboundKeyDb implements OutboundKeyDb$1 {
2107
+ declare class JsonOutboundKeyDb implements OutboundKeyDb {
1890
2108
  private readonly keysPath;
1891
2109
  constructor(keysPath: string);
1892
2110
  outboundApiKeysList(): Promise<OutboundKeyDbRow[]>;
@@ -1897,6 +2115,9 @@ declare class JsonOutboundKeyDb implements OutboundKeyDb$1 {
1897
2115
  keyHash: string;
1898
2116
  keyPrefix: string;
1899
2117
  createdAt?: number;
2118
+ kind?: 'client' | 'integration';
2119
+ allowedEndpoints?: _omnicross_core.OutboundEndpoint[];
2120
+ loopbackOnly?: boolean;
1900
2121
  }): Promise<OutboundKeyDbRow>;
1901
2122
  outboundApiKeysRevoke(id: string): Promise<boolean>;
1902
2123
  outboundApiKeysTouchLastUsed(id: string): Promise<boolean>;
@@ -1911,6 +2132,41 @@ declare class JsonOutboundKeyDb implements OutboundKeyDb$1 {
1911
2132
  private writeRows;
1912
2133
  }
1913
2134
 
2135
+ interface PricingRefreshState {
2136
+ lastAttemptAt: number | null;
2137
+ lastSuccessAt: number | null;
2138
+ lastError: string | null;
2139
+ sources: PricingSourceRefreshResult[];
2140
+ }
2141
+ interface PricingRefreshSchedulerOptions {
2142
+ staleAfterMs?: number;
2143
+ intervalMs?: number;
2144
+ now?: () => number;
2145
+ }
2146
+ interface PricingCatalogSnapshot {
2147
+ hasUsableSnapshot(): boolean;
2148
+ }
2149
+ declare class PricingRefreshScheduler {
2150
+ private readonly engine;
2151
+ private readonly catalog;
2152
+ private readonly statePath;
2153
+ private readonly logger;
2154
+ private readonly staleAfterMs;
2155
+ private readonly intervalMs;
2156
+ private readonly now;
2157
+ private timer;
2158
+ private inFlight;
2159
+ constructor(engine: PricingEngine$1, catalog: PricingCatalogSnapshot, statePath: string, logger: Logger, options?: PricingRefreshSchedulerOptions);
2160
+ /** Fire one stale check immediately and arm an unref'ed periodic check. */
2161
+ start(): void;
2162
+ dispose(): void;
2163
+ getState(): PricingRefreshState;
2164
+ /** Public for admin/manual tests; concurrent checks share one promise. */
2165
+ refreshIfStale(force?: boolean): Promise<void>;
2166
+ private runRefresh;
2167
+ private writeState;
2168
+ }
2169
+
1914
2170
  /**
1915
2171
  * AccountHealthSweeper — proactive account-health recovery tick
1916
2172
  * (subscription-account-health, design D6).
@@ -2181,24 +2437,20 @@ declare class BillingRetrySweeper {
2181
2437
  }
2182
2438
 
2183
2439
  /**
2184
- * TokenRefreshScheduler proactive background OAuth token refresh
2185
- * (external-cli-sync).
2440
+ * TokenRefreshScheduler proactive background OAuth token refresh.
2186
2441
  *
2187
- * The auth strategies already refresh LAZILY (lead-window check before each
2188
- * request + 401 retry), but a daemon that sits idle past a token's lifetime
2189
- * pays the refresh latency or a dead rotated token on the first request.
2190
- * This scheduler sweeps every account of every OAuth provider on an interval
2191
- * and refreshes any token entering the expiry lead window.
2442
+ * The auth strategies already refresh lazily (lead-window check before each
2443
+ * request + 401 retry), but an idle daemon can still reach token expiry
2444
+ * before its next request. This scheduler sweeps managed OAuth accounts on
2445
+ * an interval and refreshes tokens entering the expiry lead window.
2192
2446
  *
2193
2447
  * Safety properties:
2194
- * - the store coalesces in-flight refreshes per account, so a sweep can never
2448
+ * - the store coalesces in-flight refreshes per account, so a sweep cannot
2195
2449
  * double-spend a single-use refresh token against a concurrent lazy refresh;
2196
- * - accounts already flagged `expired` are skipped (a dead refresh token is
2197
- * not retried every tick recovery is the external-import fallback or a
2198
- * re-login);
2199
- * - the ACTIVE account routes through the provider's active refresher (which
2200
- * carries the external CLI import fallback); non-active accounts refresh
2201
- * by id;
2450
+ * - accounts already flagged `expired` are skipped and remain expired until
2451
+ * their own managed credential is repaired or re-login occurs;
2452
+ * - the active account uses its managed active refresher, while non-active
2453
+ * accounts refresh by id; both paths use only managed stored credentials;
2202
2454
  * - one sweep runs at a time (a long sweep never overlaps the next tick).
2203
2455
  *
2204
2456
  * Modeled on `ApiKeyPoolService`'s interval lifecycle: `start()` arms an
@@ -2223,8 +2475,9 @@ declare class TokenRefreshScheduler {
2223
2475
  sweep(now?: number): Promise<void>;
2224
2476
  /** Expiring within the lead window, refreshable, and not already dead. */
2225
2477
  private needsRefresh;
2226
- /** Refresh one account; failures are logged, never thrown (the store has
2227
- * already flagged the account `expired`). */
2478
+ /** Refresh one managed account; failures are logged, never thrown. The
2479
+ * store marks only the targeted account `expired` on a failed refresh.
2480
+ */
2228
2481
  private refreshOne;
2229
2482
  private refreshActive;
2230
2483
  }
@@ -2406,10 +2659,20 @@ interface Daemon {
2406
2659
  /** Subscription account service (token-free `listAll`) — now exposed for the
2407
2660
  * admin dashboard's read-only accounts panel (RT3). */
2408
2661
  readonly subscriptionAccounts: SubscriptionAccountService;
2662
+ /** Secret-free upstream subscription allowance cache/collector facade. */
2663
+ readonly accountAllowanceService: AccountAllowanceService;
2664
+ /**
2665
+ * Claude allowance cache maintenance for allowance-aware routing. Constructed
2666
+ * armed-off; the resident `start` command starts it only when the persisted
2667
+ * scheduling policy is enabled.
2668
+ */
2669
+ readonly claudeAllowanceRefreshScheduler: ClaudeAllowanceRefreshScheduler;
2409
2670
  /** File-backed pricing table (`pricing.json`; concrete for the admin DELETE). */
2410
2671
  readonly pricingStore: JsonPricingStore;
2411
2672
  /** Pricing engine (cost calc + source refresh + conflict resolution). */
2412
2673
  readonly pricingEngine: PricingEngine;
2674
+ /** Non-blocking stale-while-revalidate catalog worker (armed by `start`). */
2675
+ readonly pricingRefreshScheduler: PricingRefreshScheduler;
2413
2676
  /** Usage recorder over `usage-events.jsonl` — also the admin stats query facade. */
2414
2677
  readonly usageRecorder: UsageRecorder;
2415
2678
  /** The localhost admin/dashboard HTTP listener (RT3). Started by `start.ts`. */