Skip to content

fix(desktop): harden remaining source path validation#5914

Merged
wenshao merged 2 commits into
QwenLM:mainfrom
VectorPeak:codex/source-path-validation-hardening
Jun 27, 2026
Merged

fix(desktop): harden remaining source path validation#5914
wenshao merged 2 commits into
QwenLM:mainfrom
VectorPeak:codex/source-path-validation-hardening

Conversation

@VectorPeak

@VectorPeak VectorPeak commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

This PR hardens the remaining desktop source slug-to-path edges from #5909 without relaxing the source slug guard introduced by #5829 or duplicating the validation/RPC behavior normalization in #5911.

What this PR does

  • Validates source slugs before the session MCP server resolves a source credential cache path, so path-like inputs such as ../sessions cannot be joined into sources/../sessions/.credential-cache.json.
  • Keeps credential cache reads graceful: invalid slugs, missing cache files, expired cache entries, and malformed cache contents still return null instead of surfacing a hard tool failure.
  • Sanitizes validation result file fields for invalid source slugs by using placeholders such as sources/<invalid>/config.json and sources/<invalid>/permissions.json instead of echoing unsafe slug-derived paths.
  • Adds debug logging for raw source permission load failures, matching the nearby normalized source permission loader behavior.
  • Separates invalid source slug diagnostics from source permissions file I/O failures in the source permissions RPC handler.
  • Removes an unnecessary source storage module mock from the renew-endpoint credential tests so it no longer leaks into later source storage slug tests in the same Bun process.

Why it's needed

This is a follow-up to #5829 and #5911. #5829 added the core path traversal guard for source deletion by rejecting path-like source slugs before filesystem path construction. #5911 normalizes several validation and RPC boundary behaviors around invalid or legacy source slugs.

A few remaining defensive edges still needed cleanup. The session MCP credential cache helper directly joined sourceSlug into a filesystem path, making it the last reviewed source-slug-to-path construction site that did not explicitly run the slug guard first. Some diagnostics also made invalid slug failures harder to interpret: validation results could display paths derived from the unsafe slug itself, the raw permissions loader silently swallowed failures, and the source permissions RPC handler logged invalid slug assertions as generic permissions file read errors.

This PR keeps the same strict slug regex and focuses on the remaining API boundaries: source slugs are validated before credential cache path construction, invalid slug diagnostics are classified as caller input issues rather than disk I/O, unsafe display paths are redacted, and the local test pair that previously depended on run order is deterministic again.

Possible call chain / impact

The affected paths are session MCP credential cache reads, source validation diagnostics, source permissions RPC diagnostics, raw permissions loading, and local test isolation:

Session MCP tool execution
  -> createCredentialManager(workspaceRootPath)
    -> hasValidCredentials(...) / getToken(...)
      -> readCredentialCache(workspaceRootPath, source.config.slug)
        -> getCredentialCachePath(workspaceRootPath, sourceSlug)
          -> assertValidSourceSlug(sourceSlug)
          -> workspaceRootPath/sources/<slug>/.credential-cache.json

Before this PR, the credential cache path was constructed directly from sourceSlug. A path-like slug such as ../sessions could resolve toward sources/../sessions/.credential-cache.json before the read failed or returned data. This PR validates the slug before joining the cache path; invalid cache slugs still return null through the credential manager, preserving the existing missing-credential behavior.

Validation call: validateSource(workspaceRoot, sourceSlug)
  -> getSourceValidationFile(sourceSlug)
    -> assertValidSourceSlug(sourceSlug)
    -> sources/<invalid>/config.json for invalid input

Before this PR, invalid input such as ../sessions could be reflected in the validation result as sources/../sessions/config.json. This PR keeps the validation error message but uses a sanitized display path so validation output does not look like a resolved workspace path.

Source permissions UI/RPC request
  -> RPC sources:getPermissions
    -> getSourcePermissionsPath(workspace.rootPath, sourceSlug)
      -> getSourcePath(...)
        -> assertValidSourceSlug(...)

Before this PR, an invalid slug assertion in this path was logged as Error reading permissions config, which points maintainers toward filesystem or JSON problems. This PR logs invalid slug failures with a warning specific to source slug validation while preserving the existing null return contract.

Bun test run
  -> credential-manager-renew.test.ts
    -> unnecessary mock.module('../storage.ts')
  -> storage-source-slug.test.ts
    -> imports source storage helpers

Before this PR, running the renew-endpoint credential tests before the storage source slug tests in one Bun process could leave the storage module mocked, causing the later storage test to list no real sources. This PR removes the unused storage mock so the focused pair is order-independent.

This PR only changes these remaining hardening and diagnostics paths. It does not relax the source slug regex, change source creation or deletion behavior, alter source config parsing, change guide parsing, modify source connection tests, or duplicate the config_validate, source_test, validateAllSources, and source delete RPC behavior changes handled in #5911.

Reviewer Test Plan

How to verify

Confirm that:

  • A session MCP credential cache read for sourceSlug: "../sessions" returns null and does not read sessions/.credential-cache.json outside the source directory.
  • Invalid source validation results report sources/<invalid>/config.json or sources/<invalid>/permissions.json while preserving the underlying Invalid source slug error message.
  • Source permissions RPC reads return null for invalid slugs and log Invalid source slug for permissions: as a warning instead of logging a generic permissions file read error.
  • Running credential-manager-renew.test.ts before storage-source-slug.test.ts no longer leaves source storage mocked for the second file.

Commands run:

  • cd packages/desktop && bun test packages/session-mcp-server/src/credential-cache.test.ts packages/shared/src/config/__tests__/source-slug-validation.test.ts packages/shared/src/agent/__tests__/permissions-config-source-slug.test.ts packages/server-core/src/handlers/rpc/sources.test.ts -> 8 pass, 0 fail
  • cd packages/desktop && bun test packages/shared/src/sources/__tests__/credential-manager-renew.test.ts packages/shared/src/sources/__tests__/storage-source-slug.test.ts -> 14 pass, 0 fail
  • cd packages/desktop/packages/shared && bun run tsc --noEmit -> pass
  • cd packages/desktop/packages/server-core && bun run tsc --noEmit -> pass
  • cd packages/desktop/packages/session-mcp-server && bun run build -> pass
  • cd packages/desktop/packages/session-mcp-server && bun run tsc --noEmit -> fails in existing ../session-tools-core/src/** .ts import extension configuration errors; no errors are reported for this PR's changed session MCP files after the new local imports were switched to extensionless form
  • JD-Cloud Linux (Ubuntu 24.04.2 LTS, Linux 6.8.0-53-generic, Node v22.23.1, Bun 1.3.14): cd packages/desktop && ELECTRON_SKIP_BINARY_DOWNLOAD=1 bun install -> pass
  • JD-Cloud Linux: cd packages/desktop && bun test packages/session-mcp-server/src/credential-cache.test.ts packages/shared/src/config/__tests__/source-slug-validation.test.ts packages/shared/src/agent/__tests__/permissions-config-source-slug.test.ts packages/server-core/src/handlers/rpc/sources.test.ts -> 8 pass, 0 fail
  • JD-Cloud Linux: cd packages/desktop && bun test packages/shared/src/sources/__tests__/credential-manager-renew.test.ts packages/shared/src/sources/__tests__/storage-source-slug.test.ts -> 14 pass, 0 fail
  • JD-Cloud Linux: cd packages/desktop/packages/shared && bun run tsc --noEmit -> pass
  • JD-Cloud Linux: cd packages/desktop/packages/server-core && bun run tsc --noEmit -> pass
  • JD-Cloud Linux: cd packages/desktop/packages/session-mcp-server && bun run build -> pass
  • JD-Cloud Linux: cd packages/desktop/packages/session-mcp-server && bun run tsc --noEmit -> reproduces the existing ../session-tools-core/src/** .ts import extension configuration errors only; no changed session MCP files are reported

Evidence (Before & After)

No UI evidence; this is non-UI path handling, diagnostics, and test isolation behavior covered by focused regression tests.

Tested on

OS Status
🍏 macOS ⚠️ not tested
🪟 Windows ✅ tested
🐧 Linux ✅ tested on JD-Cloud

Environment (optional)

Windows PowerShell, Bun 1.3.12, Node v22.22.3. JD-Cloud Ubuntu 24.04.2 LTS (Linux 6.8.0-53-generic), Bun 1.3.14, Node v22.23.1.

Risk & Scope

  • Main risk or tradeoff: invalid credential cache slugs now return null before path construction, which is intentionally the same observable result as a missing or unusable credential cache.
  • Not validated / out of scope: the behavior normalization work in fix(desktop): normalize source slug validation errors #5911, broader source creation semantics, source config parsing, guide parsing, connection testing, and unrelated desktop-wide typecheck failures.
  • Breaking changes / migration notes: None expected; strict source slug validation is preserved and no valid source slug path changes.

Linked Issues

Closes #5909

中文说明

本 PR 加固 #5909 中剩余的桌面端 source slug 到文件系统路径的边界,同时不放宽 #5829 引入的 source slug guard,也不重复 #5911 已处理的 validation / RPC 行为归一化。

What this PR does

  • 在 session MCP server 解析 source credential cache 路径前先验证 source slug,因此 ../sessions 这类 path-like 输入不能被拼成 sources/../sessions/.credential-cache.json
  • 保持 credential cache read 的宽容返回:无效 slug、cache 不存在、cache 过期、cache 内容损坏都会继续返回 null,不会变成硬失败。
  • 对 invalid source slug 的 validation result file 字段做脱敏,使用 sources/<invalid>/config.jsonsources/<invalid>/permissions.json 这类占位路径,而不是回显由不安全 slug 拼出来的路径。
  • 给 raw source permissions load failure 增加 debug 日志,与旁边的 normalized source permissions loader 行为保持一致。
  • 在 source permissions RPC handler 中区分 invalid source slug 诊断和 source permissions 文件 I/O 失败。
  • 移除 renew-endpoint credential 测试中不必要的 source storage module mock,避免它泄漏到同一个 Bun 进程后续运行的 source storage slug 测试里。

Why it's needed

这是 #5829#5911 的后续。#5829 已经通过在文件系统路径构造前拒绝 path-like source slug,修复了 source deletion 的核心路径穿越防护。#5911 则统一了若干 invalid / legacy source slug 在 validation 和 RPC 边界上的行为。

剩余的一些防御性边界仍需要收尾。session MCP credential cache helper 会直接把 sourceSlug 拼进文件系统路径,它是本轮 review 中剩余的、没有显式先走 slug guard 的 source-slug-to-path 构造点。部分诊断也会让 invalid slug 失败更难排查:validation result 可能展示由不安全 slug 拼出来的路径,raw permissions loader 会静默吞掉失败,source permissions RPC handler 会把 invalid slug assertion 记录成泛化的 permissions file read error。

本 PR 保留相同的严格 slug regex,并只聚焦剩余 API 边界:credential cache 路径构造前先验证 source slug,invalid slug 诊断被归类为调用方输入问题而不是磁盘 I/O,unsafe display path 被占位符替代,本地测试组合也不再依赖运行顺序。

Possible call chain / impact

受影响路径是 session MCP credential cache read、source validation diagnostics、source permissions RPC diagnostics、raw permissions loading 以及本地测试隔离:

Session MCP tool execution
  -> createCredentialManager(workspaceRootPath)
    -> hasValidCredentials(...) / getToken(...)
      -> readCredentialCache(workspaceRootPath, source.config.slug)
        -> getCredentialCachePath(workspaceRootPath, sourceSlug)
          -> assertValidSourceSlug(sourceSlug)
          -> workspaceRootPath/sources/<slug>/.credential-cache.json

在本 PR 之前,credential cache path 直接由 sourceSlug 构造。../sessions 这类 path-like slug 可能在读取失败或返回数据前,先被解析到 sources/../sessions/.credential-cache.json。本 PR 在 join cache path 前验证 slug;无效 cache slug 仍然通过 credential manager 返回 null,保持原有 missing credential 语义。

Validation call: validateSource(workspaceRoot, sourceSlug)
  -> getSourceValidationFile(sourceSlug)
    -> assertValidSourceSlug(sourceSlug)
    -> sources/<invalid>/config.json for invalid input

在本 PR 之前,../sessions 这类 invalid input 可能在 validation result 里显示为 sources/../sessions/config.json。本 PR 保留 validation error message,但使用脱敏 display path,避免验证输出看起来像一个被解析过的 workspace path。

Source permissions UI/RPC request
  -> RPC sources:getPermissions
    -> getSourcePermissionsPath(workspace.rootPath, sourceSlug)
      -> getSourcePath(...)
        -> assertValidSourceSlug(...)

在本 PR 之前,这条路径里的 invalid slug assertion 会被记录成 Error reading permissions config,容易让维护者误判成文件系统或 JSON 问题。本 PR 对 invalid slug 使用专门 warning,同时保留原有 null 返回契约。

Bun test run
  -> credential-manager-renew.test.ts
    -> unnecessary mock.module('../storage.ts')
  -> storage-source-slug.test.ts
    -> imports source storage helpers

在本 PR 之前,如果 renew-endpoint credential tests 先于 storage source slug tests 在同一个 Bun 进程里运行,storage module mock 可能残留,导致后一个 storage test 列不出真实 source。本 PR 移除未使用的 storage mock,让这个聚焦测试组合不再依赖运行顺序。

本 PR 只改变这些剩余 hardening 和 diagnostics 路径。它不会放宽 source slug regex,不会改变 source 创建或删除行为,不会改变 source config parsing,不会改变 guide parsing,不会修改 source connection tests,也不重复 #5911 中已处理的 config_validatesource_testvalidateAllSources 和 source delete RPC 行为变更。

Reviewer Test Plan

How to verify

确认:

  • session MCP credential cache 对 sourceSlug: "../sessions" 返回 null,并且不会读取 source 目录外的 sessions/.credential-cache.json
  • invalid source validation result 会报告 sources/<invalid>/config.jsonsources/<invalid>/permissions.json,同时保留底层 Invalid source slug 错误信息。
  • Source permissions RPC 对 invalid slug 返回 null,并以 Invalid source slug for permissions: 记录 warning,而不是泛化 permissions file read error。
  • credential-manager-renew.test.tsstorage-source-slug.test.ts 前运行时,不再把 source storage mock 泄漏给第二个测试文件。

已运行命令:

  • cd packages/desktop && bun test packages/session-mcp-server/src/credential-cache.test.ts packages/shared/src/config/__tests__/source-slug-validation.test.ts packages/shared/src/agent/__tests__/permissions-config-source-slug.test.ts packages/server-core/src/handlers/rpc/sources.test.ts -> 8 pass, 0 fail
  • cd packages/desktop && bun test packages/shared/src/sources/__tests__/credential-manager-renew.test.ts packages/shared/src/sources/__tests__/storage-source-slug.test.ts -> 14 pass, 0 fail
  • cd packages/desktop/packages/shared && bun run tsc --noEmit -> pass
  • cd packages/desktop/packages/server-core && bun run tsc --noEmit -> pass
  • cd packages/desktop/packages/session-mcp-server && bun run build -> pass
  • cd packages/desktop/packages/session-mcp-server && bun run tsc --noEmit -> 因既有 ../session-tools-core/src/** .ts import extension 配置错误失败;新增 local import 改成 extensionless 之后,本 PR 改动的 session MCP 文件没有报错
  • JD-Cloud Linux(Ubuntu 24.04.2 LTSLinux 6.8.0-53-genericNode v22.23.1Bun 1.3.14):cd packages/desktop && ELECTRON_SKIP_BINARY_DOWNLOAD=1 bun install -> pass
  • JD-Cloud Linux:cd packages/desktop && bun test packages/session-mcp-server/src/credential-cache.test.ts packages/shared/src/config/__tests__/source-slug-validation.test.ts packages/shared/src/agent/__tests__/permissions-config-source-slug.test.ts packages/server-core/src/handlers/rpc/sources.test.ts -> 8 pass, 0 fail
  • JD-Cloud Linux:cd packages/desktop && bun test packages/shared/src/sources/__tests__/credential-manager-renew.test.ts packages/shared/src/sources/__tests__/storage-source-slug.test.ts -> 14 pass, 0 fail
  • JD-Cloud Linux:cd packages/desktop/packages/shared && bun run tsc --noEmit -> pass
  • JD-Cloud Linux:cd packages/desktop/packages/server-core && bun run tsc --noEmit -> pass
  • JD-Cloud Linux:cd packages/desktop/packages/session-mcp-server && bun run build -> pass
  • JD-Cloud Linux:cd packages/desktop/packages/session-mcp-server && bun run tsc --noEmit -> 仅复现既有 ../session-tools-core/src/** .ts import extension 配置错误;未报告本 PR 改动的 session MCP 文件错误

Evidence (Before & After)

无 UI 证据;这是非 UI 的 path handling、diagnostics 和 test isolation 行为,已由聚焦回归测试覆盖。

Tested on

OS Status
🍏 macOS ⚠️ not tested
🪟 Windows ✅ tested
🐧 Linux ✅ tested on JD-Cloud

Environment (optional)

Windows PowerShell, Bun 1.3.12, Node v22.22.3. JD-Cloud Ubuntu 24.04.2 LTS(Linux 6.8.0-53-generic)、Bun 1.3.14、Node v22.23.1。

Risk & Scope

  • Main risk or tradeoff: invalid credential cache slug 现在会在 path construction 之前返回 null,这与 missing / unusable credential cache 的可观察结果一致,是有意保持的行为。
  • Not validated / out of scope: fix(desktop): normalize source slug validation errors #5911 中的行为归一化、更广泛 source creation 语义、source config parsing、guide parsing、connection testing,以及无关的 desktop-wide typecheck 失败。
  • Breaking changes / migration notes: 预期没有破坏性变更;严格 source slug validation 保持不变,有效 source slug 的路径行为不变。

Linked Issues

Closes #5909

@VectorPeak
VectorPeak marked this pull request as ready for review June 27, 2026 02:38
@wenshao

wenshao commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓ — all required sections present, bilingual body complete.

On direction: clean security hardening that closes the last unvalidated source-slug-to-path construction site in the session MCP credential cache. Natural follow-up to #5829 and #5911. The three gaps — credential cache path construction without slug guard, validation diagnostics echoing unsafe paths, and RPC errors misclassified as I/O failures — are all legitimate defensive edges. Clearly aligned with the project's security posture.

On approach: scope is tight and each change addresses a named gap. Extracting credential cache code from index.ts into credential-cache.ts is justified (needs modification + independently testable). Reusing assertValidSourceSlug from @craft-agent/session-tools-core (already re-exported there) is correct. The validation file helpers (getSourceValidationFile, getSourcePermissionsValidationFile) are simple and focused. Removing the unnecessary storage mock fixes test isolation without changing behavior. No scope creep, no drive-by refactors. Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓ — 所有必填章节齐全,正文双语完整。

方向:干净的安全加固,闭合了 session MCP credential cache 中最后一个未经 slug guard 验证的 source-slug-to-path 构造点。自然承接 #5829#5911。三个缺口——credential cache 路径构造缺少 slug 验证、validation 诊断回显不安全路径、RPC 错误被误分类为 I/O 失败——都是合理的防御性边界。与项目安全姿态完全对齐。

方案:范围紧凑,每项改动针对明确缺口。将 credential cache 代码从 index.ts 提取到 credential-cache.ts 是合理的(需要修改 + 可独立测试)。复用已导出到 @craft-agent/session-tools-coreassertValidSourceSlug 是正确的。Validation 文件路径 helper(getSourceValidationFilegetSourcePermissionsValidationFile)简洁聚焦。移除不必要的 storage mock 修复测试隔离而不改变行为。没有范围蔓延,没有顺手重构。进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal (from PR description, before reading diff): add assertValidSourceSlug to credential cache path construction, sanitize validation result file fields with placeholders, separate RPC error logging by error type, add debug logging to raw permissions loader, remove test mock leaking.

Comparison with diff: the PR's approach matches or exceeds the independent proposal on every point.

credential-cache.ts — Clean extraction from index.ts. assertValidSourceSlug(sourceSlug) before join() is correct. The try-catch in readCredentialCache wraps the full function body (including path construction), so invalid slugs return null through the credential manager — preserving the existing missing-credential behavior contract. Reuses assertValidSourceSlug from @craft-agent/session-tools-core where it's already re-exported.

sources.ts (RPC handler)isInvalidSourceSlugError() uses error message string matching, which is slightly fragile but pragmatic. The error message is controlled by assertValidSourceSlug in the same monorepo, and this is a common Node.js pattern. Correctly downgrades invalid slug errors from error to warn severity.

validators.tsgetSourceValidationFile() and getSourcePermissionsValidationFile() use a clean try-catch pattern to return sanitized paths on invalid slugs. The file variable hoisting in validateSourcePermissions (before the try block) is needed so the catch block has access to the sanitized path.

permissions-config.ts — Single-line addition of debug() logging in the catch block, matching the nearby normalized loader behavior. Minimal and correct.

credential-manager-renew.test.ts — Removing the unused mock.module('../storage.ts') fixes cross-test contamination. The mock was unnecessary since the credential module is already mocked separately.

No blockers found. All changes are focused, correct, and follow project conventions.

Testing

This PR is non-UI security hardening of internal desktop path validation (credential cache reads, validation diagnostics, RPC error classification in the Electron main process). There's no CLI invocation path that exercises this behavior — tmux real-scenario testing is not applicable here. The behavior is best validated through the unit tests included in the PR (8 + 14 tests passing per the author's report).

Unable to run the desktop Bun test suite in this CI environment (Bun not installed).

中文说明

代码审查

独立方案(从 PR 描述出发,未看 diff 前):在 credential cache 路径构造前加 assertValidSourceSlug,用占位符脱敏 validation result 的 file 字段,按错误类型分离 RPC 日志级别,给 raw permissions loader 加 debug 日志,移除测试 mock 泄漏。

与 diff 对比: PR 方案在每一点上都匹配或超过独立方案。

credential-cache.ts — 从 index.ts 干净提取。join() 前的 assertValidSourceSlug(sourceSlug) 正确。readCredentialCache 的 try-catch 包裹整个函数体(包括路径构造),invalid slug 通过 credential manager 返回 null——保持原有 missing-credential 行为契约。复用已在 @craft-agent/session-tools-core 重导出的 assertValidSourceSlug

sources.ts(RPC handler)isInvalidSourceSlugError() 使用错误消息字符串匹配,略有脆弱但务实。错误消息由同一 monorepo 中的 assertValidSourceSlug 控制,这是常见的 Node.js 模式。正确将 invalid slug 错误从 error 降级为 warn

validators.tsgetSourceValidationFile()getSourcePermissionsValidationFile() 使用简洁的 try-catch 模式,对 invalid slug 返回脱敏路径。validateSourcePermissionsfile 变量提升到 try 块之前是必要的,以便 catch 块能访问脱敏路径。

permissions-config.ts — 在 catch 块中单行添加 debug() 日志,与旁边的 normalized loader 行为一致。最小化且正确。

credential-manager-renew.test.ts — 移除未使用的 mock.module('../storage.ts') 修复跨测试污染。该 mock 不必要,因为 credential 模块已单独 mock。

未发现 blocker。 所有改动聚焦、正确,遵循项目惯例。

测试

本 PR 是非 UI 安全加固,针对桌面端内部路径验证(credential cache 读取、validation 诊断、Electron 主进程 RPC 错误分类)。没有 CLI 调用路径可以触发此行为——tmux 真实场景测试不适用。行为最好通过 PR 中包含的单元测试验证(作者报告 8 + 14 测试通过)。

当前 CI 环境无法运行桌面 Bun 测试套件(未安装 Bun)。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Reflection

Going back to the independent proposal: I would have made the same changes — add slug validation before path construction, sanitize diagnostic paths, separate error logging, add debug logging, fix test isolation. The PR matches on every point and the module extraction is a reasonable improvement for testability.

The code is straightforward. assertValidSourceSlug before join() in credential-cache.ts is exactly the right fix for the credential cache gap. The validation file helpers are three lines each — no over-engineering. The RPC handler's error classification is pragmatic. The debug logging addition is a one-liner for parity. Test mock removal is a clean isolation fix.

Every change in the diff serves the stated goal. No drive-by refactors, no scope creep, no formatting churn. 9 files changed, +203/-56 — proportional to the work.

This is a focused, correct security hardening PR that closes the last unvalidated source-slug-to-path construction site. The included tests cover the key scenarios (traversal rejection, valid slug acceptance, null returns for missing/expired caches, separated RPC diagnostics, test isolation). The approach is the right one, and the execution is clean.

LGTM, looks ready to ship. ✅

中文说明

总结

回顾独立方案:我会做同样的改动——路径构造前加 slug 验证、脱敏诊断路径、分离错误日志、添加 debug 日志、修复测试隔离。PR 在每一点上都匹配,模块提取是对可测试性的合理改进。

代码简洁直接。credential-cache.tsjoin() 前的 assertValidSourceSlug 是 credential cache 缺口的正确修复。Validation 文件 helper 每个只有三行——没有过度工程。RPC handler 的错误分类务实。Debug 日志添加是一行代码的对齐。测试 mock 移除是干净的隔离修复。

diff 中的每个改动都服务于声明的目标。没有顺手重构,没有范围蔓延,没有格式化噪音。9 个文件变更,+203/-56——与工作量成正比。

这是一个聚焦、正确的安全加固 PR,闭合了最后一个未经验证的 source-slug-to-path 构造点。包含的测试覆盖了关键场景(路径遍历拒绝、有效 slug 接受、缺失/过期 cache 返回 null、分离的 RPC 诊断、测试隔离)。方案正确,执行干净。

可以合并 ✅

Qwen Code · qwen3.7-max

@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. ✅

@wenshao

wenshao commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

✅ Maintainer local verification — PASS (recommend merge)

I built and ran this PR locally on a clean worktree at the PR head (712b435), including adversarial mutation tests to confirm the new tests are load-bearing (i.e. they fail when the fix is reverted). All checks reproduce the PR's claims, and I additionally verified it on macOS, which the PR description marked as not tested.

Environment: macOS (Darwin 25.5.0), Bun 1.3.14, Node v22.22.2 · isolated worktree, ELECTRON_SKIP_BINARY_DOWNLOAD=1 bun install (1560 packages, clean).

1. Focused tests — reproduce the PR's results

Command Result
bun test credential-cache.test.ts source-slug-validation.test.ts permissions-config-source-slug.test.ts sources.test.ts 8 pass, 0 fail
bun test credential-manager-renew.test.ts storage-source-slug.test.ts 14 pass, 0 fail

2. Adversarial mutation testing (the part that matters)

A passing test proves nothing unless it fails with the fix removed — so I reverted each fix and confirmed the regression is real:

(A) The path-traversal guard is load-bearing. Removing assertValidSourceSlug() from getCredentialCachePath() makes credential-cache.test.ts fail, and readCredentialCache(workspaceRoot, '../sessions') returns the planted "secret-token" instead of null — a real credential disclosure out of the sources/ directory:

Received value: ".../credential-cache-AGbhsP/sessions/.credential-cache.json"  // traversed out of sources/
expect(readCredentialCache(...)).toBeNull()  →  Received: "secret-token"

With the PR's guard in place, both cases return null. ✅

(B) The test-isolation fix is real. Re-adding the removed mock.module('../storage.ts') to credential-manager-renew.test.ts makes the next file in the same Bun process fail — loadWorkspaceSources() returns [] instead of ['valid-source'] because the leaked loadSourceConfig mock survives across files:

(fail) storage-source-slug … legacy invalid source directories
- ["valid-source"]
+ []

Removing the unused mock (as this PR does) makes the pair order-independent again. ✅

3. Typecheck & build

Package Check Result
shared tsc --noEmit clean (0 errors)
server-core tsc --noEmit clean (0 errors)
session-mcp-server bun run build pass (bundled 391 modules) ✅
session-mcp-server tsc --noEmit 77 errors — all TS5097 in the pre-existing ../session-tools-core/ dependency; 0 in this PR's files

I confirmed every one of the 77 tsc errors lives under ../session-tools-core/src/** (the .ts-import-extension config issue that predates this PR). None touch the changed session-mcp files — the new imports use extensionless / package form, matching the PR description.

4. Reviewer-side audit (no blockers)

  • Caller safety: getCredentialCachePath (now throwing) is consumed only by readCredentialCache, which wraps the call inside its try → throw is caught → returns null. Its two real consumers (index.ts:90,95, the credential manager's getToken/hasValidCredentials) already treat null as "no credential", so the graceful contract holds end-to-end. No unguarded caller exists.
  • Minor / non-blocking: validateSource / validateSourcePermissions now call assertValidSourceSlug twice (once in the new getSource*ValidationFile helper, once on the existing path). It's an idempotent regex check — purely cosmetic redundancy. isInvalidSourceSlugError matches on the 'Invalid source slug:' message prefix; it's string-coupled to the thrown error but pinned by sources.test.ts.

5. CI / merge note

Test (ubuntu-latest, Node 22.x) is green; mac/win/integration legs show skipping (named-job gating — normal). mergeStateStatus: BLOCKED here is REVIEW_REQUIRED (awaiting a maintainer approval), not a CI failure. Note that packages/desktop/ is a separate Bun workspace excluded from the root CI, so these desktop changes are not exercised by the root pipeline — the local run above is the actual evidence.

Verdict

Behavior matches every claim in the PR description; both fixes are proven load-bearing by mutation; types/build are clean for the changed packages; the only tsc noise is pre-existing and unrelated. Looks good to merge. 👍

🇨🇳 中文版(完整对应)

✅ 维护者本地验证 —— 通过(建议合并)

我在 PR head(712b435)的干净 worktree 上本地构建并运行了本 PR,并做了对抗性变异测试来确认新增测试是“有效负载”(即把修复回退后测试会失败)。所有结果都复现了 PR 的声明,并且我额外在 macOS 上做了验证——而 PR 描述里 macOS 标的是 not tested

环境: macOS(Darwin 25.5.0)、Bun 1.3.14、Node v22.22.2 · 独立 worktree、ELECTRON_SKIP_BINARY_DOWNLOAD=1 bun install(1560 包,干净)。

1. 聚焦测试 —— 复现 PR 结果

命令 结果
bun test credential-cache.test.ts source-slug-validation.test.ts permissions-config-source-slug.test.ts sources.test.ts 8 通过,0 失败
bun test credential-manager-renew.test.ts storage-source-slug.test.ts 14 通过,0 失败

2. 对抗性变异测试(最关键的部分)

测试通过本身不说明问题,除非把修复删掉后测试会失败——所以我逐个回退修复,确认回归是真实存在的:

(A) 路径穿越 guard 是有效负载。getCredentialCachePath() 里的 assertValidSourceSlug() 删掉后,credential-cache.test.ts 失败,readCredentialCache(workspaceRoot, '../sessions') 返回事先植入的 "secret-token" 而不是 null——这是真实的、穿越出 sources/ 目录的凭据泄露:

Received value: ".../credential-cache-AGbhsP/sessions/.credential-cache.json"  // 穿越出了 sources/
expect(readCredentialCache(...)).toBeNull()  →  Received: "secret-token"

带上 PR 的 guard 后,两种情况都返回 null。✅

(B) 测试隔离修复是真实的。 把被删掉的 mock.module('../storage.ts') 重新加回 credential-manager-renew.test.ts,会让同一个 Bun 进程里后一个文件失败——loadWorkspaceSources() 返回 [] 而不是 ['valid-source'],因为泄漏的 loadSourceConfig mock 跨文件残留了:

(fail) storage-source-slug … legacy invalid source directories
- ["valid-source"]
+ []

移除这个没用到的 mock(正是本 PR 的做法)让这组测试重新与运行顺序无关。✅

3. 类型检查与构建

检查 结果
shared tsc --noEmit 干净(0 错误)
server-core tsc --noEmit 干净(0 错误)
session-mcp-server bun run build 通过(打包 391 模块)✅
session-mcp-server tsc --noEmit 77 个错误 —— 全部是既有 ../session-tools-core/ 依赖里的 TS5097;本 PR 文件 0 错误

我确认了这 77 个 tsc 错误全部位于 ../session-tools-core/src/**(早于本 PR 就存在的 .ts import 扩展名配置问题)。没有一个落在改动的 session-mcp 文件上——新增 import 用的是无扩展名 / 包形式,与 PR 描述一致。

4. 审查者视角审计(无阻塞项)

  • 调用方安全: 现在会抛错的 getCredentialCachePath readCredentialCache 调用,而后者把调用包在 try 内部 → 抛错被捕获 → 返回 null。它的两个真实消费方(index.ts:90,95,即 credential manager 的 getToken/hasValidCredentials)本就把 null 当作“无凭据”,所以宽容契约从头到尾成立。不存在未受保护的调用方。
  • 次要 / 非阻塞: validateSource / validateSourcePermissions 现在会调用 assertValidSourceSlug 两次(一次在新的 getSource*ValidationFile 辅助函数里,一次在原有路径上)。这是幂等的正则校验——纯属外观上的冗余。isInvalidSourceSlugError 通过 'Invalid source slug:' 消息前缀匹配;它与抛出的错误字符串耦合,但已被 sources.test.ts 钉住。

5. CI / 合并说明

Test (ubuntu-latest, Node 22.x) 绿;mac/win/integration 腿显示 skipping(named-job 占位,属正常)。这里的 mergeStateStatus: BLOCKEDREVIEW_REQUIRED(等待维护者 approve),不是 CI 失败。注意 packages/desktop/ 是被 root CI 排除的独立 Bun workspace,所以这些 desktop 改动不会被 root 流水线执行——上面的本地运行才是真正的证据。

结论

行为与 PR 描述的每一条声明一致;两处修复都经变异测试证明是有效负载;改动包的类型/构建干净;唯一的 tsc 噪声是既有且无关的。可以合并。 👍

@wenshao
wenshao added this pull request to the merge queue Jun 27, 2026
Merged via the queue into QwenLM:main with commit a12ca8d Jun 27, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Harden remaining slug-to-path call sites and invalid slug diagnostics

3 participants