websearch: add scope=code programming search + provider extract chain
- search(..., scope=code) restricts to 15 programming domains (official docs, GitHub, registries, Stack Overflow, CVE/NVD): native include_domains/search_domain_filter where supported (Tavily, Exa, Perplexity), post-filtering for all providers (Brave, SearXNG, DuckDuckGo have no native filter). - web_fetch now uses fetch_best: Tavily extract, then Exa contents, then direct fetch; both-fail errors name both failures, direct-fetch 403 under the egress whitelist is fail-closed, not bypassed. Default general scope and direct fetch_url behaviour unchanged. - MCP web_search gains scope enum (general/code); docs updated. - Tests all mock HTTP, no real keys.
This commit is contained in:
1 parent
27f48fe1ed
commit
397a46f850
5 files changed
+803
-36
No files matched your search
+123
-6
@@ -15,17 +15,23 @@ pi 的工具),只复用它的模式:统一工具 + provider 抽象 +
|
||||
stdout 是 JSON-RPC 流,绝不能混日志。
|
||||
- `cloud/tests/test_websearch_mcp.py`:12 个单测,全部
|
||||
mock HTTP,不联网、不需要真实 key。
|
||||
- `cloud/tests/test_code_search.py`:编程作用域与抽取回退链
|
||||
的单测,同样全部 mock HTTP、不联网、不需要真实 key。
|
||||
|
||||
## 工具
|
||||
|
||||
- `web_search(query, limit=5, provider="auto")`:返回
|
||||
`{provider, results:[{title, url, snippet, score?}], answer?, attempted}`。
|
||||
- `web_search(query, limit=5, provider="auto", scope="general")`:返回
|
||||
`{provider, results:[{title, url, snippet, score?}], answer?, attempted, scope}`,
|
||||
`scope="code"` 时另带 `code_domains`。
|
||||
`answer` 只有 provider 本身给合成答案时才有(目前是
|
||||
Perplexity Sonar);其它 provider 的总结仍由主模型做。
|
||||
- `web_fetch(url, char_limit=15000)`:返回
|
||||
`{url, title, text, links, truncated}`。超长按头 75% +
|
||||
尾 25% 截断并标 `[TRUNCATED]`;只接受 http(s),拒绝
|
||||
localhost 与字面私有 IP。
|
||||
`scope` 详见下面“编程搜索 scope=code”一节。
|
||||
- `web_fetch(url, char_limit=15000)`:走 `fetch_best` 抽取回退链
|
||||
(Tavily extract → Exa contents → 直接抓取),返回
|
||||
`{url, title, text, links, truncated, source}`(`source` 为
|
||||
`tavily-extract` / `exa-contents` / `direct-fetch`)。
|
||||
超长按头 75% + 尾 25% 截断并标 `[TRUNCATED]`;只接受 http(s),拒绝
|
||||
localhost 与字面私有 IP;抽取与直接抓取都失败时报错,不伪造页面。
|
||||
|
||||
## Provider 链(auto 顺序)
|
||||
|
||||
@@ -101,6 +107,111 @@ Docker 租户必须传容器内路径
|
||||
字段与结果映射,未用真实 key 跑过;上生产前须用真实
|
||||
key 在测试项目跑一遍,再谈默认链顺序是否合理。
|
||||
|
||||
## 编程搜索 scope=code(v2 新增,只做编程任务搜索)
|
||||
|
||||
选型:`web_search` 加 `scope="code"` 参数,不另立
|
||||
`code_search` 工具。理由:同一条 provider 链、同一套
|
||||
回退与 key 规则,只多一层域名限定;另立工具会复制
|
||||
schema、handler 与播种文档两套,且调用方要在两个
|
||||
工具间二选一。代码(`cloud/websearch.py` 的
|
||||
`CODE_SEARCH_DOMAINS` / `search(..., scope=...)`)、
|
||||
MCP 工具描述(`cloud/websearch_mcp.py` 的
|
||||
`web_search` 描述与 `scope` enum)与本文三处一致,
|
||||
只有 `general` / `code` 两个值,未知 scope 在发任何
|
||||
HTTP 之前就报 `ValueError`,绝不静默退化成无限定搜索。
|
||||
|
||||
### 允许域清单(实际落代码的 15 个,含子域名)
|
||||
|
||||
- 官方文档:`docs.python.org`、`nodejs.org`、`go.dev`、
|
||||
`pkg.go.dev`、`docs.rs`、`doc.rust-lang.org`、
|
||||
`developer.mozilla.org`
|
||||
- 代码托管:`github.com`、`githubusercontent.com`
|
||||
(GitHub 自己的 raw host,blob 页之外原始文件所用)
|
||||
- 包仓库:`pypi.org`、`npmjs.com`、`crates.io`
|
||||
- 问答与安全:`stackoverflow.com`、`nvd.nist.gov`、`cve.org`
|
||||
|
||||
匹配规则是域名本身或其子域名(点边界匹配:
|
||||
`gist.github.com` 算、`evilgithub.com` 不算)。
|
||||
changelog / release notes 只有落在上述域名时才在
|
||||
范围内(例如项目在 GitHub 的 Releases 页);没有
|
||||
“项目官方域名自动识别”——不在清单里的项目官网
|
||||
(如某框架自己的 docs 站)本版不返回,这是明确的
|
||||
缺口,不是泛化成任意站的口子。
|
||||
|
||||
### 不在范围清单(scope=code 下一律过滤掉)
|
||||
|
||||
通用新闻、购物、社交、地点、机票酒店、天气、行情、
|
||||
视频/图片站、博客聚合与其它任意站。`scope="code"`
|
||||
不是“优先编程来源”,是硬过滤:非清单域的结果不
|
||||
返回、不降级展示,只在 `attempted` 里记
|
||||
`domain_filtered_out` 数量。
|
||||
|
||||
### 各后端如何限定(先核实请求字段再写代码)
|
||||
|
||||
- 原生支持,直接传:Tavily `include_domains`、
|
||||
Exa `include_domains`、Perplexity Sonar chat
|
||||
completions 的顶层 `search_domain_filter`(上限
|
||||
20 个域,本清单 15 个,整单可传)。
|
||||
- 原生不支持,不编造参数:Brave 网页搜索端点没有
|
||||
`include_domains` 参数(多域 `site:` 改写语义未经
|
||||
证实,不改写查询),SearXNG / DuckDuckGo 同样不传;
|
||||
这三家只靠 `search()` 返回后的统一事后过滤。
|
||||
- 双保险:原生限定的后端返回后同样再过一次事后
|
||||
过滤,所以各后端在 code 作用域下的语义一致。
|
||||
- 链语义:某后端结果被全部滤掉时,该后端记为
|
||||
“no results”(`code-domain filter removed all N
|
||||
results`),链继续走下一个后端;全部后端都空才
|
||||
报错。
|
||||
|
||||
### 抓取回退链与 403 语义(web_fetch)
|
||||
|
||||
`web_fetch` 实际调 `fetch_best`,顺序固定:
|
||||
|
||||
1. Tavily extract(`POST https://api.tavily.com/extract`,
|
||||
`urls:[url]`,取 `results[0].raw_content`;空结果或
|
||||
`failed_results` 命中都算失败)——只在配了
|
||||
`TAVILY_API_KEY` 时试。
|
||||
2. Exa contents(`POST https://api.exa.ai/contents`,
|
||||
`ids:[url]` + `text.maxCharacters`,取 `results[0].text`,
|
||||
无 text 时取 highlights 拼接;空算失败)——只在
|
||||
配了 `EXA_API_KEY` 时试。
|
||||
3. 直接抓取(旧 `fetch_url` 的 HTML 清洗与截断)。
|
||||
|
||||
抽取与直接都失败时,错误同时写明两边失败,不伪造
|
||||
页面。无任何抽取 key 时 `fetch_via_provider` 单独调会
|
||||
明确报“no extract provider configured”,但 `fetch_best`
|
||||
会直接走直接抓取(与旧 web_fetch 行为一致),不是
|
||||
报错。localhost / 私有 IP 在抽取之前就拒绝,不会经
|
||||
抽取商去探内网。
|
||||
|
||||
403 语义:抽取走的是搜索商 API 域名(`tavily.com`、
|
||||
`exa.ai`,已在 Tinyproxy 白名单里),不直接碰目标站,
|
||||
所以文档站本身不在白名单时抽取仍可通;只有回退到
|
||||
直接抓取时,目标域不在白名单才会被代理 403 挡住。
|
||||
这是 fail-closed:报错、不静默绕过、不承诺“任意
|
||||
文档站都能直接抓”。本轨**没有改**
|
||||
`deploy/egress/tinyproxy-filter`(也不改主机上的文件):
|
||||
若将来主机侧要让直接抓取也通,需另行走主机变更,
|
||||
filter 行须用 POSIX BASIC 正则(`^\(.*\.\)*域名$`)、
|
||||
文件 644,extended 锚定写法会把允许域也挡成 403
|
||||
(见部署教训);该变更不在本轨授权内。
|
||||
|
||||
### 安全与隔离(不变)
|
||||
|
||||
- key 只走 MCP 进程环境变量,绝不写进 config.toml、
|
||||
日志、测试、文档;本轨测试用的 key 全是
|
||||
`*-SECRET-*` 假值。
|
||||
- worker 开关默认关;租户播种幂等、已有 section
|
||||
不重写;Docker 环境变量走 extra_env +
|
||||
`[mcp_servers.websearch.env]` 路径。
|
||||
- `web_fetch` 仍只接受 http(s)、拒 localhost 与私有
|
||||
IP,且它不是沙箱边界(danger-full-access 任务本
|
||||
来就能 curl)。
|
||||
- 老租户注意:播种助手从不覆盖内容已变的租户脚本
|
||||
(既有防覆盖规则),所以 scope 只对新播种的租户
|
||||
立即生效;老租户拿到新脚本需另行决策,不在本轨
|
||||
改动该规则。
|
||||
|
||||
## 诚实边界
|
||||
|
||||
- 本批只做代码 + 单测 + 测试租户目录的配置合并验证,
|
||||
@@ -115,3 +226,9 @@ Docker 租户必须传容器内路径
|
||||
- `web_fetch` 不是安全边界:danger-full-access 的任务
|
||||
本来就能 curl,fetch 只挡了最明显的非 http(s) 与
|
||||
本地私有地址,不应把它当沙箱用。
|
||||
- 编程搜索本轨未做(见回执):真实 key 的 live 测试、
|
||||
主机 unit / filter / env 接线、GitHub API token
|
||||
(本设计走网页搜索后端限定域名,不调 GitHub API,
|
||||
所以不需要 token;真要 GitHub 代码内容搜索 API 是
|
||||
另一功能,不在本轨)、清单外项目官网域、老租户
|
||||
脚本升级。
|
||||
Reference in new issue
Block a user