feat(web-shell): make the product name and logo configurable - #11244
Conversation
White-labelling the Web Shell meant editing six source files and rebuilding: the product name was a literal in the sidebar, the welcome header, the About panel and the document title, and the logo existed as two hand-duplicated copies of the same inline SVG. Brand is now configuration. A name and a logo path are read from the operator settings scopes, and a new process-global daemon route resolves the logo into a data URI for the client. Embedding hosts get the same control through a prop that replaces the resolved value outright. The terminal banner already had its own white-label settings, so this follows their sanitization and path-resolution conventions rather than inventing parallel ones. Two decisions carry the design. The workspace settings layer is never read, so a repository's own settings file cannot rename the product or name a file for the daemon to inline into every connected browser. And a custom logo is only ever rendered as an image, never injected as markup, which is what makes it safe for the daemon to skip sanitizing the SVG it read. An untouched installation renders exactly what it did before: the resolved brand is empty, every consumer falls back to its own built-in literal, and the served document keeps its original title and favicon bytes, so the existing visual baselines stay valid.
E2E test reportFull plan and results: Baseline (pre-implementation, global
|
| Case | Result |
|---|---|
| No brand configured | {}, 200 |
| Name + valid SVG | {"name":"QiuQiu Code","logoDataUri":"data:image/svg+xml,…"}, decoding back to the source bytes |
Relative logoPath |
resolves against the directory of the settings file that declared it — verified end to end |
Accept: text/html / application/json / */* |
all application/json, 200 — registered ahead of the SPA fallback |
| Missing file / non-SVG / symlink / directory / oversize | 200 with the name and no logo, each with an operator-visible stderr line, e.g. qwen serve: GET /brand: ui.brand.logoPath does not exist: … |
| Workspace-scope brand | ignored. Run with security.folderTrust: false so the workspace is genuinely trusted — meaning a workspace value would be visible if the route read it. A workspace .qwen/settings.json naming Repo Controlled Brand was passed over in favour of the user layer's User Level Brand. The route also loads settings with the workspace layer skipped entirely, so it is never read from disk; asserted in the route test. |
| Layer precedence | system over user over system-defaults, both directions, by unit test |
| Capability tag | web_shell_brand advertised between workspace_skill_manage and workspace_settings |
Document contract
The served index.html differs from main only by the added pre-paint block — 22 insertions, 2 deletions, both deletions being the boot-watchdog predicate this PR narrows. The favicon href's sha256 is byte-identical to main (1a14eb40…c2b3f), and the built-in <title> is unchanged, so the static handler was not turned into a body rewriter.
The pre-paint script is exercised two ways. Four unit cases run the real inline script against a fake document and storage: cached title and logo applied, title-only cache applied, no cache leaves both built-in values, corrupt cache leaves both built-in values and warns. Separately, a jsdom harness executes the real inline scripts from the built dist/web-shell/index.html in parse order and confirms a first-ever load keeps the built-in title and logo while a cached brand applies both. The ordering is load-bearing and pinned by test: the script must sit after the icon <link>, because it swaps that element's href. A counter-check reconstructs the pre-fix ordering and confirms the favicon swap does not happen there, so the pin is a real guard rather than a tautology.
Browser
- 45 Playwright visual baselines pass without re-capture. The sidebar brand row and the welcome header are in those frames, so this is the pixel-level proof that an unconfigured shell is unchanged — the property that makes the change safe to ship.
- 50 Playwright smoke specs pass in a real browser against the mocked daemon, which is what validates the mock's new route handling end to end (that file is excluded from the vitest suite, so nothing else covers it).
- Custom logo rendering is asserted in jsdom: an SVG containing
<script>reaches the document only as animgsrc, with zero script nodes present. The resolver's counterpart test asserts that same SVG is accepted — together they pin the invariant that makes the absence of a server-side sanitizer correct.
Suites
web-shell 282 files / 6400 tests · TypeScript SDK 38 files / 1820 tests · CLI brand, route, schema and settings-surface cases 121 · CLI capability-registry cases 11 — all pass on the rebased branch. Build, bundle, tsc --noEmit for all three packages, Prettier and ESLint clean.
The CLI package carries pre-existing red tests unrelated to this change: most visibly a scheduled-tasks spec requesting a /scheduled-tasks/legacy-run route that appears seven times in the test and zero times in the route source. Every affected file is byte-identical to main, and a base-tree measurement over the same selection — identical vitest invocation run against a worktree at the merge base — found no net-new failing file and no net-new failing test name on this branch.
Not covered
No screenshots; the evidence above is wire-level, DOM-level and pixel-baseline. Windows and Linux were not exercised locally, so the hardened read path (symlink, hard-link and FIFO refusal, O_NOFOLLOW) is verified on macOS and by unit test elsewhere. No Playwright spec drives a branded shell — the mock always answers an empty brand, deliberately, so the baselines stay valid; the branded path is covered by the jsdom and unit layers plus the manual daemon runs above.
🖼️ web-shell visual previewRendered against a mock daemon (no real backend): the PR base vs this PR head Screenshots · before / afterFull-resolution recordings (.webm) are attached to the workflow run. — Qwen Code · web-shell visuals |
|
Thanks for the PR! Template looks good ✓ — every required heading is present, plus a full Chinese translation. Problem: real, and I verified it against the base tree rather than taking the description's word. The product name is hardcoded in five places across four files — the sidebar brand row and the version tooltip in Direction: aligned. The precedent is what convinces me — Worth a maintainer's eye, not because I doubt it but because it is the part with a long tail: this adds public contract surface — a new daemon route, a new capability tag, a new SDK method and exported type, a new settings key, and a new component prop on the web-shell package. All additive and capability-tagged, and the default path is a genuine no-op, but once shipped it is hard to walk back. Size: core paths are touched ( Approach: scope feels right, and I asked myself the 80% question — could you ship just the name and skip the logo? Probably not worth it, since the logo is the half that is currently duplicated by hand and the half with the security reasoning attached. The one piece I would call genuinely optional is the pre-paint Risk: no elevated risk signals — none of the 46 files match the revert-correlated path patterns. Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ —— 所有必填小节都在,并附有完整中文翻译。 问题: 真实存在,我对照 base 分支实际核对过,而不是只采信 PR 描述。产品名在 4 个文件里硬编码了 5 处: 方向: 对齐。真正说服我的是既有先例—— 有一点值得维护者留意,不是因为我怀疑它,而是因为它的长尾效应:这个 PR 增加了公共契约面——新的 daemon 路由、新的 capability tag、新的 SDK 方法与导出类型、新的设置项,以及 web-shell 包上新的组件 prop。全部是增量式且带 capability tag,默认路径是真正的 no-op,但一旦发布就不好回退。 规模: 触及核心路径( 方案: 范围合理。我也问了"砍掉 80%"这个问题——能不能只做名字、不做 logo?大概不值得,因为 logo 正是目前手工重复、且承载安全推理的那一半。唯一我会称之为真正可选的是 pre-paint 风险: 无升级风险信号——46 个文件均未命中与 revert 相关的路径模式。 进入代码审查 🔍 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
Code reviewI wrote my own proposal for this before reading the diff (settings key read from operator scopes only, a small daemon route ahead of the SPA fallback returning a data URI, a client context whose consumers fall back to their literals, a cached pre-paint apply to avoid the flash, logo rendered only through an image context). The PR lands essentially that design, and beats mine in three places I did not think of: the exact-path dev proxy, the peer-dependency deferral, and the boot-watchdog carve-out. No correctness or security blocker found. What I verified rather than assumed:
Three things worth raising, none blocking: 1. The hardened read is a near-verbatim copy of an existing one (Suggestion). 2. Nothing pins the 3. No path confinement on sequenceDiagram
participant P1 as Operator settings.json
participant P2 as resolveWebShellBrand
participant P3 as daemon brand route
participant P4 as DaemonClient.brand
participant P5 as WorkspaceProvider
participant P6 as App BrandProvider
participant P7 as Sidebar, Welcome, About
participant P8 as index.html pre-paint
P1->>P2: user and system layers only (workspace skipped)
P2->>P3: name plus logo data URI, or a warning on stderr
P3->>P4: 200 JSON, empty object means built-in
P4->>P5: brand state, kept outside the status machine
P5->>P6: host prop wins over the daemon value
P6->>P7: context, consumers fall back when empty
P6->>P8: cache the resolved brand for the next load
Note over P8: on the next load only, applies title and favicon before first paint
I skipped a changed-files table: the PR touches 46 files, but TestingThis is an unattended CI run, so I did not build or execute any PR code. The evidence below is the PR's own CI, read through the API for the reviewed commit — real check names and conclusions. I am not re-running the suite and not presenting the author's self-reported numbers as evidence. Nothing is red. The checks that would actually substantiate this PR are still in flight, and I am not going to guess them: Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 Not verified, and why it matters: the hardened read path — symlink, hard-link and FIFO refusal, Sandboxed verification would settle this: 中文说明代码审查我在看 diff 之前先写了自己的方案(只在运维作用域读取的设置项、一个注册在 SPA fallback 之前、返回 data URI 的小型 daemon 路由、消费者各自回退到内置字面量的客户端 context、为避免闪烁而做的 pre-paint 缓存应用、logo 只通过 image context 渲染)。这个 PR 基本实现了该设计,并在三个我没想到的地方胜过我的方案:精确路径的 dev proxy、peer-dependency 的延迟调用、以及 boot-watchdog 的豁免。未发现正确性或安全阻断问题。 我核实过(而非默认采信)的部分:
三点值得提出,均非阻断: 1. 加固读取逻辑几乎逐字复制了既有实现(建议)。 新文件 2. 没有任何测试钉住 3. 我跳过了变更文件表:这个 PR 改了 46 个文件,但 测试这是无人值守的 CI 运行,因此我没有构建或执行任何 PR 代码。 下面的证据是 PR 自己的 CI,通过 API 针对被审阅的 commit 读取——真实的 check 名称与结论。我没有重跑测试套件,也没有把作者自报的数字当作证据呈现。 没有红色。真正能支撑这个 PR 的检查仍在运行中,我不去猜测结果: 未验证的部分及其影响: 加固读取路径——symlink、hard link、FIFO 拒绝与 沙箱化验证可以解决这个问题: — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
🩺 serve daemon A/BBuilt the PR base vs this PR head
|
| field | PR base (before) | this PR (after) |
|---|---|---|
features[] |
— | "web_shell_brand" |
— Qwen Code · serve A/B
|
Confidence: 4/5 — the design is right and the two invariants that carry it are genuinely pinned by tests; the reservations are a duplicated security-sensitive helper and two claims nothing automated would catch regressing. Stepping back. My independent proposal was the same shape as this PR, and where it differs, this one is better: I would not have thought of the exact-path dev proxy (a bare The part I would push on is not correctness, it is weight. Six months from now I would mostly thank the author. The comments explain why rather than what, which is the reason this review was fast: the trust-boundary argument, the no-sanitizer argument and the flash-avoidance argument are all stated where the code makes them, so I spent my time checking whether they hold instead of reconstructing them. The one curse would be finding that guard duplicated a fourth time. Two honest limits on this verdict. First, I did not see it run — this is an unattended CI pass, so I read code and CI metadata and executed nothing from the PR. Everything I assert above is static, and I checked the load-bearing claims against the base tree rather than the description: the five hardcoded name sites, the duplicated logo artwork, the single read site of the removed i18n key, the route ordering in Nothing here blocks. The two Suggestions and the one Question are in the review comment; my own take is that finding 1 is worth a follow-up issue and findings 2 and 3 are worth a sentence each in this PR if the author happens to touch it again. On CI: nothing is red, and three checks that matter here have landed green — Approval is therefore deferred until CI lands green on 中文说明Confidence: 4/5 —— 设计是对的,承载它的两个不变量确实被测试钉住了;我的保留意见在于一个被复制的安全敏感 helper,以及两处若发生回归、没有任何自动化能发现的断言。 退一步看。我独立写出的方案与这个 PR 形状一致,而在有差异的地方,这个 PR 更好:我想不到精确路径的 dev proxy(裸 我要施压的地方不是正确性,而是重量。 六个月后我大概会感谢作者。注释解释的是为什么而不是是什么,这也正是本次审查很快的原因:信任边界论证、不做 sanitizer 的论证、避免闪烁的论证,都写在代码作出该决定的地方,所以我把时间花在核验它们是否成立,而不是重建它们。唯一会让人抱怨的,是发现这个守卫被复制出了第四份。 关于这个结论,有两点诚实的边界。第一,我没有运行过它——这是无人值守的 CI 审查,所以我只读代码与 CI 元数据,没有执行 PR 里的任何东西。上面所有断言都是静态的,而且我把承重的断言对照 base 分支核验过,而非采信描述:五处硬编码产品名、重复的 logo 素材、被删 i18n key 的唯一读取点、 这里没有阻断项。两条建议与一个疑问在审查评论里;我自己的看法是:第 1 条值得开一个后续 issue,第 2、3 条如果作者正好再动这个 PR,各加一句说明即可。 关于 CI:没有红色,且这里重要的三项检查已经变绿—— 因此批准被推迟,直到 CI 在 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
The daemon index states how many capability tags the registry holds, and a contract test fails the build when the two disagree. Adding the brand tag made the registry 158 while the index still said 157.
CI failure on the first run: mine, now fixedCorrecting the record, because my earlier read of this run was taken while jobs were still pending and reported only one of three failures. 1. 2. 3. Also worth noting: the body's earlier paragraph about pre-existing red CLI tests described my local machine, not CI. Those files pass in CI; the body has been corrected. |
The previous push arrived while a re-run of the prior commit was still holding the workflow's concurrency slot, so GitHub suppressed the new run instead of queueing it and the event does not replay once the slot frees. This leaves the branch head with no CI at all. An empty commit re-fires the synchronize event; the repository squash-merges, so it does not survive into main.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
Not reviewed: finding R1-36 — the verifier never ruled on it (a verifier-incidental candidate; the audit loop stopped on the time budget before a further verification round).
Not reviewed: finding R1-37 — the verifier never ruled on it (a verifier-incidental candidate; the audit loop stopped on the time budget before a further verification round).
Not explored to full depth (tool budget reached): "agent reverse-audit (round 2)": the design-doc claim "Nesting under ui rather than adding a top-level section keeps the change out of the schema's asserted top-level key list" — I never loca…; "agent reverse-audit (round 2)": the "What it deliberately does not replace" claims I did not verify against code — the ToS/docs/bug-report URLs still pointing at Qwen resources, the welcome ti…; chunk 1: locating the production client-side brand fetch site to confirm §Delivery's "inside the workspace provider beside the capabilities fetch" and "deferred into a p….
Not reviewed: reverse audit — stopped before round 3 by the review time budget.
— [unverified] tag when the loop ended — the verifier never ruled on them, and they are not confirmed.
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
未审查(原文为英文):finding R1-36 — the verifier never ruled on it (a verifier-incidental candidate; the audit loop stopped on the time budget before a further verification round).
未审查(原文为英文):finding R1-37 — the verifier never ruled on it (a verifier-incidental candidate; the audit loop stopped on the time budget before a further verification round).
未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 2)":the design-doc claim "Nesting under ui rather than adding a top-level section keeps the change out of the schema's asserted top-level key list" — I never loca…;"agent reverse-audit (round 2)":the "What it deliberately does not replace" claims I did not verify against code — the ToS/docs/bug-report URLs still pointing at Qwen resources, the welcome ti…;chunk 1:locating the production client-side brand fetch site to confirm §Delivery's "inside the workspace provider beside the capabilities fetch" and "deferred into a p…。
未审查:反向审计——评审时间预算不足,未能开始第 3 轮。
— [unverified] 标记——验证者从未对它们作出裁决,它们不算已确认。
— qwen3.8-max via Qwen Code /review (v0.23.0)
# Conflicts: # packages/web-shell/README.md # packages/web-shell/client/App.test.tsx
… and docs - Expose a brandSettled flag from the workspace provider so a settled-with-no-brand outcome (older daemon, withdrawn host prop) is reported and invalidates stale cached chrome, while the in-flight state still fires nothing - Fall back to the built-in sidebar mark when a logo data URI fails to decode, and when a host logo node is falsy; remount the logo image per data URI so a decode failure cannot stick to the next logo - Report an empty host brand name as unset in onBrandResolved, and type the callback payload as the document-actionable subset - Exclude the favicon from the boot watchdog's fatal-resource classification and keep its content-free error out of the panel list - Require a namespaced <svg> root in the resolver, cover the post-decode size cap, and pin the exact stderr prefix the protocol doc publishes - Assert GET /brand answers JSON on the real app for any Accept header - Sync the capability index count (159), settings docs and the design doc with the above
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": none — but for honesty about method: jsdom does not apply CSS, so the .brandLogo img { object-fit: contain } and 26px-in-28px-box sizing claims were judged by…; "agent reverse-audit (round 3)": whether --agent-logo-gradient ( App.module.css:474 / :548 , declared inside the theme blocks on the shell's own CSS-module-scoped root, and absent from every …; "agent reverse-audit (round 4)": the Files-affected row claiming packages/vscode-ide-companion/schemas/settings.schema.json is "regenerated, not hand-edited" — I confirmed the new keys and de…; "agent reverse-audit (round 4)": the Files-affected rows for brandContext.ts ("stable empty value"), index.tsx ("export the brand prop type"), WelcomeHeader.tsx ("name") and e2e/utils/mo…; "agent reverse-audit (round 5)": none — no check was cut short (28 of ~42 calls used). Scope note, not a gap: the remainder of packages/cli/src/services/web-shell-brand.test.ts past diff line…, and 15 more.
Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:
docs/design/web-shell-brand-customization.md:213 — [probe] The design record states a cache-invalidation rule the standalone entry does not implement — storeBrand drops the pre-paint cache only when the log…docs/design/web-shell-brand-customization.md:245 — [probe] The prop-channel contract this diff publishes in prose ("a React node, not a data URI") is narrower than the shipped prop type, which admits logoDa…docs/developers/qwen-serve-protocol.md:1144 — [probe] The published backwards-compat rule for GET /brand states an unqualified 404, but a daemon without the route answers 200 text/html (the SPA index) …packages/cli/src/serve/routes/brand.ts:29 — [review] The rationale for the new route's existence cites a "documented position" of the capabilities envelope that no document states, and the capabilitie…packages/cli/src/serve/routes/brand.ts:36 — [review] The route's own failure-contract comment states that any resolution failure yields an empty body, but the logo-rejection path — the very case the s…packages/cli/src/serve/routes/brand.ts:46 — [probe] The route omits workspaceTrusted , so every GET /brand runs the primary workspace's folder-trust evaluation — work whose result the handler never …packages/cli/src/services/web-shell-brand.test.ts:132 — [probe] Every layer-precedence fixture sets the same key at both layers, so nothing pins the per-key resolution the resolver's own contract claims — name a…packages/cli/src/services/web-shell-brand.test.ts:381 — [probe] The per-layer dir scoping that ScopedBrandValue.dir exists for is only ever exercised on the user layer — makeSettings has a userPath knob and no…packages/cli/src/services/web-shell-brand.ts:96 — [probe] readBrandLeaf collapses three distinct states — absent, present-but-null, and present-but-wrong-type — into one silent "not configured", so it dive…packages/cli/src/services/web-shell-brand.ts:236 — [probe] The logo is decoded as UTF-8 unconditionally and re-emitted percent-encoded into a data URI that carries no charset, while hasSvgRoot passes the do…packages/sdk-typescript/src/index.ts:145 — [probe] The new root-entry re-export of DaemonBrand is absent from daemon-public-surface.test.ts, the fence whose stated purpose is exactly this drift, and…packages/web-shell/client/App.tsx:1083 — [probe] The published prop contract says the callback fires "again only when one of those two values changes", but brandResolved is in the effect's depende…packages/web-shell/client/App.tsx:10877 — [probe] onBrandResolved is never delivered to a handler attached after the brand resolved, contradicting the prop's own published JSDoc ("once the brand is…packages/web-shell/client/components/messages/StatusMessage.tsx:50 — [probe] The status panel's label column is a fixed 20ch grid track with white-space: nowrap and no clipping, but this diff makes that label a brand name th…packages/web-shell/client/components/sidebar/WebShellSidebar.module.css:214 — [probe] The CSS added here is the only thing that keeps an operator-supplied logo inside the fixed 28px .brandLogo box, and nothing anywhere pins it — the …packages/web-shell/client/index-html.test.ts:326 — [probe] Both new ordering assertions bound the brand script from below only (after the icon link, after the title); nothing bounds it from above, so the pr…packages/web-shell/client/main.tsx:67 — [probe] The guarantee that only the standalone entry writes to document is stated four times by this diff and is pinned by no test anywhere — including the…packages/web-shell/client/main.tsx:74 — [probe] The favicon path has no decode-failure fallback while the sidebar logo got one this round, so an SVG the daemon accepts but the browser cannot rast…packages/web-shell/client/vite-config.test.ts:77 — [probe] The new dev-proxy test builds its own RegExp from the key instead of reproducing vite's proxy matcher, so it never pins the one property that makes…
中文说明
未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)":none — but for honesty about method: jsdom does not apply CSS, so the .brandLogo img { object-fit: contain } and 26px-in-28px-box sizing claims were judged by…;"agent reverse-audit (round 3)":whether --agent-logo-gradient ( App.module.css:474 / :548 , declared inside the theme blocks on the shell's own CSS-module-scoped root, and absent from every …;"agent reverse-audit (round 4)":the Files-affected row claiming packages/vscode-ide-companion/schemas/settings.schema.json is "regenerated, not hand-edited" — I confirmed the new keys and de…;"agent reverse-audit (round 4)":the Files-affected rows for brandContext.ts ("stable empty value"), index.tsx ("export the brand prop type"), WelcomeHeader.tsx ("name") and e2e/utils/mo…;"agent reverse-audit (round 5)":none — no check was cut short (28 of ~42 calls used). Scope note, not a gap: the remainder of packages/cli/src/services/web-shell-brand.test.ts past diff line…,另有 15 条。
收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 19 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.23.0)
… SVG scanner, test fixtures - Settle the brand fetch only on a definitive outcome: an answer, or a 404 from a route-less daemon. A retryable failure (503 bootstrap, 429, transport, old SDK) is unknown rather than absent — settling it cleared the pre-paint cache and reset the tab title mid-session over a blip nothing retries (review Critical) - Rewrite the SVG root scanner quote-aware: a quoted `>`, `[` or `]` in DOCTYPE literals or the root tag no longer rejects valid documents, and an xmlns-shaped substring inside another attribute's value no longer certifies a namespace-less document as SVG - Warn (not reject) when the root svg carries no viewBox and no width/height: the browser loads such an image successfully but may paint a blank mark at the sidebar's fixed size, and only the daemon's stderr can say so - Treat a nullish host brand prop as absent in the resolution gate, matching the `??` one line above - Pin previously unwitnessed guards: the fd identity re-check and both fs soft-fail branches, the logo key remount, the favicon error-list exclusion, the unsettled-to-settled transition, whole-object host takeover against a daemon logo, and the superseded-client reset and catch-leg disposed guards - Drive GET /brand on a real app with the SPA fallback mounted and a token configured, and pin the System settings layer to empty files so a maintainer's machine-wide brand cannot leak into the assertions - Round-trip the pre-paint cache through the real inline script instead of pinning each side against its own copy of the key - Document the hard-link and decoded-size rules, the namespace requirement, the viewBox advisory, and the corrected 503 semantics (bootstrap catch-all, retryable; a draining daemon does not reject this route); the README's onBrandResolved row now names the narrower payload type
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
6 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- App.tsx:11087 late-attached onBrandResolved handler — already reported (round 2 deferral, App.tsx:10877)
- WebShellSidebar.module.css:223 custom logo sizing under real CSS — already reported (round 2 deferral, WebShellSidebar.module.css:214)
- StatusMessage.tsx:50 About-panel 20ch label column — already reported (round 2 deferral, StatusMessage.tsx:50)
- web-shell-brand.ts:198 unconditional UTF-8 decode and charset-less data URI — already reported (round 2 deferral, web-shell-brand.ts:236)
- App.tsx:1083 callback re-fire against the published change-notification contract — already reported (round 2 deferral, App.tsx:1083)
- web-shell-brand.test.ts:539 per-layer dir scoping exercised only on the user layer — already reported (round 2 deferral, web-shell-brand.test.ts:381)
Not reviewed: build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI and only the Linux suites ran locally, so the platform-sensitive hardened logo read path (O_NOFOLLOW / O_NONBLOCK guards, symlink and hard-link refusals) was not exercised on those platforms.
Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": the design doc's "Testing" section (diff lines ~365-400) makes ~40 per-test assertions ("the route test asserts … that a settings failure degrades to an empty b…; "agent reverse-audit (round 1)": the production value of waitForDeferredRuntimeRoutes at the createDelegatingServeApp(bootstrapApp, () => runtimeApp, {...}) call ( run-qwen-serve.ts:7933 ),…; "agent reverse-audit (round 1)": none — every check above ran to completion.; "agent reverse-audit (round 1)": did not trace the qwen serve dispatch far enough to prove llm.tsx:902's new SettingsWatcher(settings) block is unreachable from the daemon, so the doc's abs…; "agent reverse-audit (round 1)": did not verify the banner-alignment bullet "It rejects an oversized logo where the banner reader truncates it" against customBanner.ts's oversize path — only M…, and 14 more.
Not reviewed: reverse audit — stopped before round 4 by the review time budget.
Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round:
docs/design/web-shell-brand-customization.md:114 — [probe] The divergence inventory omits that brand expands ~ while…docs/design/web-shell-brand-customization.md:247 — [probe] The doc states a settle-and-invalidate symmetry between a…docs/developers/qwen-serve-protocol.md:1161 — [probe] The published layer order reads highest-first but the…packages/cli/src/config/settingsSchema.ts:921 — [probe] ui.brand is not in WORKSPACE_RESTRICTED_SETTINGS, so a…packages/cli/src/services/web-shell-brand.ts:303 — [probe] A ] inside a comment within a DOCTYPE internal subset ends…packages/web-shell/client/App.test.tsx:35313 — [probe] toEqual cannot see the payload's deliberate key omission,…packages/web-shell/client/App.tsx:16329 — [probe] No test observes the resolved brand reaching the rendered…packages/web-shell/client/brandContext.ts:45 — [probe] An inline host brand prop churns the context value,…packages/web-shell/client/brandContext.ts:57 — [probe] A host forwarding a whitespace-only brand name blanks the…packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx:121 — [probe] The provider's commonest outcome, 200 with an empty body,…packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx:327 — [probe] The brand effect has no in-flight dedup, so StrictMode…packages/web-shell/client/main.tsx:52 — [probe] The cache eviction guard is unpinned, and dropping it…packages/web-shell/client/main.tsx:58 — [probe] storeBrand's storage-failure catch is unwitnessed, and…
Convergence: round 3 posted 9 inline comment(s), 8 of them reported for the first time; the previous round posted 18 (14 new). Findings keep coming back to the same files: packages/cli/src/services/web-shell-brand.ts (findings in rounds 1, 2; 3 more now); packages/cli/src/serve/server.test.ts (findings in rounds 1, 2; 1 more now); packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx (findings in round 1; 1 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.)
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 6 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查(原文为英文):build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI and only the Linux suites ran locally, so the platform-sensitive hardened logo read path (O_NOFOLLOW / O_NONBLOCK guards, symlink and hard-link refusals) was not exercised on those platforms.
未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)":the design doc's "Testing" section (diff lines ~365-400) makes ~40 per-test assertions ("the route test asserts … that a settings failure degrades to an empty b…;"agent reverse-audit (round 1)":the production value of waitForDeferredRuntimeRoutes at the createDelegatingServeApp(bootstrapApp, () => runtimeApp, {...}) call ( run-qwen-serve.ts:7933 ),…;"agent reverse-audit (round 1)":none — every check above ran to completion.;"agent reverse-audit (round 1)":did not trace the qwen serve dispatch far enough to prove llm.tsx:902's new SettingsWatcher(settings) block is unreachable from the daemon, so the doc's abs…;"agent reverse-audit (round 1)":did not verify the banner-alignment bullet "It rejects an oversized logo where the banner reader truncates it" against customBanner.ts's oversize path — only M…,另有 14 条。
未审查:反向审计——评审时间预算不足,未能开始第 4 轮。
收敛姿态下延后(第 3 轮,非阻断)——已记录,本轮不要求修改:共 13 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 3 轮发布了 9 条行内评论,其中 8 条是首次提出;上一轮发布了 18 条(其中 14 条首次提出)。发现反复回到同一批文件:packages/cli/src/services/web-shell-brand.ts(第 1、2 轮已出过发现,本轮又有 3 条);packages/cli/src/serve/server.test.ts(第 1、2 轮已出过发现,本轮又有 1 条);packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx(第 1 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.23.0)
…n, scanner correctness, retry path - Read brand keys from the pre-substitution snapshot and refuse environment placeholders with a warning: placeholder substitution draws from the process-wide environment, which a workspace populates first at boot, so resolving brand placeholders would let a repository supply the value — the workspace exclusion through a side door (review Critical) - Blank quoted SVG attribute spans per UTF-16 code unit, not per code point: an astral character collapsed to one space and shifted every later index, refusing valid namespaced documents (review Critical) - Accept prefix-bound <svg:svg> roots that bind xmlns:svg to the SVG namespace (browsers render them), keep refusing <svgfoo> near-misses and prefix-only bindings on an unprefixed root, and pin the whitespace-prefixed fake-xmlns shape that actually exercises the quote-blanking - Make the scaling-geometry advisory value-aware: an empty viewBox, zero dimensions, or percentage width/height now warns (loads successfully but cannot scale into the sidebar box) - Add refreshBrand beside refreshCapabilities — same 404-only settle rule, generation-guarded, gated to the genuinely-missing state and no in-flight fetch — and call it from the connection recovery path, so a retryable brand failure no longer leaves in-app chrome and tab chrome disagreeing for the page's lifetime - Warn on console when the sidebar logo fails to decode (the one logo failure the daemon's root-tag validation cannot see) - Pin the O_NONBLOCK FIFO defence with a real mkfifo fixture, the published 429/drain transport states on the real app, and make the ordering test's body discriminating with a fixture brand - Document the placeholder carve-out, prefix-bound roots and the value-aware advisory across settings docs, schema, protocol reference and the design record
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- App.tsx:1084 onBrandResolved re-fires with a byte-identical payload on a brandResolved false->true transition — already reported (round 2 deferral, App.tsx:1083)
Not reviewed: reverse audit — stopped before round 5: rounds 1, 2, 3 and 4 each reported findings, so the loop had not converged, and the orchestrator's remaining budget could not cover another eleven-chunk fan-out plus its verification without risking compose and submit.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) and Integration Tests (no-AK, No Sandbox) were skipped in CI and their suites did not run locally, so the one changed integration-test file (integration-tests/cli/qwen-serve-routes.test.ts) was never executed.
Not explored to full depth (tool budget reached): "agent reverse-audit (round 4)": did not independently open the test files behind the design-doc Testing section's client-side claims ( index.html boot-watchdog, main.tsx standalone-entry, s…; chunk 2: the design doc's client-side Testing claims (provider settled-flag/404-vs-503 cases, app precedence cases, sidebar fallback cases, boot-watchdog favicon case, s…; chunk 2: the protocol doc's rationale clause that the daemon's process-wide environment is populated "first at boot" from the workspace .qwen/.env was not traced to a …; chunk 2: the index.html table row's second half ("boot watchdog excludes the icon from its fatal-resource classification and its error list") was not verified in the f…; "agent reverse-audit (round 2)": none — but two candidates I probed and dropped, so the drop is on the record: (1) the encodeURI encoding gap, killed by serve/routes/brand.test.ts:93 going …, and 12 more.
Deferred under the convergence posture (round 4, not a blocker) — recorded, not requested in this round:
docs/design/web-shell-brand-customization.md:232 — [probe] Design record misdescribes when the pre-paint brand cache…docs/design/web-shell-brand-customization.md:254 — [probe] A daemon-side settings failure is answered as a definitive…packages/cli/src/serve/server.test.ts:4519 — [probe] Nothing pins that GET /brand is registered outside the…packages/cli/src/services/web-shell-brand.test.ts:140 — [probe] The 80-character clamp can publish a lone surrogatepackages/cli/src/services/web-shell-brand.test.ts:143 — [probe] 80-character cap direction is unpinned (homogeneous fixture)packages/cli/src/services/web-shell-brand.test.ts:616 — [probe] Seven of the eight fd-identity legs are individually…packages/cli/src/services/web-shell-brand.test.ts:624 — [probe] The fd close and its inner swallow are unwitnessedpackages/cli/src/services/web-shell-brand.test.ts:674 — [probe] The read-error soft-fail branch inside…packages/cli/src/services/web-shell-brand.ts:172 — [probe] The name-truncation notice never reaches the…packages/cli/src/services/web-shell-brand.ts:237 — [probe] A non-UTF-8 SVG is refused with a diagnostic naming the…packages/web-shell/client/components/sidebar/WebShellSidebar.tsx:5514 — [probe] The host-node vs data-URI logo precedence has no testpackages/web-shell/client/components/sidebar/WebShellSidebar.tsx:5527 — [probe] Ellipsized brand name has no title attributepackages/web-shell/client/components/sidebar/WebShellSidebar.tsx:5527 — [probe] The sidebar paints the vendor brand for 2-3 frames on…packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx:360 — [probe] The 404-only settle predicate is pinned against 503 alone
Convergence: round 4 posted 20 inline comment(s), 19 of them reported for the first time; the previous round posted 9 (8 new). Findings keep coming back to the same files: packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx (findings in round 3; 5 more now); packages/cli/src/services/web-shell-brand.ts (findings in rounds 2, 3; 4 more now); packages/cli/src/services/web-shell-brand.test.ts (findings in round 3; 2 more now), and 1 more file(s). The rate of new findings is not falling. 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. Batching the remaining fixes and verifying them before the next push, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查(原文为英文):reverse audit — stopped before round 5: rounds 1, 2, 3 and 4 each reported findings, so the loop had not converged, and the orchestrator's remaining budget could not cover another eleven-chunk fan-out plus its verification without risking compose and submit.
未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) and Integration Tests (no-AK, No Sandbox) were skipped in CI and their suites did not run locally, so the one changed integration-test file (integration-tests/cli/qwen-serve-routes.test.ts) was never executed.
未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 4)":did not independently open the test files behind the design-doc Testing section's client-side claims ( index.html boot-watchdog, main.tsx standalone-entry, s…;chunk 2:the design doc's client-side Testing claims (provider settled-flag/404-vs-503 cases, app precedence cases, sidebar fallback cases, boot-watchdog favicon case, s…;chunk 2:the protocol doc's rationale clause that the daemon's process-wide environment is populated "first at boot" from the workspace .qwen/.env was not traced to a …;chunk 2:the index.html table row's second half ("boot watchdog excludes the icon from its fatal-resource classification and its error list") was not verified in the f…;"agent reverse-audit (round 2)":none — but two candidates I probed and dropped, so the drop is on the record: (1) the encodeURI encoding gap, killed by serve/routes/brand.test.ts:93 going …,另有 12 条。
收敛姿态下延后(第 4 轮,非阻断)——已记录,本轮不要求修改:共 14 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 4 轮发布了 20 条行内评论,其中 19 条是首次提出;上一轮发布了 9 条(其中 8 条首次提出)。发现反复回到同一批文件:packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx(第 3 轮已出过发现,本轮又有 5 条);packages/cli/src/services/web-shell-brand.ts(第 2、3 轮已出过发现,本轮又有 4 条);packages/cli/src/services/web-shell-brand.test.ts(第 3 轮已出过发现,本轮又有 2 条),另有 1 个文件。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,或将本 PR 的评审降到 --severity-floor critical,可以避免循环反复推导同一组发现。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.23.0)
# Conflicts: # docs/developers/daemon/00-index.md
…retry machinery, scanner class fix - Add QWEN_CODE_SYSTEM_SETTINGS_PATH / QWEN_CODE_SYSTEM_DEFAULTS_PATH to the hardcoded project-.env exclusions: they select which file becomes the System settings layer, so a repository could otherwise promote its own file above the operator's User layer and rebrand the shell with literal values (review Critical) - Refuse brand placeholders on substitution, not syntax: run the real resolveEnvVarsInString and refuse only when it would change the value, so a literal name containing `$` survives while anything a workspace-tainted environment could have supplied is refused - Emit one stderr line per brand warning instead of a joined line, so a log rule keyed on one key's prefix keeps firing when another misconfiguration exists - Make the SVG root matcher prefix-agnostic and entity-aware: any prefix bound to the SVG namespace qualifies, character references in attribute values are decoded before comparison, a degenerate viewBox no longer suppresses the scaling advisory, and a prefix-bound root with unprefixed children gets its own advisory - Retry a retryable brand failure once after a short delay and warn only when the retry is also refused (404 stays silent); wire refreshBrand into the recovery path with tests; gate it on in-flight; attribute retries per client instance; reset the brand in the render that observes a client change so no committed frame mixes two connections - Pin the O_NONBLOCK flag with a flags-capture witness (the FIFO case alone cannot fail fast: a blocked open wedges the worker's event loop), skip the FIFO fixture on Windows where the defence does not exist, and mock the not-stattable path instead of relying on POSIX ENOTDIR - Document the startup hold vs terminal 503 semantics correctly and retire the design record's pre-retry narrative
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
3 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- route-level one-line-per-cause stderr pin at packages/cli/src/serve/routes/brand.test.ts:132 — already reported as R4-6 (comment 3961314792)
- design-doc "once per connection" residual at docs/design/web-shell-brand-customization.md:185 — already reported as R4-8 (comment 3961314806)
- ambient-env non-hermeticity of the literal-dollar fixtures at packages/cli/src/services/web-shell-brand.test.ts:626 — already reported as R4-12
Not reviewed: build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI and only the Linux suites ran locally, so the platform-sensitive hardened logo read path (O_NOFOLLOW / O_NONBLOCK guards, symlink, hard-link and FIFO refusals) was not exercised on those platforms.
Not explored to full depth (tool budget reached): "agent reverse-audit (round 2)": did not verify, claim by claim, the ~40 test-existence assertions in the design-doc ## Testing inventory that opens this chunk (diff lines ~380–470) against t…; "agent reverse-audit (round 2)": settling whether the Web Shell auto-fetches /workspace/permissions or /workspace/models on connect (the check that decides whether Finding 1's corruption-re…; "agent reverse-audit (round 1)": did not read the render sites of WelcomeHeader.tsx:16 and StatusMessage.tsx:44 to confirm the brand name lands in the welcome-header title and the About pan…; "agent reverse-audit (round 1)": did not verify "the daemon's static server exposes only /assets/* and / " against serve/web-shell-static.ts 's route table.; "agent reverse-audit (round 1)": did not read resolvePath to confirm it expands only a leading ~ as the doc claims., and 5 more.
Not reviewed: reverse audit — stopped before round 3 by the review time budget.
Deferred under the convergence posture (round 5, not a blocker) — recorded, not requested in this round:
docs/design/web-shell-brand-customization.md:266 — [review] Withdrawal guarantee is false while the brand fetch is unsettledpackages/web-shell/client/App.tsx:12019 — [review] A brand-only failure on a healthy connection has no re-ask path, and the comment misstates the retry modelpackages/web-shell/client/App.tsx:11094 — [probe] A host that attaches onBrandResolved after resolution is never calledpackages/web-shell/client/App.tsx:16347 — [probe] The BrandProvider wire is untested on both sides, so EMPTY_BRAND would ship greenpackages/cli/src/services/web-shell-brand.test.ts:701 — [probe] The placeholder-masking test never sets the variable, so the branch it names is unreachablepackages/cli/src/services/web-shell-brand.ts:74 — [probe] A name that sanitizes to empty is dropped with no warning, unlike every other refusal pathpackages/cli/src/services/web-shell-brand.ts:316 — [probe] A non-UTF-8 logo is decoded lossily and shipped as mojibake with no warningpackages/cli/src/services/web-shell-brand.ts:236 — [probe] The open/read soft-fail branch is untested, and deleting its catch would drop the name toopackages/web-shell/README.md:348 — [probe] The published two-value re-fire trigger has only its name leg pinnedpackages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx:433 — [probe] The retry leg's bound and its 404 branch ordering are both unpinned; two mutants survivepackages/web-shell/README.md:319 — [probe] The new branding heading splits the WebShell section and reassigns two unrelated prop notespackages/web-shell/client/components/messages/StatusMessage.tsx:50 — [probe] The About version-row label now carries up to 80 chars into a fixed 20ch nowrap trackpackages/web-shell/client/components/messages/StatusMessage.test.tsx:72 — [probe] The unbranded-auth-label test asserts its own fixture value, so it cannot fail on the regression it guardspackages/web-shell/client/main.tsx:52 — [probe] The logo-only brand shape is untested, leaving the cache-clear guard's second leg unpinnedpackages/web-shell/client/index.html:64 — [probe] The pre-paint script lacks the inner storage catch its sibling theme script haspackages/web-shell/client/components/sidebar/WebShellSidebar.tsx:5541 — [probe] A host passing a string logo renders the raw path as sidebar text and suppresses the built-in mark
Convergence: round 5 posted 11 inline comment(s), 7 of them reported for the first time; the previous round posted 20 (19 new). Findings keep coming back to the same files: docs/developers/qwen-serve-protocol.md (findings in round 4; 3 more now); packages/cli/src/services/web-shell-brand.ts (findings in round 4; 3 more now); packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx (findings in round 4; 1 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.)
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 3 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查(原文为英文):build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI and only the Linux suites ran locally, so the platform-sensitive hardened logo read path (O_NOFOLLOW / O_NONBLOCK guards, symlink, hard-link and FIFO refusals) was not exercised on those platforms.
未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 2)":did not verify, claim by claim, the ~40 test-existence assertions in the design-doc ## Testing inventory that opens this chunk (diff lines ~380–470) against t…;"agent reverse-audit (round 2)":settling whether the Web Shell auto-fetches /workspace/permissions or /workspace/models on connect (the check that decides whether Finding 1's corruption-re…;"agent reverse-audit (round 1)":did not read the render sites of WelcomeHeader.tsx:16 and StatusMessage.tsx:44 to confirm the brand name lands in the welcome-header title and the About pan…;"agent reverse-audit (round 1)":did not verify "the daemon's static server exposes only /assets/* and / " against serve/web-shell-static.ts 's route table.;"agent reverse-audit (round 1)":did not read resolvePath to confirm it expands only a leading ~ as the doc claims.,另有 5 条。
未审查:反向审计——评审时间预算不足,未能开始第 3 轮。
收敛姿态下延后(第 5 轮,非阻断)——已记录,本轮不要求修改:共 16 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 5 轮发布了 11 条行内评论,其中 7 条是首次提出;上一轮发布了 20 条(其中 19 条首次提出)。发现反复回到同一批文件:docs/developers/qwen-serve-protocol.md(第 4 轮已出过发现,本轮又有 3 条);packages/cli/src/services/web-shell-brand.ts(第 4 轮已出过发现,本轮又有 3 条);packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx(第 4 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.23.1)
# Conflicts: # packages/web-shell/client/main.tsx
…s alignment, hold test
- Parse the logo with the vendored fast-xml-parser instead of the
hand-rolled root-tag scanner: well-formedness (duplicate attributes,
misplaced declarations) is now rejected by a real validator, the
namespace check reads parsed attributes with entity references
decoded, any prefix bound to the SVG namespace qualifies, and the
prefix-children advisory walks the parsed tree — construct-aware by
construction. The DOCTYPE is stripped for the validator with a
quote- and subset-aware scan because the validator's own DOCTYPE
handling is quote-blind (review Suggestion, filed as the class-level
finding)
- Bound entity decoding at Unicode's ceiling: an out-of-range
character reference no longer throws a RangeError out of the
resolver — which the route's catch-all turned into a 200 {} that
dropped the validly configured brand NAME along with the logo
(review Critical)
- Refuse brand placeholders by comparing the layer's substituted value
against its pre-substitution snapshot — the authoritative engine
WITH the home-.env fallback — and keep an unresolvable placeholder
verbatim so a typo'd variable shows as literal text; say so in all
five published artifacts, and hint when a missing logo's path still
contains a placeholder token (review Critical)
- Gate the prefix-children advisory on the default namespace being
absent: with both bindings, unprefixed children render fine
- Split the scaling advisory's message by cause (zero-area viewport,
malformed viewBox, no usable geometry) and document both advisories
- Pin the startup-hold guarantee with a real deferred-runtime test:
GET /brand issued before the runtime is ready is held and answered
200 once it settles
- Drive the two retry tests on fake timers (BRAND_RETRY_DELAY_MS is
exported for them) instead of sleeping through the production
constant, and pin the route's one-stderr-line-per-cause invariant
with a two-misconfiguration case
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
7 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- BrandProvider wire untested at packages/web-shell/client/App.tsx:16469 — already recorded as a round-5 deferral (review 5150601453, App.tsx:16347)
- Prop-withdrawal settle guarantee at docs/design/web-shell-brand-customization.md:268 — already recorded as a round-5 deferral (review 5150601453, :266)
- Open/read soft-fail branch untested, with its closeSync and O_NOFOLLOW legs, at packages/cli/src/services/web-shell-brand.test.ts:937 — already recorded as a round-5 deferral (review 5150601453, web-shell-brand.ts:236)
- Late-attached onBrandResolved never called at packages/web-shell/client/App.tsx:11103 — already recorded as a round-5 deferral (review 5150601453, App.tsx:11094)
- About version-row label in a fixed 20ch nowrap track at packages/web-shell/client/components/messages/StatusMessage.tsx:50 — already recorded as a round-5 deferral (review 5150601453, same anchor)
- Bounded-retry bound unpinned at packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx:526 — already recorded as a round-5 deferral (review 5150601453, :433)
- 404-branch ordering unpinned at packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx:434 — already recorded as a round-5 deferral (review 5150601453, :433)
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): "agent reverse-audit (round 3)": did not verify that resolvePath actually expands a leading ~ (doc line 126) — only that readBrandLogo calls it before the absolute/relative split.; "agent reverse-audit (round 3)": did not verify "the daemon's static server exposes only /assets/* and / " (doc line 149) against the route table in serve/web-shell-static.ts .; "agent reverse-audit (round 3)": did not verify "It rejects an oversized logo where the banner reader truncates it" (doc line ~112) against customBanner.ts 's size handling.; "agent reverse-audit (round 3)": did not read the daemon's ACP-child spawn site, which is what would settle the Confidence: low finding above.; "agent reverse-audit (round 3)": did not count the "roughly forty localized strings" the doc places out of scope., and 14 more.
Not reviewed: reverse audit — stopped before round 4 by the review time budget.
Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round; 1 Critical(s) among them are deferred by their axes — fails-closed on new surface, where no wrong result is certified and the merge base had neither the surface nor the defect — and remain follow-up work recorded in the findings artifact:
packages/cli/src/services/web-shell-brand.ts:383 — [review] Critical [fails-closed] [new-surface] D6-1 Unguarded XMLParser.parse() throw drops the resolved brand name toodocs/design/web-shell-brand-customization.md:1 — [review] D6-2 New design record ships English-only, with no zh-CN counterpartdocs/design/web-shell-brand-customization.md:333 — [review] D6-3 Files-affected table and migration notes omit the process-wide env changedocs/design/web-shell-brand-customization.md:178 — [review] D6-4 Design record never mentions the web_shell_brand capability tag it registersdocs/design/web-shell-brand-customization.md:137 — [review] D6-5 Design record still describes a bespoke matcher, not the new parser dependencydocs/design/web-shell-brand-customization.md:197 — [review] D6-21 The design record's premise for shipping no brand live-reload is false under qwen servedocs/users/configuration/settings.md:32 — [review] D6-11 The two newly project-refused env vars are still documented as unqualified overridesdocs/developers/qwen-serve-protocol.md:1164 — [probe] R5-4 still stands (axis 2): the scaling-advisory conjunction is still wrong after b8f0eab455packages/cli/src/serve/run-qwen-serve.test.ts:9224 — [review] D6-6 Deferred-hold test reads the ambient machine's System settings layerpackages/cli/src/services/web-shell-brand.test.ts:292 — [probe] D6-7 The unresolvable-placeholder hint on logoPath has no testpackages/cli/src/services/web-shell-brand.test.ts:487 — [review] D6-12 The advisory's recursive descent has no witnesspackages/cli/src/services/web-shell-brand.test.ts:1015 — [review] D6-20 The relative-logoPath test cannot discriminate per-layer resolutionpackages/cli/src/services/web-shell-brand.test.ts:626 — [review] R4-12 still stands: the literal-dollar fixtures depend on the ambient environmentpackages/cli/src/services/web-shell-brand.ts:119 — [probe] D6-13 A non-string brand leaf falls back to the built-in brand with no warningpackages/cli/src/services/web-shell-brand.ts:139 — [review] D6-19 Nothing pins that the real loader produces the divergence this guard comparespackages/cli/src/services/web-shell-brand.ts:455 — [probe] R5-3 still stands: the prefix advisory asserts a namespace fact it never checkspackages/web-shell/client/App.tsx:1085 — [review] D6-22 onBrandResolved re-fires on a re-settle, contradicting its published contractpackages/web-shell/client/App.tsx:12094 — [review] D6-24 Two added comments deny the bounded retry this same PR ships (+ DaemonWorkspaceProvider.test.tsx:401)packages/web-shell/client/components/messages/StatusMessage.tsx:50 — [probe] D6-15 A whitespace-only host-supplied brand name renders a blank About labelpackages/web-shell/client/components/sidebar/WebShellSidebar.tsx:661 — [probe] D6-14 The added comment states a premise about the daemon this PR falsifies- …and 8 more (see the run report)
Convergence: round 6 posted 3 inline comment(s), 3 of them reported for the first time; the previous round posted 11 (7 new). Findings keep coming back to the same files: packages/cli/src/services/web-shell-brand.ts (findings in rounds 4, 5; 2 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.)
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 7 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 3)":did not verify that resolvePath actually expands a leading ~ (doc line 126) — only that readBrandLogo calls it before the absolute/relative split.;"agent reverse-audit (round 3)":did not verify "the daemon's static server exposes only /assets/* and / " (doc line 149) against the route table in serve/web-shell-static.ts .;"agent reverse-audit (round 3)":did not verify "It rejects an oversized logo where the banner reader truncates it" (doc line ~112) against customBanner.ts 's size handling.;"agent reverse-audit (round 3)":did not read the daemon's ACP-child spawn site, which is what would settle the Confidence: low finding above.;"agent reverse-audit (round 3)":did not count the "roughly forty localized strings" the doc places out of scope.,另有 14 条。
未审查:反向审计——评审时间预算不足,未能开始第 4 轮。
收敛姿态下延后(第 6 轮,非阻断)——已记录,本轮不要求修改;其中 1 条 Critical 按其失败方向与对照基线延后——fails-closed 且 new-surface:未认证任何错误结果,且 merge base 既无该功能面也无该缺陷——作为后续工作记录在 findings 工件中:共 28 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 6 轮发布了 3 条行内评论,其中 3 条是首次提出;上一轮发布了 11 条(其中 7 条首次提出)。发现反复回到同一批文件:packages/cli/src/services/web-shell-brand.ts(第 4、5 轮已出过发现,本轮又有 2 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.23.1)
Swap fast-xml-parser (subset validator plus pre-decoded attributes) for saxes, the streaming XML parser jsdom uses: malformed documents are now refused instead of shipped, attribute values are entity-decoded exactly once so the xmlns gate cannot be faked by a second decode, and the parse stream is closed after write so an unclosed root fails at EOF. Regenerate both lockfiles for the dependency swap — the round-5 fast-xml-parser edge was never recorded, which is what turned the frozen pnpm installs red — and pin the refused well-formedness classes and the double-decode fixture in the resolver tests. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and its suite did not run locally.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally, and the platform-sensitive O_NONBLOCK / O_NOFOLLOW guards deferred as D7-4 and D7-5 are exactly what that lane would have exercised.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally, and Agent 7's scoped npm test --workspace=packages/cli excludes integration-tests.
Not explored to full depth (tool budget reached): chunk 1: did not verify the doc's client-side retry claims against DaemonSessionProvider — "a retryable rejection is re-asked once after a short delay", "a failed bran…; chunk 1: did not verify "Nesting under ui … keeps the change out of the schema's asserted top-level key list" against the settings-schema test that asserts top-level k…; chunk 1: did not verify the precedence claim "the existing sidebar branding render override" wins over the brand prop inside WebShellSidebar.tsx (only that WebShellSi…; chunk 1: did not count the "roughly forty localized strings" the doc places out of scope, and could not re-run the doc's "Verified against a running daemon" observation..
Not reviewed: reverse audit — stopped before round 2 by the review time budget.
Deferred under the convergence posture (round 7, not a blocker) — recorded, not requested in this round:
packages/cli/src/services/web-shell-brand.ts:389 — [review] D7-1 parser error reason discarded — 13 well-formedness classes are all reported as "root element is not a namespaced <svg>"package-lock.json:27276 — [review] D7-2 the saxes regeneration also deleted all 16 "libc" platform constraints and added 11 "peer": true markers — 59 of 62 lines unrelatedpackages/cli/src/services/web-shell-brand.ts:388 — [review] D7-3 entities declared in the document's own DOCTYPE internal subset are newly refused, though the design doc added this round claims tolerancepackages/cli/src/services/web-shell-brand.test.ts:1035 — [review] D7-4 the O_NONBLOCK test returns silently instead of ctx.skip(), a false green on the Windows lanepackages/cli/src/services/web-shell-brand.test.ts:1044 — [review] D7-5 the O_NOFOLLOW flag bit has no assertion — deleting it keeps the suite at 88 passedpackages/cli/src/services/web-shell-brand.ts:462 — [review] D7-6 the fallback advisory says "has no viewBox or width/height" for four pinned fixtures that carry bothpackages/cli/src/services/web-shell-brand.ts:75 — [probe] D7-7 a configured brand name that sanitizes away is the one rejection that pushes no warning, so stderr stays empty
Convergence: round 7 posted 1 inline comment(s), 1 of them reported for the first time; the previous round posted 3 (3 new). Findings keep coming back to the same files: packages/cli/src/services/web-shell-brand.ts (findings in rounds 4, 6; 1 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 — Test (macos-latest, Node 22.x) was skipped in CI and its suite did not run locally.
未审查(原文为英文):build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally, and the platform-sensitive O_NONBLOCK / O_NOFOLLOW guards deferred as D7-4 and D7-5 are exactly what that lane would have exercised.
未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally, and Agent 7's scoped npm test --workspace=packages/cli excludes integration-tests.
未探索到全部深度(达到工具调用预算):chunk 1:did not verify the doc's client-side retry claims against DaemonSessionProvider — "a retryable rejection is re-asked once after a short delay", "a failed bran…;chunk 1:did not verify "Nesting under ui … keeps the change out of the schema's asserted top-level key list" against the settings-schema test that asserts top-level k…;chunk 1:did not verify the precedence claim "the existing sidebar branding render override" wins over the brand prop inside WebShellSidebar.tsx (only that WebShellSi…;chunk 1:did not count the "roughly forty localized strings" the doc places out of scope, and could not re-run the doc's "Verified against a running daemon" observation.。
未审查:反向审计——评审时间预算不足,未能开始第 2 轮。
收敛姿态下延后(第 7 轮,非阻断)——已记录,本轮不要求修改:共 7 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 7 轮发布了 1 条行内评论,其中 1 条是首次提出;上一轮发布了 3 条(其中 3 条首次提出)。发现反复回到同一批文件:packages/cli/src/services/web-shell-brand.ts(第 4、6 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.23.2)
saxes was constructed with default options, so an undeclared namespace prefix on a child (the classic legacy-export xlink:href with no xmlns:xlink) parsed clean here while browsers fatal on it — the daemon shipped a data URI no browser renders, with nothing on stderr. Enable xmlns: true, matching jsdom (browsers always have namespace support), flatten the object-shaped attribute values that mode returns, and pin both sides: undeclared child prefixes refuse, implicitly-bound xml: and properly declared bindings still accept. Mutation-checked: reverting the option and the flattening turns the new refusal test red. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
qqqys
left a comment
There was a problem hiding this comment.
Critical-only review at head 52fe16ef. No Critical is provable in what I read, and every recent blocking finding on this PR is verifiably fixed at this commit. This is a comment rather than an approval for one reason: the diff is ~6,400 lines across ~50 files and I did not read all of it in this run's budget, so the certification is incomplete — not because I found anything.
Previously blocking findings — verified fixed in the code at this head
Read packages/cli/src/services/web-shell-brand.ts as it stands at 52fe16ef, not the thread flags:
- R4-2 (round 7): namespace well-formedness was not actually checked.
parseSvgRootnow constructsnew SaxesParser({ xmlns: true })(:392), matching the option jsdom passes for XML documents, and flattens the object-shaped attribute values that mode returns (Object.fromEntries(… attr.value),:404-407). An undeclared prefix on a child is therefore an error rather than a silent accept, and the doc comment at:369-378now states the namespace parity it has instead of overclaiming engine parity. - R4-2 (round 6): well-formedness was delegated to a subset validator. The hand-rolled matcher and
fast-xml-parser'sXMLValidatorare gone; saxes enforces well-formedness over the whole document, andparser.write(content).close()(:421) is load-bearing — the comment records that withoutclose()an unclosed root parses clean while a browser rejects it. The refusal isfailed || rootLocal !== 'svg' || rootAttrs === undefined(:425-427), and acatcharound the write setsfailed, so a throw is a refusal, not a crash. - R6-2: attribute values were entity-decoded twice, which could complete a fake SVG namespace.
decodeXmlEntitiesno longer exists anywhere in the file; saxes hands attribute values over decoded exactly once, and the namespace gate compares that single-decoded value —rootAttrs[binding] !== SVG_NAMESPACE(:432), withbindingcorrectly chosen asxmlnsorxmlns:<prefix>and a prefix binding on an unprefixed root explicitly not counting. - R6-1: a runtime dependency was added without regenerating either lockfile. The parser dependency is now
saxesand both lockfiles are in this diff (package-lock.json,pnpm-lock.yaml) alongsidepackages/cli/package.json;Install (ubuntu|macos|windows-latest)are all green at this head, which is the lane that was red on the frozen-lockfile mismatch.
The surrounding controls in the same file are consistent with the threat model the code states: a 32 KiB cap checked both on stat and again on decoded bytes (:19, :230, :329), an 80-character name cap mirroring the TUI (:22, :167-171), path resolution through resolvePath plus the declaring settings file's directory (:180-190), FD identity re-verification after the lstat (:313), refusal of environment-placeholder values with an operator-visible warning (:135-143), and the explicit note that the client renders the logo as an img src data URI and must not be switched to inline markup without adding a sanitizer here first (:364-367).
Current scan
No blocking defect is provable in the resolver or its route. scalingIssueOf (:445-471) is advisory-only and cannot refuse a document, so its loose SVG grammar re-implementation cannot cost an operator a working logo. The round-8 review posted at this same commit returned zero findings under a Critical posting floor, its only Critical-class item being the deferred fails-closed one (DOCTYPE-declared entities are refused, so some design-tool exports are rejected) — a refusal with an operator-visible warning, not a wrong result. One thread remains open and it is Suggestion-level: no test drives the resolved brand through App to a rendered consumer.
CI at this head is green across the board — unit tests, lint and static checks, integration tests, Serve A/B, the web-shell visual capture, web-shell E2E Smoke, both Desktop Shell jobs and the live-host lane. Nothing is pending or failing.
What I did not read, and why this is not an approval
The web-shell client render path (App.tsx, brandContext.ts, WelcomeHeader.tsx, StatusMessage.tsx, WebShellSidebar.tsx, DaemonWorkspaceProvider.tsx, index.html, main.tsx, vite.config.ts), the SDK surface (DaemonClient.ts, daemon/types.ts, the export barrels), settingsSchema.ts and shared-env-keys.ts, serve/capabilities.ts and server.ts, the four documentation files, and the ~2,600 lines of tests. Approving means certifying the whole current diff, and I would be doing that on the strength of the resolver half alone.
Next step
No code change is requested — the blocking history is closed and the review at this head found nothing. Either a maintainer reads the client/SDK/settings half and lands it, or re-request this route and the remaining surface can be covered in a pass scoped to it.
yiliang114
left a comment
There was a problem hiding this comment.
Approving. qqqys's Critical-only pass at this head verifies every recent blocking finding fixed in the code, not by thread flag: the SVG parser is now SaxesParser({ xmlns: true }) with write().close() load-bearing so unclosed roots and undeclared prefixes are refusals rather than silent accepts, and the double entity-decode is gone. The security invariants I care about hold: workspace settings are never read (a repo can't rebrand the shell), placeholder-bearing values are ignored pre-substitution, and the custom logo is only ever rendered through an image src (no script execution) with both sides pinned by tests.
CI is fully green (Lint, Test, web-shell E2E, Capture visuals, no-AK integration, Desktop Shell, Install). Same note as #11003: this is ~6,400 lines / ~50 files and neither qqqys nor I read every line in one pass — worth one maintainer end-to-end read before squash, not a block on this head.
Stale — filed against a prior head. Blocking findings are closed at 52fe16e (qqqys Critical-only verification + yiliang114 approval on the current head).
…ain CI (QwenLM#11525) (QwenLM#11530) The brand PR QwenLM#11244 landed on top of QwenLM#11470 and left two web-shell suites failing on main, redding the Test job: - The sidebar brand tests assert the version tooltip's title while mounting at the default 260px width, but QwenLM#11470 hides the version label below the 344px compact footer breakpoint, so the query finds nothing. Mount those brand tests at 360px, matching the convention of the footer-version suite. - The workspace-session loading test pins the exact daemon requests issued on load, but the workspace provider now fetches GET /brand beside capabilities. Answer it with a 404 (an older daemon without the route, which settles the fetch instead of arming the retry timer) and expect the call, including the StrictMode remount double. Both are test-environment alignments; no production behavior changes. Fixes QwenLM#11525 Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com> Co-authored-by: Shaojin Wen <szujobs@gmail.com> Co-authored-by: 易良 <1204183885@qq.com>
Resolve the web-shell README props-table conflict: keep main's brand and onBrandResolved rows (#11244) and append this PR's onAssistantTurnSettled row, then let prettier re-pad the table. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-conflict/jmtva7llqva
|
Released in v0.23.3. |


What this PR does
Makes the Web Shell's product name and logo configurable instead of hardcoded, so a deployment can white-label the shell without editing source and rebuilding. Two channels: an operator sets a name and a path to an SVG in
settings.json, and the daemon resolves that into the branding the browser renders; an embedding host can instead pass a brand as a component prop, which replaces the resolved value outright. The name drives the sidebar brand row, the sidebar's version tooltip, the welcome header title, the About panel's version row label, and the browser tab title; the logo drives the sidebar mark and the favicon. The terminal banner already had its own white-label settings, so this follows their sanitization and path-resolution conventions rather than inventing parallel ones — one deployment's branding reads the same on both surfaces.An installation that configures nothing renders exactly what it renders today. The resolved brand is empty, every consumer falls back to its own built-in literal, and the served document keeps its original title and favicon bytes, so the existing visual baselines still pass without re-capture.
Why it's needed
Renaming the shell meant editing six source files and rebuilding, and the logo existed as two hand-duplicated copies of the same inline SVG that had to be kept in sync by hand. Anyone deploying this behind their own brand had to carry a fork.
Two decisions carry the design, and both are about who controls what a browser renders. The workspace settings layer is never read — the route does not even load it from disk — because a workspace settings file usually arrives from a repository that the person opening the shell did not write, so it must not be able to rename the product or name a file for the daemon to read and inline into every connected browser. The same boundary covers environment placeholders: brand keys are read from each layer's pre-substitution snapshot, and a value that still contains
$VAR/${VAR}is ignored with a warning, because placeholder substitution draws from the process-wide environment a workspace populates first at boot. And a custom logo is only ever rendered as an image, never injected as markup: SVG loaded through an image context cannot execute script, injected SVG can, and that invariant is what makes it safe for the daemon to skip sanitizing the artwork it read. Both properties are pinned by tests that fail if the invariant is broken.The tab title and favicon are applied before first paint from a cached value, mirroring how the theme already avoids a flash, so a renamed deployment does not show the built-in name on every load. This needed no server-side rewriting of the served document: the static handler still sends the file untouched.
Reviewer Test Plan
How to verify
Build, then start a daemon with a brand configured and check both the wire contract and the rendered shell:
Then confirm:
curl -s localhost:8731/brandreturns the name plus adata:image/svg+xml,…logo. Note the relativelogoPathresolved against the directory of the settings file that declared it.curl -s -H 'Accept: text/html' -o /dev/null -w '%{content_type}\n' localhost:8731/brandstill returnsapplication/json— the route sits ahead of the single-page-app fallback, so a browser-likeAcceptcannot coax the HTML shell out of it.curl -s localhost:8731/capabilitiesadvertises aweb_shell_brandfeature tag, so a host can preflight instead of relying on a 404 from an older daemon.curl -s localhost:8731/still serves the built-in<title>Qwen Code Web chat</title>and the original favicon bytes even though the brand is configured — the document is not rewritten server-side; the client applies branding and caches it for the next load's pre-paint script..qwen/settings.jsoninside the directory the daemon was started from, with folder trust disabled so that workspace is trusted. The response is unchanged: a repository cannot rebrand the shell. This is the security-relevant case.For the embedded-host channel, pass a brand prop with a name and a logo node and confirm it replaces the daemon-resolved value, that the shell never writes the host page's title or favicon itself, and that an inline prop object plus an inline callback do not re-fire the resolution callback on every render.
Evidence (Before & After)
No screenshots were captured for this PR; the evidence below is from the wire and from the test suites.
Before (global
qwen0.23.0, feature absent):GET /brand→ 404 forAccept: */*andapplication/json, but 200 with the HTML shell forAccept: text/htmlbecause the fallback claimed it;GET /capabilitieshad no brand field; aui.brandblock insettings.jsonproduced no observable effect and no warning.After (this branch, local build):
GET /brand→{"name":"QiuQiu Code","logoDataUri":"data:image/svg+xml,%3Csvg%20xmlns%3D…"}for everyAcceptheader;web_shell_brandadvertised betweenworkspace_skill_manageandworkspace_settings;GET /still serving the built-in title with the favicon href's sha256 byte-identical tomain; a trusted workspace's brand ignored in favour of the user layer; each rejection degrading to name-only with an operator-visible stderr line such asqwen serve: GET /brand: ui.brand.logoPath does not exist: /tmp/nope-missing.svg.The default-path guarantee is checked three ways: the served
index.htmldiffers frommainby the added pre-paint block plus two narrowing changes to the boot watchdog (its fatal-resource classification now excludes the favicon, which can never be load-bearing for boot, and its panel error list no longer records a content-free failure from the icon), with the favicon href's sha256 unchanged; a jsdom harness executes the real built document's inline scripts in parse order and confirms a first-ever load keeps the built-in title and logo while a cached brand applies both; and all 45 Playwright visual baselines pass without re-capture, which is the pixel-level proof that an unconfigured shell looks exactly as it did.Test suites on the rebased branch: web-shell 282 files / 6400 tests, TypeScript SDK 38 files / 1820 tests, the CLI capability-registry cases, and the CLI brand/schema/settings cases all pass; build, bundle,
tsc --noEmitfor all three packages, Prettier and ESLint are clean. 50 Playwright smoke specs pass against the mocked daemon in a real browser.One CI failure on the first run was this branch's own, and is fixed in a follow-up commit: the daemon developer index states how many capability tags the registry holds, a contract test enforces the agreement, and adding the new tag moved the registry to 158 while the index still said 157. That was the only failing test in CI's unit-test job. The same contract caught the count again after a later merge brought in an upstream tag; the index now reads 159. A separate integration gate timed out at its 20-minute step limit with zero test failures — 13 of 23 files complete, all passing, including this branch's own capability-ordering assertion — and the same timeout has hit unrelated branches, so it reflects that gate's budget rather than this change. A web-shell browser smoke job also failed to start its dev server with
EMFILE: too many open fileson the runner, which is environmental.Locally the CLI package shows a set of red tests that do not reproduce in CI and are not from this change — most visibly a scheduled-tasks spec requesting a route that appears seven times in the test and zero times in the route source. Every affected file is byte-identical to
main, and a base-tree measurement over the same selection found no net-new failing file or test name on this branch; they are load-sensitive timeouts on a busy machine.Tested on
Environment
Local runtime:
npm run build && npm run bundle, thennode dist/cli.js serveon loopback with a temporaryQWEN_HOMEper case. Browser-side checks via the package's Playwright harness (smoke and visual configs) and a jsdom harness over the built document.Risk & Scope
srcwith no script node present. Anyone changing the sidebar to inline the logo must add a sanitizer first, and those tests are written to fail loudly if they do not.O_NOFOLLOW) is only verified on macOS and by unit test elsewhere. The roughly forty localized strings that mention the product inside longer prose still say the built-in name — covering them needs a message-override mechanism the localization provider does not have, so a renamed shell still reads "Qwen Code" in body copy. The auth provider label still reads "Qwen OAuth" on purpose: that names the identity provider, not the product, and renaming it would misdescribe the flow to the person about to grant access. Terms-of-service, documentation and bug-report links still point at the real resources. The terminal CLI is unaffected and keeps its own separate banner settings.Linked Issues
None.
中文说明
这个 PR 做了什么
让 Web Shell 的产品名称与 Logo 变成可配置项而不再是硬编码,于是部署方可以做白标而无需改源码重新构建。两条通道:运维在
settings.json里填名称和一个 SVG 路径,daemon 把它解析成浏览器要渲染的品牌;嵌入式宿主也可以改为通过组件 prop 传入品牌,此时整体取代解析出来的值。名称驱动侧边栏品牌行、侧边栏版本 tooltip、欢迎页标题、About 面板的版本行标签以及浏览器标签页标题;Logo 驱动侧边栏标记和 favicon。终端 banner 早已有自己的白标设置,因此这里沿用它的清洗与路径解析约定,而不是另造一套 —— 同一个部署在两个界面上的品牌表现一致。未做任何配置的安装,渲染结果与今天完全相同。解析出的品牌为空,每个消费方回退到各自的内置字面量,服务端返回的文档保持原有的标题和 favicon 字节,因此现有视觉基线无需重录即通过。
为什么需要
改名过去意味着修改六个源文件并重新构建,而 Logo 以两份手工复制的相同内联 SVG 存在,必须靠人工保持同步。任何想把它放在自己品牌下部署的人都得长期维护一个 fork。
设计里有两条决定是承重的,都与「谁能控制浏览器渲染什么」有关。工作区设置层完全不被读取 —— 该路由甚至不会把它从磁盘加载进来 —— 因为工作区的设置文件通常来自一个打开 shell 的人并未撰写的仓库,所以它不能改名产品,也不能指定一个文件让 daemon 读取并内联进每一个连上的浏览器。同一条边界也覆盖环境变量占位符:品牌键读取的是各层替换前的快照,仍含
$VAR/${VAR}的值会被忽略并给出告警,因为占位符替换取自进程级环境,而工作区在启动时优先填充它。而自定义 Logo 只作为图片渲染,绝不注入为 markup:通过图片上下文加载的 SVG 不能执行脚本,注入的可以,正是这条不变量让 daemon 可以不对读到的素材做净化。两条性质都由测试钉住,破坏不变量即失败。标签页标题和 favicon 在首绘前由缓存值应用,沿用主题已有的避免闪烁机制,因此改名的部署不会每次加载都闪一下内置名称。这不需要服务端改写返回的文档:静态处理器仍然原样发送文件。
审阅测试计划
如何验证
构建后,用一个配置了品牌的 daemon 启动,同时检查线上契约与渲染结果:
然后确认:
curl -s localhost:8731/brand返回名称加一个data:image/svg+xml,…的 Logo。注意相对的logoPath是按声明它的那个设置文件所在目录解析的。curl -s -H 'Accept: text/html' -o /dev/null -w '%{content_type}\n' localhost:8731/brand仍返回application/json—— 该路由位于单页应用 fallback 之前,所以浏览器式的Accept骗不出 HTML 外壳。curl -s localhost:8731/capabilities广告了web_shell_brand特性标签,宿主可以先探测,而不必依赖旧 daemon 返回的 404。curl -s localhost:8731/仍然返回内置的<title>Qwen Code Web chat</title>和原始 favicon 字节 —— 文档没有被服务端改写;品牌由客户端应用并缓存,供下次加载的首绘前脚本使用。.qwen/settings.json里写一个不同的品牌,并关闭文件夹信任使该工作区被视为受信任。响应不变:仓库无法给 shell 改品牌。这是安全相关的一例。嵌入式宿主通道:传入带名称和 Logo 节点的 brand prop,确认它取代 daemon 解析值、shell 自身从不改写宿主页面的标题或 favicon,且内联的 prop 对象加内联回调不会在每次渲染时重放解析回调。
证据(前后对比)
本 PR 未采集截图;以下证据来自线上接口与测试套件。
改前(全局
qwen0.23.0,功能不存在):GET /brand对Accept: */*与application/json返回 404,但对Accept: text/html返回 200 加 HTML 外壳,因为 fallback 认领了它;GET /capabilities没有品牌字段;settings.json里的ui.brand块不产生任何可观察效果,也没有告警。改后(本分支,本地构建):
GET /brand对每种Accept都返回{"name":"QiuQiu Code","logoDataUri":"data:image/svg+xml,%3Csvg%20xmlns%3D…"};web_shell_brand广告在workspace_skill_manage与workspace_settings之间;GET /仍返回内置标题,favicon href 的 sha256 与main逐字节相同;受信任工作区的品牌被忽略、采用用户层的值;每种拒绝都降级为仅名称,并给出运维可见的 stderr 行,例如qwen serve: GET /brand: ui.brand.logoPath does not exist: /tmp/nope-missing.svg。默认路径的保证用三种方式核验:服务端返回的
index.html相对main的差异是新增的首绘前代码块,加上对 boot watchdog 的两处收窄(其致命资源判定不再包含 favicon —— favicon 对启动永远不可能是 load-bearing —— 其面板错误列表也不再记录来自 icon 的无内容失败),favicon href 的 sha256 未变;一个 jsdom 装置按解析顺序执行真实构建产物里的内联脚本,确认首次加载保持内置标题与 Logo、而有缓存时两者都生效;45 个 Playwright 视觉基线全部通过且无需重录,这是未配置品牌的 shell 外观完全不变的像素级证明。rebase 后分支上的测试套件:web-shell 282 文件 / 6400 用例、TypeScript SDK 38 文件 / 1820 用例、CLI 的 capability registry 用例、CLI 的 brand/schema/settings 用例全部通过;三个包的构建、打包、
tsc --noEmit、Prettier 与 ESLint 均干净。50 个 Playwright smoke 用例在真实浏览器中针对 mock daemon 通过。首次 CI 运行中有一个失败是本分支自己的,已在后续提交修掉:daemon 开发者索引里写死了 registry 中 capability tag 的数量,且有契约测试强制两者一致;新增标签使 registry 变成 158,而索引仍写 157。这是 CI 单测 job 里唯一失败的用例。之后一次合并又带入了一个上游标签,同一契约再次拦住计数,索引现为 159。另一个集成门禁在 20 分钟的步骤上限超时、零测试失败 —— 23 个文件跑完 13 个且全部通过,其中包含本分支自己的 capability 排序断言 —— 同样的超时也发生在无关分支上,因此那反映的是该门禁的预算,不是本改动。还有一个 web-shell 浏览器 smoke job 因 runner 上
EMFILE: too many open files而没能启动 dev server,属环境问题。在本地,CLI 包会显示一批在 CI 上不复现、也与本改动无关的红测试 —— 最明显的是一个 scheduled-tasks 用例请求的路由在测试里出现七次、在路由源码里出现零次。所有受影响文件与
main逐字节相同,且针对同一选择的 base-tree 对照测量显示本分支没有新增的失败文件或失败用例名;它们是繁忙机器上对负载敏感的超时。测试环境
环境
本地运行时:
npm run build && npm run bundle,然后每个用例用独立的临时QWEN_HOME在 loopback 上跑node dist/cli.js serve。浏览器侧检查通过该包的 Playwright 装置(smoke 与 visual 两套配置)以及一个针对构建产物文档的 jsdom 装置。风险与范围
src进入文档且文档中不存在 script 节点。谁要把侧边栏改成内联 Logo,就必须先加净化器,而这些测试被写成在不变量破坏时立即失败。O_NOFOLLOW)只在 macOS 上实测,其余平台靠单测覆盖。约四十处正文里提到产品的本地化字符串仍然写内置名称 —— 覆盖它们需要本地化 provider 目前不具备的消息覆盖机制,所以改名后的 shell 在正文文案里仍会出现 “Qwen Code”。auth provider 标签仍然写 “Qwen OAuth”,这是有意的:那是身份提供方的名字而非产品名,改了会向即将授权的人错误描述该流程。服务条款、文档与缺陷上报链接仍指向真实资源。终端 CLI 不受影响,保留自己独立的 banner 设置。关联 Issue
无。