/** * Provider budgets — the data access layer. * * One class over the `cms_provider_*` stored procedures from migrations * 0049-0053. It is deliberately thin: every rule about who may do what, and * every counter decision, lives in SQL (see the migration headers for why), * so nothing here re-implements a policy. What it does add is shape — the * database's snake_case rows become the objects the API, the MCP server, the * agent tools and the portal all share — and a typed error, so a refusal * arrives at the caller as a code rather than a string to grep. */ import type { Pool } from "pg"; import type { BudgetPeriod, BudgetRuleState, TurnAdmission } from "./provider-budgets.js"; export type ProviderClass = "shared" | "personal"; /** * What a workload-identity provider stores instead of a key. * * It holds no secret. It exists so the row is distinguishable from one whose * credential was never set: `has_credential` in SQL is `secret_ref <> '{}'`, * and everything from the portal's greying-out of a provider to refusing it * as a default reads that. Nothing ever reads a value out of it — the token * is minted at the moment of use from the worker's own identity. */ export declare const WORKLOAD_IDENTITY_KIND = "workloadIdentity"; /** Does this stored blob say "authenticate as the worker"? */ export declare function isWorkloadIdentitySecret(secretRef: Record | null | undefined): boolean; export type ChargeClass = "user" | "system" | "unattributed"; /** A refusal from the database, with the marker turned into a code. */ export declare class ProviderError extends Error { readonly code: string; constructor(code: string, message: string); } export interface ProviderRow { name: string; typeId: string; class: ProviderClass; ownerUserId: number | null; ownerEmail: string | null; ownerDisplayName: string | null; baseUrl: string | null; allowancePct: number; holdUntilUtc: string | null; holdIndefinite: boolean; hasCredential: boolean; /** False for another person's provider, which an admin can see but not run. */ usableByMe: boolean; systemUseEnabled: boolean; systemEligible: boolean; isClusterDefault: boolean; isMyDefault: boolean; isSystemDefault: boolean; ruleCount: number; createdAt: string; } export interface ProviderStatusRow { name: string; class: ProviderClass; allowancePct: number; holdUntilUtc: string | null; holdIndefinite: boolean; rules: BudgetRuleState[]; } /** * One period's cell on the provider table, for one row. * * Four numbers, because the screen shows two of them at a time and must never * mix them up: `usedTokens / quotaTokens` is what everyone spent against the * limit, `yourUsedTokens / yourQuotaTokens` is what you spent against your * share of it. * * A null quota means no limit for that period. The used number beside it is * still real — the meter runs whether or not anybody capped it. A null * `yourUsedTokens` means nobody is signed in, which is not zero. */ export interface UsageGridCell { /** The limit that set this quota, or null when the period is uncapped. */ ruleId: string | null; quotaTokens: number | null; usedTokens: number; yourQuotaTokens: number | null; /** * The four parts of usedTokens / yourUsedTokens (0069): same window, * scope and turns as the meter, so they add up to it. The viewer's four * are null when nobody is signed in, like yourUsedTokens. */ inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; yourInputTokens: number | null; yourOutputTokens: number | null; yourCacheReadTokens: number | null; yourCacheWriteTokens: number | null; yourUsedTokens: number | null; windowStartUtc: string; resetsAtUtc: string; } /** * One line of the provider table: a provider, or one model it has a limit on. * Both kinds carry the same three cells, so a model limit reads exactly like * the provider above it. */ export interface UsageGridRow { providerName: string; rowKind: "provider" | "model"; /** '*' on a provider row; the qualified model reference on a model row. */ scope: string; class: ProviderClass; allowancePct: number; holdUntilUtc: string | null; holdIndefinite: boolean; /** How many model rows follow this provider. Always 0 on a model row. */ modelRowCount: number; /** * Yours to read as your own — your provider, or a shared one, which is * everyone's. False is somebody ELSE's personal provider, which only an * administrator ever sees: it has no share for you, so its `yourQuota` * cells are null rather than a headroom figure you cannot spend. */ ownedByMe: boolean; /** * Yours to CHANGE: admin on a shared provider, owner on a personal one — * the same rule `cms_provider_assert_manage` enforces. The screen greys * out what this refuses instead of arming a control the server will * reject after the form is filled in. */ manageable: boolean; /** * Whose provider this is, written out — the display name, or the email, * or "user " for a sparse row. Null on a shared provider, which is * everybody's and has no owner to name. */ ownerLabel: string | null; periods: { day: UsageGridCell; week: UsageGridCell; month: UsageGridCell; }; } export interface DefaultTuple { provider: string | null; model: string | null; reasoning: string | null; context: string | null; } export interface SystemAgentModelOverride extends DefaultTuple { agentId: string; updatedBy: number; updatedAt: string; } export interface ProviderDefaults { cluster: DefaultTuple; mine: DefaultTuple; system: DefaultTuple; systemUpdatedBy: number | null; systemUpdatedAt: string | null; } export interface LegacyProviderMigrationStatus { regularKeys: number; migratedRegularKeys: number; systemKeyPresent: boolean; systemKeyAdopted: boolean; } export interface SystemRestartRolloutState { agentId: string; operationId: string; targetModel: string; targetReasoning: string | null; targetContext: string | null; disposition: "complete" | "terminate" | "hard_delete"; status: "in_progress" | "failed" | "complete"; claimedAt: string; completedAt: string | null; lastError: string | null; } export interface PausedSessionRow { sessionId: string; title: string | null; model: string | null; ownerUserId: number | null; ownerEmail: string | null; state: string; pause: Record | null; updatedAt: string; } export interface UsageFilters { days?: number; ownerUserId?: number | null; provider?: string | null; model?: string | null; sessionId?: string | null; chargeClass?: ChargeClass | null; } export interface SettleTurnInput { sessionId: string; turnIndex: number; providerName: string | null; modelQualified: string | null; ownerUserId: number | null; chargeClass: ChargeClass; agentId?: string | null; tokensInput?: number; tokensOutput?: number; tokensCacheRead?: number; tokensCacheWrite?: number; } /** What the runtime needs to actually talk to a provider's backend. */ export interface ProviderCredential { name: string; typeId: string; class: ProviderClass; ownerUserId: number | null; baseUrl: string | null; secretRef: Record; systemUseEnabled?: boolean; } /** * What a caller may put in a credential, and what they may not. * * A credential that arrives in a request is a VALUE. The deployment's config * file may store a POINTER instead (`env:AZURE_KEY`, resolved against the * worker's own environment), and that indirection is safe only because the * deployment wrote it. Honouring it on a request let anyone with an account * name a variable — the cluster's API key, the database URL, anything the * worker holds — pair it with a base URL of their own, and have the worker * post that secret to them. * * So: one shape in, one shape stored. Every friendly spelling people * actually send (`{apiKey}`, `{githubToken}`, `{token}`, a bare string) is * accepted and stored as a literal under `value`. `ref` and `source` are * dropped, and a literal that looks like a pointer is refused outright * rather than silently stored as something that will never resolve. */ export declare function normalizeCallerSecret(input: Record | string | null | undefined, options?: { workloadIdentity?: boolean; }): Record; export declare class ProviderStore { private readonly pool; private readonly schema; constructor(pool: Pool, schema: string); private fn; private call; /** * Principal → user id, WITHOUT creating the row. Everything here takes a * numeric actor; this is the only translation point, and it deliberately * returns null for someone who has never signed in rather than minting a * user as a side effect of a read. */ lookupUserId(principal: { provider: string; subject: string; } | null | undefined): Promise; /** * Who owns this session, and is it machinery? Settlement needs both, and * the turn activity carries neither — it knows a session id and a model * reference, nothing about people. Reading them here keeps the two facts * in the one place that already decides charging, rather than threading * a principal through the whole turn path so it can be re-derived. */ sessionChargeFacts(sessionId: string): Promise<{ ownerUserId: number | null; isSystem: boolean; }>; /** The admission call. One round trip, made once per turn. */ checkTurn(sessionId: string, model?: string | null): Promise; /** * The next free settlement index for a session, across every lifetime * the ledger has seen. The ledger PK (session_id, turn_index) is the * exactly-once claim; a fresh orchestration for a RETAINED session id * (system restart) must count from here or settle silently drops its * spend as "already settled". */ nextLedgerTurnIndex(sessionId: string): Promise; /** * Advance the session's ledger base past every row the session id has, * so a fresh orchestration lifetime (system restart) settles on new * keys instead of silently colliding with the old lifetime's. */ bumpLedgerBase(sessionId: string): Promise; /** Settlement. Returns false when this turn was already settled. */ settleTurn(input: SettleTurnInput): Promise; listProviders(viewer: number | null, isAdmin: boolean): Promise; providerStatus(viewer: number | null, isAdmin: boolean, names?: string[] | null): Promise; /** * Everything the provider table draws, in one call. * * Rows arrive in the order the table renders them: shared providers * first, then your own, each immediately followed by its model rows. The * caller does not sort and does not make a second call per provider. */ usageGrid(viewer: number | null, isAdmin: boolean): Promise; /** * The credential the runtime should use, resolved by NAME in the session * owner's namespace — the same resolution the gate made, so accounting * and the actual API call can never name different credentials. */ /** * Every provider with its credential, for the worker's own runtime * catalog. Not a viewer-scoped read and never shown to anyone: a worker * runs turns for everybody, and what a person may spend from is settled * by the admission gate long before a credential is reached for. */ allCredentials(): Promise; getCredential(name: string, viewer: number | null): Promise; getSystemCredential(name: string): Promise; getDefaults(actor: number | null): Promise; listSystemAgentModels(): Promise; listPaused(viewer: number | null, isAdmin: boolean, limit?: number, clusterScoped?: boolean): Promise; /** Which sessions are waiting on this provider — the wake query. */ pausedFor(name: string): Promise; private usageArgs; usageTotals(viewer: number | null, isAdmin: boolean, f?: UsageFilters): Promise<{ tokensTotal: number; turns: number; sessions: number; }>; usageDaily(viewer: number | null, isAdmin: boolean, f?: UsageFilters): Promise<{ dayUtc: string; tokensTotal: number; turns: number; tokensInput: number; tokensOutput: number; tokensCacheRead: number; tokensCacheWrite: number; }[]>; usageBreakdown(viewer: number | null, isAdmin: boolean, dimension: "session" | "user" | "provider" | "model" | "agent", f?: UsageFilters, limit?: number, clusterScoped?: boolean): Promise<{ key: any; label: any; tokensTotal: number; turns: number; }[]>; /** * The cluster summary: today / week / month totals, a per-UTC-day series * and the per-MODEL pivot, all from the ledger over one scope and one * provider filter. An admin sees the cluster; anyone else their own * turns. `providers` empty or absent means every provider. */ usageSummary(viewer: number | null, isAdmin: boolean, days?: number, providers?: string[] | null): Promise>; /** * The agent pivot over the same ledger rows and viewer scope as * usageSummary: per-agent aggregates (models, per-day sparkline) plus a * flat day×agent series for the stacked chart. '(none)' collects turns * from sessions bound to no agent. */ usageAgents(viewer: number | null, isAdmin: boolean, days?: number, providers?: string[] | null, clusterScoped?: boolean): Promise>; createProvider(input: { name: string; typeId: string; class: ProviderClass; ownerUserId?: number | null; secretRef?: Record | null; baseUrl?: string | null; /** * The type authenticates as the worker, so no credential is required * and none is kept. Decided by the caller, which is the layer that * holds the type catalog; the store only knows a type by its name. */ workloadIdentity?: boolean; }, actor: number | null, isAdmin: boolean): Promise<{ name: string; typeId: string; class: ProviderClass; }>; updatePersonalCredential(name: string, secretRef: Record | null | undefined, actor: number | null): Promise<{ name: string; typeId: string; class: ProviderClass; }>; /** * Rotate a SHARED provider's key. Admin-only, enforced in SQL by the same * manage gate every other shared mutation uses. * * Merges rather than replaces, like the personal path: the caller is a * credential form and sends one field, so anything else stored in the blob * (apiVersion, and whatever is added later) has to survive. */ updateSharedCredential(name: string, secretRef: Record | null | undefined, actor: number | null, isAdmin: boolean): Promise<{ name: string; typeId: string; class: ProviderClass; }>; /** Returns how many sessions now name a provider that no longer exists. */ deleteProvider(name: string, actor: number | null, isAdmin: boolean): Promise; clearRoutingDependencies(name: string, actor: number | null, isAdmin: boolean): Promise<{ clusterDefault: number; systemDefault: number; userDefaults: number; systemOverrides: number; }>; claimSystemRestart(input: { agentId: string; operationId: string; claimId: string; model: string; reasoning?: string | null; context?: string | null; disposition: "complete" | "terminate" | "hard_delete"; }): Promise<"claimed" | "busy" | "complete">; finishSystemRestart(agentId: string, claimId: string, error?: string | null): Promise; getSystemRestart(agentId: string): Promise; setSystemUse(name: string, enabled: boolean, actor: number | null, isAdmin: boolean): Promise; /** * Save a limit. Nothing is seeded and nothing is reset — the meter this * limit reads has been counting since the provider's first turn. * * `seededTokens` is what the new limit ALREADY counts in the current * period: a read of that meter, so the editor can warn that sessions will * pause on their next turn. The name is the old one, kept because four * other surfaces send it over the wire. */ setLimit(input: { name: string; period: BudgetPeriod; modelQualified?: string | null; limitTokens: number; }, actor: number | null, isAdmin: boolean): Promise<{ ruleId: string; seededTokens: number; }>; removeLimit(name: string, period: BudgetPeriod, modelQualified: string | null, actor: number | null, isAdmin: boolean): Promise; setAllowance(name: string, pct: number, actor: number | null, isAdmin: boolean): Promise; setHold(name: string, opts: { untilUtc?: string | null; indefinite?: boolean; }, actor: number | null, isAdmin: boolean): Promise; setClusterDefault(t: DefaultTuple, isAdmin: boolean): Promise; setUserDefault(actor: number | null, t: DefaultTuple): Promise; setSystemDefault(actor: number | null, isAdmin: boolean, t: DefaultTuple): Promise; setSystemAgentModel(agentId: string, actor: number | null, isAdmin: boolean, t: DefaultTuple): Promise; clearSystemAgentModel(agentId: string, actor: number | null, isAdmin: boolean): Promise; adoptLegacySystemGitHubKey(name: string, actor: number | null, isAdmin: boolean): Promise<{ name: string; typeId: string; class: ProviderClass; ownerUserId: number | null; }>; getLegacyKeyMigrationStatus(): Promise; /** * The deployment seed. Safe to call on every boot: the claim is atomic * and only the first caller on a fresh cluster does anything. */ bootstrap(instances: Array<{ name: string; typeId: string; secretRef?: Record; baseUrl?: string | null; }>, defaults: DefaultTuple | null): Promise<{ claimed: boolean; created: number; }>; } //# sourceMappingURL=provider-store.d.ts.map