import type { Model } from "@sema-agent/core"; import { type SealedKeyPoison } from "./sealed-key.js"; /** * Build a per-model API-key resolver for `TaskSpec.getApiKeyAndHeaders` from the catalog's * name → env-var-NAME map (sema-registry `apiKeyEnv`). core threads this per brain call — and per cascade * rung — so each model/rung authenticates with its OWN upstream account/key. * * Additive by construction: a model absent from the map — or whose env var is unset — returns * `undefined`, so core falls back to the brain's construction-time gateway key (today's behavior, no * regression). When no per-model keys are configured at all this returns `undefined`, leaving the spec * field unset so core's default path is byte-for-byte unchanged. * * Only the api KEY is per-model; the base URL stays brain-owned ([ref]) — core's seam carries * `{ apiKey, headers? }`, not a baseUrl. Same-account-rotation could round-robin inside this function. * * `modelApiKeys` (sealed-box custody) carries per-model entries unsealed from `ModelEntry.sealedApiKey` — * resolved FIRST (sealed outranks the env-NAME reference, the registry-core mutual-exclusion ruling). Each * entry is either the PLAINTEXT key (successful unseal) or a `SealedKeyPoison` marker (unseal FAILED at * apply time): a poisoned model THROWS `SealedKeyPoisonedError` on every resolve — task-level fail-loud — * because returning `undefined` here means "use the gateway key", and a broken sealed key must NEVER * silently burn the shared gateway account (the core invariant of the poison-pill fix; a poisoned model * skips its env reference too, same mutual-exclusion as a healthy sealed key). The maps are disjoint by * construction in applyEffective (a sealed model never lands in modelApiKeyEnv); the ordering here is * belt-and-suspenders. 🔴 Plaintext values are live secrets: memory-only, never log them. */ /** The ONE field the per-model key plane keys on. The resolver's parameter is deliberately this minimal * named ref (not the full core `Model`): full models are structural supersets and pass unchanged, while * callers that never materialize a full Model (hook-llm resolves keys for roster entries by catalog name) * can mint `{ name }` legally instead of forcing it through an `as never` escape hatch (B1). Contravariance * keeps the resolver assignable wherever `(model: Model) => …` is expected. */ export type ModelKeyRef = Pick; export declare function createKeyResolver(modelApiKeyEnv: Record, env?: NodeJS.ProcessEnv, modelApiKeys?: Record): ((model: ModelKeyRef) => Promise<{ apiKey: string; } | undefined>) | undefined; /** * The ONE per-model key chain, synchronous: **sealed custody → env-NAME reference → (caller's) gateway key**, * with a poisoned sealed entry THROWING instead of returning undefined. `createKeyResolver` is the async * `getApiKeyAndHeaders` face of exactly this function — every other consumer must call it rather than * re-implement a subset of the chain. * * 复审 2026-07-29(同族缺口):`brain.ts` 的反应式降级 hop 曾只查 `modelApiKeyEnv` —— sealed-box 托管密钥的 * 降级目标(按互斥契约**永不**落 modelApiKeyEnv)于是静默拿网关 key 打自己的网关(错 key 打上游),毒丸更 * 被当成"没配 key"。链只有一条,重复实现就会长出这种半条链的偏差,故此处抽出同步内核共用。 * * `undefined` = this model has no per-model key ⇒ the CALLER's gateway/default credential applies (the * additive contract). An empty env value counts as unset (unchanged from the original inline chain). */ export declare function resolveModelApiKey(modelName: string, modelApiKeyEnv: Record, env?: NodeJS.ProcessEnv, modelApiKeys?: Record): string | undefined; //# sourceMappingURL=key-resolver.d.ts.map