Skip to content

feat(mcp-web-fetch): expose allowed_domains and blocked_domains on web_fetch schema (#380) - #393

Open
NanluQingshi wants to merge 3 commits into
ceilf6:developfrom
NanluQingshi:feat/mcp-web-fetch-domain-filters
Open

feat(mcp-web-fetch): expose allowed_domains and blocked_domains on web_fetch schema (#380)#393
NanluQingshi wants to merge 3 commits into
ceilf6:developfrom
NanluQingshi:feat/mcp-web-fetch-domain-filters

Conversation

@NanluQingshi

Copy link
Copy Markdown

Linked Issue Or Context

Closes #380

Repo-evolver iteration 24 found that @frontagent/mcp-web-fetch engine already
supports host filters via allowHosts/denyHosts, but the MCP tool schema did
not expose them — so the agent could not pass domain restrictions that the
engine already enforces.

Summary

Add optional allowed_domains: string[] and blocked_domains: string[] to the
web_fetch MCP tool schema, mapped to the engine's existing allowHosts/
denyHosts host filters. The agent can now pass domain restrictions aligned
with Anthropic web fetch semantics, enabling least-privilege fetches without
changing SSRF defaults.

  • allowed_domains → engine allowHosts (only listed domains permitted)
  • blocked_domains → engine denyHosts (listed domains rejected; takes
    precedence over allow list)
  • Both fields are optional and additive — omitting either preserves existing
    behavior

Impact Scope

Single file change in packages/mcp-web-fetch:

  • src/tools.ts — two new schema properties + two handler arg mappings (+14
    lines)
  • src/tools.domain-filters.test.ts — 4 focused no-network tests (+41 lines,
    new file)
  • CHANGELOG.md[Unreleased]### Added entry

No engine, SSRF, or url-safety logic changed. No other packages touched.

GitNexus Impact Summary

  • Risk level: LOW
  • Critical skeleton changes: None. Change is confined to packages/mcp-web- fetch (mcp-boundary), schema + handler only; no public MCP contract break —
    fields are additive optional.
  • GitNexus impact: detect_changes could not run locally (no indexed
    repository in this environment). The change adds optional schema fields and a
    passthrough mapping; no symbol signatures or call-graph edges are altered.
  • Verification: npx vitest run (48/48 pass), tsc --noEmit (clean), biome check (clean) on packages/mcp-web-fetch.

Verification

  • npx vitest run in packages/mcp-web-fetch → 7 test files, 48 tests passed
    (was 44; +4 new)
  • npx tsc --noEmit → exit 0
  • npx biome check on changed files → clean, no fixes applied

New tests (no network required — host filtering happens before any fetch):

  • blocked_domains rejects the listed host (asserts /denied/i)
  • allowed_domains rejects hosts not in the allow list (asserts /not in allow list/i)
  • schema exposes both fields as type: array
  • omitting both filters preserves SSRF guard behavior

Checklist

  • I have linked an issue or explained why this PR stands alone.
  • I have kept the diff focused on the stated change.
  • I have run pnpm quality:precommit, or explained why it could not run.
    (Ran equivalent focused gates manually — lint + typecheck + test on packages/ mcp-web-fetch; pre-commit hook env has a broken pnpm/corepack 9.0.0 self-
    update, CI remains authoritative.)
  • I have run pnpm quality:local for critical skeleton changes, or
    explained why it could not run. (Not a critical skeleton change; scoped to one
    MCP adapter.)
  • I have updated docs or tests when behavior, public APIs, or Harness
    contracts changed. (Added CHANGELOG entry + focused tests.)
  • For critical skeleton changes, I have filled the GitNexus impact summary
    with concrete results. (N/A — LOW risk, non-critical.)

…b_fetch schema (ceilf6#380)

The engine already supported allowHosts/denyHosts host filters, but the
MCP tool schema/handler did not expose them. Add optional allowed_domains
and blocked_domains to the web_fetch tool, mapped to the engine's
allowHosts/denyHosts, so the agent can pass domain restrictions aligned
with Anthropic web fetch semantics for least-privilege fetches.

SSRF defaults are unchanged; the filters are additive and optional.
@NanluQingshi
NanluQingshi requested a review from ceilf6 as a code owner July 25, 2026 10:04
@ceilf6

ceilf6 commented Jul 28, 2026

Copy link
Copy Markdown
Owner

/review

1 similar comment
@ceilf6

ceilf6 commented Jul 28, 2026

Copy link
Copy Markdown
Owner

/review

@ceilf6

ceilf6 commented Jul 28, 2026

Copy link
Copy Markdown
Owner

@ceilf6/repo-guard

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ ceilf6/repo-guard

代码评审报告: feat(mcp-web-fetch): expose allowed_domains and blocked_domains on web_fetch schema (#380)

风险等级:
处理建议: 请求修改
决策摘要: 管线本身是安全的加法式改动(省略参数时行为不变,重定向每跳都会复用过滤器),但新增的 schema 描述把"精确主机名相等"包装成"与 Anthropic web fetch 一致"的域名过滤,blocked_domains: ['example.com'] 不会拦截 www.example.com(安全控制方向上是 fail-open);同时仓库内没有任何消费者读取 inputSchema,agent 事实上仍无法传这两个字段——即 issue 的 problem statement 未被真正解决。两处最小修复都在 diff 内(改描述 + 修正 CHANGELOG 声明或补上 planner 参数通路)。

级联分析

  • 变更符号: webFetchSchema.inputSchema.properties(新增 2 个属性)、handleWebFetchTool(新增 2 个 arg 映射)。函数签名、返回形状、错误行为均未变。
  • 受影响流程: handleWebFetchToolfetchUrl(engine.ts:107)→ parseAndValidateUrl(url-safety.ts:192)。首跳与每个重定向跳(engine.ts:119-122、156-159)都传入相同的 allowHosts/denyHosts,不存在重定向绕过过滤器的缺口。deny 先于 allow 检查(url-safety.ts:218 早于 222),schema 中"优先级高于 allowed_domains"的描述属实。
  • 变更集外调用方: 全仓仅 packages/runtime-node/src/mcp-clients.ts:24 导入本包,且只导入 handleWebFetchTool,在 mcp-clients.ts:147-148 原样透传 args——不会剥离新字段,无破坏。webFetchSchema / allWebFetchSchemas 虽从 index.ts:12-17 导出,但没有任何仓库内消费者(text search);packages/mcp-web-fetch 无 server/bin 入口(package.json 无 bin),也不作为独立 MCP server 对外提供 tools/list
  • 置信度: degraded — PR 自述 GitNexus detect_changes 未能运行,本次结论基于 Read/Grep 文本证据;调用面很窄(单一跨包导入),但无图谱交叉验证。

问题发现

  1. [高] 新增 schema 描述与引擎实际匹配语义不符,黑名单方向 fail-open

    • 证据: tools.ts:44-4550-51 向模型声明"与 Anthropic web fetch 的 allowed_domains/blocked_domains 一致";但 url-safety.ts:218223h.toLowerCase() === lowerHost 精确相等比较,无后缀/子域匹配、无 scheme 或端口归一化。因此 blocked_domains: ['example.com'] 不拦截 www.example.comevil.example.com;若调用方按 Anthropic 文档习惯写成 ['https://example.com'],黑名单将完全不生效且静默通过。白名单方向反过来是 fail-closed(example.com 不放行 docs.example.com),安全但会造成"配了却抓不到"的困惑。
    • 受影响调用方/流程: 任何直接调用 handleWebFetchTool 并依赖 blocked_domains 做最小权限约束的调用方;这是新增的公共工具契约文本,模型会按描述行事。
    • 最小可行修复: 保持行为不变(issue 非目标明确禁止改 URL safety policy),把两条 description 改成明确的"精确主机名匹配,子域需逐一列出,不含 scheme/端口"。若确实要 Anthropic 的子域语义,应作为独立 issue 修改 parseAndValidateUrl 并同步 url-safety 测试,不要在描述里先行承诺。
  2. [高] issue 的 problem statement 未被解决:agent 仍无法传这两个字段

    • 证据: issue #380 的问题陈述是"the agent cannot pass the domain restrictions"。但 (a) packages/core/src/executor/types.ts:15MCPClient.listTools() 返回类型只有 { name, description },全仓无任何位置读取 webFetchSchema.inputSchema;(b) packages/runtime-node/src/mcp-clients.ts:154-171 是手工维护的工具清单,只有名称和描述,不含参数 schema;(c) agent 的 web_fetch 步骤参数受 packages/core/src/llm/schemas.ts:19-37STEP_PARAMS_SCHEMA 约束,这是一个封闭对象(有 url,无 allowed_domains/blocked_domains),zod 默认 strip 未知键;(d) packages/core/src/executor/executor.ts:155 透传的正是这份被 strip 过的 step.params。全仓 allowed_domains|blocked_domains 仅出现在 tools.ts、新测试、CHANGELOG 三处(text search)。
    • 受影响调用方/流程: LLM 规划 → executor → mcp-clients → handleWebFetchTool 的唯一 agent 路径。当前该能力只对直接编程调用者可用。
    • 最小可行修复: 二选一,由维护者决定范围——要么在 STEP_PARAMS_SCHEMA 补上这两个可选数组字段(小改动,贴合 issue 的问题陈述);要么保持本 PR 的 schema-only 范围,但把 CHANGELOG 与 PR 描述中"The agent can now pass domain restrictions"改成准确表述(引擎能力已在工具契约层暴露,agent 侧接线待跟进),并开跟进 issue。不要让 CHANGELOG 宣称一个当前不成立的用户可见效果。
  3. [中] 空数组语义未文档化,allowed_domains: [] 等于"不限制"

    • 证据: url-safety.ts:222opts.allowHosts && opts.allowHosts.length > 0 判定,空数组直接跳过白名单校验。调用方程序化构造白名单、结果为空时,期待"什么都不许抓",实际得到"全部放行"。
    • 受影响调用方/流程: 任何动态构造 allowed_domains 的调用方;schema 描述目前对此只字未提。
    • 最小可行修复: 在 allowed_domains 描述中写明"空数组视为不启用白名单"。行为改动(空数组=全拒)会触及 URL safety policy,属 issue 非目标,不建议在本 PR 做。
  4. [低] 非数组入参只会抛出不透明的 TypeError

    • 证据: tools.ts:75-76 直接 as string[] | undefined,无运行时校验。模型误传 blocked_domains: "example.com" 时,url-safety.ts:218.someTypeError,被 tools.ts:83-88 兜住,返回 denyHosts.some is not a function 之类的错误文案。结果是 fail-closed(不会发出请求),所以不是安全缺口,但诊断信息很差,且与数值参数由 clampLimit(engine.ts:50-53)做防御的既有模式不一致。
    • 受影响调用方/流程: LLM 生成的工具调用参数(数组类型最易被写错)。
    • 最小可行修复: 在 handler 里做一次归一化——非数组丢弃、元素过滤为非空字符串后再传给引擎。

行级发现

  • [packages/mcp-web-fetch/src/tools.ts:51] 描述声称与 Anthropic blocked_domains 一致,但 url-safety.ts:218 是精确主机名相等,['example.com'] 不会拦截 www.example.com;改为明确说明"精确主机名匹配,子域需逐条列出,不含 scheme/端口"。
  • [packages/mcp-web-fetch/src/tools.ts:45] 同样问题 + 空数组语义:url-safety.ts:222[] 直接跳过白名单校验,描述里需写明"空数组视为不启用白名单",避免调用方误以为是"全部拒绝"。
  • [packages/mcp-web-fetch/src/tools.ts:75] as string[] | undefined 无运行时校验,非数组入参会在引擎内抛 .some is not a function;建议先归一化为字符串数组(非数组则视为未提供),与 clampLimit 的防御模式对齐。
  • [packages/mcp-web-fetch/src/tools.domain-filters.test.ts:33] 这条用例与 tools.security.test.ts:5-10 的 URL、断言正则完全相同,属重复覆盖;换成子域匹配用例(blocked_domains: ['example.com'] + https://www.example.com/)才能锁定真实语义。
  • [CHANGELOG.md:9] "The agent can now pass domain restrictions" 与当前代码不符:core/src/executor/types.ts:15listTools 不带 schema,core/src/llm/schemas.ts:19-37 的封闭 STEP_PARAMS_SCHEMA 会 strip 掉这两个键;请改为准确表述或在本 PR 补上 planner 侧字段。

Karpathy 评审

  • 假设: PR 假设"暴露 schema 字段 = agent 可用",该假设不成立(见发现 2);同时假设引擎的 host 过滤与 Anthropic 的 domain 语义等价,实际是精确主机名相等(见发现 1)。两个假设都未在代码或测试中被验证。
  • 简洁性: 良好。纯 passthrough 映射,无新抽象、无新层级、无配置膨胀,复用引擎既有的 allowHosts/denyHosts,没有重复造 helper。
  • 结构质量: 无退化。tools.ts 89 行,schema 属性与既有 format/timeoutMs/maxBytes 同构,没有新增分支、flag 或特例路径。snake_case 与既有 camelCase 参数混用属 issue 明确要求(对齐 Anthropic 命名),不作为 finding。
  • 变更范围: 精准。3 个文件、无无关重构、无格式噪声,每一行都能追溯到 issue 的 Scope;未触碰 SSRF、私网拦截、重定向、clamp 逻辑,符合非目标约束。
  • 验证: 强度不足以匹配契约文本。4 条测试只覆盖精确匹配的拒绝路径,没有一条覆盖最容易出错的子域行为;第 4 条与既有 SSRF 测试重复;没有正向用例证明白名单内的 URL 能通过过滤器(handler 层无 fetchImpl 注入口,可在 engine 层用 FetchOptions.fetchImpl 补,engine.ts:32)。

缺失覆盖

  • 子域语义用例:blocked_domains: ['example.com'] + https://www.example.com/,断言当前的精确匹配行为(会放行),把这一意外语义锁进测试并与修正后的描述对齐。
  • 白名单正向路径:在 engine 层用 fetchImpl 注入 mock,验证 allowHosts: ['example.com']https://example.com/ 能通过校验并完成抓取;当前 3 条行为测试全是拒绝断言。
  • 重定向 + 过滤器组合:首跳在白名单内、Location 指向白名单外主机时应被 engine.ts:156-159 拒绝——这是过滤器最有价值的防护场景,目前无覆盖。
  • 畸形入参:blocked_domains 传字符串或包含空串的数组时,应有可读的错误信息而非 .some is not a function
  • 若采纳发现 2 的接线方案:需要一条 planner/executor 层测试,证明 allowed_domains 能从计划步骤参数一路到达 handleWebFetchTool 而不被 zod strip。

Comment thread packages/mcp-web-fetch/src/tools.ts Outdated
type: 'array',
items: { type: 'string' },
description:
'可选域名黑名单。提供后拒绝抓取列表中的域名(与 Anthropic web fetch 的 blocked_domains 一致)。优先级高于 allowed_domains。',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

描述声称与 Anthropic blocked_domains 一致,但 url-safety.ts:218 是精确主机名相等,['example.com'] 不会拦截 www.example.com;改为明确说明"精确主机名匹配,子域需逐条列出,不含 scheme/端口"。

Comment thread packages/mcp-web-fetch/src/tools.ts Outdated
type: 'array',
items: { type: 'string' },
description:
'可选域名白名单。提供后仅允许抓取列表中的域名(与 Anthropic web fetch 的 allowed_domains 一致)。用于最小权限场景,限制 Agent 只能访问受信任的文档站点。',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

同样问题 + 空数组语义:url-safety.ts:222[] 直接跳过白名单校验,描述里需写明"空数组视为不启用白名单",避免调用方误以为是"全部拒绝"。

format: args.format as 'text' | 'html' | undefined,
timeoutMs: args.timeoutMs as number | undefined,
maxBytes: args.maxBytes as number | undefined,
allowHosts: args.allowed_domains as string[] | undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as string[] | undefined 无运行时校验,非数组入参会在引擎内抛 .some is not a function;建议先归一化为字符串数组(非数组则视为未提供),与 clampLimit 的防御模式对齐。

expect(result.error).toMatch(/not in allow list/i);
});

it('omitting both filters preserves existing behavior (SSRF guard still applies)', async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这条用例与 tools.security.test.ts:5-10 的 URL、断言正则完全相同,属重复覆盖;换成子域匹配用例(blocked_domains: ['example.com'] + https://www.example.com/)才能锁定真实语义。

Comment thread CHANGELOG.md Outdated

### Added

- **mcp-web-fetch**: Exposed optional `allowed_domains` and `blocked_domains` on the `web_fetch` MCP tool schema, mapped to the engine's existing `allowHosts`/`denyHosts` host filters. The agent can now pass domain restrictions aligned with Anthropic web fetch semantics, enabling least-privilege fetches without changing SSRF defaults.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"The agent can now pass domain restrictions" 与当前代码不符:core/src/executor/types.ts:15listTools 不带 schema,core/src/llm/schemas.ts:19-37 的封闭 STEP_PARAMS_SCHEMA 会 strip 掉这两个键;请改为准确表述或在本 PR 补上 planner 侧字段。

…ut (ceilf6#380)

Address PR ceilf6#393 review feedback so the agent can actually pass domain
restrictions end-to-end, with no new failure modes.

- Thread allowed_domains/blocked_domains through STEP_PARAMS_SCHEMA and
  ErrorRecoveryPlanSchema (optional z.array(z.string())). The closed zod
  object previously stripped these keys before they reached
  handleWebFetchTool, so the capability was only available to direct
  programmatic callers — the planner/executor wiring now preserves them.
- Rewrite schema descriptions to state exact-hostname matching semantics
  (no scheme/port; subdomains must be listed individually) and that an
  empty array disables the filter rather than denying all. Drops the
  misleading "aligned with Anthropic" claim.
- Add normalizeHostList in engine.ts (mirrors the clampLimit defensive
  pattern) so non-array input is normalized to undefined instead of
  throwing ".some is not a function" inside the engine. Applied on both
  the initial URL and every redirect hop.
- Tests: replace the redundant SSRF-omission case with subdomain-semantics,
  empty-array, and malformed-input coverage; add a url-safety exact-match
  subdomain test; add a core schema test proving the fields survive
  GeneratedPlanSchema.parse (not stripped).
- CHANGELOG: rephrase to accurately reflect agent-usable end-to-end behavior.

Quality gate run manually (pnpm is environmentally broken on Node 18:
corepack pins the unpublished pnpm@9.0.0). biome + tsc clean; affected
mcp-web-fetch / core llm suites pass. Pre-existing env-only failures
(langgraph crypto, zod z.file, mcp-filesense fixture cleanup) reproduce
identically on the baseline commit and are unrelated to this change.

Co-Authored-By: Claude <noreply@anthropic.com>
@ceilf6

ceilf6 commented Jul 28, 2026

Copy link
Copy Markdown
Owner

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ ceilf6/repo-guard

代码评审报告: feat(mcp-web-fetch): expose allowed_domains and blocked_domains on web_fetch schema (#380)

风险等级:
处理建议: 请求修改
决策摘要: 功能映射与包内级联是正确的,但 PR 在两个 generateObject 结构化输出 schema 中首次引入了 .optional() 字段,而该文件既有约定是"全部必填 + 空值哨兵";这条链路完全没有验证证据(PR 只跑了 packages/mcp-web-fetch),建议改成必填空数组后再合。

级联分析

  • 变更符号: webFetchSchema / handleWebFetchTool(tools.ts)、fetchUrl + 新增 normalizeHostList(engine.ts)、STEP_PARAMS_SCHEMAErrorRecoveryPlanSchema.recoverySteps[].params(core/llm/schemas.ts)
  • 受影响流程: planner 结构化出计划 → normalizePlanPlanner.convertLLMSteps(planner.ts:323 直接透传 llmStep.params)→ Executor 展开 {...step.params}(executor/executor.ts:155,177)→ mcp-clients.ts:147handleWebFetchToolfetchUrlparseAndValidateUrl(url-safety.ts:218-226)。链路端到端成立,CHANGELOG 关于"此前被 zod 闭合对象剥离"的说明属实。
  • 变更集外调用方: allowHosts/denyHosts 仅由 fetchUrl 与包内测试使用(text search,无包外消费者);STEP_PARAMS_SCHEMAGeneratedPlanSchema/StepExpansionSchema 共用,generateObject 调用点为 plan-generation.ts:321、plan-generation.ts:468、code-generation.ts:398(ErrorRecoveryPlanSchema)。url-safety.ts 未改动,SSRF 默认行为保持不变。
  • 置信度: medium(基于文件阅读 + text search,无代码图谱;PR 自述 detect_changes 未能运行。provider 侧 JSON Schema 转换行为无法在本 checkout 验证——依赖未安装)

问题发现

  1. [高] 首次向结构化输出 schema 引入 optional 字段,与既有"全部必填"约定冲突且零验证

    • 证据: STEP_PARAMS_SCHEMA 其余 15 个字段全部必填并用哨兵值表达"不适用",描述文字明写"所有字段必填,不适用的字符串填空字符串"(schemas.ts:19-51)。本 PR 新增的 allowed_domains/blocked_domains 是该文件中唯二进入 generateObject 路径的 .optional() 字段(GeneratedPatchSchema 虽有 optional,但 grep 显示它没有任何 generateObject 调用点,因此不构成先例)。llm-service.ts:130-134 使用 openai.chat(model)(AI SDK v5,ai@^5.0.199 / @ai-sdk/openai@^2.0.106),OpenAI 严格 structured outputs 要求 required 覆盖全部 properties;optional 字段在该模式下是已知的不兼容点。本环境未安装依赖,无法实证,PR 也未提供任何 provider 侧或 core 侧验证。
    • 受影响调用方/流程: generatePlan(plan-generation.ts:468)、generatePlanInTwoPhases(plan-generation.ts:321)、generateErrorRecoveryPlan(code-generation.ts:398)——即 OpenAI provider 下的主计划生成路径。
    • 最小可行修复: 去掉 .optional(),改为必填 z.array(z.string()),描述沿用文件现有约定"不适用时填空数组"。行为完全等价:空数组在 normalizeHostList 中已折叠为 undefinedurl-safety.ts:222 也把空 allow 列表视为不启用。这样既消除 provider 兼容风险,又与同文件其余字段保持一致。若坚持保留 optional,请补一次真实 provider structured-output 调用或 JSON Schema 转换断言作为证据。
  2. [中] normalizeHostList 只挡住了类型错误,挡不住最常见的格式错误,deny 列表会静默失效

    • 证据: engine.ts:66-75 只做 Array.isArray + typeof string + trimurl-safety.ts:218 用完整 hostname 全等比较。模型按 schema 描述"不含 scheme/端口"是软约束,实际最常见的错误输入是 "https://evil.com""evil.com:443"。这类值进入 blocked_domains 后永远匹配不上,黑名单静默变成空操作(fail-open);进入 allowed_domains 则变成全量拒绝。该函数的注释自称是为"模型写错"兜底,但恰恰漏了这一类。
    • 受影响调用方/流程: 所有带域名过滤的 web_fetch 调用;SSRF 默认防护不受影响,但最小权限这一层会无声失效。
    • 最小可行修复: 在 normalizeHostList 中对每个条目做一次规范化——若含 ://new URL(entry).hostname,否则剥离尾部 :port、结尾 .,并 toLowerCase();无法解析的条目丢弃。同时为 "https://example.com" 形式补一条测试。
  3. [中] 精确主机名匹配与 Anthropic 同名参数的语义不一致,容易造成 agent 自我拒绝

    • 证据: tools.ts:41-52 的描述已明确说明"子域需逐条列出",url-safety.test.ts:97-115 也固化了该语义。但 issue #380 的显式约束是"Keep naming aligned with Anthropic's documented tool parameters"——采用同名参数却是更严格的精确匹配。模型对 allowed_domains: ["example.com"] 的自然预期是覆盖 docs.example.com,实际会在抓文档站时被自己设的白名单挡下(Host is not in allow list),且这是硬失败而非降级。
    • 受影响调用方/流程: planner 生成的最小权限 web_fetch 步骤;失败发生在网络请求前,会直接进入 executor 的错误路径。
    • 最小可行修复: 二选一并明确记录——(a) 保持精确匹配(符合 issue 的"不改 URL 安全策略"非目标),但在 STEP_PARAMS_SCHEMA 的描述里也把"子域不自动包含"写全(当前 core 侧描述已有一句,但比 tools.ts 弱),并在 CHANGELOG 中标注与 Anthropic 语义的差异;(b) 改为后缀匹配 host === d || host.endsWith('.' + d)。注意 (b) 会放宽 allow 列表语义,属于安全策略变更,需单独 PR。
  4. [中] 验证范围没有覆盖实际改动的包,PR 描述与 diff 不符

    • 证据: PR 描述称"Single file change in packages/mcp-web-fetch"、"No engine, SSRF, or url-safety logic changed"、"No other packages touched",并据此给出 LOW 风险与"非关键骨架变更"的结论。实际 diff 改了 packages/mcp-web-fetch/src/engine.tspackages/core/src/llm/schemas.tspackages/core/src/llm/plan-generation.test.tspackages/mcp-web-fetch/src/url-safety.test.ts。验证一节只列了 packages/mcp-web-fetch 的 48 个测试,packages/core 一次都没跑——而问题 1 正好落在 core 里。
    • 受影响调用方/流程: 评审者的风险判断依据;packages/core 的 planner/executor 测试集。
    • 最小可行修复: 更新 PR 描述与 GitNexus 影响摘要以反映真实范围,并补跑 packages/core 的测试(至少 plan-generationplannerexecutor)后把结果贴进验证一节。

行级发现

  • [packages/core/src/llm/schemas.ts:38] 该文件其余字段一律必填 + 哨兵值,这里的 .optional() 是进入 generateObject 的首个可选字段,OpenAI 严格 structured outputs 下有兼容风险;改成必填 z.array(z.string()) + "不适用时填空数组",行为等价(空数组已被 normalizeHostList 折叠为 undefined)。
  • [packages/core/src/llm/schemas.ts:140] 同上,ErrorRecoveryPlanSchemacode-generation.ts:398generateObject,需与 STEP_PARAMS_SCHEMA 采取一致处理,不要一处 optional 一处必填。
  • [packages/mcp-web-fetch/src/engine.ts:71] 只 trim() 无法处理 "https://example.com" / "example.com:443" 这类模型最常写错的格式,blocked_domains 会静默失效;此处补一次 hostname 规范化(含 :// 时取 new URL(entry).hostname,否则剥离端口/结尾点并小写)。
  • [packages/mcp-web-fetch/src/tools.ts:45] 与 Anthropic 同名参数的子域语义不一致,模型给出 example.com 却抓 docs.example.com 时会被自己的白名单硬拒;若保持精确匹配,请把这条约束同步写进 STEP_PARAMS_SCHEMA 的描述并在 CHANGELOG 标注差异。

Karpathy 评审

  • 假设: 隐含假设"planner 结构化输出可以接受 optional 字段",未验证(问题 1);隐含假设"模型会按描述给出裸 hostname",normalizeHostList 的兜底并未覆盖该假设失效的主要形态(问题 2)。
  • 简洁性: 映射本身足够薄——两个 schema 属性 + 两行透传,没有多余抽象。normalizeHostList 是合理的边界防御,放在 engine(安全边界)而非 tools 层是正确取舍。
  • 结构质量: 无退化。没有新增分支扩散、没有薄 wrapper、没有重复 canonical helper,文件规模均在可扫描范围内(tools.ts 89 行、engine.ts 273 行、schemas.ts 175 行)。ErrorRecoveryPlanSchema 内联复制 params 形状是本就存在的重复,本 PR 只是跟随,不算新增债务。
  • 变更范围: 基本外科手术式,但比 PR 自述宽(跨 2 个包 3 个源文件)。engine 的规范化与 core 的 schema 透传都是让功能真正端到端可用所必需,属于合理扩展,问题在于描述没有同步(问题 4)。
  • 验证: 包内测试设计得好——4 个 handler 级测试全部在网络请求前短路,确实无网络依赖,且反向覆盖了 mapping 是否写反(denied vs not in allow list)。缺口在 core 侧:新增的 GeneratedPlanSchema.parse 测试只证明 zod 运行时不剥离字段,没有证明 provider 侧 JSON Schema 转换可用,而后者才是问题 1 的风险面。

缺失覆盖

  • provider 侧结构化输出兼容性:对 GeneratedPlanSchema 做一次 JSON Schema 转换断言(例如断言 required 覆盖全部 properties,或按当前 provider 约定断言可选字段的表示形式),否则问题 1 无法被 CI 捕获。
  • packages/core 全量测试结果:schemas.ts 被改动但该包测试未在 PR 中报告运行。
  • 域名格式健壮性:allowed_domains: ['https://example.com']['example.com:443']['EXAMPLE.com '] 三类输入的行为断言(当前 normalizeHostList 对前两类会静默产生永不匹配的过滤器)。
  • 正向放行路径:目前只有"被拒绝"方向的 handler 级断言;建议补一条 allowed_domains 命中后仍能进入 fetch 阶段的断言(可经 fetchImpl 注入或直接在 url-safety 层断言,避免真实网络)。

Comment thread packages/core/src/llm/schemas.ts Outdated
changeDescription: z.string().describe('要做的修改描述(不适用时填空字符串)'),
allowed_domains: z
.array(z.string())
.optional()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

该文件其余字段一律必填 + 哨兵值,这里的 .optional() 是进入 generateObject 的首个可选字段,OpenAI 严格 structured outputs 下有兼容风险;改成必填 z.array(z.string()) + "不适用时填空数组",行为等价(空数组已被 normalizeHostList 折叠为 undefined)。

Comment thread packages/core/src/llm/schemas.ts Outdated
changeDescription: z.string().describe('要做的修改描述(不适用时填空字符串)'),
allowed_domains: z
.array(z.string())
.optional()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

同上,ErrorRecoveryPlanSchemacode-generation.ts:398generateObject,需与 STEP_PARAMS_SCHEMA 采取一致处理,不要一处 optional 一处必填。

const cleaned: string[] = [];
for (const entry of value) {
if (typeof entry !== 'string') continue;
const trimmed = entry.trim();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trim() 无法处理 "https://example.com" / "example.com:443" 这类模型最常写错的格式,blocked_domains 会静默失效;此处补一次 hostname 规范化(含 :// 时取 new URL(entry).hostname,否则剥离端口/结尾点并小写)。

type: 'array',
items: { type: 'string' },
description:
'可选域名白名单,仅 web_fetch 适用,可省略。采用精确主机名匹配(不含 scheme/端口):提供后仅允许抓取主机名恰好出现在列表中的 URL,子域需逐条列出(如 example.com 不会自动放行 www.example.com)。空数组视为不启用白名单(而非全部拒绝)。用于最小权限场景,限制 Agent 只能访问受信任的文档站点。',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

与 Anthropic 同名参数的子域语义不一致,模型给出 example.com 却抓 docs.example.com 时会被自己的白名单硬拒;若保持精确匹配,请把这条约束同步写进 STEP_PARAMS_SCHEMA 的描述并在 CHANGELOG 标注差异。

Use z.array(z.string()).nullish() instead of .optional() for
allowed_domains/blocked_domains in STEP_PARAMS_SCHEMA and
ErrorRecoveryPlanSchema. Models sometimes emit null for optional fields;
.optional() only accepts undefined and would reject null, forcing an
object-repair retry. .nullish() accepts null/undefined, and the engine's
normalizeHostList already treats null identically to undefined (filter
disabled). Adds a regression test proving null survives
GeneratedPlanSchema.parse without error.

Co-Authored-By: Claude <noreply@anthropic.com>
@ceilf6

ceilf6 commented Jul 29, 2026

Copy link
Copy Markdown
Owner

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ ceilf6/repo-guard

代码评审报告: feat(mcp-web-fetch): expose allowed_domains and blocked_domains on web_fetch schema (#380)

风险等级:
处理建议: 请求修改
决策摘要: 功能设计本身正确、契约是附加式的,主路径未发现破坏性问题;但畸形过滤器输入会让新加的安全限制静默失效(fail-open,并被新测试固化),且 PR 自述范围与实际 diff 不符——占一半改动的 packages/core LLM 契约没有任何验证证据,建议补这两项后合入。

级联分析

  • 变更符号: webFetchSchema.inputSchema.propertieshandleWebFetchToolfetchUrl、新增导出 normalizeHostList(engine.ts:66)、STEP_PARAMS_SCHEMAErrorRecoveryPlanSchema.recoverySteps.params
  • 受影响流程: planner 结构化输出(GeneratedPlanSchema / StepExpansionSchema / ErrorRecoveryPlanSchema,覆盖全部 13 个 action,不只 web_fetch)→ Executor.executeStep(executor.ts:155 {...step.params} 原样透传)→ ExecutorToolCallHandler.callToolWebMCPClient.callTool(mcp-clients.ts:147) → handleWebFetchToolfetchUrlparseAndValidateUrl(首跳 + 每个重定向跳)
  • 变更集外调用方: fetchUrl 仅被 tools.ts:71engine.test.ts 调用;parseAndValidateUrl 的 host filter 仅由 engine 传入——包内闭合,无外部签名破坏。FetchOptions 类型未变,既有编程式调用方兼容。models/frontagent-planner/prompts/schema.jsonGeneratedPlanSchema 的镜像副本,未同步(该副本本就已漂移,action enum 里连 web_fetch 都没有)。
  • 置信度: medium(无代码图谱:PR 自述 detect_changes 未能运行,本环境亦无索引。结论来自 grep + 直接读取全部调用点;@ai-sdk/openai 的 strict structured-output 行为因 node_modules 未安装而无法实测,仅通过仓库内既有先例交叉验证)

问题发现

  1. [中] 畸形过滤器输入让 deny list 静默失效(fail-open),且被新测试固化

    • 证据: normalizeHostList(engine.ts:66-75) 对任何非数组输入返回 undefined。改动前 blocked_domains: 'evil.com'(裸字符串)会在 opts?.denyHosts?.some(...)(url-safety.ts:218) 抛 TypeError,被 handleWebFetchTool 的 catch 转成失败——请求不会发出(fail-closed)。改动后同样输入被当作"未提供过滤器",抓取照常进行(fail-open)。tools.domain-filters.test.ts:46-57 把这一行为写成了期望。同理,['evil.com', 123] 中的非字符串项被静默丢弃。
    • 受影响调用方/流程: 任何直接调用 handleWebFetchTool 的 MCP host(runtime-node 内的 executor 路径参数未做校验,见 tool-call-handler.ts:74);FrontAgent 自身走 zod 校验,可达性较低,但这正是该特性存在意义的场景——调用方打算限制却被静默忽略。
    • 最小可行修复: normalizeHostList 区分"缺省"与"畸形":value == null 返回 undefined;非数组、或数组中含非字符串项时抛 UrlSafetyError('Invalid domain filter: ...')(handler 已有 catch,会转成 success:false)。空数组仍保留"不启用"语义。同步调整该测试断言为期望明确报错。
  2. [中] PR 自述范围与验证证据不覆盖实际 diff 的一半

    • 证据: PR 描述称 "Single file change in packages/mcp-web-fetch"、"No engine ... changed"、"No other packages touched"、风险 LOW;实际 diff 改了 engine.tspackages/core/src/llm/schemas.ts(planner 的 LLM 输出契约,被 GeneratedPlanSchema/StepExpansionSchema/ErrorRecoveryPlanSchema 共用,影响全部 action 的规划调用)以及 packages/core/src/llm/plan-generation.test.ts。Verification 段落只列了 packages/mcp-web-fetch 的 48 tests / tsc / biome,pnpm quality:precommit 因环境问题未跑。
    • 受影响调用方/流程: 所有 LLMService.generateObject 规划调用。注:STEP_PARAMS_SCHEMA 原本每个字段都必填(描述明写"所有字段必填"),本 PR 引入其首批可选字段;我检查后确认 code-quality-subagent.ts:160,163 已在同一 generateObject 通道上使用 .optional(),因此没有证据表明会触发 provider 的 strict-schema 报错——但这一判断依赖 provider 行为,需要真实跑一次核心包的规划测试来兜底。
    • 最小可行修复: 补跑并贴出 packages/core 的 vitest + tsc 结果(至少 packages/core/src/llm),并把 PR 描述/GitNexus 影响摘要更正为实际范围(含 core 的 LLM 契约变更),不要以 "single file / LOW" 的前提让人评审。
  3. [中] blocked_domains 采用精确匹配,对子域 fail-open,与其借用的 Anthropic 语义存在落差

    • 证据: parseAndValidateUrl(url-safety.ts:218) 只做全等比较,url-safety.test.ts:97-113 明确固化 denyHosts: ['example.com'] 不拦截 www.example.com。issue #380 要求"命名对齐 Anthropic 文档参数",同时非目标是"不改 URL safety policy"——所以取名对齐、语义不对齐是必然结果。白名单方向是 fail-closed(安全),黑名单方向是 fail-open(列了 evil.com 仍会抓 www.evil.com)。
    • 受影响调用方/流程: 使用该黑名单做最小权限约束的 agent/调用方。当前缓解手段只有文档——tools.ts:47-52schemas.ts:42-47 的描述都明确写了"子域需逐条列出",模型侧可见,缓解到位。
    • 最小可行修复: 本 PR 内不必改(符合 issue 非目标);建议在 CHANGELOG/issue 中留下明确的后续项:deny 侧改为后缀匹配 host === h || host.endsWith('.' + h),或提供显式的 *.example.com 语法。请维护者确认接受当前语义。
  4. [低] ErrorRecoveryPlanSchema 的 params 副本继续扩大重复

    • 证据: schemas.ts:126-147 内联了一份 STEP_PARAMS_SCHEMA 的近似副本(本就缺 query/filePattern/globOnly/maxResults),本 PR 又把两个新字段以不同措辞的 describe 复制了进去(schemas.ts:138-145 vs 36-47)。
    • 受影响调用方/流程: analyzeErrorsAndGenerateRecovery(code-generation.ts:398);两份描述会随时间继续漂移。
    • 最小可行修复: 恢复步骤的 params 直接复用 STEP_PARAMS_SCHEMA(或抽出共享 base),本次即可顺手收敛,diff 反而更小。
  5. [低] 规划提示词没提这两个参数,端到端能力实际难以被触发

    • 证据: 两阶段展开的"参数填写指南"里 plan-generation.ts:277 仍是"web_fetch: params 需要 url";WebMCPClient.listTools()(mcp-clients.ts:166-169) 只返回 name/description,不下发 webFetchSchema 的 inputSchema。模型能看到的只有 STEP_PARAMS_SCHEMA 的字段描述。
    • 受影响调用方/流程: CHANGELOG 声称"the agent can actually pass domain restrictions end-to-end"——链路确实通了,但没有任何提示引导模型使用。
    • 最小可行修复: 在 plan-generation.ts:277 追加一行,例如"抓取受信任文档站点时可加 allowed_domains(精确主机名)"。

行级发现

  • [packages/mcp-web-fetch/src/engine.ts:67] 非数组输入返回 undefined 会把调用方明确设置的 deny list 静默变成"无过滤"(改动前会抛错并 fail-closed);建议区分 value == null(视为未提供)与 present-but-malformed(抛 UrlSafetyError,由 handler 的 catch 转成失败结果)。
  • [packages/mcp-web-fetch/src/tools.domain-filters.test.ts:46] 该用例把"畸形安全过滤器被静默忽略后继续抓取"固化为期望;随 engine 改为 fail-closed 后,应改成断言返回明确的 Invalid domain filter 错误。
  • [packages/mcp-web-fetch/src/tools.ts:75] args.allowed_domains as string[] | undefined 对未经校验的 MCP 入参做了不成立的断言;边界处应显式校验并拒绝非法形状,而不是靠下游 normalizeHostList 把它抹掉。
  • [packages/mcp-web-fetch/src/url-safety.test.ts:100] 这条断言固化了"deny list 不覆盖子域";请在 CHANGELOG 或 issue 中显式记录该语义与 Anthropic 同名参数的差异,避免调用方按直觉误用黑名单。
  • [packages/core/src/llm/schemas.ts:138] 这里是 STEP_PARAMS_SCHEMA 的第二份副本,且新字段 describe 措辞与 36-47 行不一致;建议直接复用 STEP_PARAMS_SCHEMA 而不是继续维护两份。

Karpathy 评审

  • 假设: 隐含假设"到达 handler 的 allowed_domains 一定是数组"——在 FrontAgent 内部由 zod 保证,但作为 MCP 公共工具不成立,而兜底策略选择了 fail-open。另一处隐含假设是新的可选字段不会影响 provider 的结构化输出约束;仓库内已有 .optional() 先例(code-quality-subagent.ts:160),但本 PR 未提供核心包的运行证据。
  • 简洁性: normalizeHostListclampLimit 姿态一致,没有多余抽象;集中在 fetchUrl 内归一化、重定向跳复用同一份结果(engine.ts:140-141,182-183)是正确做法,避免了逐跳重复清洗。
  • 结构质量: 无新增分支缠绕、无薄 wrapper、无文件规模跨界(engine.ts 273 行,tools.ts 89 行)。唯一退化是 ErrorRecoveryPlanSchema 的 params 副本被进一步复制(见发现 4)。
  • 变更范围: 超出 PR 自述范围。core LLM 契约的改动是必要的(否则字段在 planner 侧就被 strip,特性无法端到端使用),但必须在描述与风险评估里如实反映,而不是以 "single file / LOW risk" 呈现。
  • 验证: mcp-web-fetch 侧覆盖扎实且确实无网络(host 过滤在任何 fetch 之前发生,127.0.0.1 用例由 isBlockedHostname 提前拒绝)。core 侧新增了两个 schema 用例但缺少运行证据;重定向跳的过滤器复检(engine.ts:181-184)这一安全相关路径没有任何测试。

缺失覆盖

  • 重定向场景下的域名过滤:白名单内 URL 302 跳到白名单外主机时必须被拒绝(engine.test.ts 已有 fetchImpl 重定向脚手架,可直接复用)。
  • normalizeHostList 的直接单测:null/undefined/非数组/含空白与非字符串项的混合数组/全空白数组,以及(按发现 1 修复后)畸形输入应报错的断言。
  • packages/core 的实际执行证据:至少 packages/core/src/llm 的 vitest 与 tsc --noEmit,确认新增可选字段未破坏既有规划路径。
  • ErrorRecoveryPlanSchema 侧未新增任何用例:恢复计划里的 allowed_domains/blocked_domains 是否同样能被解析并透传,目前完全未验证。

* "filter disabled" rather than "deny all" (see `url-safety.ts`).
*/
export function normalizeHostList(value: unknown): string[] | undefined {
if (!Array.isArray(value)) return undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

非数组输入返回 undefined 会把调用方明确设置的 deny list 静默变成"无过滤"(改动前会抛错并 fail-closed);建议区分 value == null(视为未提供)与 present-but-malformed(抛 UrlSafetyError,由 handler 的 catch 转成失败结果)。

expect(result.error).toMatch(/private|blocked|loopback|safety/i);
});

it('non-array filter input is normalized away instead of crashing the engine', async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

该用例把"畸形安全过滤器被静默忽略后继续抓取"固化为期望;随 engine 改为 fail-closed 后,应改成断言返回明确的 Invalid domain filter 错误。

format: args.format as 'text' | 'html' | undefined,
timeoutMs: args.timeoutMs as number | undefined,
maxBytes: args.maxBytes as number | undefined,
allowHosts: args.allowed_domains as string[] | undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

args.allowed_domains as string[] | undefined 对未经校验的 MCP 入参做了不成立的断言;边界处应显式校验并拒绝非法形状,而不是靠下游 normalizeHostList 把它抹掉。

it('host filters use exact hostname matching — subdomains are not implied', () => {
// denyHosts: ['example.com'] must NOT deny www.example.com (exact match only).
const allowed = parseAndValidateUrl('https://www.example.com/', {
denyHosts: ['example.com'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这条断言固化了"deny list 不覆盖子域";请在 CHANGELOG 或 issue 中显式记录该语义与 Anthropic 同名参数的差异,避免调用方按直觉误用黑名单。

fullPage: z.boolean().describe('是否全页截图,不适用时填false'),
codeDescription: z.string().describe('要生成的代码的描述(不适用时填空字符串)'),
changeDescription: z.string().describe('要做的修改描述(不适用时填空字符串)'),
allowed_domains: z

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里是 STEP_PARAMS_SCHEMA 的第二份副本,且新字段 describe 措辞与 36-47 行不一致;建议直接复用 STEP_PARAMS_SCHEMA 而不是继续维护两份。

@ceilf6

ceilf6 commented Jul 31, 2026

Copy link
Copy Markdown
Owner

🛡️ ceilf6/repo-guard

代码评审报告: feat(mcp-web-fetch): expose allowed_domains and blocked_domains on web_fetch schema (#380)

风险等级:
处理建议: 评论
决策摘要: 可以合并——变更全部为可选字段的增量式扩展、handler/schema 层验收标准均有测试覆盖且无回归风险;但建议合并前确认 core 执行器确实会把 step params 透传给 web_fetch 工具调用(diff 中无此路径的变更或测试),并补跑 packages/core 的测试。

级联分析

  • 变更符号: webFetchSchema(新增两个可选 schema 属性,additive)、handleWebFetchTool(新增两行 args 映射,签名不变)、fetchUrl(签名不变,但对 allowHosts/denyHosts 增加归一化语义)、normalizeHostList(engine 新导出符号)、STEP_PARAMS_SCHEMA/GeneratedPlanSchema(closed object 现在保留 allowed_domains/blocked_domains)、ErrorRecoveryPlanSchema(同样新增两字段)。
  • 受影响流程: planner 生成 plan → GeneratedPlanSchema.parse(此前会静默剥离两字段,本 PR 修复的关键一环)→ core 执行器构造 MCP 工具调用参数 → handleWebFetchTool → fetchUrl → parseAndValidateUrl(host 过滤在网络请求前生效);错误恢复计划流程(ErrorRecoveryPlanSchema)同步获得两字段。
  • 变更集外调用方: (inferred) core 执行器将 step params 转为 MCP 工具调用参数的位置未出现在 diff 中——若该处按白名单/显式字段构造工具参数,allowed_domains/blocked_domains 仍会在执行器层被丢弃,planner schema 的修复将无法端到端生效;无图谱证据可确认执行器是全量透传还是白名单。(inferred) fetchUrl 的其他直接调用方若依赖空 allowHosts 数组的旧语义(拒绝一切),归一化后行为变为不启用过滤。
  • 置信度: degraded(本环境无 GitNexus/代码图谱;跨包执行器路径只能通过 diff 推断;PR 描述的验证仅覆盖 packages/mcp-web-fetch,packages/core 的测试运行未见报告)。

问题发现

  1. [中] 执行器是否透传 allowed_domains/blocked_domains 到 web_fetch 工具调用未经验证
    证据: diff 修复了 planner 层(STEP_PARAMS_SCHEMA 不再剥离字段)和 handler 层(tools.ts 接收并映射到引擎),但 core 执行器把 plan step params 转成 MCP 工具调用参数的那一跳没有任何变更或测试。plan-generation.test.ts 的新测试只断言 GeneratedPlanSchema.parse 保留字段;tools.domain-filters.test.ts 只直接调用 handleWebFetchTool。影响: 若执行器按显式字段白名单构造工具参数(对 url/format/timeoutMs/maxBytes 逐一取值),则 allowed_domains 会在执行器层被静默丢弃,CHANGELOG 宣称的端到端能力(agent 可实际传递域名限制)不成立,且无任何测试会失败。最小可行修复: 确认执行器是全量透传 step params;若是白名单构造则补充两个字段的映射,并增加一个从 parsed plan params 到 web_fetch 工具调用参数的转发测试。
  2. [低] engine 对空数组/非法 host filter 的归一化是 fail-open 语义变化,未在描述中披露
    证据: fetchUrl 现在经 normalizeHostList 把空数组、非数组输入统一归一化为 undefined(不启用过滤)。此前 engine 将 opts.allowHosts 原样传给 parseAndValidateUrl,空数组在 allow-list 语义下等价于拒绝一切(fail-closed)。影响: fetchUrl 的直接调用方(engine 作为包导出,fetchUrl 是公开符号)若依赖空 allowHosts 拒绝一切的旧行为,合并后会被静默放开;且 PR 描述声称 "No engine, SSRF, or url-safety logic changed" 与实际 diff 不符(CHANGELOG 记录了字段暴露但未说明空数组语义变化)。该选择本身可辩护(非法输入不应崩溃、handler 层已有测试锁定行为),评为 LOW。最小可行修复: 在 FetchOptions 注释或 CHANGELOG 中明确空数组/非法输入视为不启用过滤的语义,并确认无调用方依赖旧行为。
  3. [低] ErrorRecoveryPlanSchema 内联复制 params 对象,新字段需在两处手工保持同步
    证据: schemas.ts 中 STEP_PARAMS_SCHEMA 是作为 const 存在的 canonical 定义,但 ErrorRecoveryPlanSchema 内联复制了一份 params 对象(预先存在的重复),本次新字段分别加在第 36-47 行和第 138-145 行,且两处 describe 文案已开始漂移(一处详述精确主机名匹配与空数组语义,另一处仅写"可省略")。影响: 后续每次新增工具参数都必须记得改两处,漏改会导致错误恢复流程生成的 plan 静默剥离新字段——正是本 PR 修复的那类 bug。最小可行修复: 后续 PR 中提取共享 params schema 供两个 plan schema 复用(非阻塞,不建议塞进本 PR)。

行级发现

  • [packages/mcp-web-fetch/src/engine.ts:140] fetchUrl 现在把空数组/非数组 allowHosts 归一化为"不启用过滤"(fail-open);若有直接调用方依赖空 allowHosts 拒绝一切的旧语义会被静默放开,且与 PR 描述"engine 未改动"不一致——请在 FetchOptions 注释或 CHANGELOG 明确该语义并确认无调用方依赖旧行为。
  • [packages/core/src/llm/schemas.ts:138] ErrorRecoveryPlanSchema 内联复制了 params 对象而非复用 STEP_PARAMS_SCHEMA,新字段需在两处维护且 describe 已开始漂移;建议后续提取共享 params schema,避免错误恢复流程漏同步字段(非阻塞)。

Karpathy 评审

  • 假设: 两处描述与实现不一致——PR body 的 Impact Scope 仍写 "Single file change in packages/mcp-web-fetch",但 diff 实际修改了 packages/core 的 planner schema(CHANGELOG 反而准确记录了这一点);PR 声称 "No engine logic changed",但 fetchUrl 新增了归一化逻辑(fail-open 语义变化)。隐含假设"执行器会全量透传 step params 到 MCP 工具调用"未经验证,这是端到端能力成立的前提。.nullish() 而非 .optional() 的选择有测试注释说明理由(模型会输出 null,避免触发 object-repair 重试),假设已显式化,处理得当。
  • 简洁性: normalizeHostList 复用 clampLimit 的防御式风格,无生造抽象;tools.ts 的 as string[] cast 因引擎归一化而在运行时安全;无过度配置或猜测性通用层。方案与问题规模匹配。
  • 结构质量: 无文件膨胀、无错误层级;唯一的结构性观察是 params schema 在 STEP_PARAMS_SCHEMA 与 ErrorRecoveryPlanSchema 间双写(预存重复,本次保持一致但加深了同步负担,见 LOW finding)。归一化放在 engine 而非 handler 属于合理的纵深防御,与 url-safety 层的语义边界清晰。
  • 变更范围: 超出 issue Scope(issue 只要求 tools.ts/handler/测试),额外修改了 core planner schema——但这是让功能对 agent 端到端可用的必要补充(否则字段在 plan parse 时被剥离),且 CHANGELOG 如实记录,属合理扩展而非 scope creep;url-safety.test.ts 新增的是对既有精确匹配行为的文档化测试,与目标一致。
  • 验证: issue 的四条 handler/schema 层验收标准均有对应测试(schema 暴露字段、blocklist 拒绝、allowlist 拒绝、空数组不误杀、非法输入不崩溃);缺口: 执行器→工具调用转发无测试;packages/core 的测试未在 PR 描述中报告运行(验证只覆盖 mcp-web-fetch);quality:precommit 因 corepack 故障未跑,依赖 CI 作为权威门禁。

缺失覆盖

  • 端到端转发测试: 从含 allowed_domains/blocked_domains 的 plan step 经 core 执行器到 web_fetch 工具调用参数,证明 planner schema 保留的字段真正抵达 MCP handler(当前 diff 中最关键的一条未验证链路)。
  • packages/core 的测试运行证据: plan-generation.test.ts 有改动,但 PR 只报告了 mcp-web-fetch 的 48/48;合并前需 CI 或本地确认 core 包测试与 typecheck 通过。
  • 若存在 fetchUrl 的包外直接调用方,补一条 engine 层测试锁定空数组/非法 host list 归一化为不启用过滤的语义(当前仅在 handler 层覆盖)。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(mcp-web-fetch): expose allowed_domains and blocked_domains in web_fetch schema

2 participants