Skip to content

fix(cli): reuse base URL sanitizer for provider warnings - #8524

Closed
LHMQ878 wants to merge 9 commits into
QwenLM:mainfrom
LHMQ878:fix/warning-sanitizer-reuse-base-url
Closed

fix(cli): reuse base URL sanitizer for provider warnings#8524
LHMQ878 wants to merge 9 commits into
QwenLM:mainfrom
LHMQ878:fix/warning-sanitizer-reuse-base-url

Conversation

@LHMQ878

@LHMQ878 LHMQ878 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Provider warning URL spans now use the existing authority-aware base URL sanitizer directly. The warning-specific credential heuristics and duplicate URL matcher are removed; span splitting remains unchanged so warnings can still contain prose and multiple URLs.

Why it's needed

The duplicate warning sanitizer searched for the first colon and first at sign across the whole span. A port could therefore be mistaken for a password delimiter and a later email address for userinfo, truncating the warning. Conversely, an at sign inside a raw password could end the cut early and expose the rest of the password. Reusing the existing authority-scoped sanitizer fixes both defects without introducing another host grammar.

Reviewer Test Plan

How to verify

Confirm that a warning containing https://api.example:8443/v1 followed by admin@example.com is returned byte-for-byte unchanged, that https://user:p@ssw0rd-tail@broken.example/v1 becomes https://broken.example/v1, and that an uncredentialed URL with an explicit port remains byte-for-byte unchanged. The two existing warning and construction-error credential tests should continue to pass.

Evidence (Before & After)

N/A (non-UI sanitization fix). The focused test file passes 22/22 tests.

Tested on

OS Status
🍏 macOS ⚠️ not tested
🪟 Windows ✅ tested
🐧 Linux ⚠️ not tested

Environment (optional)

Node.js 24.13.0 on Windows. Verified with the focused Vitest file, ESLint on both changed files, package typecheck, and Prettier check.

Risk & Scope

  • Main risk or tradeoff: Warning sanitization now intentionally follows the same authority parsing semantics as configured base URL sanitization.
  • Not validated / out of scope: URL shapes that the shared sanitizer does not already support; those belong in a focused follow-up against the shared function.
  • Breaking changes / migration notes: None.

Linked Issues

Fixes #8136

中文说明

本 PR 的改动

Provider warning 中切分出的 URL 片段现在直接复用现有的、能识别 authority 边界的 base URL 清理函数。删除 warning 专用的凭据启发式逻辑和重复 URL 匹配器;原有片段切分保持不变,因此 warning 仍可包含说明文字和多个 URL。

为什么需要

重复实现的 warning 清理逻辑会在整个片段中寻找第一个冒号和第一个 @。因此端口可能被误判为密码分隔符,后面的邮箱地址又被误判为 userinfo 终点,导致 warning 被截断。反过来,原始密码中的 @ 也可能让截取过早结束并泄露剩余密码。复用现有按 authority 限定范围的清理逻辑,可以修复这两个问题,且无需再引入一套 host 语法。

审查测试计划

如何验证

确认包含 https://api.example:8443/v1 且后面跟有 admin@example.com 的 warning 原样返回;确认 https://user:p@ssw0rd-tail@broken.example/v1 变为 https://broken.example/v1;确认带显式端口但无凭据的 URL 原样返回。现有两项 warning 和构造错误凭据测试应继续通过。

证据(修改前后)

不适用(非 UI 清理修复)。聚焦测试文件 22/22 通过。

测试平台

操作系统 状态
🍏 macOS ⚠️ 未测试
🪟 Windows ✅ 已测试
🐧 Linux ⚠️ 未测试

环境(可选)

Windows 上的 Node.js 24.13.0。已验证聚焦 Vitest 文件、两个修改文件的 ESLint、包级 typecheck 和 Prettier 检查。

风险与范围

  • 主要风险或取舍:warning 清理现在有意与配置 base URL 的 authority 解析语义保持一致。
  • 未验证或超出范围:共享清理函数本身尚不支持的 URL 形态;这些应在针对共享函数的独立后续修改中处理。
  • 破坏性变更或迁移说明:无。

关联 Issue

修复 #8136

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finished — CI landed green on 2025841 and the deferred approval was posted. finalize run

Qwen Triage 已完成 —— 2025841 的 CI 全绿,延迟审批已提交。查看 finalize 运行

@LHMQ878

LHMQ878 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

CI note: the label job failed in repository automation before applying review/self-reported; gh pr edit --add-label exited on the deprecated repository.pullRequest.projectCards GraphQL field. This is unrelated to the diff. The main test and Serve A/B jobs are still running, and the Java/E2E matrix has passed.

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head 67f7899, drove a fixed endpoint set against each, and diffed the JSON responses. Only fields that changed are shown.

No response changes against the PR base across 12 scenario(s).

Qwen Code · serve A/B

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR — and for the honest wrap-up on #8137 that led to this one.

  • Template: complete ✓ (all sections, bilingual body).
  • Problem: observed bug with solid evidence. Provider warning sanitizer truncates messages containing a port, and leaks a password containing @ #8136 carries before/after output for both defects (port-induced truncation, @-in-password leak), names the exact call sites, and a maintainer confirmed both bugs by code inspection. Not theoretical.
  • Direction: aligned — this is exactly the "smaller form" requested on the issue after fix(cli): scope warning credential stripping to the URL authority #8137 closed: reuse the authority-aware sanitizeProviderBaseUrl instead of maintaining a second host grammar in the warning wrapper. The previous attempt showed that extending the shared parser is where regressions bred; deleting the duplicate heuristics is the right direction.
  • Size: not applicable — no core-module paths touched (both files live in packages/cli/src/serve/); 2 files, +33/−32 (production +1/−32, tests +32).
  • Approach: minimal. One call swapped, three bespoke helpers deleted, span splitting kept — that splitter is what finds passwords containing spaces, which a single global URL regex cannot match (the existing sec ret test pins it). Matches the issue's suggested fix verbatim; no drive-by changes.
  • Risk: no high-risk-path matches against the revert-history signal.

Moving on to code review. 🔍

中文说明

感谢这个 PR——也感谢你在 #8137 收尾时的坦诚,促成了这个版本。

  • 模板:完整 ✓(各节齐全,中英双语)。
  • 问题:已观测到的 bug,证据充分。Provider warning sanitizer truncates messages containing a port, and leaks a password containing @ #8136 给出了两个缺陷(端口导致截断、密码中含 @ 导致泄露)的 before/after 输出,点名了具体调用位置,且维护者已通过代码走查确认两个 bug 属实。不是理论性问题。
  • 方向:对齐——这正是 fix(cli): scope warning credential stripping to the URL authority #8137 关闭后 issue 中要求的"更小的形态":复用识别 authority 边界的 sanitizeProviderBaseUrl,而不是在 warning 包装层维护第二套 host 语法。上一次尝试表明,扩展共享解析器正是回归滋生的地方;删除重复的启发式才是正确方向。
  • 规模:不适用——未触及核心模块路径(两个文件都在 packages/cli/src/serve/);2 个文件,+33/−32(生产代码 +1/−32,测试 +32)。
  • 方案:最小化。只换了一处调用,删除三个专用辅助函数,保留片段切分——正是切分器才能发现含空格的密码,单个全局 URL 正则无法匹配(现有的 sec ret 测试钉住了这一点)。与 issue 建议的修复逐字一致;无夹带改动。
  • 风险:对照回滚历史信号,未命中任何高风险路径。

进入代码审查。🔍

Qwen Code · qwen3.8-max

Reviewed at 2025841b19b070c452843eb415dfb957d7c361a7 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Code review

Independent proposal before reading the diff: the wrapper's whole-span indexOf(':') / indexOf('@') heuristics re-implement (buggily) what sanitizeProviderBaseUrl already does authority-scoped, so the fix is to delete the bespoke segment logic and call the shared sanitizer per span, keeping the span splitter. That is exactly what this PR does — including keeping the splitter, which matters: a password containing a space (https://user:sec ret@broken.example/v1, pinned by the existing test) cannot be matched by a single global URL regex, so the loop structure must stay.

I statically traced the shared sanitizer (unchanged on mainacpModelUtils.ts is not touched here) through every scenario, since this whole change is a composition of the existing span loop with that one function:

  • https://api.example:8443/v1 — contact admin@example.com → authority ends at /v1, no @ inside it, new URL() rejects the prose, and the fallback's port check sees all-digit 8443 → returned byte-identical ✓ (under the old code this produced Cannot reach https://example.com — port, path and prose all deleted)
  • https://user:p@ssw0rd-tail@broken.example/v1 → parses, lastIndexOf('@') within the authority lands on the delimiter → https://broken.example/v1 ✓ (old code cut at the @ inside the password and emitted the rest of it)
  • https://api.example:8443/v1 → parses, no userinfo → byte-identical ✓
  • the existing sec ret space-password test → authority ends at /v1, strip at the authority's @ → still passes ✓

No blockers. One observation, which is not a defect in this PR: when a span's URL has no /, ? or # after the authority (e.g. https://example.com — contact admin@example.com), findAuthorityEnd runs to the end of the string, so a prose @ gets swallowed into the authority and stripped. That limitation belongs to the shared function, was already identified and reproduced during the #8137 thread, and the PR description explicitly defers it to a follow-up against the shared function — the right call, since extending sanitizeProviderBaseUrl is precisely where #8137 went wrong. Worth filing that follow-up so it isn't lost; note the old code only handled the colon-less sub-case of this shape, so the residual is strictly narrower than the two bugs being fixed here.

Also checked: the deleted helpers (sanitizeProviderWarningSegment, hasCredentialPrefix, URL_LIKE_PATTERN) are module-private with no other references, and the three new it.each cases pin exact input→output pairs on the construction-error path (the same sanitizer as the warnings path). Cases 1–2 assert outputs the old code provably cannot produce; case 3 guards the uncredentialed-with-port shape. The change is pure string transformation with no platform-specific code, so the Linux CI suites are the relevant evidence; the author's local Windows run is their claim, not evidence relied on here.

Test evidence (PR's own CI at the reviewed commit, fetched once via API — no polling)

At fetch time the two suites that matter are still running: Test (ubuntu-latest, Node 22.x) and Serve A/B (ubuntu-latest, Node 22.x). The macOS/Windows test jobs are skipped. The red label check is the self-report labeler job exiting 1 after a GraphQL deprecation warning — bot infra noise, unrelated to PR code. The finalize job rewrites the table below once CI settles. The behavioural claim is settled by the PR's own tests once green — they assert the exact outputs above, which the old code cannot produce — plus the live-daemon Serve A/B diff, so no extra sandboxed lane is needed to pin this change.

Final CI results for 2025841 (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Classify PR ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
macos-latest / Java 21 ✅ success
Real daemon E2E / Java 11 ✅ success
Serve A/B (ubuntu-latest, Node 22.x) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
ubuntu-latest / Java 11 ✅ success
ubuntu-latest / Java 17 ✅ success
ubuntu-latest / Java 21 ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
windows-latest / Java 21 ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

中文说明

代码审查

读 diff 之前的独立方案:包装层对整个片段做 indexOf(':') / indexOf('@') 的启发式,是把 sanitizeProviderBaseUrl 已有的 authority 范围逻辑又(带 bug 地)实现了一遍,所以修法就是删掉专用的片段逻辑、每个片段直接调用共享清理函数,并保留片段切分器。这个 PR 正是这么做的——保留切分器很关键:含空格的密码(https://user:sec ret@broken.example/v1,现有测试已钉住)是单个全局 URL 正则匹配不到的,所以循环结构必须保留。

由于整个变更就是"现有片段循环 + 那一个共享函数"的组合,我对 main 上未改动的共享清理函数(本 PR 不动 acpModelUtils.ts)逐场景做了静态走查:

  • https://api.example:8443/v1 — contact admin@example.com → authority 止于 /v1,其中无 @new URL() 拒绝带说明文字的输入,回退分支的端口检查看到纯数字 8443 → 原样返回 ✓(旧代码会输出 Cannot reach https://example.com——端口、路径、说明文字全部被删)
  • https://user:p@ssw0rd-tail@broken.example/v1 → 可解析,authority 内的 lastIndexOf('@') 正是分隔符 → https://broken.example/v1 ✓(旧代码在密码内部的 @ 处截断,并把密码剩余部分输出)
  • https://api.example:8443/v1 → 可解析、无 userinfo → 原样返回 ✓
  • 现有 sec ret 空格密码测试 → authority 止于 /v1,在 authority 的 @ 处截取 → 仍然通过 ✓

无阻塞项。一个观察,不算本 PR 的缺陷:当片段中的 URL 在 authority 之后没有 /?# 时(如 https://example.com — contact admin@example.com),findAuthorityEnd 会延伸到字符串末尾,说明文字中的 @ 会被并入 authority 而截掉。这个局限属于共享函数本身,在 #8137 的讨论中已被发现并复现,PR 描述也明确将其留给针对共享函数的后续修复——这是正确的取舍,因为扩展 sanitizeProviderBaseUrl 正是 #8137 出错的地方。建议把那个后续 issue 立起来以免被遗忘;注意旧代码对该形态中无冒号的子场景才恰好处理正确,所以残留面严格窄于这里修复的两个 bug。

另已核查:被删除的辅助函数(sanitizeProviderWarningSegmenthasCredentialPrefixURL_LIKE_PATTERN)均为模块私有、无其他引用;新增的三个 it.each 用例在构造错误路径上钉住了精确的输入→输出(与 warnings 路径用同一清理函数)。用例 1–2 断言的输出是旧代码必然产生不了的;用例 3 守护无凭据带端口的形态。该变更是纯字符串变换、无平台相关代码,因此 Linux CI 套件就是相关证据;作者的 Windows 本地运行是其自述,不作为这里依据的证据。

测试证据(受审提交上该 PR 自身的 CI,通过 API 一次性获取,不轮询)

获取时两个关键套件仍在运行:Test (ubuntu-latest, Node 22.x)Serve A/B (ubuntu-latest, Node 22.x)。macOS/Windows 测试任务为跳过状态。红色的 label 检查是自报告打标任务在 GraphQL 弃用警告后以 exit 1 失败——机器人基础设施噪音,与 PR 代码无关。CI 落定后 finalize 任务会重写下表。行为性声明由 PR 自己的测试在变绿后定案——它们断言的正是上述旧代码产生不了的精确输出——再加上真实 daemon 的 Serve A/B 差分,因此无需额外的沙箱验证通道来钉住该变更。

(CI 表格见英文版,finalize 任务会在 CI 完成后原地更新。)

Qwen Code · qwen3.8-max

Reviewed at 2025841b19b070c452843eb415dfb957d7c361a7 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — clean, minimal fix in the exact shape requested on the issue; the only open item is the unit suite still running, plus the known, explicitly deferred shared-sanitizer limitation (named in the Stage 2 comment).

Stepping back: this is what good iteration looks like. The first attempt (#8137) tried to make the warning wrapper smarter and accumulated regressions across six review rounds; the author closed it themselves, verified every finding against the merge base first, and this PR goes the other way — it deletes the duplicate heuristics entirely and leans on the authority-aware sanitizer that already existed. The diff matches my independent proposal exactly, the problem was reproduced and maintainer-confirmed, the fix removes 31 production lines while adding pinned input→output tests for both reported shapes, and every edit is necessary for the stated goal. In six months this reads as obviously correct: there is now exactly one place where credentials get stripped from provider URLs. The Serve A/B daemon diff already came back clean; once the unit suite lands green, this is ready.

Approval is deferred until CI lands green on 2025841b19b070c452843eb415dfb957d7c361a7Test (ubuntu-latest, Node 22.x) was still in flight at review time (Serve A/B passed). The finalize job posts the commit-pinned approval if everything settles green.

中文说明

信心:4/5 —— 干净、最小的修复,形态与 issue 中要求的完全一致;唯一未决项是单元测试套件仍在运行,外加那个已知且已明确延后处理的共享清理函数局限(见 Stage 2 评论)。

退一步看:这是良性迭代的样板。第一次尝试(#8137)试图让 warning 包装层更聪明,在六轮 review 中不断累积回归;作者先对合并基点逐一验证了所有发现,然后自己关掉了它,而这个 PR 走了反方向——彻底删除重复的启发式,依赖已有的识别 authority 边界的清理函数。diff 与我的独立方案完全一致;问题有复现且经维护者确认;修复净删 31 行生产代码,同时为两个上报形态新增了钉住输入→输出的测试;每一处改动都为既定目标所必需。六个月后再看,这段代码显然是对的:现在只有一个地方负责从 provider URL 中剥离凭据。Serve A/B 的 daemon 差分已经返回干净结果;单元测试套件变绿后即可合入。

批准延迟到 CI 在 2025841b19b070c452843eb415dfb957d7c361a7 上全部变绿之后——审查时 Test (ubuntu-latest, Node 22.x) 仍在运行(Serve A/B 已通过)。全部变绿后,finalize 任务会发布钉在该提交上的批准。

Qwen Code · qwen3.8-max

Reviewed at 2025841b19b070c452843eb415dfb957d7c361a7 · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship — CI landed green after the review. ✅

Comment on lines 261 to +262
const segment = warning.slice(next.index, segmentEnd);
result += sanitizeProviderWarningSegment(segment, next.marker.length);
result += sanitizeProviderBaseUrl(segment);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] The whole warning segment (URL + trailing prose to end-of-line) is now passed to sanitizeProviderBaseUrl, whose input contract everywhere else is a single standalone base URL. When the URL is pathless (no /, ?, or # after the host), findAuthorityEnd finds no authority terminator, so the "authority" spans the entire remainder of the line including the prose, and the credential strip lands on an unrelated later @ (e.g. a contact email). Independently, when the URL fails WHATWG parsing (even with a path), the catch-branch fallback findUnescapedUserInfoFallbackAt resolves @ via whole-string lastIndexOf — past the correctly-bounded authority and into the prose. — Failure scenario: a provider warning or ModelsConfig construction error of the shape Cannot reach https://api.example — contact admin@example.com (pathless base URL + a contact email on the same line) reaches sanitizeProviderWarning via the warnings map or the catch path; this PR then outputs Cannot reach https://example.com — the host is replaced by the email's domain and the contact text is deleted (probe-verified A/B: the merge base kept this input byte-identical). Credentialed variant Cannot reach https://user:pass@host.io please contact admin@corp.ioCannot reach https://corp.io; unparseable variant Cannot reach https://host bad:x/v1 — contact admin@corp.ioCannot reach https://corp.io. The three new tests pass only because their inputs have a correctly-bounded, parseable authority; the corrupting shapes are untested. The harm is message corruption (over-strip), not a credential leak.

Suggested change
const segment = warning.slice(next.index, segmentEnd);
result += sanitizeProviderWarningSegment(segment, next.marker.length);
result += sanitizeProviderBaseUrl(segment);
const segment = warning.slice(next.index, segmentEnd);
const urlBreak = segment.search(/[\s'"`<>]/);
const urlToken = urlBreak === -1 ? segment : segment.slice(0, urlBreak);
const trailing = urlBreak === -1 ? '' : segment.slice(urlBreak);
result += sanitizeProviderBaseUrl(urlToken) + trailing;
中文说明

现在整个 warning 片段(URL + 一直到行尾的后续文字)被直接传入 sanitizeProviderBaseUrl,而该函数在其他调用点的输入契约是单个独立的 base URL。当 URL 没有路径(host 后没有 /?#)时,findAuthorityEnd 找不到 authority 终止符,因此 "authority" 会延伸到该行剩余的全部内容包括后续文字,凭据截取就会落在后面某个无关的 @(例如联系邮箱)上。此外,当 URL 无法被 WHATWG 解析时(即使有路径),catch 分支的回退逻辑 findUnescapedUserInfoFallbackAt 会通过整个字符串的 lastIndexOf 定位 @ —— 超出正确界定的 authority 而落到后续文字中。—— 失败场景:形如 Cannot reach https://api.example — contact admin@example.com(无路径 base URL + 同一行的联系邮箱)的 provider warning 或 ModelsConfig 构造错误,经由 warnings map 或 catch 路径进入 sanitizeProviderWarning;本 PR 会输出 Cannot reach https://example.com —— host 被替换为邮箱的域名,联系文字被删除(探针 A/B 验证:merge base 对该输入逐字节保持不变)。带凭据变体 Cannot reach https://user:pass@host.io please contact admin@corp.ioCannot reach https://corp.io;不可解析变体 Cannot reach https://host bad:x/v1 — contact admin@corp.ioCannot reach https://corp.io。三个新增测试之所以全部通过,仅因为其输入的 authority 界定正确且可解析;导致损坏的形态未被测试覆盖。危害是消息被破坏(过度截取),而非凭据泄露。

建议修复:在委托给 sanitizeProviderBaseUrl 之前,先把输入限定到 URL token(在第一个空白/引号/反引号/尖括号处截断),只对 URL token 做清理,并把后续文字原样拼接回去(等价于恢复被删除的 URL_LIKE_PATTERN 的应用方式)。也可考虑在 findAuthorityEnd 中把空白视为 authority 终止符(真实 base URL 不含空白),并补充"无路径 URL + 后续邮箱"的回归用例。

— qwen3.8-max via Qwen Code /review (v0.21.5)

@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Aug 5, 2026
@LHMQ878

LHMQ878 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Good catch — I hit the same thing when I ran sanitizeProviderBaseUrl against the pathless shapes locally.

Went with the findAuthorityEnd change rather than trimming the segment at the first whitespace before calling it. Trimming first would leave https://user:sec ret@broken.example/v1 completely unstripped, and that case is already pinned here.

Whitespace is now an authority terminator (raw whitespace isn't valid in a URL anyway). Node's URL parser still accepts a space in userinfo by percent-encoding it, so when @ lands past the truncated authority the strip falls through to the same recovery path the catch branch already used. Added the pathless + trailing-email cases on both the shared sanitizer and the warning wrapper.

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review timed out. Qwen review timed out after 10800 seconds (of the 180-minute budget). For large PRs, retry with a longer timeout by commenting: @qwen-code /review --timeout=360. See workflow logs.

@LHMQ878

LHMQ878 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

The review timeout looks like infra on the bot run, not a code failure — the test matrix on d8f6033 is green (ubuntu Node 22, Serve A/B, desktop shell, Java matrix). Happy to re-kick review on this head if maintainers want another bot pass.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not reviewed: reverse audit — stopped before round 2 by the review time budget.

中文说明

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未审查:反向审计——评审时间预算不足,未能开始第 2 轮。

— qwen3.8-max via Qwen Code /review (v0.21.6)

Comment thread packages/cli/src/utils/acpModelUtils.ts Outdated
Comment on lines +188 to +191
// WHATWG accepts spaces in userinfo (encoded as %20), but our authority
// span stops at raw whitespace — so `@` can sit past authorityEnd for a
// password that contains a space. Fall through to the same recovery used
// when parsing fails.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R2-1: The parse-success fall-through leaks credentials for whitespace-containing userinfo. When WHATWG parsing succeeds and reports username/password but the credential @ sits past the new whitespace-truncated authority span, control falls through to findUnescapedUserInfoFallbackAt, whose guards bail for these shapes — so the input is returned unchanged with credentials intact. A/B-probed against the merge base, which stripped them.

Failure scenario: sanitizeProviderBaseUrl('https://user:12 34@host.io') → returned byte-identical (password prefix 12 matches the /^\d+$/ port guard and is mistaken for a port); https://user name:pass@host.io (colon past authorityEnd) and https://user name@host.io / https://us er@host (no colon) leak the same way. Warning path: Cannot reach https://user:12 34@host.io leaks unchanged into serve status errors[].error. Reachable via configured baseUrl (settings schema constrains it to a plain string, so a WHATWG-valid URL with a space in userinfo can be configured) and via provider warning/error text routed through sanitizeProviderWarning.

Suggested fix: when parsed.username || parsed.password is set, WHATWG has already proven userinfo exists — strip at the userinfo-terminating @ directly instead of re-deriving it through the parse-failure heuristics (the colon/digit guards only disambiguate the genuinely ambiguous parse-failure case). Add regression rows for the leaking shapes.

中文说明

解析成功时的 fall-through 会泄露包含空白的 userinfo 凭据。当 WHATWG 解析成功并报告 username/password、但凭据中的 @ 位于新的"以空白截断 authority"范围之后时,控制流落入 findUnescapedUserInfoFallbackAt,其守卫对这些形态直接退出 —— 输入被原样返回,凭据完整保留。已对 merge base 做 A/B 探针验证:旧代码会剥掉这些凭据。

失败场景:sanitizeProviderBaseUrl('https://user:12 34@host.io') → 原样返回(密码前缀 12 命中 /^\d+$/ 端口守卫,被误判为端口);https://user name:pass@host.io(冒号在 authorityEnd 之后)以及 https://user name@host.io / https://us er@host(没有冒号)以同样方式泄露。warning 路径:Cannot reach https://user:12 34@host.io 原样泄露进 serve status 的 errors[].error。可通过配置的 baseUrl(settings schema 仅约束为普通字符串,因此带空白的 WHATWG 合法 URL 可以被配置进来)以及经由 sanitizeProviderWarning 的 provider warning/错误文本到达。

建议修复:当 parsed.username || parsed.password 成立时,WHATWG 已证明 userinfo 存在 —— 直接在 userinfo 结尾的 @ 处剥离,而不是通过解析失败专用的启发式重新推导(冒号/数字守卫只应在真正有歧义的解析失败分支中使用)。并为这些泄露形态补充回归用例。

— qwen3.8-max via Qwen Code /review (v0.21.6)

Comment on lines +198 to +202
const fallbackAt = findUnescapedUserInfoFallbackAt(
baseUrl,
authorityStart,
authorityEnd,
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R2-2: The promoted fallback's unbounded baseUrl.lastIndexOf('@') is now reachable from the parse-success path. A whitespace-in-password URL followed by any later @ after the authority (a contact email in trailing prose, or an @ in the path) is stripped at the LATER @ — destroying the real host, path, and prose. This is the same over-strip class this PR exists to fix, reintroduced for the space-in-password variant via the new routing.

Failure scenario (A/B-probed end-to-end through the status provider): warning Failed loading provider https://user:pa ss@host.io please contact admin@corp.io → merge base produced …https://host.io please contact admin@corp.io, this PR produces Failed loading provider https://corp.io — the host is replaced by the contact email's domain and the contact text is deleted; https://user:p w@host.io/a@bhttps://b (regresses on both the direct and the warning paths). The surfaced error names the wrong domain as the unreachable provider — actively misleading diagnostics from the function whose purpose is safe display.

Suggested fix: bound the fallback's search — strip at the first @ at or after authorityEnd (baseUrl.indexOf('@', authorityEnd)) instead of the last @ in the whole string, and/or reserve the whole-string recovery for the parse-failure path only. Probes confirmed this preserves the port/email guard cases while restoring the host for these shapes.

中文说明

被提升为公共路径的 fallback 中无界的 baseUrl.lastIndexOf('@') 现在可以从解析成功路径到达。密码含空白的 URL 后面如果跟着 authority 之后的其他 @(后续文字中的联系邮箱,或路径中的 @),会在后面那个 @ 处剥离 —— 真实的 host、路径和说明文字全部被破坏。这正是本 PR 要修复的过度截取类别,经由新的路由在"密码含空白"变体上被重新引入。

失败场景(已通过 status provider 端到端 A/B 探针验证):warning Failed loading provider https://user:pa ss@host.io please contact admin@corp.io → merge base 输出 …https://host.io please contact admin@corp.io,本 PR 输出 Failed loading provider https://corp.io —— host 被联系邮箱的域名替换,联系文字被删除;https://user:p w@host.io/a@bhttps://b(直接调用路径和 warning 路径均回退)。对外展示的错误会把错误的域名说成不可达的 provider —— 在一个以安全展示为目的的函数里产生误导性诊断。

建议修复:限定 fallback 的搜索范围 —— 在 authorityEnd 之后的第一个 @baseUrl.indexOf('@', authorityEnd))处剥离,而不是整个字符串的最后一个 @;或者只把全串恢复逻辑保留在解析失败路径。探针确认这样既保留端口/邮箱守卫用例的行为,又能恢复这些形态下的 host。

— qwen3.8-max via Qwen Code /review (v0.21.6)

Comment on lines +256 to +259
[
'https://user:pass@host.io please contact admin@corp.io',
'https://host.io please contact admin@corp.io',
],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R2-3: The stripping side of the new fall-through is unpinned by tests. In the added row 'https://user:pass@host.io please contact admin@corp.io' the credential @ sits inside the whitespace-truncated span, so the strip happens in the ordinary try-branch without ever reaching the fall-through; 'https://api.example — contact admin@example.com' reaches the fall-through only to exercise its no-op bailout. (The pre-existing row 'https://user:p ass@api.example/v1' does pin the path-bearing space-password strip — the pathless variant is what is missing.)

Concrete cost: the documented recovery case in its pathless form ('https://user:pa ss@host.io''https://host.io', confirmed working by probe) is pinned by nothing, and none of the R2-1/R2-2 broken shapes were ever put in front of the suite — the whole regression family ships green (58/58 pass while probes show wrong outputs for the unpinned shapes).

Suggested fix: add rows such as ['https://user:pa ss@host.io', 'https://host.io'] (pins the documented recovery) plus the R2-1/R2-2 shapes with their intended stripped/preserved expectations.

中文说明

新 fall-through 的"执行剥离"一侧没有测试固定。新增用例 'https://user:pass@host.io please contact admin@corp.io' 的凭据 @ 位于空白截断范围内,剥离发生在普通 try 分支,根本不会到达 fall-through;'https://api.example — contact admin@example.com' 到达 fall-through 只是触发其"原样返回"的退出分支。(已有用例 'https://user:p ass@api.example/v1' 确实固定了带路径的空白密码剥离 —— 缺的是无路径变体。)

具体代价:注释中记录的恢复用例的无路径形态('https://user:pa ss@host.io''https://host.io',探针确认当前行为正确)没有任何测试固定;R2-1/R2-2 的损坏形态也从未进入测试套件 —— 整组回退会在测试全绿的情况下合入(58/58 通过,而探针显示这些未固定形态的输出是错误的)。

建议修复:补充用例行,例如 ['https://user:pa ss@host.io', 'https://host.io'](固定注释记录的恢复行为),并为 R2-1/R2-2 的形态补上期望的剥离/保留断言。

— qwen3.8-max via Qwen Code /review (v0.21.6)

Truncate URL tokens in provider warnings before sanitizing, strip whitespace-containing userinfo on parse success without leaking or over-stripping trailing contact emails, and add regression tests.
@LHMQ878

LHMQ878 commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the critical review items in 7cb5da1:

  1. sanitizeProviderWarning — URL tokens are truncated at the first whitespace/quote/backtick/angle bracket before calling \sanitizeProviderBaseUrl; trailing prose is appended unchanged.
  2. Parse-success userinfo — whitespace-containing credentials are stripped at the first @\ after \�uthorityEnd\ (with a gap guard so pathless URL + contact-email prose is not corrupted).
  3. Fallback @\ search — bounded to \indexOf('@', authorityEnd)\ instead of whole-string \lastIndexOf.
  4. Tests — added regression rows for pathless space-in-password, whitespace userinfo leak shapes, and trailing contact-email over-strip cases.

All 41 \�cpModelUtils\ sanitizer tests pass.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not reviewed: reverse audit — stopped before round 3 by the review time budget.

中文说明

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未审查:反向审计——评审时间预算不足,未能开始第 3 轮。

— qwen3.8-max via Qwen Code /review (v0.21.7)

Comment on lines +262 to +265
const urlBreak = segment.search(/[\s'"`<>]/);
const urlToken = urlBreak === -1 ? segment : segment.slice(0, urlBreak);
const trailing = urlBreak === -1 ? '' : segment.slice(urlBreak);
result += sanitizeProviderBaseUrl(urlToken) + trailing;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R3-1: Truncating each URL token at the first whitespace/quote/backtick/angle bracket before delegating to sanitizeProviderBaseUrl severs credentials whose userinfo contains one of those characters — the token no longer contains the @, so the sanitizer returns it unchanged and the credential half is re-appended as "trailing prose". The result is a full credential leak into provider warnings/status, and the pre-existing pinned test sanitizes credentials from provider construction errors now FAILS at this head (it passes on the merge base; this file runs 1 failed | 23 passed, and CI's Test (ubuntu-latest, Node 22.x) is failing on it). — Failure scenario: warning Failed loading provider https://user:sec ret@broken.example/v1 → token cut to https://user:sec (no @, unparseable port, returned unchanged) + ret@broken.example/v1 appended verbatim → username user and password sec ret reach the serve /status errors[].error payload served to ACP clients. sanitizeProviderBaseUrl handles the FULL string correctly ('https://user:pa ss@host.io''https://host.io', pinned by this PR's own new unit rows) — the wrapper's pre-truncation is what makes that logic unreachable from the warning path.

Suggested change
const urlBreak = segment.search(/[\s'"`<>]/);
const urlToken = urlBreak === -1 ? segment : segment.slice(0, urlBreak);
const trailing = urlBreak === -1 ? '' : segment.slice(urlBreak);
result += sanitizeProviderBaseUrl(urlToken) + trailing;
result += sanitizeProviderBaseUrl(segment);

findAuthorityEnd's whitespace terminator and the bounded fallback @ search added by this PR already protect trailing prose — probe-verified: all five new warning-level cases stay green under whole-segment delegation. If a split is kept for another reason it must not cut inside userinfo, and the failing pre-existing test must go green.

中文说明

在把 URL 片段交给 sanitizeProviderBaseUrl 之前先按第一个空白/引号/反引号/尖括号截断 token,会把 userinfo 中含这些字符的凭据从中切断 —— token 里不再包含 @,清理函数原样返回 token,凭据的后半段被当作"后续文字"原样拼回。结果是凭据完整泄露进 provider warning/status,且既有的固定测试 sanitizes credentials from provider construction errors 在本 head 上失败(merge base 上通过;本文件 1 failed | 23 passed,CI 的 Test (ubuntu-latest, Node 22.x) 也因此失败)。—— 失败场景:warning Failed loading provider https://user:sec ret@broken.example/v1 → token 被截成 https://user:sec(无 @、端口不可解析、原样返回)+ ret@broken.example/v1 原样拼接 → 用户名 user 和密码 sec ret 进入对外提供给 ACP 客户端的 serve /status errors[].error 载荷。sanitizeProviderBaseUrl 对完整字符串的处理是正确的('https://user:pa ss@host.io''https://host.io',本 PR 新增的单测行已固定该行为)—— 正是包装层的预截断使 warning 路径永远到不了那段逻辑。建议修复:直接委托整个片段(见 suggestion 块);本 PR 新增的 findAuthorityEnd 空白终止符和受限的 fallback @ 搜索已经能保护后续文字(探针验证:整段委托下五个新增 warning 用例仍然全绿)。若出于其他原因必须保留切分,也不能在 userinfo 中间切断,并且必须让既有的固定测试恢复通过。

— qwen3.8-max via Qwen Code /review (v0.21.7)

Comment thread packages/cli/src/utils/acpModelUtils.ts Outdated
// span stops at raw whitespace — so `@` can sit past authorityEnd for a
// password that contains a space. Strip at the first `@` after authorityEnd
// when the gap still looks like userinfo, not trailing prose.
const userinfoAt = baseUrl.indexOf('@', authorityEnd);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R3-2: With findAuthorityEnd now terminating the authority span at whitespace, the @ searches land on the FIRST/inner @ when userinfo contains both whitespace and an additional @ — stripping only the prefix and leaving password material in the sanitized output. WHATWG's userinfo delimiter is the LAST @ before / ? #; this PR's own strips a password containing an at sign row asserts last-@ semantics for @-in-password inputs. This branch is one of three affected strip sites: the in-span lastIndexOf('@') (~line 176), this userinfoAt search, and the fallback's indexOf('@', authorityEnd) (~line 219). — Failure scenario (probe-verified A/B against merge base d91c661): sanitizeProviderBaseUrl('https://user:p ss@real@host.io')https://real@host.io through this branch (base: https://host.io); https://user:p@ss w0rd@host.iohttps://ss w0rd@host.io via the in-span strip (base: https://host.io). Reachable via user-configured baseUrl values displayed through acpAgent.ts, modelConfigUtils.ts, and the workspace provider status. Suggested fix: when credentials are confirmed, strip at the LAST @ before the first / ? # terminator after authorityStart (or end of string); keep the whitespace-bounded span only as the trailing-prose guard — and add regression rows for the whitespace + multi-@ shapes.

中文说明

由于 findAuthorityEnd 现在把 authority 范围终止于空白,当 userinfo 同时包含空白和额外的 @ 时,各处 @ 搜索会落在第一个/内部的 @ 上 —— 只剥掉前缀,密码的剩余部分留在"已清理"的输出里。WHATWG 的 userinfo 分隔符是 / ? # 之前的最后一个 @;本 PR 自己的 strips a password containing an at sign 用例也断言了对含 @ 密码采用最后一个 @ 的语义。本分支是三个受影响的剥离点之一:span 内的 lastIndexOf('@')(约第 176 行)、此处的 userinfoAt 搜索、以及 fallback 的 indexOf('@', authorityEnd)(约第 219 行)。—— 失败场景(已对 merge base d91c661 做 A/B 探针验证):sanitizeProviderBaseUrl('https://user:p ss@real@host.io') 经本分支输出 https://real@host.io(base 为 https://host.io);https://user:p@ss w0rd@host.io 经 span 内剥离输出 https://ss w0rd@host.io(base 为 https://host.io)。可通过用户配置的 baseUrl(经 acpAgent.tsmodelConfigUtils.ts 和 workspace provider status 展示)到达。建议修复:当确认存在凭据时,在 authorityStart 之后第一个 / ? # 终止符(或串尾)之前的最后一个 @ 处剥离;仅把空白界定的范围用作后续文字的防护 —— 并为"空白 + 多 @"形态补充回归用例。

— qwen3.8-max via Qwen Code /review (v0.21.7)

Comment thread packages/cli/src/utils/acpModelUtils.ts Outdated
): number {
const at = baseUrl.lastIndexOf('@');
if (at < authorityStart || authorityEnd >= at) {
const at = baseUrl.indexOf('@', authorityEnd);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R3-2: Same wrong-@ class as the comment at line 192, on the catch path: this fallback search strips at the FIRST @ at/after authorityEnd, which is the wrong delimiter when the password contains both a space and an @ and the URL fails WHATWG parsing. — Failure scenario (probe-verified A/B against merge base d91c661): sanitizeProviderBaseUrl('https://h:p x@y@[bad')https://y@[bad (base: https://[bad) — the password fragment y survives in a URL that visibly still carries userinfo. Suggested fix: same as line 192 — strip at the LAST @ before the first / ? # terminator (or end of string), or reserve first-@ semantics for shapes that provably have no second @; add a catch-path regression row.

中文说明

与第 192 行评论相同的"错误 @"类别,发生在 catch 路径:该 fallback 搜索在 authorityEnd 之后的第一个 @ 处剥离;当密码同时包含空白和 @ 且 URL 无法通过 WHATWG 解析时,这是错误的分隔符。—— 失败场景(已对 merge base d91c661 做 A/B 探针验证):sanitizeProviderBaseUrl('https://h:p x@y@[bad')https://y@[bad(base 为 https://[bad)—— 密码片段 y 残留在一个明显仍带 userinfo 的 URL 中。建议修复:同第 192 行 —— 在第一个 / ? # 终止符(或串尾)之前的最后一个 @ 处剥离;并为 catch 路径补充回归用例。

— qwen3.8-max via Qwen Code /review (v0.21.7)

Comment thread packages/cli/src/utils/acpModelUtils.ts Outdated
const userinfoAt = baseUrl.indexOf('@', authorityEnd);
if (userinfoAt !== -1) {
const gap = baseUrl.slice(authorityEnd, userinfoAt);
if (parsed.password || /^\s*\S+$/.test(gap)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R3-3: The gap gate requires exactly one non-whitespace run, so username-only userinfo whose gap between the whitespace-truncated authorityEnd and the @ is space-only or multi-word is rejected and returned unchanged with credentials intact — the merge base stripped them. — Failure scenario (probe-verified A/B against merge base d91c661): sanitizeProviderBaseUrl('https://user @host.io') returns the input unchanged (base: https://host.io); https://foo bar baz@corp.io unchanged (base: https://corp.io); https://user name @host.io unchanged (base: https://host.io). Reachable via every direct caller that displays a configured base URL (ACP model listings, workspace provider status). Suggested fix: when parsed.username || parsed.password holds, WHATWG has already proven userinfo exists — strip at the userinfo-terminating @ directly instead of re-deriving it through the gap heuristic (or widen the gate for username-only shapes), and add regression rows for these shapes.

中文说明

gap 守卫要求恰好一个非空白连续段,因此当 userinfo 只有用户名、且空白截断后的 authorityEnd@ 之间的 gap 为纯空白或多个词时,会被拒绝剥离,输入带着凭据原样返回 —— merge base 会剥掉它们。—— 失败场景(已对 merge base d91c661 做 A/B 探针验证):sanitizeProviderBaseUrl('https://user @host.io') 原样返回(base 为 https://host.io);https://foo bar baz@corp.io 原样返回(base 为 https://corp.io);https://user name @host.io 原样返回(base 为 https://host.io)。可通过所有展示已配置 base URL 的直接调用点到达(ACP 模型列表、workspace provider status)。建议修复:当 parsed.username || parsed.password 成立时,WHATWG 已证明 userinfo 存在 —— 直接在 userinfo 结尾的 @ 处剥离,而不是通过 gap 启发式重新推导(或为仅用户名的形态放宽守卫),并为这些形态补充回归用例。

— qwen3.8-max via Qwen Code /review (v0.21.7)

Comment on lines +244 to +246
for (let i = authorityStart; i < end; i++) {
if (/\s/.test(baseUrl.charAt(i))) {
return i;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R3-6: This new whitespace terminator truncates authorityEnd, and findUnescapedUserInfoFallbackAt's unchanged guards (~lines 225/229) evaluate against the truncated span — so on the catch path NO strip happens at all for WHATWG-unparseable URLs with whitespace-containing credentials, and they are returned fully intact. Regression: the merge base stripped them. — Failure scenario (probe-verified A/B against merge base d91c661): sanitizeProviderBaseUrl('https://user pa:ss@[bad') → unchanged, full user pa:ss leak (the password's colon sits past the truncated authorityEnd, so the colon > authorityEnd guard bails; base: https://[bad); https://user:12 34@[bad and https://user:12 34@[::1 → unchanged (portCandidate truncates to 12, mistaken for a port; base stripped both). Control: https://user:pa ss@[bad is stripped by both versions — the defect is the guard interaction with whitespace truncation, not the input class. No test covers the catch path with whitespace credentials, which is how this shipped green. Suggested fix: evaluate the fallback guards against the whitespace-agnostic authority extent (min of / ? # or end, computed from authorityStart); whitespace inside portCandidate disqualifies it as a port. Add catch-path regression tests.

中文说明

这个新增的空白终止符会截断 authorityEnd,而 findUnescapedUserInfoFallbackAt 中未改动的守卫(约第 225/229 行)仍按截断后的范围判断 —— 于是在 catch 路径上,WHATWG 无法解析且凭据含空白的 URL 完全不会被剥离,原样返回。这是回退:merge base 会剥掉它们。—— 失败场景(已对 merge base d91c661 做 A/B 探针验证):sanitizeProviderBaseUrl('https://user pa:ss@[bad') → 原样返回,user pa:ss 完整泄露(密码中的冒号位于截断后的 authorityEnd 之后,colon > authorityEnd 守卫直接退出;base 为 https://[bad);https://user:12 34@[badhttps://user:12 34@[::1 → 原样返回(portCandidate 被截成 12,误判为端口;base 两者均剥离)。对照:https://user:pa ss@[bad 两个版本都能剥离 —— 缺陷是守卫与空白截断的相互作用,而非输入类别。没有任何测试覆盖 catch 路径 + 含空白凭据,因此它在测试全绿的情况下合入。建议修复:让 fallback 守卫按"忽略空白的 authority 范围"(从 authorityStart 起取 / ? # 或串尾的最小值)判断;portCandidate 中含空白即不能视为端口。并补充 catch 路径的回归测试。

— qwen3.8-max via Qwen Code /review (v0.21.7)

Whitespace truncation in sanitizeProviderWarning was cutting passwords
like "sec ret" before the authority @, so construction-error sanitizing
no longer produced https://broken.example/v1. Extend single-token gaps
before @ into the URL token; leave multi-word trailing prose alone.
@LHMQ878

LHMQ878 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

CI failure on sanitizes credentials from provider construction errors was from warning URL tokenization cutting space-containing passwords before the authority @. Pushed a follow-up so single-token gaps before @ stay in the URL token (multi-word trailing prose is unchanged). Local: workspace-providers-status 24/24 and acpModelUtils 41/41 green.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not explored to full depth (tool budget reached): PR #8524 reuses the authority-aware sanitizeProviderBase...: running the actual vitest suites ( packages/cli/src/utils/acpModelUtils.test.ts , packages/cli/src/serve/workspace-providers-status.test.ts ) — the review work…; PR #8524 reuses the authority-aware sanitizeProviderBase...: running the PR's own vitest files ( acpModelUtils.test.ts , workspace-providers-status.test.ts ) — skipped because dependencies are not installed in this workt….

Not reviewed: reverse audit — stopped before round 3 by the review time budget.

中文说明

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未探索到全部深度(达到工具调用预算):PR #8524 reuses the authority-aware sanitizeProviderBase...:running the actual vitest suites ( packages/cli/src/utils/acpModelUtils.test.ts , packages/cli/src/serve/workspace-providers-status.test.ts ) — the review work…;PR #8524 reuses the authority-aware sanitizeProviderBase...:running the PR's own vitest files ( acpModelUtils.test.ts , workspace-providers-status.test.ts ) — skipped because dependencies are not installed in this workt…

未审查:反向审计——评审时间预算不足,未能开始第 3 轮。

— qwen3.8-max via Qwen Code /review (v0.21.8)

Comment on lines +295 to +296
const gap = rest.slice(0, userinfoAt);
if (/^\s*\S+$/.test(gap)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R4-1: Multi-word userinfo leaks through the warning path. The gap gate /^\s*\S+$/ accepts only a single non-whitespace run, so a URL whose userinfo contains two or more whitespace-separated words is truncated at the first space before sanitizeProviderBaseUrl sees the terminating @ — the token is returned unchanged and the credential half is re-appended as prose. This is the residual sibling of R3-1, whose own single-space input is fixed at this head. — Failure scenario: probe end-to-end through createWorkspaceProvidersStatusProvider (serve /status errors[].error): warning Failed loading provider https://user:pa ss word@host.io/v1 is emitted unchanged at this head — full user:pa ss word credential displayed; the merge base returned Failed loading provider https://host.io/v1. The direct-call path strips the same URL correctly at this head (https://host.io) — the leak exists only because the wrapper cuts the token before delegation. Note: the multi-word gap is genuinely ambiguous with trailing prose (https://api.example:8443 — contact admin@example.com has the same token shape, and folding on a pre-gap colon reproduces the port+prose corruption), so if stripping is not worth that risk, at minimum document that userinfo with 2+ raw spaces is not stripped from warnings.

中文说明

多词 userinfo 经由 warning 路径泄露。gap 守卫 /^\s*\S+$/ 只接受单个非空白连续段,因此 userinfo 含两个或以上空白分隔词的 URL 会在第一个空白处被截断,sanitizeProviderBaseUrl 永远看不到结尾的 @ —— token 原样返回,凭据后半段被当作“后续文字”原样拼回。这是 R3-1 的残留兄弟形态(R3-1 自身的单空白输入已在本 head 修复)。—— 失败场景:通过 createWorkspaceProvidersStatusProvider 端到端探针(serve /statuserrors[].error):warning Failed loading provider https://user:pa ss word@host.io/v1 在本 head 原样输出 —— 完整暴露 user:pa ss word 凭据;merge base 输出 Failed loading provider https://host.io/v1。直接调用路径在本 head 能正确剥离同一 URL(https://host.io)—— 泄露仅因包装层在委托前截断了 token。注意:多词 gap 与后续文字存在真正的歧义(https://api.example:8443 — contact admin@example.com 具有相同 token 形态,按 gap 前冒号折叠会重现“端口+散文”破坏),若不值得为此承担风险,至少应文档说明含两个及以上空白的 userinfo 不会从 warning 中剥离。

— qwen3.8-max via Qwen Code /review (v0.21.8)

Comment thread packages/cli/src/utils/acpModelUtils.ts Outdated
Comment on lines +219 to +220
const at = baseUrl.indexOf('@', authorityEnd);
if (at === -1 || at < authorityStart) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R4-4: at < authorityStart is dead code left over from the lastIndexOf shape this line replaced. indexOf('@', authorityEnd) returns either -1 or an index ≥ authorityEnd, and findAuthorityEnd always returns a value ≥ authorityStart — so the comparison can never be true. In a credential-stripping function it misleads readers into thinking an @ before the authority can reach this branch.

Suggested change
const at = baseUrl.indexOf('@', authorityEnd);
if (at === -1 || at < authorityStart) {
const at = baseUrl.indexOf('@', authorityEnd);
if (at === -1) {
中文说明

at < authorityStart 是本行所替换的 lastIndexOf 形态遗留的死代码。indexOf('@', authorityEnd) 只会返回 -1 或 ≥ authorityEnd 的下标,而 findAuthorityEnd 的返回值总是 ≥ authorityStart —— 因此该比较永远不可能成立。在一个凭据剥离函数中,它会误导读者以为 authority 之前的 @ 可能到达此分支。

— qwen3.8-max via Qwen Code /review (v0.21.8)

Comment on lines +264 to +265
['https://user:pa ss@host.io', 'https://host.io'],
['https://user:12 34@host.io', 'https://host.io'],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R4-3: The parsed.password || disjunct of the new beyond-authority strip branch (acpModelUtils.ts:195) is unpinned — every password-bearing row in the added table has a single-word gap that /^\s*\S+$/ alone accepts. — Failure scenario: mutation probe — deleting parsed.password || leaves 67/67 tests green while sanitizeProviderBaseUrl('https://user:pa ss word@host.io') regresses from https://host.io to the unchanged credential-bearing input; the same mutant also silently removes the port+prose corruption noted in the R4-7 discussion. The untested disjunct is simultaneously the only handler of multi-word passwords and the cause of that corruption — neither direction is pinned.

Suggested change
['https://user:pa ss@host.io', 'https://host.io'],
['https://user:12 34@host.io', 'https://host.io'],
['https://user:pa ss@host.io', 'https://host.io'],
['https://user:12 34@host.io', 'https://host.io'],
['https://user:pa ss word@host.io', 'https://host.io'],
中文说明

新的“authority 之外剥离”分支(acpModelUtils.ts:195)中的 parsed.password || 分支没有测试固定 —— 新增表格中所有含密码的用例都是单词 gap,仅靠 /^\s*\S+$/ 即可通过。—— 失败场景:变异探针 —— 删除 parsed.password || 后 67/67 测试仍全绿,而 sanitizeProviderBaseUrl('https://user:pa ss word@host.io')https://host.io 回退为原样返回(凭据完整保留);同一变异还会悄悄消除 R4-7 讨论中提到的“端口+散文”破坏。该未被测试固定的分支既是多词密码的唯一处理器,又是该破坏的成因 —— 两个方向都没有被测试固定。

— qwen3.8-max via Qwen Code /review (v0.21.8)

Comment on lines +284 to +287
const urlBreak = segment.search(/[\s'"`<>]/);
if (urlBreak === -1) {
return { urlToken: segment, trailing: '' };
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R4-2: A bare <url> <one-word>@<host> prose pattern is folded into the URL token: the single-word gap extension treats admin@example.com as userinfo, WHATWG reads the real host as part of that userinfo, and the strip rewrites the displayed endpoint to the email's domain. — Failure scenario: probe through the real provider error path: Cannot reach https://api.example admin@example.comCannot reach https://example.com; colon variant Cannot reach https://host Contact:admin@corp.ioCannot reach https://corp.io — the displayed error names the wrong endpoint and deletes the contact text (the merge base left both unchanged). Adjudicated Suggestion rather than Critical: the doc comment documents the boundary (multi-word gaps stay prose; single-word gaps read as userinfo), and the pinned rows (user name@host.io, us er@host) mandate stripping for structurally identical inputs — no lossless discriminator exists, and the harm is display-only. Consider stating the accepted limitation explicitly (a single-word gap before @ is always treated as userinfo, so <url> <email> loses the URL), or a lossy refinement such as not folding when the pre-gap token contains a dot and no colon — knowing it reopens a leak window for dotted usernames and does not fix the colon variant.

中文说明

单个词的散文邮箱会被折叠进 URL token:单词 gap 延伸把 admin@example.com 当作 userinfo,WHATWG 把真实 host 读成该 userinfo 的一部分,剥离后展示的 endpoint 被改写为邮箱域名。—— 失败场景:经真实 provider 错误路径探针:Cannot reach https://api.example admin@example.comCannot reach https://example.com;带冒号变体 Cannot reach https://host Contact:admin@corp.ioCannot reach https://corp.io —— 展示的错误指向错误的 endpoint 且联系文字被删除(merge base 对两者均原样保留)。裁定为 Suggestion 而非 Critical:文档注释已声明该边界(多词 gap 保留为散文;单词 gap 视为 userinfo),且固定用例(user name@host.ious er@host)要求对结构完全相同的输入执行剥离 —— 不存在无损的判别条件,危害仅限展示层。建议或明确声明该已接受的局限(@ 前的单词 gap 一律视为 userinfo,因此 <url> <email> 会丢失 URL),或采用有损改进,例如当 gap 前的 token 含点号且无冒号时不折叠 —— 但须知这会对带点号的用户名重新打开泄露窗口,且无法修复带冒号的变体。

— qwen3.8-max via Qwen Code /review (v0.21.8)

Comment on lines +293 to +294
const userinfoAt = rest.indexOf('@');
if (userinfoAt !== -1) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R4-5: The single-word-gap predicate /^\s*\S+$/ now exists in two packages with divergent behaviour — the acpModelUtils.ts copy (line 195) is bypassed by parsed.password ||, this warning-path copy is not. That divergence is exactly how R4-1 leaks: the direct path strips https://user:pa ss word@host.io to https://host.io while the warning path emits the credential unchanged. Note (from verification): do not unify the two gates into one — the wrapper's stricter gate is forced by the R4-2 ambiguity (giving it a password disjunct would fold https://api.example:8443 — contact admin@example.com and reproduce the port+prose corruption inside the warning path). Cross-reference the two sites instead (a comment at each naming the other and why the gates must differ), or export a shared helper that makes the differing input contracts explicit.

中文说明

单词 gap 判定 /^\s*\S+$/ 现在存在于两个包中且行为不一致 —— acpModelUtils.ts(第 195 行)的那份会被 parsed.password || 短路,此 warning 路径的这份不会。这一差异正是 R4-1 泄露的成因:直接调用路径把 https://user:pa ss word@host.io 剥成 https://host.io,而 warning 路径原样输出凭据。注意(来自验证):不要把两个守卫统一为一个 —— 包装层更严格的守卫是 R4-2 歧义所迫(给它加上 password 分支会折叠 https://api.example:8443 — contact admin@example.com,在 warning 路径内重现“端口+散文”破坏)。建议改为在两处互相引用(各自注释指明另一处及守卫必须不同的原因),或导出一个共享辅助函数使两种不同的输入契约显式化。

— qwen3.8-max via Qwen Code /review (v0.21.8)

…ping

Remove splitProviderWarningUrlToken pre-truncation so multi-word password gaps reach sanitizeProviderBaseUrl. Strip at the last host @ before path or prose when userinfo spans past whitespace, and add regression tests.
@LHMQ878

LHMQ878 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

Addressed round 4 critical feedback in c821d81:

R4-1 (Critical): Removed splitProviderWarningUrlToken pre-truncation on the warning path. Each URL segment is now passed whole to sanitizeProviderBaseUrl, so multi-word password gaps like https://user:pa ss word@host.io/v1 are sanitized correctly.

R3-2 / R3-3: When credentials are confirmed and the host @ sits past whitespace-bounded authority, stripping now uses the last host @ before path or prose (not the first). Multi-word username/password gaps no longer require the old single-word gate. Prose after a complete host (e.g. https://api.example — contact admin@example.com) stays unchanged.

R4-4: Fallback userinfo lookup now uses findLastAtAfter and drops the dead at < authorityStart guard.

Added regression tests in acpModelUtils.test.ts and workspace-providers-status.test.ts. Vitest: 71/71 passing on both files.

@LHMQ878

LHMQ878 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

CI Test (ubuntu-latest) failed on an unrelated flake in scheduled-tasks.test.ts (nextRunAt vs now timing: expected nextRunAt > now, got a past timestamp). Our sanitizer suites were green in that run. Re-triggered the failed job.

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review timed out. Qwen review timed out after 10800 seconds (of the 180-minute budget). For large PRs, retry with a longer timeout by commenting: @qwen-code /review --timeout=360. See workflow logs.

@LHMQ878

LHMQ878 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

@qwen-code /review --timeout=360

Ubuntu Test is green on the current head (including the sanitizer suites). Previous review timed out after the 180-minute budget; re-kicking with a longer timeout so the round-4/5 findings can be re-evaluated against c821d81 + follow-ups.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not reviewed: reverse audit — stopped before round 4 by the review time budget.

Convergence: round 5 posted 7 inline comment(s), 6 of them reported for the first time. Findings keep coming back to the same files: packages/cli/src/utils/acpModelUtils.ts (findings in round 4; 6 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

仅完成部分审查,审查缺口已披露。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未审查:反向审计——评审时间预算不足,未能开始第 4 轮。

收敛情况:第 5 轮发布了 7 条行内评论,其中 6 条是首次提出。发现反复回到同一批文件:packages/cli/src/utils/acpModelUtils.ts(第 4 轮已出过发现,本轮又有 6 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)

— qwen3.8-max via Qwen Code /review (v0.22.3)

Comment on lines +308 to +312
for (let i = authorityStart; i < end; i++) {
if (/\s/.test(baseUrl.charAt(i))) {
return i;
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R5-1: [certifies-falsely] [regression] Credential leak: four probe-confirmed shapes share one root — the whitespace authority terminator truncates the authority span mid-userinfo, and every branch that re-derives the userinfo boundary afterwards fails on a different corner, so credentials survive sanitization.

The probe matrix at this head, all against the merge base: (a) the in-span lastIndexOf('@') strips at the inner @ when a password contains @ before whitespace — sanitizeProviderBaseUrl('https://user:pa@ss w0rd@host.io') returns https://ss w0rd@host.io where the base returned https://host.io; (b) the extended search's proseEnd bound stops at the whitespace after an inner @, so the strip again lands inside the userinfo — https://us a@b er@host.io returns https://b er@host.io, which re-parses with username b%20er (base: https://host.io); (c) the dotted-token gate suppresses the search entirely even though WHATWG parsing proved userinfo exists — https://john.doe @internal.corp.io is returned unchanged and the username reaches the served workspace status and ACP responses (base: https://internal.corp.io); (d) on the catch path the fallback's colon/port guards evaluate against the truncated span and bail — https://user pa:ss@[bad, https://user:12 34@[bad, https://user:12 34@[::1 and nine further WHATWG-unparseable shapes (e.g. https://user name:pass@broken host, https://user name:secret@host:99999999) are returned fully intact where the merge base stripped every one (deterministic split 19/19 base vs 15/19 PR on the probe file). Every shape reaches every display call site (acpAgent.ts, modelConfigUtils.ts, workspace-providers-status.ts). Shapes (a) and (d) are the carried blockers R3-2 and R3-6, still standing in these shapes; (b) and (c) are new this round. This family has produced a new leaking shape in every round since round 2, and per-entrance patching has not converged it — when parsed.username || parsed.password holds, WHATWG has already proven userinfo exists, so strip at the LAST @ before the first / ? # terminator after authorityStart (or end of string) instead of re-deriving the boundary per branch, and key the catch-path guards to the whitespace-agnostic extent (min of / ? # or end); a username-only unparseable shape is genuinely ambiguous with a prose email — decide it together with the over-strip direction in R5-2.

Witness:

BASE: 'https://user:pa@ss w0rd@host.io' -> 'https://host.io'
PR:   'https://user:pa@ss w0rd@host.io' -> 'https://ss w0rd@host.io'
BASE: 'https://us a@b er@host.io' -> 'https://host.io'
PR:   'https://us a@b er@host.io' -> 'https://b er@host.io'  (re-parse: username 'b%20er')
BASE: 'https://john.doe @internal.corp.io' -> 'https://internal.corp.io'
PR:   'https://john.doe @internal.corp.io' -> unchanged (leak)
PR probe V-R36: Expected "https://[bad" Received "https://user pa:ss@[bad";
                Expected "https://[bad" Received "https://user:12 34@[bad"
Nine further unparseable shapes: UNCHANGED on PR arm, stripped on BASE arm.
// Direction — in the parse-success arm, when credentials are proven, strip at the
// last '@' before the first '/ ? #' after authorityStart (WHATWG's own userinfo
// separator) instead of the truncated-span heuristics; on the catch arm, evaluate
// the guards against that same whitespace-agnostic extent.

The fix must keep this diff's own prose-keep pins byte-identical — ['https://api.example — contact admin@example.com', …], ['https://user:pa ss@host.io please contact admin@corp.io', 'https://host.io please contact admin@corp.io'] (packages/cli/src/utils/acpModelUtils.test.ts:253-254, ~264) and ['https://host bad:x/v1 — contact admin@corp.io', …] (acpModelUtils.test.ts:261-262); trusting the parser's userinfo split unconditionally or extending the colon search past the first / ? # breaks them. Please add rows ['https://user:pa@ss w0rd@host.io', 'https://host.io'], ['https://us a@b er@host.io', 'https://host.io'], ['https://john.doe @internal.corp.io', 'https://internal.corp.io'], ['https://user pa:ss@[bad', 'https://[bad'] to packages/cli/src/utils/acpModelUtils.test.ts, then prove each branch by removing your fix for one shape at a time and confirming the matching row goes red.

中文说明

凭据泄露:四个经探针确认的形态同根 —— 空白 authority 终止符把 authority 范围从 userinfo 中间截断,之后每个重新推导 userinfo 边界的分支各漏一角,凭据残留在“已清理”的输出里。

探针矩阵(本 head 对 merge base):(a) span 内 lastIndexOf('@') 落在内部 @ 上 —— 密码在空白之前含 @ 时,sanitizeProviderBaseUrl('https://user:pa@ss w0rd@host.io') 输出 https://ss w0rd@host.io(base 为 https://host.io);(b) 扩展搜索的 proseEnd 边界停在内部 @ 之后的空白处,剥离再次落在 userinfo 内部 —— https://us a@b er@host.io 输出 https://b er@host.io,重新解析用户名为 b%20er(base 为 https://host.io);(c) 点号 token 守卫在 WHATWG 已证明存在 userinfo 时仍压制搜索 —— https://john.doe @internal.corp.io 原样返回,用户名进入对外的 workspace status 与 ACP 响应(base 为 https://internal.corp.io);(d) catch 路径上 fallback 的冒号/端口守卫按截断后的范围判断并直接退出 —— https://user pa:ss@[badhttps://user:12 34@[badhttps://user:12 34@[::1 以及另外九个 WHATWG 无法解析的形态(如 https://user name:pass@broken hosthttps://user name:secret@host:99999999)全部原样返回,而 merge base 全部剥离(探针文件确定性对比:base 19/19 对 PR 15/19)。所有形态都能到达全部展示调用点(acpAgent.ts、modelConfigUtils.ts、workspace-providers-status.ts)。形态 (a)、(d) 即原有阻塞项 R3-2、R3-6,在本 head 仍然成立;(b)、(c) 为本轮新发现。该家族自第 2 轮起每轮都出现新的泄露形态,逐入口修补并未收敛 —— 当 parsed.username || parsed.password 成立时,WHATWG 已证明 userinfo 存在:在 authorityStart 之后第一个 / ? # 终止符(或串尾)之前的最后一个 @ 处剥离,而不是逐分支重新推导边界;catch 路径的守卫也应按同一“忽略空白的范围”(/ ? # 或串尾的最小值)判断;仅有用户名且无法解析的形态与“散文邮箱”存在真正的歧义,请与 R5-2 的过度截取方向一并决策。

修复必须保持本 PR 自己固定的“保留散文”用例逐字节不变 —— ['https://api.example — contact admin@example.com', …]['https://user:pa ss@host.io please contact admin@corp.io', 'https://host.io please contact admin@corp.io'](packages/cli/src/utils/acpModelUtils.test.ts:253-254、约 264)以及 ['https://host bad:x/v1 — contact admin@corp.io', …](acpModelUtils.test.ts:261-262);无条件信任 parser 的 userinfo 切分、或把冒号搜索延伸到第一个 / ? # 之后都会破坏这些用例。请补充用例行 ['https://user:pa@ss w0rd@host.io', 'https://host.io']['https://us a@b er@host.io', 'https://host.io']['https://john.doe @internal.corp.io', 'https://internal.corp.io']['https://user pa:ss@[bad', 'https://[bad'],然后逐个移除你对各形态的修复并确认相应用例变红,以此证明每个分支。

— qwen3.8-max via Qwen Code /review (v0.22.3)

Comment on lines +208 to +211
function shouldExtendUserInfoSearch(
authoritySlice: string,
parsed: URL,
): boolean {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R5-2: [certifies-falsely] [regression] Over-strip: prose after a URL keeps being read as userinfo, so warnings are rewritten to the contact email's domain — an unbounded surface this family has exposed in every round since round 1.

Warning-level A/B at the exact surface this diff changed: Cannot reach https://localhost — contact admin@example.com becomes Cannot reach https://example.com at this head while the merge base kept it byte-identical; Cannot reach https://api.example — error at 10:30, contact admin@example.com becomes Cannot reach https://example.com (base kept it) — a colon anywhere in the prose makes WHATWG report a password, which bypasses the dotted-host gate entirely; and R4-2's colon variant Cannot reach https://host Contact:admin@corp.io still becomes Cannot reach https://corp.io where the base kept it. The displayed error names a host that was never the provider and deletes the contact instruction — misleading diagnostics from the function whose purpose is safe display. The round-4 fix closed only dotted-host prose; the pre-existing siblings http://localhost:11434 — contact ops@corp.io and the https://api.example:v1[/x] — contact admin@example.com pair behave the same on both arms, and with the R5-1 candidate fix applied the localhost shape is still destroyed — per-entrance patching does not close this surface. Close it structurally: for spans new URL parses, reconstruct the output from the parser's own userinfo/host split only when the pre-@ text is provably userinfo (e.g. a whitespace-only gap), and for free-text segments that do not parse cleanly fail closed (redact the whole URL span) instead of performing indexOf-surgery on prose — the pattern getRouteEndpointIdentity in this same file already uses.

Witness:

W1 BASE: 'Cannot reach https://localhost — contact admin@example.com' -> unchanged
W1 PR:   same -> 'Cannot reach https://example.com'
W2 BASE: 'Cannot reach https://api.example — error at 10:30, contact admin@example.com' -> unchanged
W2 PR:   same -> 'Cannot reach https://example.com'
R4-2 colon variant PR: Expected 'Cannot reach https://host Contact:admin@corp.io'
                       Received 'Cannot reach https://corp.io' (BASE arm passes)

The fix must keep multi-word credentials stripping — ['https://user:pa ss word@host.io/v1', 'https://host.io/v1'] (packages/cli/src/utils/acpModelUtils.test.ts:277) — and the pinned keep Cannot reach https://api.example:8443/v1 — contact admin@example.com (packages/cli/src/serve/workspace-providers-status.test.ts, ~line 646); any port/digit heuristic must reconcile with the user:12 userinfo slice, which is also token:digits. Please add keep-rows ['https://localhost — contact admin@example.com', …] and ['https://api.example — error at 10:30, contact admin@example.com', …] (both RED at this head) to both test files, then remove your guard and confirm they go red again.

中文说明

过度截取:URL 之后的散文仍被当作用户信息,warning 被改写为联系邮箱的域名 —— 该家族自第 1 轮起每轮都暴露新的入口,是无界表面。

在本 diff 改动的表面上做 warning 级 A/B:Cannot reach https://localhost — contact admin@example.com 在本 head 变为 Cannot reach https://example.com,merge base 逐字节保留;Cannot reach https://api.example — error at 10:30, contact admin@example.com 变为 Cannot reach https://example.com(base 保留)—— 散文中任意位置的冒号都会让 WHATWG 报出 password,从而完全绕过点号 host 守卫;R4-2 的冒号变体 Cannot reach https://host Contact:admin@corp.io 仍变为 Cannot reach https://corp.io(base 保留)。对外展示的错误指向一个从未是 provider 的域名并删除联系方式 —— 在一个以安全展示为目的的函数里产生误导性诊断。第 4 轮的修复只封闭了“点号 host + 散文”一种形态;http://localhost:11434 — contact ops@corp.iohttps://api.example:v1[/x] — contact admin@example.com 等既有兄弟形态在两侧表现相同,且在 R5-1 的候选修复之上 localhost 形态仍被破坏 —— 逐入口修补无法封闭该表面。请结构性关闭:对 new URL 可解析的片段,仅当 @ 之前的文本可证明是 userinfo(例如纯空白 gap)时按 parser 自身的 userinfo/host 切分重建输出;对无法干净解析的自由文本片段则失败关闭(整个 URL 片段打码),而不是对散文做 indexOf 手术 —— 同文件的 getRouteEndpointIdentity 已在用这一模式。

修复必须保持多词凭据继续被剥离 —— ['https://user:pa ss word@host.io/v1', 'https://host.io/v1'](packages/cli/src/utils/acpModelUtils.test.ts:277)—— 以及固定保留用例 Cannot reach https://api.example:8443/v1 — contact admin@example.com(packages/cli/src/serve/workspace-providers-status.test.ts 约 646 行);任何端口/数字启发式都必须与 user:12 这种同为 token:数字 的 userinfo 片段兼容。请在两个测试文件中补充保留用例 ['https://localhost — contact admin@example.com', …]['https://api.example — error at 10:30, contact admin@example.com', …](本 head 均为红),然后移除你的守卫并确认它们再次变红。

— qwen3.8-max via Qwen Code /review (v0.22.3)

Comment on lines +212 to +214
if (parsed.password) {
return true;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R4-3: The parsed.password disjunct is still unpinned at this head — every password-bearing row added since round 4 has a colon in its whitespace-truncated authority slice, so the colon branch alone carries them.

Still stands from round 4. Mutation probe at this head: deleting parsed.password || from the early-return guard (if (!(parsed.username || parsed.password))if (!parsed.username)) leaves all 73 tests in the two suites green, and the mutant is live — discriminator https://:pass@host.io returns unchanged under the mutant while intact code returns https://host.io. The same unpinned disjunct shape sits in shouldExtendUserInfoSearch's if (parsed.password) branch (this thread), whose inputs are exactly the prose-password shapes R5-2 names, so neither direction is pinned. Add rows that only the password disjunct can carry: ['https://:pass@host.io', 'https://host.io'] plus a prose-password shape whose truncated slice has no colon, with its intended stripped/preserved expectation pinned deliberately. The new rows must go red when the disjunct is deleted from either gate — the https://:pass@host.io discriminator is exactly that test.

Witness:

Mutant (parsed.password removed): 73/73 tests pass — none of the password rows fail.
Discriminator: 'https://:pass@host.io' -> unchanged under mutant;
               intact HEAD -> 'https://host.io'.
中文说明

parsed.password 分支在本 head 仍然没有测试固定 —— 第 4 轮之后新增的所有含密码用例,其空白截断后的 authority 片段都含冒号,仅靠冒号分支即可通过。

第 4 轮的结论仍然成立。本 head 变异探针:从早退守卫中删除 parsed.password || if (!(parsed.username || parsed.password))if (!parsed.username))后,两个测试文件 73/73 全绿;变异体是活的 —— 判别输入 https://:pass@host.io 在变异体下原样返回,完整代码输出 https://host.io。同一未固定分支也存在于 shouldExtendUserInfoSearchif (parsed.password)(本条所在线),其输入正是 R5-2 点名的“散文密码”形态,两个方向都没有被测试固定。请补充只有 password 分支能承载的用例:['https://:pass@host.io', 'https://host.io'],以及一个截断片段不含冒号的散文密码形态,并明确固定其剥离/保留期望。新用例必须在从任一处删除该分支时变红 —— https://:pass@host.io 判别输入正是该测试。

— qwen3.8-max via Qwen Code /review (v0.22.3)

Comment on lines +249 to +252
function findPathDelimiterStart(
baseUrl: string,
authorityStart: number,
): number {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R5-3: findPathDelimiterStart re-implements, byte-identically, the first-of / ? # scan that findAuthorityEnd (~line 297) already contains in this same file — security-relevant boundary logic now lives in two copies, and this diff itself demonstrates the drift pressure: one copy gained whitespace termination, the other did not.

A future authority-terminator change applied to one copy only makes sanitizeProviderBaseUrl's authority span and findHostAtBeforePathOrProse's after-authority search disagree about where the path starts for the same input, so credential stripping silently diverges between a bare base URL and the same URL embedded in a provider warning — the exact drift this PR exists to eliminate. Compute the delimiter end once and keep the whitespace difference where it belongs:

function findAuthorityEnd(baseUrl: string, authorityStart: number): number {
  const end = findPathDelimiterStart(baseUrl, authorityStart);
  for (let i = authorityStart; i < end; i++) {
    if (/\s/.test(baseUrl.charAt(i))) return i;
  }
  return end;
}

Witness:

witness: not run — static duplication claim; both bodies quoted byte-identical at HEAD
(findPathDelimiterStart lines 249-261 are the first eight lines of findAuthorityEnd
lines 297-304); no behavior to execute without reimplementing both blocks.

The whitespace difference is deliberate and must survive the dedup: findHostAtBeforePathOrProse searches past intervening prose whitespace per const searchEnd = Math.min(pathStart, proseEnd); (packages/cli/src/utils/acpModelUtils.ts:245) — making pathStart stop at whitespace would cut the search before the @ in inputs like https://user:pa ss@host.io/v1 and leak the credential. The pinned rows ['https://user:pa ss word@host.io/v1', 'https://host.io/v1'] and ['https://user:p?x@api.example/v1', 'https://api.example/v1'] in packages/cli/src/utils/acpModelUtils.test.ts pin both sides of the difference — after the dedup, remove the whitespace scan (or make findPathDelimiterStart whitespace-aware) and confirm those rows go red.

中文说明

findPathDelimiterStartfindAuthorityEnd(约第 297 行)中已有的“取 / ? # 最小值”扫描逐字节重复 —— 安全相关的边界逻辑现在有两份,且本 diff 本身就展示了漂移压力:其中一份加了空白终止符,另一份没有。

将来若只修改其中一份的 authority 终止符,sanitizeProviderBaseUrl 的 authority 范围与 findHostAtBeforePathOrProse 的 authority 后搜索就会对同一路径起点判断不一致,凭据剥离会在“裸 base URL”与“嵌在 provider warning 中的同一 URL”之间悄悄分叉 —— 正是本 PR 要消除的那种漂移。建议把分隔符终点的计算合并为一处,并把空白差异放到它应在的位置(见上方代码)。

该空白差异是有意的,去重时必须保留:findHostAtBeforePathOrProse 依据 const searchEnd = Math.min(pathStart, proseEnd);(packages/cli/src/utils/acpModelUtils.ts:245)需要越过散文中的空白继续搜索 —— 若让 pathStart 停在空白处,https://user:pa ss@host.io/v1 这类输入的搜索会在 @ 之前被截断,导致凭据泄露。固定用例 ['https://user:pa ss word@host.io/v1', 'https://host.io/v1']['https://user:p?x@api.example/v1', 'https://api.example/v1'] 分别固定了差异的两侧 —— 去重后,移除空白扫描(或让 findPathDelimiterStart 感知空白)并确认这些用例变红。

— qwen3.8-max via Qwen Code /review (v0.22.3)

return true;
}
// A dotted token without ':' is a complete host; trailing prose may follow.
return !(authoritySlice.includes('.') && !authoritySlice.includes(':'));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R5-4: The !authoritySlice.includes(':') clause in this final return is always true — the colon case already early-returned two lines above, so the expression is exactly return !authoritySlice.includes('.');, and the doubled negation hides the rule the comment states.

A maintainer investigating colon-bearing authorities (a truncated user:pa… slice or host:port) reads this return and reasons about a colon case that is unreachable at that line, or edits the clause believing it is load-bearing — in a credential-stripping function. A routing probe over the 18 test-table inputs shows five reach this final return, every one with a colon-free slice, and the real function produced the expected outputs for all of them.

Witness:

Probe: 5/18 table inputs reach the final return — 'https://api.example — contact admin@example.com',
'https://user name@host.io', 'https://us er@host', 'https://user @host.io', 'https://foo bar baz@corp.io'
— every reaching slice colon-free; outputs all as pinned.
Suggested change
return !(authoritySlice.includes('.') && !authoritySlice.includes(':'));
return !authoritySlice.includes('.');
中文说明

最终返回里的 !authoritySlice.includes(':') 永远为真 —— 含冒号的情况已在上方两行提前返回,该表达式实际等价于 return !authoritySlice.includes('.');,双重否定掩盖了注释所描述的规则。维护者若调查含冒号的 authority(截断的 user:pa… 片段或 host:port),会在这行推理一个不可达的冒号分支,或误以为该子句起承重作用 —— 这发生在一个凭据剥离函数里。对 18 个测试表输入的路由探针显示 5 个到达该最终返回,其片段均不含冒号,且真实函数输出全部符合预期。

— qwen3.8-max via Qwen Code /review (v0.22.3)

baseUrl: string,
authorityEnd: number,
): number {
const pathStart = findPathDelimiterStart(baseUrl, authorityEnd);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R5-5: findPathDelimiterStart's parameter is named authorityStart, but this — its only call site — passes the authority's END, while the sibling findAuthorityEnd(baseUrl, authorityStart) takes a genuine start and contains a verbatim copy of the same delimiter block: the same parameter name means start in one helper and receives an end in the other.

A maintainer reusing the helper as its name and sibling suggest — from a real authority start, e.g. when de-duplicating the delimiter block per R5-3 — gets back the authority terminator itself instead of the path start, so any span computed from that "end" collapses and credential stripping silently misbehaves or no-ops. Rename the parameter to from (or searchStart); the signature at lines 249-252 and this sole call site are the only two touch points.

Witness:

witness: not run — static call-site fact; signature at lines 249-252 and this sole
call site quoted at HEAD; the misuse outcome follows from the quoted bodies.
中文说明

findPathDelimiterStart 的参数名为 authorityStart,但此处(唯一调用点)传入的是 authority 的终点;而同族的 findAuthorityEnd(baseUrl, authorityStart) 接收的是真正的起点并包含同一分隔符块的逐字节副本 —— 同一个参数名在一个辅助函数里表示起点,在另一个里接收的却是终点。维护者若按名称与同族函数的暗示复用该函数(例如按 R5-3 去重时从真正的 authority 起点调用),会得到 authority 终止符本身而非路径起点,基于该“终点”计算的范围会塌缩,凭据剥离会悄悄出错或空转。请将参数改名为 from(或 searchStart);只需改 249-252 行的签名与本调用点两处。

— qwen3.8-max via Qwen Code /review (v0.22.3)

Comment on lines +238 to +239
/\s/.test(baseUrl.charAt(i)) &&
baseUrl.indexOf('@', authorityEnd) < i

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R5-6: baseUrl.indexOf('@', authorityEnd) in this loop is loop-invariant — neither baseUrl nor authorityEnd depends on i — yet it is recomputed for every whitespace character scanned, making the prose scan O(k·(n − authorityEnd)) instead of O(n).

A warning segment whose post-authority region holds k whitespace characters before the first @ re-runs an O(n − authorityEnd) scan k times. This is a cold path over short strings, so the practical magnitude is small; the fix is a one-line hoist:

const firstAtAfterAuthority = baseUrl.indexOf('@', authorityEnd);
for (let i = authorityEnd + 1; i < baseUrl.length; i++) {
  if (/\s/.test(baseUrl.charAt(i)) && firstAtAfterAuthority < i) {
    proseEnd = i;
    break;
  }
}

Witness:

witness: not run — invariance is a static data-flow fact quoted from the loop body;
a timing measurement cannot discriminate hoisted vs unhoisted semantics.
中文说明

本循环中的 baseUrl.indexOf('@', authorityEnd) 是循环不变量 —— baseUrlauthorityEnd 都不依赖 i —— 却按扫描到的每个空白字符重复计算,使散文扫描从 O(n) 退化为 O(k·(n − authorityEnd))。authority 之后、第一个 @ 之前含 k 个空白的 warning 会把 O(n − authorityEnd) 的扫描重复 k 次。这是短字符串上的冷路径,实际影响很小;修复为一行外提(见上方代码)。

— qwen3.8-max via Qwen Code /review (v0.22.3)

@LHMQ878

LHMQ878 commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Closing this after reproducing both round-5 Critical findings against the exact reviewed head (67f7899) and PR base (80b34ec).

The current head is strictly worse than the base for every witness I checked: four credential-bearing inputs either leak credentials or strip at the wrong at-sign, and three ordinary warnings are rewritten to the contact email's domain. The common cause is structural: the warning path passes the remainder of a line (URL plus prose) into a sanitizer whose contract is a standalone base URL. Extending that sanitizer across whitespace makes userinfo and prose email ambiguous, and five review rounds have not converged on a safe rule.

I do not think another per-shape heuristic is responsible here. The original narrow bugs remain valid, but this PR should not merge while it regresses credential handling. A future attempt should first separate warning tokenization/redaction from standalone base-URL sanitization and use the round-5 base/head witness matrix as acceptance tests. The remaining suggestions are moot for this implementation.

Thanks for the detailed review and the concrete probes.

@LHMQ878 LHMQ878 closed this Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/self-reported The linked issue was opened by the PR author (self-reported)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Provider warning sanitizer truncates messages containing a port, and leaks a password containing @

3 participants