@omnicross/daemon 0.3.0 → 0.4.0

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
@@ -5,8 +5,8 @@ import { AllowanceSchedulingConfig, AccountProbeConfig, OutboundKeyDb as Outboun
5
5
  import { RouteLeaseManager, ProviderProxy } from '@omnicross/core/provider-proxy';
6
6
  import { UsageRecorder, PricingEngine } from '@omnicross/core/usage';
7
7
  import { SubscriptionCredentialStore, FetchLike, CodexImageCapabilityEvidenceSource, CodexImageCapabilityEvidenceRequest, CodexImageCapabilityEvidence, CodexImageCapabilityObservation, ImageExecutionScheduler, ImageExecutionAccountKey, ImageExecutionSchedulerRequest, ImageExecutionSchedulerGrant, SubscriptionAccountService, SubscriptionProviderRegistry } from '@omnicross/subscriptions';
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';
8
+ import { AccountAllowanceSnapshot, AllowanceWindow } from '@omnicross/contracts/account-allowance-types';
9
+ import { ClaudeTokenConfig, CodexTokenConfig, GeminiTokenConfig, KimiTokenConfig, GrokTokenConfig, CopilotTokenConfig, AccountTokensConfig, ProxyConfig, SubscriptionAccountSanitized, AccountClientIdentity, SubscriptionAccountEntry } from '@omnicross/contracts/account-tokens-types';
10
10
  import * as _omnicross_contracts_subscription_types from '@omnicross/contracts/subscription-types';
11
11
  import { OpenCodeGoTokenConfig, SubscriptionProviderId } from '@omnicross/contracts/subscription-types';
12
12
  import { AccountAllowanceStore } from '@omnicross/core/pipeline/AccountAllowanceStore';
@@ -536,6 +536,18 @@ interface DaemonProviderConfig {
536
536
  * round-trips verbatim on GET. Management-UI only.
537
537
  */
538
538
  modelsEndpoint?: string;
539
+ /**
540
+ * OPTIONAL static extra request headers merged into every BYO request for
541
+ * this row — identity/attribution contracts some gateways hard-gate on
542
+ * (e.g. the Cline client-identity set). Additive + back-compat: absent
543
+ * reads as undefined. NON-SECRET — round-trips verbatim on GET; values may
544
+ * carry `{{platform}}` (resolved to `process.platform` at request time).
545
+ * ENFORCED: the outbound header funnel (`getProviderHeaders`) +
546
+ * discover-models + test-model merge them; auth/content header NAMES are
547
+ * dropped by the load guard (`validateExtraHeaders`) so credentials stay
548
+ * key-field-only.
549
+ */
550
+ extraHeaders?: Record<string, string>;
539
551
  /**
540
552
  * OPTIONAL provider transformer config (app-parity child 5). Additive +
541
553
  * back-compat: absent reads as the prior default (undefined). NON-SECRET —
@@ -626,6 +638,32 @@ declare function loadConfig(path: string): DaemonConfig;
626
638
  * `enc:` untouched) before serializing. */
627
639
  declare function saveConfig(path: string, cfg: DaemonConfig): void;
628
640
 
641
+ /**
642
+ * atomicFile — the daemon's shared ATOMIC file-replace for credential-bearing
643
+ * state files (`keys.json`, `tokens.json`, …).
644
+ *
645
+ * A bare `writeFileSync(target, …)` is truncate-then-write: a crash, ENOSPC,
646
+ * or power loss mid-write leaves the target HALF-WRITTEN and the prior content
647
+ * is gone. For `tokens.json` that is account data — the 2026-09-06 incident
648
+ * lost every stored OAuth account exactly this way (a truncated file that the
649
+ * reader then silently treated as "no accounts"). Same-directory temp-write +
650
+ * fsync + rename means a FAILED write only ever discards the temp file; the
651
+ * prior target survives byte-equal.
652
+ *
653
+ * The temp name embeds pid + random bytes (never collides with a live temp of
654
+ * another process) and lives in the target's own directory so the final
655
+ * `renameSync` never crosses a volume (POSIX and win32 both make the replace
656
+ * atomic there; Node maps rename-over-existing to MoveFileEx with
657
+ * REPLACE_EXISTING on Windows).
658
+ *
659
+ * Extracted verbatim from `JsonOutboundKeyDb` (its original + only user) so
660
+ * every file-backed port shares ONE implementation instead of re-deriving it.
661
+ *
662
+ * @module @omnicross/daemon/ports/atomicFile
663
+ */
664
+ /** Injectable seam (tests pass a throwing/stubbed replace to prove the prior file survives). */
665
+ type AtomicFileReplace = (targetPath: string, contents: string) => void;
666
+
629
667
  /**
630
668
  * account-multi — daemon-side pure helpers for the subscription multi-account
631
669
  * layout.
@@ -640,7 +678,7 @@ declare function saveConfig(path: string, cfg: DaemonConfig): void;
640
678
  */
641
679
 
642
680
  /** Provider id → owned contract field names. */
643
- type DaemonProvider = 'claude' | 'codex' | 'gemini' | 'opencodego';
681
+ type DaemonProvider = 'claude' | 'codex' | 'gemini' | 'opencodego' | 'kimi' | 'grok' | 'copilot';
644
682
  interface AccountMetadataPatch {
645
683
  label?: string;
646
684
  enabled?: boolean;
@@ -710,9 +748,14 @@ type ExternalCliReader = (provider: ExternalCliProvider) => ExternalCliCredentia
710
748
  * over a sibling `tokens.json` holding an `AccountTokensConfig`-shaped object
711
749
  * (`{ claude?, codex?, gemini?, opencodego?, updatedAt }`). Modeled on
712
750
  * `JsonOutboundKeyDb`: the constructor takes the path; reads are
713
- * `existsSync` `readFileSync` `JSON.parse`, tolerating a missing/corrupt
714
- * file by returning a minimal `{ updatedAt }` config (the strategies already
715
- * guard `?.accessToken`, so a partial/empty config never crashes dispatch).
751
+ * `existsSync` `readFileSync` `JSON.parse`. A MISSING file returns a minimal
752
+ * `{ updatedAt }` config (first boot; the strategies already guard
753
+ * `?.accessToken`, so a partial/empty config never crashes dispatch). A file
754
+ * that EXISTS but will not parse as a JSON object is first QUARANTINED to a
755
+ * sibling `tokens.json.corrupt-<stamp>` backup (see `quarantineCorrupt`) and
756
+ * only then treated as empty — a corrupt file is DATA, not "no accounts"
757
+ * (2026-09-06 incident: a truncated write read as empty, then the re-login
758
+ * persist overwrote the only copy of every stored account).
716
759
  *
717
760
  * The PORT surface is read-only by design: the codex / gemini strategies pull
718
761
  * their access token via `getFullConfig().<provider>.accessToken`; only claude /
@@ -736,6 +779,12 @@ type ExternalCliReader = (provider: ExternalCliProvider) => ExternalCliCredentia
736
779
  * at-rest with NO extra work (the store API guarantees it). The "re-read on every
737
780
  * call, no cache" semantics are unchanged.
738
781
  *
782
+ * DURABILITY: `persist` writes through the shared same-directory temp + fsync +
783
+ * rename (`atomicReplaceUtf8`) — a crashed/failed/interrupted write discards
784
+ * only the temp file and the prior `tokens.json` survives byte-equal. This is
785
+ * the other half of the 2026-09-06 fix: the store previously used a bare
786
+ * truncate-then-write `writeFileSync`, which could leave a half-written file.
787
+ *
739
788
  * REAL TOKEN REFRESH (oauth design D4): `refresh{Claude,Codex,Gemini}Token` mint
740
789
  * a new access token via the shared host-clean OAuth refresh functions
741
790
  * (`@omnicross/subscriptions/oauth`, injected `FetchLike` default global
@@ -758,13 +807,15 @@ type ExternalCliReader = (provider: ExternalCliProvider) => ExternalCliCredentia
758
807
  * shapes), keyed by `SubscriptionProviderId` the daemon admin layer validates
759
808
  * the wire body to one of these before calling the writer.
760
809
  */
761
- type SubscriptionTokenBlock = ClaudeTokenConfig | CodexTokenConfig | GeminiTokenConfig | OpenCodeGoTokenConfig;
810
+ type SubscriptionTokenBlock = ClaudeTokenConfig | CodexTokenConfig | GeminiTokenConfig | OpenCodeGoTokenConfig | KimiTokenConfig | GrokTokenConfig | CopilotTokenConfig;
762
811
  declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialStore {
763
812
  private readonly tokensPath;
764
813
  private readonly box;
765
814
  private readonly fetchImpl;
766
815
  /** Injectable, strictly read-only external CLI native-store reader. */
767
816
  private readonly externalCliReader;
817
+ /** Injectable atomic-replace seam (tests prove a failed write keeps the prior file). */
818
+ private readonly atomicReplace;
768
819
  /**
769
820
  * @param tokensPath on-disk `tokens.json` location.
770
821
  * @param box at-rest `SecretBox` (encrypt-on-write / decrypt-on-read).
@@ -779,7 +830,9 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
779
830
  */
780
831
  constructor(tokensPath: string, box: SecretBox, fetchImpl?: FetchLike | undefined,
781
832
  /** Injectable, strictly read-only external CLI native-store reader. */
782
- externalCliReader?: ExternalCliReader);
833
+ externalCliReader?: ExternalCliReader,
834
+ /** Injectable atomic-replace seam (tests prove a failed write keeps the prior file). */
835
+ atomicReplace?: AtomicFileReplace);
783
836
  /**
784
837
  * The proxy-aware `FetchLike` for one refresh round-trip (upstream-proxy M1). A
785
838
  * TEST-injected `fetchImpl` is returned verbatim; otherwise the refresh routes
@@ -858,13 +911,36 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
858
911
  * destroy the ability to refresh again). HONEST `false` when no refresh_token.
859
912
  */
860
913
  refreshGeminiToken(): Promise<boolean>;
914
+ /**
915
+ * Refresh the Kimi Code (Moonshot) OAuth access token (device-flow grant).
916
+ * Kimi ROTATES the refresh token, so the response's pair is written back
917
+ * whole; the account's stable `deviceId` (fingerprint header input) is
918
+ * preserved. The refresh call carries the CLI fingerprint headers. HONEST
919
+ * `false` when no refresh_token.
920
+ */
921
+ refreshKimiToken(): Promise<boolean>;
922
+ /**
923
+ * Refresh the Grok (xAI SuperGrok) OAuth access token. The token endpoint is
924
+ * resolved through OIDC discovery on every refresh (process-cached 1h by the
925
+ * flow module) so a rotated endpoint document is picked up without a daemon
926
+ * restart. HONEST `false` when no refresh_token.
927
+ */
928
+ refreshGrokToken(): Promise<boolean>;
929
+ /**
930
+ * "Refresh" a GitHub Copilot token — there is nothing to refresh (ghu_
931
+ * tokens are long-lived with no exchange endpoint). A call here means the
932
+ * strategy saw a 401 (the token was revoked); mark the account `expired`
933
+ * with a re-authenticate message and return `false` (the proxy then declines
934
+ * the retry instead of looping on a dead token).
935
+ */
936
+ refreshCopilotToken(): Promise<boolean>;
861
937
  /**
862
938
  * Refresh a SPECIFIC managed account by id (background scheduler sweep and
863
939
  * account-pool resolution). It uses only that account's stored refresh
864
940
  * token. Coalesced per `provider:id`; on failure flags ONLY that account
865
941
  * `expired`.
866
942
  */
867
- refreshAccountById(provider: 'claude' | 'codex' | 'gemini', id: string): Promise<boolean>;
943
+ refreshAccountById(provider: 'claude' | 'codex' | 'gemini' | 'kimi' | 'grok' | 'copilot', id: string): Promise<boolean>;
868
944
  /**
869
945
  * Resolve a SPECIFIC account's access token by id (design D6). Mirrors each
870
946
  * provider's ACTIVE-getter policy, keyed by id: claude returns the stored token
@@ -1030,22 +1106,49 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
1030
1106
  /** Write the merged config to disk as pretty JSON (mkdir parent if needed).
1031
1107
  * Encrypt-on-write: the token-material fields are encrypted (legacy plaintext
1032
1108
  * `enc:v1:`; already-`enc:`/`$ENV` untouched) before serializing, so any
1033
- * write incl. child 4's future refresh writes lands encrypted. */
1109
+ * write incl. child 4's future refresh writes lands encrypted.
1110
+ * ATOMIC: temp + fsync + rename (`atomicReplaceUtf8`) — a failed or
1111
+ * interrupted write discards only the temp file; the prior `tokens.json`
1112
+ * survives byte-equal (bare `writeFileSync` truncate-writes lost every
1113
+ * account on a mid-write failure, 2026-09-06). */
1034
1114
  private persist;
1035
1115
  /**
1036
- * Read + parse `tokens.json`, tolerating a missing/corrupt file, then DECRYPT
1037
- * the token-material fields so every getter returns plaintext (the
1038
- * subscription bearer path is byte-identical).
1116
+ * Read + parse `tokens.json`, then DECRYPT the token-material fields so every
1117
+ * getter returns plaintext (the subscription bearer path is byte-identical).
1118
+ *
1119
+ * A MISSING file is a legitimate first-boot state → minimal `{ updatedAt: '' }`.
1120
+ * A file that EXISTS but cannot be parsed as a JSON object is CORRUPT →
1121
+ * `quarantineCorrupt` moves it aside (once) before the empty config is
1122
+ * returned, so the unreadable accounts survive for manual recovery.
1039
1123
  *
1040
- * The fs-read + JSON-parse tolerance is INSIDE the try (a missing or corrupt
1041
- * file empty `{ updatedAt: '' }`). The DECRYPT runs OUTSIDE the try, so a
1042
- * wrong/missing master key or a tampered `enc:` envelope FAILS FAST with the
1043
- * box's clear, secret-free error (secrets spec "/ UX":
1044
- * SHALL fail-fast, SHALL NOT a swallowed decrypt would report "no
1045
- * tokens" and silently send the WRONG bearer upstream 401). Mirrors
1046
- * `config.ts loadConfig`, which decrypts outside its parse try.
1124
+ * The DECRYPT runs OUTSIDE any try, so a wrong/missing master key or a
1125
+ * tampered `enc:` envelope FAILS FAST with the box's clear, secret-free
1126
+ * error (secrets spec "/ UX": SHALL fail-fast, SHALL NOT a swallowed
1127
+ * decrypt would report "no tokens" and silently send the WRONG bearer
1128
+ * upstream 401). Mirrors `config.ts loadConfig`, which decrypts outside
1129
+ * its parse try.
1047
1130
  */
1048
1131
  private readConfig;
1132
+ /** One-shot latch: a corrupt file is quarantined (or found unmovable) at
1133
+ * most once per process, so the hot read path never re-attempts or re-logs. */
1134
+ private corruptQuarantined;
1135
+ /**
1136
+ * Quarantine a present-but-corrupt `tokens.json`, then treat it as empty.
1137
+ *
1138
+ * Renames the file to a sibling `tokens.json.corrupt-<stamp>` backup and
1139
+ * logs loudly (the daemon's stderr log; secret-free — reason + paths only).
1140
+ * The daemon KEEPS SERVING (API-key routing is unaffected; subscription
1141
+ * routing reports no credential, same as an absent file) while the corrupt
1142
+ * bytes survive for manual recovery — and, critically, the NEXT persist
1143
+ * (e.g. the user re-logging in) can no longer overwrite the only copy of
1144
+ * the old accounts, which is exactly how the 2026-09-06 incident turned a
1145
+ * recoverable truncated file into permanent account loss.
1146
+ *
1147
+ * Best-effort: if the rename fails (file locked, permissions), the corrupt
1148
+ * file is left in place and every later read still tolerates it as empty;
1149
+ * the latch still trips so the attempt + log happen exactly once.
1150
+ */
1151
+ private quarantineCorrupt;
1049
1152
  }
1050
1153
 
1051
1154
  /**
@@ -1349,9 +1452,239 @@ declare class ClaudeAllowanceCollector {
1349
1452
  private unsupportedSnapshot;
1350
1453
  }
1351
1454
 
1455
+ /**
1456
+ * Codex (ChatGPT) OAuth usage collector.
1457
+ *
1458
+ * Actively polls `GET https://chatgpt.com/backend-api/wham/usage` per codex
1459
+ * account (Bearer + `ChatGPT-Account-Id` decoded from the OAuth id_token),
1460
+ * mirroring the Claude collector's cache/coalescing contract: 5-minute cache,
1461
+ * per-account in-flight merging, one 401→refresh→retry. The passive
1462
+ * `x-codex-*` response-header tap (`upstreamFetch`) remains the complement —
1463
+ * it keeps windows fresh mid-flight; this collector makes the quota visible
1464
+ * with ZERO traffic (previously codex reported not-observed until a real
1465
+ * model response, and its refresh button had to spend a probe request).
1466
+ *
1467
+ * `reset_at` (absolute epoch seconds) is preferred over `reset_after_seconds`
1468
+ * so deadlines do not accumulate observation-clock skew. Tokens and raw
1469
+ * upstream payloads never leave this module.
1470
+ */
1471
+
1472
+ interface CodexAllowanceCredentialReader {
1473
+ getAccessTokenForAccount(providerId: 'codex', accountId: string): Promise<string | null>;
1474
+ refreshAccountToken(providerId: 'codex', accountId: string): Promise<boolean>;
1475
+ }
1476
+ type CodexAllowanceFetch = (url: string, init: RequestInit, accountId: string) => Promise<Response>;
1477
+ interface CodexAllowanceCollectOptions {
1478
+ force?: boolean;
1479
+ /** Treat an otherwise valid cache entry as due when it expires within this window. */
1480
+ refreshAheadMs?: number;
1481
+ }
1482
+ declare class CodexAllowanceCollector {
1483
+ private readonly credentials;
1484
+ private readonly store;
1485
+ private readonly fetchImpl;
1486
+ private readonly now;
1487
+ private readonly inFlight;
1488
+ constructor(credentials: CodexAllowanceCredentialReader, store?: AccountAllowanceStore, fetchImpl?: CodexAllowanceFetch, now?: () => number);
1489
+ collectMany(accounts: readonly SubscriptionAccountEntry<CodexTokenConfig>[], options?: CodexAllowanceCollectOptions): Promise<AccountAllowanceSnapshot[]>;
1490
+ collect(account: SubscriptionAccountEntry<CodexTokenConfig>, options?: CodexAllowanceCollectOptions): Promise<AccountAllowanceSnapshot>;
1491
+ /**
1492
+ * A response-header snapshot stays a valid cache hit only while fresh; an
1493
+ * active oauth-usage snapshot is honored on the same 5-minute cadence as
1494
+ * Claude's (the poll is cheap and quota is the scheduling input).
1495
+ */
1496
+ private isCacheValid;
1497
+ private fetchAccount;
1498
+ private request;
1499
+ private failureSnapshot;
1500
+ private unsupportedSnapshot;
1501
+ }
1502
+
1503
+ /**
1504
+ * Kimi Code OAuth usage collector.
1505
+ *
1506
+ * Polls `GET https://api.kimi.com/coding/v1/usages` per kimi account (Bearer +
1507
+ * the CLI fingerprint headers), mirroring the Claude/Codex collectors' cache
1508
+ * contract: 5-minute cache, per-account in-flight merging, one 401→refresh→
1509
+ * retry. The payload's `usage` aggregate is a weekly row; `limits[]` carries
1510
+ * the per-window rows — the 300-minute burst window normalizes to `five-hour`
1511
+ * and whole-day spans to `seven-day` (the same canonical ids the Claude view
1512
+ * and the UI's window labels use).
1513
+ */
1514
+
1515
+ interface KimiAllowanceCredentialReader {
1516
+ getAccessTokenForAccount(providerId: 'kimi', accountId: string): Promise<string | null>;
1517
+ refreshAccountToken(providerId: 'kimi', accountId: string): Promise<boolean>;
1518
+ }
1519
+ type KimiAllowanceFetch = (url: string, init: RequestInit, accountId: string) => Promise<Response>;
1520
+ interface KimiAllowanceCollectOptions {
1521
+ force?: boolean;
1522
+ refreshAheadMs?: number;
1523
+ }
1524
+ declare class KimiAllowanceCollector {
1525
+ private readonly credentials;
1526
+ private readonly store;
1527
+ private readonly fetchImpl;
1528
+ private readonly now;
1529
+ private readonly inFlight;
1530
+ constructor(credentials: KimiAllowanceCredentialReader, store?: AccountAllowanceStore, fetchImpl?: KimiAllowanceFetch, now?: () => number);
1531
+ collectMany(accounts: readonly SubscriptionAccountEntry<KimiTokenConfig>[], options?: KimiAllowanceCollectOptions): Promise<AccountAllowanceSnapshot[]>;
1532
+ collect(account: SubscriptionAccountEntry<KimiTokenConfig>, options?: KimiAllowanceCollectOptions): Promise<AccountAllowanceSnapshot>;
1533
+ private isCacheValid;
1534
+ private fetchAccount;
1535
+ private request;
1536
+ private failureSnapshot;
1537
+ private unsupportedSnapshot;
1538
+ }
1539
+
1540
+ /**
1541
+ * Grok (xAI SuperGrok) OAuth usage collector.
1542
+ *
1543
+ * Polls the Grok CLI billing proxy (`cli-chat-proxy.grok.com/v1/billing` —
1544
+ * NOT `*.x.ai`, and it REJECTS paid API keys: OAuth bearer only) per grok
1545
+ * account, mirroring the Claude/Codex/Kimi collectors' cache contract:
1546
+ * 5-minute cache, per-account in-flight merging, one 401→refresh→retry.
1547
+ *
1548
+ * Dual shape (mirrors the audit source's semantics):
1549
+ * - WEEKLY credits (`?format=credits`): `config.creditUsagePercent` +
1550
+ * `config.currentPeriod{start,end,type}` (+ per-product rows and an
1551
+ * on-demand cap, both optional). A missing `creditUsagePercent` on an
1552
+ * ACTIVE weekly period reads as 0 (a fresh period has no usage row).
1553
+ * - UNIFIED monthly (default URL): `config.{billingPeriodStart,
1554
+ * billingPeriodEnd, monthlyLimit{val}, used{val}}` — accounts flagged
1555
+ * `isUnifiedBillingUser` omit the weekly percentage and meter a monthly
1556
+ * included quota instead.
1557
+ *
1558
+ * Probe policy: always probe weekly first; probe monthly when weekly is
1559
+ * missing OR the account is flagged unified. An INFERRED weekly percentage
1560
+ * (field absent) on a unified account is only kept when the monthly probe
1561
+ * positively confirms there is no monthly quota — otherwise the monthly
1562
+ * window wins (an inferred 0% weekly on a unified account is a lie), and a
1563
+ * failed monthly probe falls through to a failure snapshot so the store
1564
+ * retains the last good one.
1565
+ *
1566
+ * Every request carries `X-XAI-Token-Auth: xai-grok-cli` — the same product
1567
+ * gate the official CLI uses on this host.
1568
+ */
1569
+
1570
+ interface GrokAllowanceCredentialReader {
1571
+ getAccessTokenForAccount(providerId: 'grok', accountId: string): Promise<string | null>;
1572
+ refreshAccountToken(providerId: 'grok', accountId: string): Promise<boolean>;
1573
+ }
1574
+ type GrokAllowanceFetch = (url: string, init: RequestInit, accountId: string) => Promise<Response>;
1575
+ interface GrokAllowanceCollectOptions {
1576
+ force?: boolean;
1577
+ refreshAheadMs?: number;
1578
+ }
1579
+ declare class GrokAllowanceCollector {
1580
+ private readonly credentials;
1581
+ private readonly store;
1582
+ private readonly fetchImpl;
1583
+ private readonly now;
1584
+ private readonly inFlight;
1585
+ constructor(credentials: GrokAllowanceCredentialReader, store?: AccountAllowanceStore, fetchImpl?: GrokAllowanceFetch, now?: () => number);
1586
+ collectMany(accounts: readonly SubscriptionAccountEntry<GrokTokenConfig>[], options?: GrokAllowanceCollectOptions): Promise<AccountAllowanceSnapshot[]>;
1587
+ collect(account: SubscriptionAccountEntry<GrokTokenConfig>, options?: GrokAllowanceCollectOptions): Promise<AccountAllowanceSnapshot>;
1588
+ private isCacheValid;
1589
+ private fetchAccount;
1590
+ private failureSnapshot;
1591
+ private unsupportedSnapshot;
1592
+ }
1593
+
1594
+ /**
1595
+ * CopilotAllowanceCollector — GitHub Copilot quota via the internal user API.
1596
+ *
1597
+ * Polls `GET api.github.com/copilot_internal/user` per copilot account
1598
+ * (Bearer ghu_ + the mirrored Copilot CLI user-agent), reading
1599
+ * `quota_snapshots`:
1600
+ * - `premium_interactions` — the plan's premium-request monthly window
1601
+ * (`{entitlement, remaining, percent_remaining, unlimited}` + the
1602
+ * account-level `quota_reset_date`). Reported as a single `thirty-day`
1603
+ * window; an `unlimited` entitlement reports 0% (never blocks).
1604
+ * - `chat` — the legacy chat-completions quota, reported only when it is
1605
+ * NOT unlimited (newer plans fold it into premium).
1606
+ *
1607
+ * Mirrors the Claude/Codex/Kimi/Grok collectors' cache contract: 5-minute
1608
+ * cache, per-account in-flight merging, one 401→refresh→retry (for copilot a
1609
+ * "refresh" marks the account expired — ghu_ tokens cannot be refreshed — so
1610
+ * the retry path is exercised only by transient upstream 401 flaps and never
1611
+ * loops). The GitHub API base honors the account's `enterpriseUrl` (GHE).
1612
+ */
1613
+
1614
+ interface CopilotAllowanceCredentialReader {
1615
+ getAccessTokenForAccount(providerId: 'copilot', accountId: string): Promise<string | null>;
1616
+ refreshAccountToken(providerId: 'copilot', accountId: string): Promise<boolean>;
1617
+ }
1618
+ type CopilotAllowanceFetch = (url: string, init: RequestInit, accountId: string) => Promise<Response>;
1619
+ interface CopilotAllowanceCollectOptions {
1620
+ force?: boolean;
1621
+ refreshAheadMs?: number;
1622
+ }
1623
+ declare class CopilotAllowanceCollector {
1624
+ private readonly credentials;
1625
+ private readonly store;
1626
+ private readonly fetchImpl;
1627
+ private readonly now;
1628
+ private readonly inFlight;
1629
+ constructor(credentials: CopilotAllowanceCredentialReader, store?: AccountAllowanceStore, fetchImpl?: CopilotAllowanceFetch, now?: () => number);
1630
+ collectMany(accounts: readonly SubscriptionAccountEntry<CopilotTokenConfig>[], options?: CopilotAllowanceCollectOptions): Promise<AccountAllowanceSnapshot[]>;
1631
+ collect(account: SubscriptionAccountEntry<CopilotTokenConfig>, options?: CopilotAllowanceCollectOptions): Promise<AccountAllowanceSnapshot>;
1632
+ private isCacheValid;
1633
+ private fetchAccount;
1634
+ private request;
1635
+ private failureSnapshot;
1636
+ private unsupportedSnapshot;
1637
+ }
1638
+
1639
+ /**
1640
+ * OpenCodeGo usage collector.
1641
+ *
1642
+ * Polls `GET {go-base}/v1/usage` per OpenCodeGo account with the account's
1643
+ * static bearer key, mirroring the other collectors' cache contract (5-minute
1644
+ * cache, per-account in-flight merging). The payload reports three percent
1645
+ * windows; only rolling(≈5h) + weekly are surfaced — the MONTHLY window is
1646
+ * deliberately dropped: the console's "Use balance" fallback keeps a
1647
+ * monthly-exhausted key SERVING, and the scheduling policy pauses on the worst
1648
+ * reported window, so reporting monthly would strand usable keys (oh-my-pi
1649
+ * reached the same conclusion for its ranking scopes).
1650
+ *
1651
+ * `status: "rate-limited"` is authoritative over the percent (→ 100%).
1652
+ */
1653
+
1654
+ interface OpenCodeGoAllowanceCredentialReader {
1655
+ getAccessTokenForAccount(providerId: 'opencodego', accountId: string): Promise<string | null>;
1656
+ }
1657
+ type OpenCodeGoAllowanceFetch = (url: string, init: RequestInit, accountId: string) => Promise<Response>;
1658
+ declare class OpenCodeGoAllowanceCollector {
1659
+ private readonly credentials;
1660
+ private readonly store;
1661
+ private readonly fetchImpl;
1662
+ private readonly now;
1663
+ private readonly inFlight;
1664
+ constructor(credentials: OpenCodeGoAllowanceCredentialReader, store?: AccountAllowanceStore, fetchImpl?: OpenCodeGoAllowanceFetch, now?: () => number);
1665
+ collectMany(accounts: readonly SubscriptionAccountEntry<OpenCodeGoTokenConfig>[], options?: {
1666
+ force?: boolean;
1667
+ refreshAheadMs?: number;
1668
+ }): Promise<AccountAllowanceSnapshot[]>;
1669
+ collect(account: SubscriptionAccountEntry<OpenCodeGoTokenConfig>, options?: {
1670
+ force?: boolean;
1671
+ refreshAheadMs?: number;
1672
+ }): Promise<AccountAllowanceSnapshot>;
1673
+ private fetchAccount;
1674
+ private failureSnapshot;
1675
+ }
1676
+
1352
1677
  /** Secret-free account allowance query/refresh facade used by the admin API. */
1353
1678
 
1354
- interface AccountAllowanceCredentialReader extends ClaudeAllowanceCredentialReader {
1679
+ /**
1680
+ * The credential surface the collectors share. Declared explicitly (not via
1681
+ * interface extension) because the collectors narrow
1682
+ * `getAccessTokenForAccount`/`refreshAccountToken` to different provider
1683
+ * literals — extending them all would make the overloads conflict.
1684
+ */
1685
+ interface AccountAllowanceCredentialReader {
1686
+ getAccessTokenForAccount(providerId: 'claude' | 'codex' | 'kimi' | 'opencodego' | 'grok' | 'copilot', accountId: string): Promise<string | null>;
1687
+ refreshAccountToken(providerId: 'claude' | 'codex' | 'kimi' | 'grok' | 'copilot', accountId: string): Promise<boolean>;
1355
1688
  getFullConfig(): Promise<AccountTokensConfig>;
1356
1689
  }
1357
1690
  interface AccountAllowanceFilter {
@@ -1367,18 +1700,41 @@ declare class AccountAllowanceService {
1367
1700
  private readonly store;
1368
1701
  private readonly now;
1369
1702
  readonly claudeCollector: ClaudeAllowanceCollector;
1370
- constructor(credentials: AccountAllowanceCredentialReader, store?: AccountAllowanceStore, collector?: ClaudeAllowanceCollector, now?: () => number);
1703
+ readonly codexCollector: CodexAllowanceCollector;
1704
+ readonly kimiCollector: KimiAllowanceCollector;
1705
+ readonly grokCollector: GrokAllowanceCollector;
1706
+ readonly copilotCollector: CopilotAllowanceCollector;
1707
+ readonly opencodegoCollector: OpenCodeGoAllowanceCollector;
1708
+ constructor(credentials: AccountAllowanceCredentialReader, store?: AccountAllowanceStore, collector?: ClaudeAllowanceCollector, codexCollector?: CodexAllowanceCollector, kimiCollector?: KimiAllowanceCollector, opencodegoCollector?: OpenCodeGoAllowanceCollector, grokCollector?: GrokAllowanceCollector, copilotCollector?: CopilotAllowanceCollector, now?: () => number);
1371
1709
  /**
1372
- * Read all/filtered snapshots. Claude's five-minute cache is refreshed lazily;
1373
- * Codex remains passive and reports not-observed until a real model response.
1710
+ * Read all/filtered snapshots. Claude's and Codex's five-minute caches are
1711
+ * refreshed lazily on read (Codex polls `/backend-api/wham/usage`; the
1712
+ * passive `x-codex-*` header tap still feeds mid-flight updates).
1374
1713
  */
1375
1714
  list(filter?: AccountAllowanceFilter): Promise<AccountAllowanceSnapshot[]>;
1715
+ private knownAccounts;
1376
1716
  /** Force-refresh Claude usage for one account or every stored Claude account. */
1377
1717
  refreshClaude(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
1378
1718
  /**
1379
- * Keep Claude snapshots warm for allowance-aware routing. This deliberately
1380
- * excludes Codex (whose quota is learned from real response headers) and
1381
- * preserves the collector's cache + per-account in-flight coalescing.
1719
+ * Force-refresh Codex usage (`/backend-api/wham/usage`) for one account or
1720
+ * every stored Codex account. Replaces the old probe-request workaround
1721
+ * no quota is spent reading the usage endpoint.
1722
+ */
1723
+ refreshCodex(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
1724
+ /** Force-refresh OpenCodeGo usage (`{go}/v1/usage`) for one/all accounts. */
1725
+ refreshOpenCodeGo(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
1726
+ /** Force-refresh Kimi usage (`/coding/v1/usages`) for one/all accounts. */
1727
+ refreshKimi(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
1728
+ /** Force-refresh Copilot usage (copilot_internal/user) for one/all accounts. */
1729
+ refreshCopilot(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
1730
+ /** Force-refresh Grok usage (CLI billing proxy) for one/all accounts. */
1731
+ refreshGrok(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
1732
+ /**
1733
+ * Keep Claude + Codex + Kimi snapshots warm for allowance-aware routing. All
1734
+ * collectors preserve their cache + per-account in-flight coalescing; a tick
1735
+ * normally performs no network I/O. (Codex joined the warm path when it
1736
+ * gained an active `/wham/usage` collector — the passive `x-codex-*` header
1737
+ * tap alone could not keep the policy fed while idle.)
1382
1738
  */
1383
1739
  maintainClaudeCache(refreshAheadMs: number): Promise<void>;
1384
1740
  /** Remove a cache row as soon as an account is deleted by the admin path. */
@@ -1743,6 +2099,68 @@ type AuditCompactor = () => {
1743
2099
  /** The read surface the AdminServer consumes (bootstrap binds it to the ledger dir). */
1744
2100
  type BillingStatusReader = () => BillingDeliveryStatus;
1745
2101
 
2102
+ /**
2103
+ * ProviderKeyQuota — BYO provider-row key quota parsing (pure functions).
2104
+ *
2105
+ * Subscription accounts have the daemon's allowance collectors; BYO rows (API
2106
+ * keys pasted from a provider console) had NO quota surface at all. Several CN
2107
+ * coding-plan providers expose a same-key usage endpoint:
2108
+ *
2109
+ * - Z.AI / Zhipu bigmodel (GLM Coding Plan):
2110
+ * GET {origin}/api/monitor/usage/quota/limit
2111
+ * Raw `Authorization: <key>` (NO Bearer prefix). Envelope
2112
+ * `{success, data: {limits[], level}}`; each limit carries
2113
+ * `{type, usage(limit), currentValue(used), percentage, remaining,
2114
+ * nextResetTime, unit(3=h/4=d/5=mo/6=w), number, usageDetails[]}`.
2115
+ * A coding-plan key reports 5h + weekly credit windows; a PAYG key's shape
2116
+ * is unknown → defensive parse, unavailable on surprise.
2117
+ * - MiniMax Token Plan:
2118
+ * GET {origin}/v1/token_plan/remains
2119
+ * `Authorization: Bearer <key>`. HTTP is ALWAYS 200 — `base_resp
2120
+ * .status_code === 0` is the real success gate. `model_remains[]` buckets
2121
+ * each carry a rolling interval + weekly window as REMAINING percent
2122
+ * (0-100); the `"general"` bucket is the plan-wide shared quota.
2123
+ * - Cline Pass:
2124
+ * GET {origin}/api/v1/users/me/plan/usage-limits
2125
+ * `Authorization: Bearer <key>` PLUS the Cline client-identity header set
2126
+ * (the row's `extraHeaders` — the gateway 403s without the full mirror).
2127
+ * `limits[]` rows carry `{type: five_hour|weekly|monthly, percentUsed,
2128
+ * resetsAt}` — pure percentage windows, no absolute meters.
2129
+ *
2130
+ * Everything here is pure; the fetch/cache lifecycle lives in
2131
+ * `ProviderKeyQuotaService`. Windows reuse the subscription `AllowanceWindow`
2132
+ * DTO so the UI renders one shape.
2133
+ */
2134
+
2135
+ /** Which quota adapter applies to a provider row (by resolved endpoint). */
2136
+ type ProviderKeyQuotaAdapter = 'zai' | 'minimax-token-plan' | 'umans' | 'synthetic' | 'cline-pass';
2137
+
2138
+ /**
2139
+ * ProviderKeyQuotaService — read-through quota cache for BYO provider-row keys.
2140
+ *
2141
+ * The UI polls `GET /admin/api/providers/:id/keys` every few seconds for pool
2142
+ * health; this service backs the optional `quota` field on that DTO. A read is
2143
+ * cache-first (5-minute TTL) with per-key in-flight coalescing, so the UI's poll
2144
+ * cadence never translates into upstream request cadence. `force` (the refresh
2145
+ * button) bypasses the cache. Failures degrade to a stale marker — quota
2146
+ * telemetry must never break the keys view.
2147
+ *
2148
+ * The key plaintext is resolved from the live row (same synthesis as the pool
2149
+ * loader: explicit `apiKeys[]` else the single-key fallback) and decrypted via
2150
+ * the injected box; it is used ONLY for the upstream Authorization header and
2151
+ * never appears in any returned DTO.
2152
+ */
2153
+
2154
+ /** Secret-free quota view for one pool key. */
2155
+ interface ProviderKeyQuota {
2156
+ adapter: ProviderKeyQuotaAdapter;
2157
+ observedAt: string;
2158
+ expiresAt: string;
2159
+ windows: AllowanceWindow[];
2160
+ /** Stable display-safe diagnostic code on a failed probe. */
2161
+ errorCode?: string;
2162
+ }
2163
+
1746
2164
  type PreparedImageRuntimeGeneration = {
1747
2165
  readonly id: string;
1748
2166
  readonly enabled: true;
@@ -2511,6 +2929,16 @@ interface AccountAllowanceAdminReader {
2511
2929
  accountId?: string;
2512
2930
  }): Promise<AccountAllowanceSnapshot[]>;
2513
2931
  refreshClaude(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
2932
+ /** Optional: Codex active `/wham/usage` refresh (absent on older daemons). */
2933
+ refreshCodex?(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
2934
+ /** Optional: Kimi `/coding/v1/usages` refresh (absent on older daemons). */
2935
+ refreshKimi?(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
2936
+ /** Optional: OpenCodeGo `/v1/usage` refresh (absent on older daemons). */
2937
+ refreshOpenCodeGo?(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
2938
+ /** Optional: Grok CLI-billing refresh (absent on older daemons). */
2939
+ refreshGrok?(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
2940
+ /** Optional: Copilot user-quota refresh (absent on older daemons). */
2941
+ refreshCopilot?(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
2514
2942
  removeAccountSnapshot?(providerId: SubscriptionProviderId, accountId: string): void;
2515
2943
  removeProviderSnapshots?(providerId: SubscriptionProviderId): void;
2516
2944
  getSchedulingStatus?(): AccountAllowanceSchedulingStatus;
@@ -2535,6 +2963,16 @@ interface PoolKeyHealth {
2535
2963
  interface PoolHealthReader {
2536
2964
  getKeyHealth(providerId: string): Promise<Record<string, PoolKeyHealth>>;
2537
2965
  }
2966
+ /**
2967
+ * The BYO provider-key quota surface the keys view needs — structurally
2968
+ * satisfied by `ProviderKeyQuotaService`. Read-only; the DTO is secret-free by
2969
+ * construction (normalized windows + diagnostic codes only).
2970
+ */
2971
+ interface ProviderKeyQuotaReader {
2972
+ quotaFor(row: DaemonProviderConfig, keyId: string, options?: {
2973
+ force?: boolean;
2974
+ }): Promise<ProviderKeyQuota | null>;
2975
+ }
2538
2976
  interface AdminImagesStatusReader {
2539
2977
  inspectCapability(apiKeyId: string): Promise<ImageRuntimeCapabilityInspection>;
2540
2978
  status(): ImageRuntimeManagerStatus;
@@ -2612,6 +3050,12 @@ interface AdminApiDeps {
2612
3050
  readonly apiKeyPool: PoolHealthReader;
2613
3051
  /** In-memory auto-disable store (design D5) — read-only for the health view. */
2614
3052
  readonly autoDisableStore: AutoDisableStore;
3053
+ /**
3054
+ * OPTIONAL BYO provider-key quota service — same-key usage/quota probes for
3055
+ * provider rows with a known adapter (Z.AI coding plan, MiniMax Token Plan).
3056
+ * Absent ⇒ the keys view carries no `quota` field (light embedders).
3057
+ */
3058
+ readonly providerKeyQuota?: ProviderKeyQuotaReader;
2615
3059
  /**
2616
3060
  * Pending interactive-OAuth sessions (app-parity child 4, design D1) — the
2617
3061
  * in-memory `{ codeVerifier, state }` map keyed by a minted `sessionId`,
@@ -2639,6 +3083,21 @@ interface AdminApiDeps {
2639
3083
  * flight (port 1455 is one resource). Wired in `bootstrap.ts`.
2640
3084
  */
2641
3085
  readonly codexSessions: CodexOAuthSessionStore;
3086
+ /**
3087
+ * Kimi interactive-OAuth flow store (device code). Same token-free polled
3088
+ * shape as codex; one sign-in at a time. Wired in `bootstrap.ts`.
3089
+ */
3090
+ readonly kimiSessions: CodexOAuthSessionStore;
3091
+ /**
3092
+ * Grok interactive-OAuth flow store (device code). Same token-free polled
3093
+ * shape as codex; one sign-in at a time. Wired in `bootstrap.ts`.
3094
+ */
3095
+ readonly grokSessions: CodexOAuthSessionStore;
3096
+ /**
3097
+ * Copilot interactive-OAuth flow store (device code). Same token-free
3098
+ * polled shape; one sign-in at a time. Wired in `bootstrap.ts`.
3099
+ */
3100
+ readonly copilotSessions: CodexOAuthSessionStore;
2642
3101
  /**
2643
3102
  * Codex loopback listener (app-parity-2 child 5) — defaults to `awaitLoopbackCode`
2644
3103
  * (binds 127.0.0.1:1455) in `bootstrap.ts`; tests inject a mock so no real port
@@ -3467,7 +3926,6 @@ declare function createImageRuntimeGeneration(options: ImageRuntimeGenerationFac
3467
3926
  * @module @omnicross/daemon/ports/JsonOutboundKeyDb
3468
3927
  */
3469
3928
 
3470
- type AtomicFileReplace = (targetPath: string, contents: string) => void;
3471
3929
  declare class JsonOutboundKeyDb implements OutboundKeyDb {
3472
3930
  private readonly keysPath;
3473
3931
  private readonly secretBox?;