/** * [ref] [CONFIRMED high](2026-08-19 三轴组复审,真机探针复现)—— WebFetch 摘要面的 * **per-model key plane**,与 `boot/side-query-lane.ts` 同一只 wrapper、同一条纪律。 * * ## 病灶([ref] 的第四面,逐字同形) * * [ref] 的头注写「side-query 是 key plane 三面里**唯一**缺席的那一面」—— 那句话是假的。 * `createWebFetchSummarizer(brain, model)` 的 `brain` 是 `RunnerDeps.brain` 那只**裸网关 brain** * (`boot/budget-tracing.ts` 的 `createBrain(config, …)`,`get apiKey()` 恒回 `config.gatewayApiKey`), * 而 core 的摘要腿(`dist/tools/web.js`)铸 options 只放 `{ signal }`——`WebFetchSummarizerOptions` * 结构性没有 key 座位。于是 openai brain 的那两行照旧: * `const apiKey = options?.apiKey ?? config.apiKey;` ← 缺席即回落**主网关 key** * `const root = (model.baseUrl || config.baseUrl || "")` ← 却**按模型**选路 * ⇒ summarize 角色解析到一只带自有 `baseUrl` + `apiKeyEnv/sealedApiKey` 的模型时,**主网关 key 被发到 * 那只模型自己的外部 URL**;而且这不落在 [ref] 已具名接受的那条残余里(那条说的是「**没有** per-model * key 的 off-route 模型仍收网关 key」)—— 这只模型**有**自己的 key,却拿不到。 * * 第二半同样成立:毒丸(sealed key 解不开)在本面**静默回落网关账号** —— 本面根本不经 * `resolveModelApiKey`,`key-resolver.ts` 顶注那句「a broken sealed key must NEVER silently burn the * shared gateway account」在这条路上不成立。 * * ## 修形 = 照 [ref] 的配方接第四面(不是新造语义) * * brain 换成 `createPerModelKeyBrain(brain, getKeyResolver)`: * · 有 per-model key ⇒ 注入 `options.apiKey`,请求带模型自己的凭据; * · 无 ⇒ 不注入 ⇒ 交回 core 按它自己的 key↔URL 配对法判(core 5.57.0 起:部署根声明了而 entry URL * 不在那个根上 ⇒ `route.credential_mismatch` 响亮拒;未声明部署根 = `"unpinned"` 快速起步姿态 ⇒ * 照旧回落网关 key。[ref] 的具名残余与其 P-DEBT 腿已在源头清偿,见 side-query-lane.ts 顶注); * · 毒丸 ⇒ **上抛**,一个字节都不发(CLAUDE.md [ref] 安全轴 fail-closed)。 * wrapper 只包本席这一只引用,`RunnerDeps.brain` 零扰动(与 side-query 席同形)。 * * ## 为什么住在 `boot/` 而不是继续躺在 `main()` 的闭包里 * * ① 这是一条**可发射的 LLM 面**,必须能被黑盒探针(真 fetchImpl 捕 Authorization)直接驱动 —— 躺在 * `main()` 里只能靠源码字面锚,而本仓已成文「源码字面锚有致命假绿形」([ref]/.11 那一族); * ② 发射面枚举门(`test/per-model-key.test.ts` 的 `DIRECT_EMISSION_FACES`)按**文件**登记在册,第四面 * 有自己的文件才登记得住。 * * 原地保留的两条既有语义(E-MED-2/E-MED-3,[ref]/[ref] 复审批): * · **惰性解析**:模型每次调用现取 —— `config.models`/`config.roles` 会被 center 的 effective 热应用 * `mutateInPlace` 整表替换,boot 期急求值会把摘要器永久 pin 在占位/旧模型上; * · **fail-soft**:`resolveTaskModel` 在无可解析角色时 throw,这是全仓唯一 boot 期裸调用点;解析失败 * ⇒ 本次不摘要(core 的 summarize 抛错是 fail-open:回退原文 + note),绝不崩 boot。 */ import { type Brain, type WebFetchConfig } from "@sema-agent/core"; import type { ServiceConfig } from "../config-types.js"; import type { Logger } from "../observability/logger.js"; import { type KeyResolver } from "./side-query-lane.js"; /** WebFetch 摘要执行席(= `ScenarioDeps.webFetchSummarize`,core 的 `WebFetchConfig.summarize` 逐字同形)。 */ export type WebFetchSummarizeLane = NonNullable>; export interface WebFetchSummarizeLaneCtx { /** 与 `RunnerDeps.brain` **同一只**实例(wrapper 在外面包,不改这只)。 */ brain: Brain; /** 活配置引用:目录/角色表被 config-center 就地热应用(`mutateInPlace`),按调用取值才跟得上。 */ config: ServiceConfig; /** 活引用取值 —— registry 热应用整个换 resolver 引用(`boot/config-center.ts` 每次 apply 重铸)。 */ getKeyResolver: () => KeyResolver; logger: Logger; } export declare function createWebFetchSummarizeLane(ctx: WebFetchSummarizeLaneCtx): WebFetchSummarizeLane; //# sourceMappingURL=webfetch-summarize-lane.d.ts.map