/** * [ref] (K-8 CC full-body) — the WebSearch BACKEND (BRAIN / model-service leg) that core leaves * deployment-injected (`WebSearchConfig.search`; core ships NONE, the same boundary as the model gateway). Wiring a * backend here is what makes `assembleFullBodyTools({ webSearch })` actually mount the WebSearch tool. * * Multi-provider, picked by `WEB_SEARCH_PROVIDER`: * - `brave` — Brave Web Search API (GET, `X-Subscription-Token` header) * - `tavily` — Tavily Search API (POST, `Authorization: Bearer`, native include/exclude_domains) * - `searxng` — a self-hosted SearXNG instance (GET `?format=json`, no auth) * * Each returns `{ title, url, snippet }[]`; core does the rest — it `delimitUntrusted`-fences the (UNTRUSTED) results * and re-enforces the `allowed_domains`/`blocked_domains` FLOOR on them, so honoring `opts` here is an optimization, * not a correctness requirement (a backend MAY ignore it). The API key comes from deployment env — it never enters * the model prompt or the tool args. `effect:"read"` (idempotent) is core's concern; this is just the transport. * * ── 立案:装配层四条缺口([ref] 我方认领,2026-08-01 调研,**尚未实施**)──────────────────── * * ① **`opts` 仍有 brave 腿不消费**(只剩 `brave` 的 switch 分支不传 `opts`;`searxng` 腿已随 ② 的 * 还债改动接住 `opts` 并透传给 core 的 adapter,由它把 `allowedDomains` 原生下推成 `site:` 前缀。 * 立案时的原话是「`opts` 只有 tavily 腿在消费(`brave`/`searxng` 都不传)」,② 落地后已部分还清)。 * 定性:**优化缺失,不是正确性缺口** —— 上面那句「core 再执行一次 FLOOR」经亲验属实 * (core `dist/tools/web.js` 的 `webSearchResultAllowed(url, allowed, blocked)`,按 hostname * 逐条过滤 blocked/allowed)。所以模型请求的域限制**不会**被静默丢弃。 * 但代价真实:后端搜回一堆注定被 core 丢掉的结果 ⇒ 白花配额、白等延迟,极端情况下 * 「限域搜索」返回 0 条(拿回来的 N 条全不在 allowed 里)而后端其实能做原生过滤。 * ⚠️ 判据留痕:我第一轮把这条判成了「约束被静默丢弃」的安全缺口,是**亲读 core 结果侧代码** * 才推翻的。「别处已经处理了」这种声明必须验证 —— 这次它是真的,但真假只能靠读。 * * ② ~~**searxng 腿是自铸的第二真源**~~ —— ✅ **已还(2026-08-01,[ref] 裁定 searxng 为国内可达性 * 主路线之后)**:本腿改为消费 core 的 `createSearxngSearchBackend`,外层保住 maxResults 截断 / * 统一形状 / 坏 payload 观测器三样(各有红先钉 + 变异实测),白得 `site:` 原生下推、`extraParams`、 * 自带 timeout。以下为原始立案文本,留档: * core 2.10 起已导出 `createSearxngSearchBackend` * (+`SearxngBackendOptions`),而且比本文件的实现多三样:`allowedDomains` 拼 `site:` 原生下推、 * `extraParams`(实例特定的 `engines=`/`language=` 等)、**自带 timeout** * (`AbortSignal.any([signal, timeout])`;本文件的 searxng 腿只吃外部 signal)。 * 我在黑板 [ref] 已公开表态「按 core 的接、不再自铸第二真源」—— 这条是待兑现的债。 * 换的时候要核对的等价点:本文件的 `normalize()` 统一形状 + `max` 截断 + * `setWebSearchBadPayloadObserver` 观测器,core 的 adapter 都没有,得在外层补回来。 * * ③ **四级优先级语义未定**(env / config-center / per-scenario / per-request 谁压谁)。 * 🔴 三问里「谁被伤」这条决定了默认值:一个**只配了模型 key** 的既有部署,若新语义让某一级 * 能自行打开搜索,它就在升级后**凭空长出一条出网工具** —— 那是安全姿态变更,不是特性。 * ⇒ 语义定稿前,任何新的开启通道**默认 OFF**;开启必须是部署方的显式动作。 * * ④ ~~**探活 verb 缺席**~~ —— ✅ **已还(2026-08-01)**:`shouldProbeWebSearchOnBoot` 门控 + * `main.ts` 消费 core 的 `probeSearchBackend`。**默认 OFF**(开着=给每个既有部署的每次启动凭空 * 加一次出网)、**只认明确真值**(含糊值当没开,别替部署方猜)、**失败只 warn 不拒启**(功能型 * 能力缺席 ⇒ 降级)、**不 await**(拖住 boot 就把可选工具变成启动依赖)。以下为原始立案文本,留档: * core 2.10 起导出 `probeSearchBackend(search, {timeoutMs}) → * {ok:true,results:n} | {ok:false,error}`,正好填这条:部署方现在只能靠「跑一个真任务」 * 验证搜索配对不对。挂哪儿(boot 期一次性 warn / `/health` 子字段 / 显式端点)未定 —— * 注意 boot 期探活会给每次启动加一次出网,多租/离线部署要能关。 */ /** * Search providers this backend speaks —— **词表的唯一属主**(运行期与类型同一份)。 * Absent `WEB_SEARCH_PROVIDER` ⇒ no backend ⇒ WebSearch is not assembled. * * 🔴 **为什么是常量元组而不是裸 union**(车GW 对抗复审 F1[medium],2026-09-17):改前这张表在仓里有**四份** * —— 类型一份 + 两只解析器各一条 `provider !== "brave" && …` 后置链 + `config-catalog` 的 * `enumValues: [...]` 字面量(那个字段是 `readonly string[]`,与本类型零类型联系)。加一只后端时 tsc * **只红** `createWebSearchBackend` 的 switch:守卫链把 provider 收窄成旧三词,而旧三词是新 union 的 * 子集 ⇒ 赋值合法 ⇒ 改完 switch 全绿发版,`WEB_SEARCH_PROVIDER=<新词>` 却被判 undefined、能力位 * (`projectWebSearchCapability`)诚实报 `"none"`,运维以为自己配上了。收成一只元组之后:类型从它派生、 * 两只解析器读它、目录行**就用它本人**(身份相等有钉),规则集从四张表降到一张。 */ export declare const WEB_SEARCH_PROVIDERS: readonly ["brave", "tavily", "searxng"]; export type WebSearchProvider = (typeof WEB_SEARCH_PROVIDERS)[number]; /** * S-382:`GET /v1/capabilities` 的 `webSearch` 位 —— **部署默认后端**的闭集自述。 * * ## 病 * 「这台 worker 的搜索走哪只后端」此前只在 operator 面可知(`GET /v1/config/catalog` 的 * `WEB_SEARCH_PROVIDER` 行)。消费端(cli doctor / client-core 的能力投影)要答「本部署有没有搜索、 * 是哪一只」,只能让用户跑一个真任务去撞 —— 而搜索缺席时工具**根本不装配**(`capabilities/scenarios.ts` * 的 `assembleCodeTools({ webSearch })`),模型只会说「我没有这个工具」,那句话在 UX 上与「后端配错了」 * 不可区分。 * * ## 形 * `{ backend: "brave" | "tavily" | "searxng" | "none" }`。三条纪律逐条写明: * ① **词源单点** —— 闭集是 {@link WebSearchProvider} **取型** ∪ `"none"`,而该类型又派生自 * {@link WEB_SEARCH_PROVIDERS}(全仓**唯一**一张 provider 词表:两只解析器读它、`config-catalog` 的 * `enumValues` **就用它本人**〔身份相等有钉〕)。加一只后端 = 改那只元组一处,这一位自动跟着长; * 没跟上的地方只剩 `createWebSearchBackend` 的穷尽 `switch`,而那一处是**编译红**。 * ② **只出词,不出内情** —— 端点(SearXNG 实例 URL)、API key、配额一个字都不上这条 wire:本面在 * service-credential 门之后但**非 operator 专属**,任何拿得到凭据的调用方都读得到它。 * ③ **广告的是部署默认** —— 每请求的 `body.settings.webSearch`(单用户车道上**压过**部署座,见 * `capabilities/scenarios.ts` 的 default 场景)**刻意不在这一面回显**:那是请求方自己送进来的东西, * 把它回显出去等于让能力面随调用方漂,而消费端正是拿这一位去渲「这台机器有没有搜索」的。 */ export interface WebSearchCapability { readonly backend: WebSearchProvider | "none"; } /** * 部署席 → 消费端窄投影。**唯一**写 `"none"` 的地方(缺席折词的属主),形照 `projectSqlEngineCapability` / * `projectWriteProtectionCapability`。 * * 🔴 缺席折 `"none"` **不是**兜底 fail-open:`undefined`(装配没喂席位 / `WEB_SEARCH_PROVIDER` 未设或拼错) * 与「本部署没有搜索后端」在**行为上是同一件事** —— `webSearchConfigFromEnv` 对这两种输入都返回 * `undefined`,于是 WebSearch 工具都不装配。方向也是安全的那一向:本位只会**少报**能力(说没有、其实 * 也真没有),不会替一台没配后端的部署点亮一格。「键整个缺席」才是另一回事(= 老 server),那一层由 * 消费端的探位纪律管,见契约附录 B.1 本行的「缺席何义」。 */ export declare function projectWebSearchCapability(provider: WebSearchProvider | undefined): WebSearchCapability; export interface WebSearchBackendConfig { readonly provider: WebSearchProvider; /** API key (brave/tavily). From `WEB_SEARCH_API_KEY` — never reaches the model. */ readonly apiKey?: string; /** SearXNG 实例特定查询参数(`engines=` / `language=` 等)——透传给 core adapter 的 `extraParams`。 * 一键装 SearXNG 的向导用它把「选了哪些上游引擎」落到引擎侧。仅 searxng 腿消费。 */ readonly searxngParams?: Record; /** SearXNG instance base URL (REQUIRED for searxng); for brave/tavily an optional base-URL override (proxy/test). */ readonly endpoint?: string; /** Max results returned to the model (clamped 1..20; default 10). */ readonly maxResults?: number; /** Per-search wall-clock (ms, default 10000). */ readonly timeoutMs?: number; /** Injectable for tests. */ readonly fetchImpl?: typeof fetch; } export interface WebSearchResult { title: string; url: string; snippet: string; } export interface WebSearchOpts { allowedDomains?: string[]; blockedDomains?: string[]; } /** The shape core's `WebSearchConfig.search` expects (back-compatible 2- or 3-arg). */ export type WebSearchFn = (query: string, signal?: AbortSignal, opts?: WebSearchOpts) => Promise; /** The live object `createWebSearchBackend` produces — a `search` fn (provider + API key captured in the closure) + the effective result cap. */ export interface WebSearchBackend { search: WebSearchFn; maxResults: number; } export declare function setWebSearchBadPayloadObserver(fn: ((provider: string) => void) | undefined): void; /** * Build the `WebSearchConfig` (a `search` fn + `maxResults`) for `assembleFullBodyTools({ webSearch })`. The API key * stays captured in this closure — it is never surfaced to the model or the tool args. */ export declare function createWebSearchBackend(cfg: WebSearchBackendConfig): WebSearchBackend; /** * `WEB_SEARCH_SEARXNG_PARAMS` / `settings.webSearch.searxngParams` 的解析 —— SearXNG 实例侧参数 * (`engines=` / `language=` 等)。 * * 🔴 [ref] cli 逐串直证:3.18.0 我加了 `searxngParams` 字段和消费点,**却没开任何配置入口** —— * 两条门都没有位置填它,于是我在 [ref] 说的「这条是给你们的」是句空话。单测直接构造 * `WebSearchBackendConfig` 传进去,**天然跳过了配置入口这一层**,于是「能力在」与「配得进」之间 * 断开,而两边的绿都是真的。判据:**按消费端实际能填的那个入口验,不是按自己构造的对象验。** * * 形状:`k=v` 用 `;` 分隔(`engines=bing,duckduckgo;language=zh-CN`)——值里本来就常含逗号, * 所以分隔符取 `;` 而不是 `,`。整串解析不出任何一对 ⇒ **返回 undefined,键整个不铸** * (不产出空对象:空对象会让下游以为「配了但是空的」,与本仓「缺席就要缺席得干净」同源)。 */ export declare function parseSearxngParams(raw: unknown): Record | undefined; /** * #81④ 探活门控 —— **默认 OFF**,显式开才探。 * * 为什么默认 OFF:探活是一次**真出网**。开着就等于给每个既有部署的每次启动凭空加一次外呼 —— * 多租/离线部署不能被这样动。这与 #81③「任何新的开启通道默认 OFF」同源: * 一个只配了模型 key 的部署,不该因为升级而多出网络行为。 * * 为什么只认明确真值:`"yes"`/`"on"` 这类**含糊值当没开**。含糊值上放行 = 替部署方猜意图, * 而猜错的方向是「凭空出网」。缺席要缺席得干净,含糊也一样。 */ export declare function shouldProbeWebSearchOnBoot(env?: NodeJS.ProcessEnv): boolean; /** Parse the deployment env into a backend config; `undefined` when no provider is set (⇒ WebSearch not assembled). */ export declare function webSearchConfigFromEnv(env?: NodeJS.ProcessEnv): WebSearchBackendConfig | undefined; /** * Parse an UNTRUSTED per-request `body.settings.webSearch` (the user's shell settings — `{provider, apiKey?, endpoint?, * maxResults?}`) into a backend config. Returns `undefined` for a missing/invalid provider — NEVER throws, so a bad * per-request config falls back to the deployment-env backend rather than failing the task. 🔒 The CALLER gates this to * the single-user host lane (a per-request `endpoint`/`apiKey` is a capability config; on multi-tenant a tenant could * point `searxng` at an internal URL = SSRF, so multi-tenant uses ONLY the deployment-env backend — [ref] axis). */ export declare function webSearchConfigFromSettings(raw: unknown): WebSearchBackendConfig | undefined; //# sourceMappingURL=web-search.d.ts.map