@omnicross/daemon 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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
- import { AuditRecord, AuditConfig } from '@omnicross/contracts/audit-types';
18
+ import { AuditRecord, AuditStats, 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,27 @@ 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.
582
+ *
583
+ * `redactBodies` is REQUIRED here: this round-trip sends the refresh_token and
584
+ * receives a fresh access/refresh token pair. Carrying a `providerId` opts the
585
+ * call into the upstream trace (so a failing refresh is diagnosable), and the
586
+ * trace captures bodies verbatim — without this flag every refresh would write
587
+ * a plaintext token pair into `upstream-trace.jsonl`.
585
588
  */
586
589
  buildRefreshFetch(providerId: string, accountId?: string): FetchLike;
587
590
  /**
588
- * In-flight refresh coalescing (external-cli-sync). OAuth refresh tokens are
591
+ * In-flight refresh coalescing. OAuth refresh tokens are
589
592
  * SINGLE-USE: two concurrent refreshes of one account each spend the same
590
593
  * token and the loser bricks a healthy account. Every refresh entry point
591
594
  * (auth-strategy lazy refresh, 401 retry, background scheduler) funnels
@@ -594,11 +597,11 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
594
597
  private readonly inFlightRefreshes;
595
598
  private coalesce;
596
599
  /** 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
600
+ * file is absent/corrupt). This is the hot read the codex / gemini auth
598
601
  * strategies pull `accessToken` / `expiresAt` / `status` from it. */
599
602
  getFullConfig(): Promise<AccountTokensConfig>;
600
603
  /** 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
604
+ * refresh here the lead-window / 401-retry refresh is driven by the
602
605
  * subscription auth strategy, which calls `refreshClaudeToken` (now real). */
603
606
  getValidClaudeAccessToken(): Promise<string | null>;
604
607
  /** Current OpenCodeGo static API key, or `null` when none is stored. */
@@ -614,28 +617,25 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
614
617
  /**
615
618
  * DAEMON-ONLY sanitized accounts list (design D8, NOT on the port). Projects
616
619
  * each provider's accounts to the secret-free `SubscriptionAccountSanitized`
617
- * shape (id/label/status/expiresAt/hasAccessToken/isActive) NEVER a token.
620
+ * shape (id/label/status/expiresAt/hasAccessToken/isActive) NEVER a token.
618
621
  * Used by the admin accounts GET (secret-IN-never-OUT).
619
622
  */
620
623
  listSanitizedAccounts(): Promise<Record<string, SubscriptionAccountSanitized[]>>;
621
624
  /**
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.
625
+ * List-time managed-credential conflict warnings. Computed, not persisted:
626
+ * `duplicate-token` is projected when two accounts of one provider share a
627
+ * credential. This deliberately does not inspect either native CLI file.
628
628
  */
629
- private attachSyncWarnings;
629
+ private attachDuplicateWarnings;
630
630
  /** Read the external CLI store, never letting an fs/parse error escape. */
631
631
  private safeReadExternal;
632
632
  /**
633
633
  * 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
634
+ * the block has no refresh_token (setup-token / manual) no upstream call, the
635
635
  * block is untouched. Otherwise mint via the shared claude refresh flow and
636
636
  * write back access+refresh+expiresAt+status:authorized+lastRefreshedAt.
637
- * On failure status:expired +
638
- * errorMessage `false`.
637
+ * On failure status:expired +
638
+ * errorMessage `false`.
639
639
  */
640
640
  refreshClaudeToken(): Promise<boolean>;
641
641
  /**
@@ -653,11 +653,10 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
653
653
  */
654
654
  refreshGeminiToken(): Promise<boolean>;
655
655
  /**
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`.
656
+ * Refresh a SPECIFIC managed account by id (background scheduler sweep and
657
+ * account-pool resolution). It uses only that account's stored refresh
658
+ * token. Coalesced per `provider:id`; on failure flags ONLY that account
659
+ * `expired`.
661
660
  */
662
661
  refreshAccountById(provider: 'claude' | 'codex' | 'gemini', id: string): Promise<boolean>;
663
662
  /**
@@ -672,7 +671,7 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
672
671
  /**
673
672
  * Refresh a SPECIFIC account's OAuth token by id (design D6/D7). Delegates to
674
673
  * `refreshAccountById` (coalesced per `provider:id`); opencodego is a static key
675
- * `false` (no refresh affordance).
674
+ * `false` (no refresh affordance).
676
675
  */
677
676
  refreshAccountToken(providerId: SubscriptionProviderId, accountId: string): Promise<boolean>;
678
677
  /**
@@ -686,7 +685,7 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
686
685
  * (subscription-client-fingerprint #7, P2). Entry-metadata only (NON-secret
687
686
  * whitelisted fingerprint headers; the token mirror is untouched); a no-op for
688
687
  * 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
688
+ * freeze / TTL refresh, so it stays infrequent. Never throws to the caller the
690
689
  * store's port wrapper swallows a rejection so the relay hot path is unaffected.
691
690
  */
692
691
  setAccountIdentity(providerId: SubscriptionProviderId, accountId: string, identity: AccountClientIdentity): Promise<void>;
@@ -702,7 +701,7 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
702
701
  * DAEMON-ONLY set/clear per-account proxy (upstream-proxy, admin write, NOT on
703
702
  * the port). Passing `undefined` clears the override. Write-only password: when
704
703
  * the incoming structured proxy omits the password but the account already had
705
- * one, the current (decrypted) password is preserved editing host/port never
704
+ * one, the current (decrypted) password is preserved editing host/port never
706
705
  * wipes the secret. Persist re-encrypts `proxy.password` via the tokens SecretBox.
707
706
  */
708
707
  setAccountProxy(providerId: SubscriptionProviderId, accountId: string, proxy: ProxyConfig | undefined): Promise<{
@@ -719,44 +718,36 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
719
718
  }>;
720
719
  /** Dispatch one OAuth refresh round-trip to the provider's shared flow. */
721
720
  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;
721
+ /** Atomically patch one account's non-secret management metadata. */
722
+ patchAccountMetadata(providerId: SubscriptionProviderId, accountId: string, patch: AccountMetadataPatch): Promise<{
723
+ ok: boolean;
724
+ }>;
725
+ /** Validate every target, then persist one all-or-nothing batch mutation. */
726
+ batchManageAccounts(refs: AccountRef[], mutation: AccountBatchMutation): Promise<{
727
+ ok: true;
728
+ affected: number;
729
+ } | {
730
+ ok: false;
731
+ missing: AccountRef;
732
+ }>;
744
733
  /**
745
734
  * DAEMON-ONLY (admin import button): which providers have a usable external
746
- * CLI credential on THIS machine. Pure detection reads the native files,
735
+ * CLI credential on THIS machine. Pure detection reads the native files,
747
736
  * never mutates anything, never returns a token.
748
737
  */
749
738
  listExternalCliAvailability(): Promise<Record<ExternalCliProvider, boolean>>;
750
739
  /**
751
740
  * 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.
741
+ * as a NEW account (+ activate). This is a COPY-ONLY import: Omnicross never
742
+ * claims, writes, moves, restores, or deletes the native CLI credential file
743
+ * or any legacy `.omnicross-managed` marker/backup beside it. Subsequent
744
+ * refreshes persist only Omnicross's encrypted token store.
756
745
  */
757
746
  importExternalCliAccount(provider: ExternalCliProvider, label?: string): Promise<{
758
747
  ok: true;
759
748
  id: string;
749
+ nativeCredentialMode: 'read-only';
750
+ refreshWritesNativeCredentials: false;
760
751
  } | {
761
752
  ok: false;
762
753
  reason: 'no-credential';
@@ -788,13 +779,13 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
788
779
  * `updatedAt`, and re-persist `tokens.json` as pretty JSON. Preserves every
789
780
  * OTHER provider's existing block (read-merge-write, not overwrite). Reuses the
790
781
  * 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
782
+ * so a first-ever write still produces a valid config. No cache the next read
792
783
  * sees this write.
793
784
  */
794
785
  writeProviderTokens(providerId: SubscriptionProviderId, config: SubscriptionTokenBlock): Promise<void>;
795
786
  /**
796
787
  * 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
788
+ * (optional label) and set it active, then re-derive the mirror used by
798
789
  * `omnicross login <provider> --label` to add an account instead of overwriting.
799
790
  */
800
791
  appendProviderAccount(providerId: SubscriptionProviderId, config: SubscriptionTokenBlock, label?: string): Promise<{
@@ -817,7 +808,7 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
817
808
  }>;
818
809
  /**
819
810
  * 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
811
+ * rejects an unknown id. Label-only no token material is read or written
821
812
  * (the secret-free invariant holds).
822
813
  */
823
814
  renameAccount(providerId: SubscriptionProviderId, id: string, label: string): Promise<{
@@ -832,8 +823,8 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
832
823
  clearProvider(providerId: SubscriptionProviderId): Promise<void>;
833
824
  /** Write the merged config to disk as pretty JSON (mkdir parent if needed).
834
825
  * 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. */
826
+ * `enc:v1:`; already-`enc:`/`$ENV` untouched) before serializing, so any
827
+ * write incl. child 4's future refresh writes lands encrypted. */
837
828
  private persist;
838
829
  /**
839
830
  * Read + parse `tokens.json`, tolerating a missing/corrupt file, then DECRYPT
@@ -841,11 +832,11 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
841
832
  * subscription bearer path is byte-identical).
842
833
  *
843
834
  * 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
835
+ * file empty `{ updatedAt: '' }`). The DECRYPT runs OUTSIDE the try, so a
845
836
  * 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
837
+ * box's clear, secret-free error (secrets spec "/ UX":
838
+ * SHALL fail-fast, SHALL NOT a swallowed decrypt would report "no
839
+ * tokens" and silently send the WRONG bearer upstream 401). Mirrors
849
840
  * `config.ts loadConfig`, which decrypts outside its parse try.
850
841
  */
851
842
  private readConfig;
@@ -891,6 +882,18 @@ interface SubscriptionTokenWriter {
891
882
  setAccountPriority(providerId: SubscriptionProviderId, id: string, priority: number): Promise<{
892
883
  ok: boolean;
893
884
  }>;
885
+ /** Patch non-secret account management metadata in one write. */
886
+ patchAccountMetadata(providerId: SubscriptionProviderId, id: string, patch: AccountMetadataPatch): Promise<{
887
+ ok: boolean;
888
+ }>;
889
+ /** All-or-nothing multi-provider account management mutation. */
890
+ batchManageAccounts(refs: AccountRef[], mutation: AccountBatchMutation): Promise<{
891
+ ok: true;
892
+ affected: number;
893
+ } | {
894
+ ok: false;
895
+ missing: AccountRef;
896
+ }>;
894
897
  /** Set (or CLEAR, with `undefined`) one account's per-account proxy override
895
898
  * (upstream-proxy). The `proxy.password` is a secret (encrypted at rest, masked
896
899
  * in the sanitized view). Rejects an unknown id. */
@@ -912,6 +915,8 @@ interface SubscriptionTokenWriter {
912
915
  importExternalCliAccount(providerId: 'claude' | 'codex', label?: string): Promise<{
913
916
  ok: true;
914
917
  id: string;
918
+ nativeCredentialMode: 'read-only';
919
+ refreshWritesNativeCredentials: false;
915
920
  } | {
916
921
  ok: false;
917
922
  reason: 'no-credential';
@@ -924,11 +929,12 @@ interface SubscriptionTokenWriter {
924
929
  * app-parity child 4, design D1).
925
930
  *
926
931
  * `start` mints a crypto-random `sessionId` and stashes the per-session PKCE
927
- * `{ providerId, codeVerifier, state }` here; `complete` does a SINGLE-USE
928
- * `take(sessionId)` (returns + deletes) and exchanges the code. The map is
932
+ * `{ providerId, codeVerifier, state }` here; `complete` `peek`s the session,
933
+ * exchanges the code, and `consume`s it ONLY once a token was minted so a
934
+ * failed exchange leaves the session retryable instead of burning it. The map is
929
935
  * NEVER serialized to the client — only the opaque `sessionId` + the public
930
936
  * `authUrl` cross the wire. Sessions are short-lived (OQ3 = 10-min TTL); a sweep
931
- * reaps abandoned sessions, and `take` re-checks the TTL so an expired-but-not-
937
+ * reaps abandoned sessions, and `peek` re-checks the TTL so an expired-but-not-
932
938
  * yet-swept session is still rejected. A daemon restart simply drops in-flight
933
939
  * logins (correct fail-safe — no partial token is ever written).
934
940
  *
@@ -964,13 +970,25 @@ declare class OAuthSessionStore {
964
970
  */
965
971
  put(session: Omit<PendingOAuthSession, 'createdAt'>): string;
966
972
  /**
967
- * SINGLE-USE consume: return + delete the session for `sessionId`, or `null`
968
- * when it is unknown, already used, or past its TTL (in which case it is
969
- * dropped). A `null` return means the completer must reject (no exchange, no
970
- * write).
973
+ * NON-DESTRUCTIVE lookup: return the session for `sessionId`, or `null` when
974
+ * it is unknown, already consumed, or past its TTL (an expired entry is
975
+ * dropped here). A `null` return means the completer must reject (no
976
+ * exchange, no write).
977
+ *
978
+ * Deliberately NOT a consume: the completer peeks, runs the token exchange,
979
+ * and only {@link consume}s once a token has actually been minted. Consuming
980
+ * up-front burned the session on EVERY failed exchange (a mistyped/expired
981
+ * pasted code, a proxy hiccup), so the user's natural retry hit
982
+ * "session is unknown, expired, or already used" and the login became
983
+ * unrecoverable without restarting the whole flow.
984
+ */
985
+ peek(sessionId: string): PendingOAuthSession | null;
986
+ /**
987
+ * SINGLE-USE burn: drop the session so the same `sessionId` can never be
988
+ * completed twice. Called ONLY after a successful token exchange.
971
989
  */
972
- take(sessionId: string): PendingOAuthSession | null;
973
- /** Drop every session past its TTL. Called on each put/take. */
990
+ consume(sessionId: string): void;
991
+ /** Drop every session past its TTL. Called on each put/peek. */
974
992
  private sweep;
975
993
  }
976
994
 
@@ -984,10 +1002,12 @@ declare class OAuthSessionStore {
984
1002
  * provider's authorize params and stashes the per-session `{ codeVerifier, state }`
985
1003
  * in the `OAuthSessionStore` keyed by a minted opaque `sessionId`, returning ONLY
986
1004
  * `{ authUrl, sessionId }` (the `authUrl` carries client_id + PKCE challenge +
987
- * state — all public). `complete` does a SINGLE-USE `take(sessionId)`, validates
988
- * state (claude's `code#state`), `exchangeCodeForTokens(...)`, persists the minted
989
- * token through the encrypted credential store (`appendProviderAccount`) + marks
990
- * it active, and responds ONLY the sanitized `SubscriptionListEntry`.
1005
+ * state — all public). `complete` `peek`s the session, validates state (claude's
1006
+ * `code#state`), `exchangeCodeForTokens(...)`, then and ONLY then — `consume`s
1007
+ * the session (single-use), persists the minted token through the encrypted
1008
+ * credential store (`appendProviderAccount`) + marks it active, and responds ONLY
1009
+ * the sanitized `SubscriptionListEntry`. A FAILED exchange leaves the session
1010
+ * intact so the user can retry within its TTL instead of being locked out by a 410.
991
1011
  *
992
1012
  * SECRET SPINE (the load-bearing invariant): the minted access/refresh token
993
1013
  * NEVER appears in any response body or log; the `codeVerifier` / session map is
@@ -1084,6 +1104,127 @@ declare class CodexOAuthSessionStore {
1084
1104
  private sweep;
1085
1105
  }
1086
1106
 
1107
+ /**
1108
+ * Claude OAuth usage collector.
1109
+ *
1110
+ * Fetches one account's five-hour, seven-day, and seven-day Sonnet windows,
1111
+ * coalesces concurrent refreshes by account, and caches every result for five
1112
+ * minutes. Tokens and raw upstream payloads never leave this module.
1113
+ */
1114
+
1115
+ interface ClaudeAllowanceCredentialReader {
1116
+ getAccessTokenForAccount(providerId: 'claude', accountId: string): Promise<string | null>;
1117
+ refreshAccountToken(providerId: 'claude', accountId: string): Promise<boolean>;
1118
+ }
1119
+ type ClaudeAllowanceFetch = (url: string, init: RequestInit, accountId: string) => Promise<Response>;
1120
+ interface ClaudeAllowanceCollectOptions {
1121
+ force?: boolean;
1122
+ /**
1123
+ * Treat an otherwise valid cache entry as due when it will expire within this
1124
+ * window. The resident background scheduler uses this to refresh shortly
1125
+ * before expiry without bypassing the normal cache on every tick.
1126
+ */
1127
+ refreshAheadMs?: number;
1128
+ }
1129
+ declare class ClaudeAllowanceCollector {
1130
+ private readonly credentials;
1131
+ private readonly store;
1132
+ private readonly fetchImpl;
1133
+ private readonly identityStore;
1134
+ private readonly now;
1135
+ private readonly inFlight;
1136
+ constructor(credentials: ClaudeAllowanceCredentialReader, store?: AccountAllowanceStore, fetchImpl?: ClaudeAllowanceFetch, identityStore?: SubscriptionIdentityStore, now?: () => number);
1137
+ collectMany(accounts: readonly SubscriptionAccountEntry<ClaudeTokenConfig>[], options?: ClaudeAllowanceCollectOptions): Promise<AccountAllowanceSnapshot[]>;
1138
+ collect(account: SubscriptionAccountEntry<ClaudeTokenConfig>, options?: ClaudeAllowanceCollectOptions): Promise<AccountAllowanceSnapshot>;
1139
+ private isCacheValid;
1140
+ private fetchAccount;
1141
+ private request;
1142
+ private failureSnapshot;
1143
+ private unsupportedSnapshot;
1144
+ }
1145
+
1146
+ /** Secret-free account allowance query/refresh facade used by the admin API. */
1147
+
1148
+ interface AccountAllowanceCredentialReader extends ClaudeAllowanceCredentialReader {
1149
+ getFullConfig(): Promise<AccountTokensConfig>;
1150
+ }
1151
+ interface AccountAllowanceFilter {
1152
+ providerId?: SubscriptionProviderId;
1153
+ accountId?: string;
1154
+ }
1155
+ interface AccountAllowanceSchedulingStatus {
1156
+ config: AllowanceSchedulingConfig;
1157
+ history: AllowanceSchedulingDecision[];
1158
+ }
1159
+ declare class AccountAllowanceService {
1160
+ private readonly credentials;
1161
+ private readonly store;
1162
+ private readonly now;
1163
+ readonly claudeCollector: ClaudeAllowanceCollector;
1164
+ constructor(credentials: AccountAllowanceCredentialReader, store?: AccountAllowanceStore, collector?: ClaudeAllowanceCollector, now?: () => number);
1165
+ /**
1166
+ * Read all/filtered snapshots. Claude's five-minute cache is refreshed lazily;
1167
+ * Codex remains passive and reports not-observed until a real model response.
1168
+ */
1169
+ list(filter?: AccountAllowanceFilter): Promise<AccountAllowanceSnapshot[]>;
1170
+ /** Force-refresh Claude usage for one account or every stored Claude account. */
1171
+ refreshClaude(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
1172
+ /**
1173
+ * Keep Claude snapshots warm for allowance-aware routing. This deliberately
1174
+ * excludes Codex (whose quota is learned from real response headers) and
1175
+ * preserves the collector's cache + per-account in-flight coalescing.
1176
+ */
1177
+ maintainClaudeCache(refreshAheadMs: number): Promise<void>;
1178
+ /** Remove a cache row as soon as an account is deleted by the admin path. */
1179
+ removeAccountSnapshot(providerId: SubscriptionProviderId, accountId: string): void;
1180
+ /** Remove all allowance rows for a provider block that was deleted. */
1181
+ removeProviderSnapshots(providerId: SubscriptionProviderId): void;
1182
+ /** Secret-free policy diagnostics for the settings/accounts UI. */
1183
+ getSchedulingStatus(): AccountAllowanceSchedulingStatus;
1184
+ }
1185
+
1186
+ /**
1187
+ * Low-frequency, non-blocking cache maintenance for Claude allowance snapshots.
1188
+ *
1189
+ * The routing policy intentionally ignores stale quota data. Without a resident
1190
+ * UI poll, the five-minute Claude snapshot would therefore age out and silently
1191
+ * stop influencing account selection. This worker checks once a minute and asks
1192
+ * the existing collector to refresh entries that are close to expiry. The
1193
+ * collector remains the cache/coalescing authority, so a tick normally performs
1194
+ * no network I/O and can share an in-flight request with the admin UI.
1195
+ *
1196
+ * Zero-regression invariant: the worker does not arm a timer or perform an
1197
+ * initial sweep unless `server.allowanceScheduling.enabled` is true.
1198
+ */
1199
+
1200
+ interface ClaudeAllowanceCacheMaintainer {
1201
+ maintainClaudeCache(refreshAheadMs: number): Promise<void>;
1202
+ }
1203
+ declare class ClaudeAllowanceRefreshScheduler {
1204
+ private readonly service;
1205
+ private readonly logger;
1206
+ private readonly intervalMs;
1207
+ private readonly refreshAheadMs;
1208
+ private timer;
1209
+ private started;
1210
+ private enabled;
1211
+ private sweeping;
1212
+ constructor(service: ClaudeAllowanceCacheMaintainer, logger: Logger, intervalMs?: number, refreshAheadMs?: number);
1213
+ /**
1214
+ * Apply live server policy. Once started, enable/disable changes arm or disarm
1215
+ * immediately; the initial enabled sweep is fire-and-forget.
1216
+ */
1217
+ configure(config: AllowanceSchedulingConfig | undefined): void;
1218
+ /** Start the lifecycle. Disabled policy remains completely inert. */
1219
+ start(): void;
1220
+ /** Stop all future checks. Idempotent and safe during an in-flight refresh. */
1221
+ dispose(): void;
1222
+ /** One non-overlapping cache-maintenance pass. Exposed for focused tests. */
1223
+ sweep(): Promise<void>;
1224
+ private arm;
1225
+ private disarm;
1226
+ }
1227
+
1087
1228
  /**
1088
1229
  * ProbeStrategy — the per-provider two-tier probe plan
1089
1230
  * (subscription-account-probe #8, design D1).
@@ -1163,7 +1304,14 @@ interface ProbeRecord {
1163
1304
  /** Upstream round-trip latency (ms); absent for a local-tier record. */
1164
1305
  latencyMs?: number;
1165
1306
  /** Which tier produced this record. */
1166
- tier: 'local' | 'upstream';
1307
+ tier: 'local' | 'upstream' | 'generation';
1308
+ }
1309
+ /** Secret-free outcome returned by the manual account connection-test route. */
1310
+ interface AccountConnectionProbeOutcome {
1311
+ ok: boolean;
1312
+ marked: boolean;
1313
+ tier: ProbeRecord['tier'];
1314
+ model?: string;
1167
1315
  }
1168
1316
  /** Per-account probe history for the authed admin surface (names account ids). */
1169
1317
  interface AccountProbeHistorySnapshot {
@@ -1183,6 +1331,8 @@ interface AccountProbeHistoryReader {
1183
1331
  interface ProbeCredentialStore {
1184
1332
  getFullConfig(): Promise<AccountTokensConfig>;
1185
1333
  getAccessTokenForAccount(providerId: SubscriptionProviderId, accountId: string): Promise<string | null>;
1334
+ /** Optional serving-parity retry seam for an upstream Codex 401. */
1335
+ refreshAccountToken?(providerId: SubscriptionProviderId, accountId: string): Promise<boolean>;
1186
1336
  }
1187
1337
  /** Proxy-aware upstream fetch signature (#3 `fetchUpstream`). */
1188
1338
  type ProbeFetch = typeof fetchUpstream;
@@ -1232,10 +1382,13 @@ declare class AccountHealthProbeScheduler implements AccountProbeHistoryReader {
1232
1382
  * no upstream); else the upstream tier when a verified endpoint exists. Records
1233
1383
  * the rolling history entry either way; returns whether the tracker was MARKED.
1234
1384
  */
1235
- probeAccount(providerId: SubscriptionProviderId, accountId: string): Promise<{
1236
- ok: boolean;
1237
- marked: boolean;
1238
- }>;
1385
+ probeAccount(providerId: SubscriptionProviderId, accountId: string): Promise<AccountConnectionProbeOutcome>;
1386
+ /**
1387
+ * Manual connection test. Codex performs a real, quota-consuming generation;
1388
+ * every other provider keeps its existing cheap probe. Scheduled sweeps never
1389
+ * call this method, so they remain non-billable.
1390
+ */
1391
+ testAccountConnection(providerId: SubscriptionProviderId, accountId: string): Promise<AccountConnectionProbeOutcome>;
1239
1392
  /** Per-account rolling history for the authed admin surface (design D5). */
1240
1393
  getAllHistory(): AccountProbeHistorySnapshot[];
1241
1394
  /**
@@ -1254,6 +1407,7 @@ declare class AccountHealthProbeScheduler implements AccountProbeHistoryReader {
1254
1407
  private record;
1255
1408
  /** Read a bounded slice of the response body for the 403-ban sniff (never throws). */
1256
1409
  private readBounded;
1410
+ private runCodexGenerationAttempt;
1257
1411
  private key;
1258
1412
  private parseKey;
1259
1413
  }
@@ -1280,6 +1434,18 @@ interface AuditQuery {
1280
1434
  limit?: number;
1281
1435
  }
1282
1436
 
1437
+ /**
1438
+ * Metadata-only audit aggregation. The overview needs request/error counts, not
1439
+ * multi-gigabyte request/response bodies. A compact per-day sidecar is updated
1440
+ * with each new record; legacy files are scanned once with a bounded prefix per
1441
+ * JSONL row and then cached in the same sidecar.
1442
+ */
1443
+
1444
+ interface AuditStatsQuery {
1445
+ from?: number;
1446
+ to?: number;
1447
+ }
1448
+
1283
1449
  /**
1284
1450
  * auditQueryApi — the AUTHED `GET /admin/api/audit?keyId=&from=&to=&limit=`
1285
1451
  * handler (request-audit-log, design D6).
@@ -1299,6 +1465,8 @@ interface AuditQuery {
1299
1465
 
1300
1466
  /** The read surface the AdminServer consumes (bootstrap binds it to the store). */
1301
1467
  type AuditQueryReader = (query: AuditQuery) => AuditRecord[];
1468
+ /** Metadata-only aggregate reader used by the overview. */
1469
+ type AuditStatsReader = (query: AuditStatsQuery) => Promise<AuditStats> | AuditStats;
1302
1470
 
1303
1471
  /**
1304
1472
  * billingStatusApi — the AUTHED `GET /admin/api/billing-status` handler
@@ -1498,6 +1666,14 @@ declare class JsonApiServerSettingsStore implements ApiServerSettingsStore {
1498
1666
  declare class JsonPricingStore implements PricingStore {
1499
1667
  private readonly pricingPath;
1500
1668
  constructor(pricingPath: string);
1669
+ /**
1670
+ * Return whether the durable snapshot can actually serve at least one price.
1671
+ *
1672
+ * This intentionally checks the file itself instead of relying on refresh
1673
+ * metadata: a recent `lastSuccessAt` must not hide a deleted, truncated, or
1674
+ * otherwise unusable pricing table after a crash or manual file edit.
1675
+ */
1676
+ hasUsableSnapshot(): boolean;
1501
1677
  getAll(): Promise<PricingEntry[]>;
1502
1678
  /**
1503
1679
  * Insert or update one row keyed (providerId, modelId). `asUserEdit` stamps
@@ -1510,10 +1686,10 @@ declare class JsonPricingStore implements PricingStore {
1510
1686
  /**
1511
1687
  * Apply a batch fetched from a pricing source. Rows whose local copy is
1512
1688
  * 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.
1689
+ * conflicts; everything else is upserted with the supplied automatic source.
1690
+ * ONE file write for the whole batch.
1515
1691
  */
1516
- bulkApplyFromSource(entries: PricingEntryInput[]): Promise<{
1692
+ bulkApplyFromSource(entries: PricingEntryInput[], source?: AutomaticPricingSource): Promise<{
1517
1693
  applied: PricingEntry[];
1518
1694
  conflicts: Array<{
1519
1695
  current: PricingEntry;
@@ -1539,6 +1715,98 @@ declare class JsonPricingStore implements PricingStore {
1539
1715
  /** Read the pricing rows, tolerating a missing/corrupt file (→ empty list). */
1540
1716
  private readRows;
1541
1717
  private writeRows;
1718
+ /** Isolated for deterministic failure testing; never removes the target. */
1719
+ private replaceFile;
1720
+ }
1721
+
1722
+ type IntegrationClientId = 'codex' | 'claude';
1723
+ type IntegrationStatusKind = 'not-installed' | 'enabled' | 'configuration-drift' | 'configuration-missing' | 'key-missing';
1724
+ interface IntegrationClientStatus {
1725
+ client: IntegrationClientId;
1726
+ status: IntegrationStatusKind;
1727
+ configPath: string;
1728
+ installedAt?: number;
1729
+ gatewayBaseUrl?: string;
1730
+ message?: string;
1731
+ }
1732
+ interface IntegrationChangePlan {
1733
+ client: IntegrationClientId;
1734
+ configPath: string;
1735
+ action: 'install' | 'none' | 'repair';
1736
+ canApply: boolean;
1737
+ /** Redacted logical fields only; never file contents or credential values. */
1738
+ changes: string[];
1739
+ warnings: string[];
1740
+ }
1741
+ interface IntegrationInstallRecord {
1742
+ client: IntegrationClientId;
1743
+ configPath: string;
1744
+ originalExisted: boolean;
1745
+ /** Encrypted by IntegrationStateStore before it reaches disk. */
1746
+ originalContent: string;
1747
+ originalHash: string;
1748
+ installedHash: string;
1749
+ installedAt: number;
1750
+ gatewayBaseUrl: string;
1751
+ /** Codex auth.json snapshot and installed hash; absent on legacy records. */
1752
+ credentialFile?: IntegrationManagedFileRecord;
1753
+ }
1754
+ interface IntegrationManagedFileRecord {
1755
+ path: string;
1756
+ originalExisted: boolean;
1757
+ /** Encrypted by IntegrationStateStore before it reaches disk. */
1758
+ originalContent: string;
1759
+ originalHash: string;
1760
+ installedHash: string;
1761
+ }
1762
+ interface IntegrationGatewayKeyRecord {
1763
+ id: string;
1764
+ /** Encrypted by IntegrationStateStore before it reaches disk. */
1765
+ secret: string;
1766
+ createdAt: number;
1767
+ }
1768
+ interface IntegrationState {
1769
+ version: 1;
1770
+ gatewayKey?: IntegrationGatewayKeyRecord;
1771
+ clients: Partial<Record<IntegrationClientId, IntegrationInstallRecord>>;
1772
+ }
1773
+
1774
+ /** Encrypted, Omnicross-owned state for reversible native CLI configuration. */
1775
+ declare class IntegrationStateStore {
1776
+ readonly path: string;
1777
+ private readonly box;
1778
+ constructor(path: string, box: SecretBox);
1779
+ load(): IntegrationState;
1780
+ save(state: IntegrationState): void;
1781
+ }
1782
+
1783
+ interface IntegrationManagerOptions {
1784
+ configPath: string;
1785
+ gatewayBaseUrl: string;
1786
+ keyDb: OutboundKeyDb;
1787
+ stateStore: IntegrationStateStore;
1788
+ homeDir?: string;
1789
+ }
1790
+ /** Coordinates a least-privilege gateway key with reversible native CLI config edits. */
1791
+ declare class IntegrationManager {
1792
+ private readonly options;
1793
+ private readonly homeDir;
1794
+ constructor(options: IntegrationManagerOptions);
1795
+ listStatus(): Promise<IntegrationClientStatus[]>;
1796
+ plan(client: IntegrationClientId, configPath?: string): Promise<IntegrationChangePlan>;
1797
+ install(client: IntegrationClientId, configPath?: string): Promise<IntegrationClientStatus>;
1798
+ repair(client: IntegrationClientId): Promise<IntegrationClientStatus>;
1799
+ remove(client: IntegrationClientId): Promise<IntegrationClientStatus>;
1800
+ rotateGatewayKey(): Promise<{
1801
+ keyId: string;
1802
+ }>;
1803
+ getGatewayToken(): Promise<string>;
1804
+ private ensureGatewayKey;
1805
+ private isKeyUsable;
1806
+ private statusFor;
1807
+ private defaultConfigPath;
1808
+ private codexAuthPathForConfig;
1809
+ private renderInstalled;
1542
1810
  }
1543
1811
 
1544
1812
  /**
@@ -1623,22 +1891,18 @@ interface MigrationCredentialStore extends SubscriptionAccountAppender {
1623
1891
  getFullConfig(): Promise<AccountTokensConfig>;
1624
1892
  }
1625
1893
 
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
- */
1894
+ /** Minimal, auth-gated admin API for secret-free account allowance snapshots. */
1895
+
1896
+ interface AccountAllowanceAdminReader {
1897
+ list(filter?: {
1898
+ providerId?: SubscriptionProviderId;
1899
+ accountId?: string;
1900
+ }): Promise<AccountAllowanceSnapshot[]>;
1901
+ refreshClaude(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
1902
+ removeAccountSnapshot?(providerId: SubscriptionProviderId, accountId: string): void;
1903
+ removeProviderSnapshots?(providerId: SubscriptionProviderId): void;
1904
+ getSchedulingStatus?(): AccountAllowanceSchedulingStatus;
1905
+ }
1642
1906
 
1643
1907
  /** Token-free subscription account list entry (passthrough from core's service). */
1644
1908
  interface AdminAccountsLister {
@@ -1666,7 +1930,7 @@ interface AdminApiDeps {
1666
1930
  /** Live provider catalog (hot-reload target). */
1667
1931
  readonly llmConfig: ConfigFileProviderConfigSource;
1668
1932
  /** Named outbound-key store. */
1669
- readonly keyDb: OutboundKeyDb;
1933
+ readonly keyDb: OutboundKeyDb$1;
1670
1934
  /**
1671
1935
  * OPTIONAL voucher (redemption-card) store (voucher-redemption #9). When wired,
1672
1936
  * the `/admin/api/voucher` surface can generate/list/revoke cards. Absent ⇒ the
@@ -1685,6 +1949,26 @@ interface AdminApiDeps {
1685
1949
  readonly outboundApiServer: OutboundApiServer;
1686
1950
  /** Subscription accounts (token-free `listAll`). */
1687
1951
  readonly subscriptionAccounts: AdminAccountsLister;
1952
+ /**
1953
+ * Secret-free upstream allowance facade. Optional for lightweight embedders;
1954
+ * the standalone daemon wires it and the route returns 501 when absent.
1955
+ */
1956
+ readonly accountAllowanceService?: AccountAllowanceAdminReader;
1957
+ /** Live Claude cache worker; hot-reconfigured with allowance scheduling. */
1958
+ readonly allowanceRefreshScheduler?: Pick<ClaudeAllowanceRefreshScheduler, 'configure'>;
1959
+ /** Optional secret-free account connection probe + rolling history surface. */
1960
+ readonly accountProbeService?: AccountProbeHistoryReader & {
1961
+ probeAccount(providerId: _omnicross_contracts_subscription_types.SubscriptionProviderId, accountId: string): Promise<{
1962
+ ok: boolean;
1963
+ marked: boolean;
1964
+ }>;
1965
+ testAccountConnection(providerId: _omnicross_contracts_subscription_types.SubscriptionProviderId, accountId: string): Promise<{
1966
+ ok: boolean;
1967
+ marked: boolean;
1968
+ tier: 'local' | 'upstream' | 'generation';
1969
+ model?: string;
1970
+ }>;
1971
+ };
1688
1972
  /**
1689
1973
  * Least-authority subscription-token WRITER (design D4) — ONLY the mutation
1690
1974
  * methods (`writeProviderTokens` / `clearProvider`), never a token-returning
@@ -1706,11 +1990,13 @@ interface AdminApiDeps {
1706
1990
  */
1707
1991
  readonly oauthSessions: OAuthSessionStore;
1708
1992
  /**
1709
- * Injected token-exchange `FetchLike` (oauth design D2-a) — defaults to global
1710
- * `fetch` in `bootstrap.ts`; tests inject a mock so no real token endpoint is
1711
- * hit. Mirrors how `login.ts` injects its exchange fetch.
1993
+ * Injected token-exchange `FetchLike` FACTORY (oauth design D2-a) — built per
1994
+ * provider in `bootstrap.ts` so the exchange carries a `{ providerId }` egress
1995
+ * ctx (per-provider proxy layer + upstream trace, bodies redacted); tests
1996
+ * inject a mock so no real token endpoint is hit. Mirrors how `login.ts`
1997
+ * injects its exchange fetch.
1712
1998
  */
1713
- readonly oauthExchangeFetch: FetchLike;
1999
+ readonly oauthExchangeFetch: (providerId: SubscriptionProviderId) => FetchLike;
1714
2000
  /**
1715
2001
  * NARROW append handle (oauth design D2-a) — the OAuth complete handler needs
1716
2002
  * `appendProviderAccount` (NOT on the least-authority `SubscriptionTokenWriter`).
@@ -1765,6 +2051,8 @@ interface AdminApiDeps {
1765
2051
  * actually runs.
1766
2052
  */
1767
2053
  readonly cliCommandRunner?: CommandRunner;
2054
+ /** Factory so each request observes the outbound server's current loopback port. */
2055
+ readonly integrationManagerFactory?: () => IntegrationManager;
1768
2056
  }
1769
2057
  /**
1770
2058
  * Dispatch one `/admin/api/*` request. `path` is the already-extracted pathname
@@ -1830,6 +2118,8 @@ interface AdminServerDeps extends AdminApiDeps {
1830
2118
  * unauthenticated, NEVER on `/health`.
1831
2119
  */
1832
2120
  auditReader?: AuditQueryReader;
2121
+ /** Metadata-only audit aggregate used by the overview error-rate metric. */
2122
+ auditStatsReader?: AuditStatsReader;
1833
2123
  /**
1834
2124
  * OPTIONAL billing delivery-status reader (billing-event-stream, design D5).
1835
2125
  * When wired (bootstrap → the ledger dir), the AUTHED `GET /admin/api/billing-status`
@@ -1872,23 +2162,18 @@ declare class AdminServer {
1872
2162
  getStatus(): AdminServerStatus;
1873
2163
  }
1874
2164
 
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 {
2165
+ declare class JsonOutboundKeyDb implements OutboundKeyDb {
1890
2166
  private readonly keysPath;
1891
- constructor(keysPath: string);
2167
+ private readonly secretBox?;
2168
+ /**
2169
+ * @param secretBox OPTIONAL reversible-secret codec. When present, a created
2170
+ * key's plaintext is persisted as a `keySecret` `enc:` envelope (enabling the
2171
+ * operator "view key" affordance via `outboundApiKeysReveal`). When absent the
2172
+ * store stays hash-only (byte-identical to the legacy behavior) and reveal
2173
+ * always returns `null`. Existing 1-arg call sites (tests, lightweight
2174
+ * embedders) keep working.
2175
+ */
2176
+ constructor(keysPath: string, secretBox?: SecretBox | undefined);
1892
2177
  outboundApiKeysList(): Promise<OutboundKeyDbRow[]>;
1893
2178
  outboundApiKeysGetByHash(hash: string): Promise<OutboundKeyDbRow | null>;
1894
2179
  outboundApiKeysCreate(input: {
@@ -1897,7 +2182,13 @@ declare class JsonOutboundKeyDb implements OutboundKeyDb$1 {
1897
2182
  keyHash: string;
1898
2183
  keyPrefix: string;
1899
2184
  createdAt?: number;
2185
+ kind?: 'client' | 'integration';
2186
+ allowedEndpoints?: _omnicross_core.OutboundEndpoint[];
2187
+ loopbackOnly?: boolean;
2188
+ plaintext?: string;
1900
2189
  }): Promise<OutboundKeyDbRow>;
2190
+ outboundApiKeysReveal(id: string): Promise<string | null>;
2191
+ outboundApiKeysDelete(id: string): Promise<boolean>;
1901
2192
  outboundApiKeysRevoke(id: string): Promise<boolean>;
1902
2193
  outboundApiKeysTouchLastUsed(id: string): Promise<boolean>;
1903
2194
  outboundApiKeysSetEnabled(id: string, enabled: boolean): Promise<boolean>;
@@ -1911,6 +2202,41 @@ declare class JsonOutboundKeyDb implements OutboundKeyDb$1 {
1911
2202
  private writeRows;
1912
2203
  }
1913
2204
 
2205
+ interface PricingRefreshState {
2206
+ lastAttemptAt: number | null;
2207
+ lastSuccessAt: number | null;
2208
+ lastError: string | null;
2209
+ sources: PricingSourceRefreshResult[];
2210
+ }
2211
+ interface PricingRefreshSchedulerOptions {
2212
+ staleAfterMs?: number;
2213
+ intervalMs?: number;
2214
+ now?: () => number;
2215
+ }
2216
+ interface PricingCatalogSnapshot {
2217
+ hasUsableSnapshot(): boolean;
2218
+ }
2219
+ declare class PricingRefreshScheduler {
2220
+ private readonly engine;
2221
+ private readonly catalog;
2222
+ private readonly statePath;
2223
+ private readonly logger;
2224
+ private readonly staleAfterMs;
2225
+ private readonly intervalMs;
2226
+ private readonly now;
2227
+ private timer;
2228
+ private inFlight;
2229
+ constructor(engine: PricingEngine$1, catalog: PricingCatalogSnapshot, statePath: string, logger: Logger, options?: PricingRefreshSchedulerOptions);
2230
+ /** Fire one stale check immediately and arm an unref'ed periodic check. */
2231
+ start(): void;
2232
+ dispose(): void;
2233
+ getState(): PricingRefreshState;
2234
+ /** Public for admin/manual tests; concurrent checks share one promise. */
2235
+ refreshIfStale(force?: boolean): Promise<void>;
2236
+ private runRefresh;
2237
+ private writeState;
2238
+ }
2239
+
1914
2240
  /**
1915
2241
  * AccountHealthSweeper — proactive account-health recovery tick
1916
2242
  * (subscription-account-health, design D6).
@@ -2181,24 +2507,20 @@ declare class BillingRetrySweeper {
2181
2507
  }
2182
2508
 
2183
2509
  /**
2184
- * TokenRefreshScheduler proactive background OAuth token refresh
2185
- * (external-cli-sync).
2510
+ * TokenRefreshScheduler proactive background OAuth token refresh.
2186
2511
  *
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.
2512
+ * The auth strategies already refresh lazily (lead-window check before each
2513
+ * request + 401 retry), but an idle daemon can still reach token expiry
2514
+ * before its next request. This scheduler sweeps managed OAuth accounts on
2515
+ * an interval and refreshes tokens entering the expiry lead window.
2192
2516
  *
2193
2517
  * Safety properties:
2194
- * - the store coalesces in-flight refreshes per account, so a sweep can never
2518
+ * - the store coalesces in-flight refreshes per account, so a sweep cannot
2195
2519
  * 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;
2520
+ * - accounts already flagged `expired` are skipped and remain expired until
2521
+ * their own managed credential is repaired or re-login occurs;
2522
+ * - the active account uses its managed active refresher, while non-active
2523
+ * accounts refresh by id; both paths use only managed stored credentials;
2202
2524
  * - one sweep runs at a time (a long sweep never overlaps the next tick).
2203
2525
  *
2204
2526
  * Modeled on `ApiKeyPoolService`'s interval lifecycle: `start()` arms an
@@ -2223,8 +2545,9 @@ declare class TokenRefreshScheduler {
2223
2545
  sweep(now?: number): Promise<void>;
2224
2546
  /** Expiring within the lead window, refreshable, and not already dead. */
2225
2547
  private needsRefresh;
2226
- /** Refresh one account; failures are logged, never thrown (the store has
2227
- * already flagged the account `expired`). */
2548
+ /** Refresh one managed account; failures are logged, never thrown. The
2549
+ * store marks only the targeted account `expired` on a failed refresh.
2550
+ */
2228
2551
  private refreshOne;
2229
2552
  private refreshActive;
2230
2553
  }
@@ -2406,10 +2729,20 @@ interface Daemon {
2406
2729
  /** Subscription account service (token-free `listAll`) — now exposed for the
2407
2730
  * admin dashboard's read-only accounts panel (RT3). */
2408
2731
  readonly subscriptionAccounts: SubscriptionAccountService;
2732
+ /** Secret-free upstream subscription allowance cache/collector facade. */
2733
+ readonly accountAllowanceService: AccountAllowanceService;
2734
+ /**
2735
+ * Claude allowance cache maintenance for allowance-aware routing. Constructed
2736
+ * armed-off; the resident `start` command starts it only when the persisted
2737
+ * scheduling policy is enabled.
2738
+ */
2739
+ readonly claudeAllowanceRefreshScheduler: ClaudeAllowanceRefreshScheduler;
2409
2740
  /** File-backed pricing table (`pricing.json`; concrete for the admin DELETE). */
2410
2741
  readonly pricingStore: JsonPricingStore;
2411
2742
  /** Pricing engine (cost calc + source refresh + conflict resolution). */
2412
2743
  readonly pricingEngine: PricingEngine;
2744
+ /** Non-blocking stale-while-revalidate catalog worker (armed by `start`). */
2745
+ readonly pricingRefreshScheduler: PricingRefreshScheduler;
2413
2746
  /** Usage recorder over `usage-events.jsonl` — also the admin stats query facade. */
2414
2747
  readonly usageRecorder: UsageRecorder;
2415
2748
  /** The localhost admin/dashboard HTTP listener (RT3). Started by `start.ts`. */