Skip to content

fix(desktop): validate credential cache source slugs#5913

Closed
tt-a1i wants to merge 1 commit into
QwenLM:mainfrom
tt-a1i:fix/source-credential-cache-slug-validation
Closed

fix(desktop): validate credential cache source slugs#5913
tt-a1i wants to merge 1 commit into
QwenLM:mainfrom
tt-a1i:fix/source-credential-cache-slug-validation

Conversation

@tt-a1i

@tt-a1i tt-a1i commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Routes the session MCP server's credential-cache path construction through the existing validated source-directory helper, so unsafe source slugs are rejected before any credential-cache filesystem access.

It also keeps ordinary cache misses quiet while logging corrupt or otherwise unreadable cache files, and adds focused tests for the valid, missing, expired, unreadable, and traversal-slug cases.

Why it's needed

#5909 identified one remaining source-slug-to-path call site in the credential cache reader. The old path construction joined the workspace root, sources, and sourceSlug directly, which left this subprocess path out of sync with the validated source helpers used elsewhere.

Invalid slug failures should not look the same as "no cached credential". This PR lets slug validation fail before the cache-read catch block, while preserving the existing null-return behavior for normal cache misses and expired entries.

Reviewer Test Plan

How to verify

From packages/desktop/packages/session-mcp-server:

bun test src/credential-cache.test.ts
bun run build
bunx tsc -p tsconfig.json --noEmit --allowImportingTsExtensions

From the repository root:

git diff --check HEAD

Evidence (Before & After)

Before: credential-cache paths were built by directly joining sourceSlug, so a malformed slug like ../sessions could be resolved before any slug validation.

After: ../sessions throws Invalid source slug: "../sessions" before filesystem access; missing cache files still return null; corrupt cache JSON returns null with a diagnostic log. The focused Bun test covers these outcomes.

Tested on

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

Environment (optional)

macOS 26.5.1, Bun 1.3.14.

Risk & Scope

  • Main risk or tradeoff: Invalid source slugs now surface as validation errors from credential-cache reads instead of being treated as cache misses. That is intentional so unsafe slugs cannot silently proceed through the credential cache path.
  • Not validated / out of scope: This PR is scoped to the session MCP server credential-cache path and diagnostics slice. Shared config validator error wording is being handled separately in fix(desktop): normalize source slug validation errors #5911; the broader test-isolation note in Harden remaining slug-to-path call sites and invalid slug diagnostics #5909 is not changed here.
  • Breaking changes / migration notes: None expected for valid source slugs. Existing missing, expired, and empty cache behavior stays null.

Linked Issues

Refs #5909

AI Assistance Disclosure

I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.

中文说明

What this PR does

这个 PR 让 session MCP server 的 credential-cache 路径构造复用已有的、带 source slug 校验的 source-directory helper,因此不安全的 source slug 会在任何 credential-cache 文件访问前被拒绝。

同时,它保留普通 cache miss 的静默行为,但会为损坏或其它不可读的 cache 文件记录诊断日志,并补充了覆盖有效 cache、缺失 cache、过期 cache、不可读 cache 和路径遍历 slug 的 focused tests。

Why it's needed

#5909 指出了 credential cache reader 里还存在一个 source-slug-to-path 调用点。旧实现直接拼接 workspace root、sourcessourceSlug,导致这个 subprocess 路径没有和其它地方使用的已校验 source helper 保持一致。

非法 slug 失败不应该和“没有缓存凭证”表现得一样。这个 PR 让 slug 校验在 cache-read catch 块之前失败,同时保留普通 cache miss 和过期 cache 返回 null 的现有行为。

Reviewer Test Plan

How to verify

packages/desktop/packages/session-mcp-server 下运行:

bun test src/credential-cache.test.ts
bun run build
bunx tsc -p tsconfig.json --noEmit --allowImportingTsExtensions

在仓库根目录运行:

git diff --check HEAD

Evidence (Before & After)

Before:credential-cache 路径直接拼接 sourceSlug,因此像 ../sessions 这样的异常 slug 可能在任何 slug 校验前被解析成路径。

After:../sessions 会在文件系统访问前抛出 Invalid source slug: "../sessions";缺失的 cache 文件仍返回 null;损坏的 cache JSON 返回 null 并记录诊断日志。focused Bun test 覆盖了这些结果。

Tested on

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

Environment (optional)

macOS 26.5.1,Bun 1.3.14。

Risk & Scope

  • Main risk or tradeoff:非法 source slug 现在会从 credential-cache 读取中表现为校验错误,而不是被当成 cache miss。这是有意的,避免不安全 slug 静默进入 credential cache 路径。
  • Not validated / out of scope:这个 PR 只覆盖 session MCP server credential-cache 路径和诊断这一块。shared config validator 的错误文案由 fix(desktop): normalize source slug validation errors #5911 单独处理;Harden remaining slug-to-path call sites and invalid slug diagnostics #5909 中更宽的 test-isolation 事项不在本 PR 范围内。
  • Breaking changes / migration notes:对合法 source slug 没有预期破坏性变化。缺失、过期、空 cache 仍保持返回 null 的现有行为。

Linked Issues

Refs #5909

AI Assistance Disclosure

我使用 Codex 审查改动、按现有模式做 sanity-check,并辅助发现潜在边界情况。

@tt-a1i
tt-a1i marked this pull request as ready for review June 27, 2026 02:57
@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 ✓

On direction: this is a clean security hardening fix that closes the last unvalidated source-slug-to-path call site identified in #5909. Using the shared getSourcePath helper (which calls assertValidSourceSlug before any join()) is exactly the right approach — it brings the session MCP server's credential cache reader in line with every other path construction site. Clearly aligned with the project's security posture.

On approach: the scope is tight — extract the inline credential cache code from index.ts into a focused module, route path construction through getSourcePath, add targeted tests, and wire in error logging for non-ENOENT failures. The extraction into credential-cache.ts is justified since the code needs modification anyway, and the resulting module is easier to test in isolation. The removal of the existsSync pre-check in favor of letting ENOENT fall through the catch is a minor improvement (eliminates a TOCTOU race). No scope creep, no drive-by refactors.

One note for the reviewer: the CredentialCacheReadErrorLogger type alias and the logger-parameter pattern add a small abstraction layer over what could be a direct console.error call. Not a blocker — it makes the module independently testable — but worth being aware of.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

方向:这是一个干净的安全加固修复,闭合了 #5909 中发现的最后一个未经验证的 source-slug-to-path 调用点。使用共享的 getSourcePath helper(在任何 join() 之前调用 assertValidSourceSlug)是正确做法——让 session MCP server 的 credential cache reader 与所有其它路径构造点保持一致。与项目的安全姿态完全对齐。

方案:范围紧凑——将 index.ts 中内联的 credential cache 代码提取到独立模块,路径构造走 getSourcePath,补充针对性测试,并为非 ENOENT 错误接入日志记录。提取到 credential-cache.ts 是合理的,因为代码本身就需要修改,提取后的模块也更容易独立测试。移除 existsSync 预检查、改为让 ENOENT 落入 catch 块是一个小改进(消除了 TOCTOU 竞争)。没有范围蔓延,没有顺手重构。

给 reviewer 的一个备注:CredentialCacheReadErrorLogger 类型别名和 logger 参数模式在本可以直接 console.error 的地方增加了一层小抽象。不是 blocker——它让模块可以独立测试——但值得留意。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: given "last unvalidated source-slug-to-path call site in credential cache reader," I would have made a minimal inline fix — import getSourcePath, replace the raw join(workspaceRootPath, 'sources', sourceSlug) with it inside the existing inline functions in index.ts, and add a test for the traversal case. About 3 lines changed.

PR's approach: extracts the credential cache block into a focused credential-cache.ts module, routes path construction through getSourcePath, improves error handling (drops the TOCTOU existsSync pre-check, differentiates ENOENT from other failures, adds a logger parameter for non-ENOENT errors), and ships comprehensive tests covering valid, missing, expired, empty, unreadable, and traversal-slug cases.

Verdict: the PR's approach exceeds the minimal fix in useful ways. The extraction makes the code independently testable, the error handling improvements are genuine (silent ENOENT + logged corrupt JSON is better than silent catch-all), and the test coverage is thorough. Every change in the diff serves the stated goal — no scope creep, no drive-by refactors.

No correctness bugs, security holes, or regressions found. The isMissingFileError helper correctly checks error.code === 'ENOENT', and getCredentialCachePath properly delegates to getSourcePath which validates before any join(). The test fixture helper (writeCredentialCache) bypasses getSourcePath intentionally for setup, which is correct.

One minor observation (not a blocker): the CredentialCacheReadErrorLogger type alias is a small abstraction over (message: string) => void. It pays for itself in readability at the call site, so it's fine.

Real-Scenario Testing

This PR modifies internal credential cache infrastructure in the session MCP server — the code reads .credential-cache.json files written by the Electron main process. There is no CLI invocation that directly exercises this code path. The behavior change (path traversal rejection) is best verified by the unit tests, which cover the key scenarios:

  • ../sessions → throws Invalid source slug before filesystem access ✓
  • Valid slug → reads cache normally ✓
  • Missing/expired/empty → returns null silently ✓
  • Corrupt JSON → returns null with diagnostic log ✓

bun is not available in this CI environment, so I couldn't run bun test src/credential-cache.test.ts directly. CI will cover test execution. Manual code review confirms the test logic is correct.

No tmux before/after applicable — this is non-user-facing infrastructure code.

中文说明

代码审查

独立方案: 给定"credential cache reader 中最后一个未经验证的 source-slug-to-path 调用点",我会做一个最小内联修复——导入 getSourcePath,把原来的 join(workspaceRootPath, 'sources', sourceSlug) 替换掉,然后补一个路径遍历的测试。大约改 3 行。

PR 的方案: 将 credential cache 代码块提取到独立的 credential-cache.ts 模块,路径构造走 getSourcePath,改进了错误处理(去掉了 TOCTOU 的 existsSync 预检查,区分 ENOENT 和其它错误,为非 ENOENT 错误添加 logger 参数),并提供了覆盖有效、缺失、过期、空、不可读和路径遍历 slug 场景的全面测试。

结论: PR 的方案在有用的方面超越了最小修复。提取使代码可以独立测试,错误处理改进是实质性的(静默 ENOENT + 记录损坏的 JSON 优于全静默 catch),测试覆盖很全面。diff 中的每个改动都服务于声明的目标——没有范围蔓延,没有顺手重构。

没有发现正确性 bug、安全漏洞或回归。

真实场景测试

这个 PR 修改的是 session MCP server 内部的 credential cache 基础设施——代码读取由 Electron 主进程写入的 .credential-cache.json 文件。没有 CLI 调用能直接触发这个代码路径。行为变化(路径遍历拒绝)最好由单元测试验证。

当前 CI 环境没有 bun,无法直接运行 bun test。CI 会覆盖测试执行。手动代码审查确认测试逻辑正确。

tmux before/after 不适用——这是非用户可见的基础设施代码。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

This is a clean, well-scoped security fix. The credential cache reader had one remaining call site that built paths from raw sourceSlug without validation — this PR routes it through the shared getSourcePath helper, which rejects path traversal before any filesystem access. The motivation is clear (closing #5909), the implementation is straightforward, and the test coverage is thorough.

The extraction from index.ts into credential-cache.ts is justified: the code needed modification anyway, and the resulting module is independently testable. The error handling improvements (silent ENOENT, logged corrupt JSON, no more TOCTOU existsSync) are genuine quality gains on top of the security fix. Every line in the diff serves the stated goal.

I'd maintain this happily. Approving. ✅

中文说明

这是一个干净、范围合理的安全修复。credential cache reader 还有一个调用点在未经验证的情况下从原始 sourceSlug 构建路径——这个 PR 把它改为走共享的 getSourcePath helper,在任何文件系统访问之前拒绝路径遍历。动机清晰(闭合 #5909),实现直接,测试覆盖全面。

index.ts 提取到 credential-cache.ts 是合理的:代码本身需要修改,提取后的模块可以独立测试。错误处理改进(静默 ENOENT、记录损坏的 JSON、去掉 TOCTOU existsSync)是在安全修复之上的真实质量提升。diff 中的每一行都服务于声明的目标。

维护起来没问题。批准。✅

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

✅ Local real-runtime verification (maintainer)

Built and ran the real desktop package at this PR head (ca167ce) and verified the credential-cache path-traversal fix four ways: the PR's own test plan, a non-vacuity mutation, a real on-disk traversal A/B with the actual reader, and a regression check. The traversal is genuinely closed and the diagnostics behave as described. LGTM from a verification standpoint.

What the fix does: base built the cache path with a raw join(workspaceRootPath, 'sources', sourceSlug, '.credential-cache.json')no slug validation, so a slug like ../sessions escaped the sources/ sandbox. The PR routes path construction through the existing getSourcePath()assertValidSourceSlug() (regex ^[a-z0-9]+(?:-[a-z0-9]+)*$), which throws before any filesystem access. It also adds an optional logger so corrupt/unreadable cache files emit a diagnostic while ordinary misses stay quiet.

Environment: macOS (Darwin), Bun 1.3.14, worktree on ca167ce, bun install (1560 pkgs) in packages/desktop.


1. PR test plan — green

Check Result
bun test src/credential-cache.test.ts 5 / 5 pass (11 expect calls)
bunx tsc -p tsconfig.json --noEmit --allowImportingTsExtensions 0 errors
bun run build ✅ bundled 391 modules → index.js
git diff --check HEAD ✅ clean

2. Mutation — the traversal test is non-vacuous

Reverting getCredentialCachePath to the base raw join (join(root, 'sources', sourceSlug, …)) reintroduces the exact vulnerability, and the focused test catches it:

(fail) rejects unsafe source slugs before reading from the filesystem
  expect(received).toThrow("Invalid source slug: \"../sessions\"")
  Received function did not throw
  Received value: ".../session-credential-cache-XXXX/sessions/.credential-cache.json"   ← escaped sources/
4 pass | 1 fail

The "Received value" is the smoking gun — under the raw join the path resolves outside sources/, to the sibling sessions/ dir.

3. Real traversal A/B — actual reader, real on-disk fixtures — 10 / 10

Ran the real readCredentialCache against a workspace with a valid source, expired/corrupt entries, and a planted secret at root/sessions/.credential-cache.json (the traversal target, outside the sandbox):

  • ✅ PR readCredentialCache(root, "../sessions") THROWS Invalid source slug: "../sessions" — and never returns the sibling secret
  • ✅ PR getCredentialCachePath(root, "../sessions") throws too — the unsafe path is never even built
  • ✅ the exact base raw join join(root,'sources','../sessions',…) normalizes to root/sessions/.credential-cache.json and WOULD read the planted SIBLING-SECRET — the closed hole, demonstrated concretely
  • ✅ behavior matrix: valid → token · missing → null (no log) · expired → null · corrupt JSON → null + one Failed to read credential cache for source "corrupt" diagnostic · only the corrupt read logged

So invalid slugs now surface as a validation error distinct from a cache miss, exactly as the PR intends, and the secret is never reachable through the credential-cache path.

4. No regression

session-mcp-server has a single test file (the new one) — 5/5; the code moved out of index.ts (now calling readCredentialCache with the logger) compiles and bundles cleanly.


Notes for the maintainer

  • packages/desktop is a separate Bun workspace and is excluded from the root npm-workspace PR CI, so these bun tests do not run in the PR's required checks — this local run is the actual coverage for the change. Worth keeping in mind when relying on green CI.
  • ../sessions traverses via / on macOS (verified above); ..\sessions would traverse on Windows. The regex rejects both regardless of OS, so the guard is platform-independent.
  • The slug validator (assertValidSourceSlug/getSourcePath) already existed in session-tools-core; this PR's contribution is routing the one remaining credential-cache call site through it (the gap Harden remaining slug-to-path call sites and invalid slug diagnostics #5909 flagged) — a minimal, low-risk change.
  • Verified on macOS only (matches the PR's Tested on table).
  • Merge shows BLOCKED = REVIEW_REQUIRED (needs a maintainer approval); the mac/win/tmux-testing required checks are SKIPPED placeholders, not failures — orthogonal to the code.
🇨🇳 中文版(完整对应)

✅ 本地真实运行时验证(维护者)

在本 PR head(ca167ce)构建并运行了真实的 desktop 包,从四个角度验证 credential-cache 路径遍历修复:PR 自带测试计划、非空过变异测试、用真实 reader 跑真实磁盘的遍历 A/B,以及回归检查。遍历确实被堵住,诊断行为也与描述一致。从验证角度 LGTM。

这个修复做了什么: base 用原始 join(workspaceRootPath, 'sources', sourceSlug, '.credential-cache.json') 构造缓存路径 —— 没有 slug 校验,所以像 ../sessions 这样的 slug 能逃出 sources/ 沙箱。本 PR 把路径构造改走已有的 getSourcePath()assertValidSourceSlug()(正则 ^[a-z0-9]+(?:-[a-z0-9]+)*$),它在任何文件系统访问前抛错。同时新增一个可选 logger,让损坏/不可读的缓存文件输出诊断,而普通 cache miss 保持静默。

环境: macOS,Bun 1.3.14,worktree 在 ca167ce,在 packages/desktopbun install(1560 包)。

1. PR 测试计划 — 全绿

检查 结果
bun test src/credential-cache.test.ts 5 / 5 通过(11 个 expect)
bunx tsc -p tsconfig.json --noEmit --allowImportingTsExtensions 0 错误
bun run build ✅ 打包 391 模块 → index.js
git diff --check HEAD ✅ 干净

2. 变异测试 —— 遍历测试非空过

getCredentialCachePath 还原成 base 的原始拼接(join(root, 'sources', sourceSlug, …))就重新引入了漏洞,而 focused 测试抓住了它:

(fail) rejects unsafe source slugs before reading from the filesystem
  expect(received).toThrow("Invalid source slug: \"../sessions\"")
  Received function did not throw
  Received value: ".../session-credential-cache-XXXX/sessions/.credential-cache.json"   ← 逃出了 sources/
4 pass | 1 fail

"Received value" 就是铁证 —— 原始拼接下路径解析到了 sources/ 之外的兄弟目录 sessions/

3. 真实遍历 A/B —— 真实 reader + 真实磁盘 —— 10 / 10

真实 readCredentialCache 跑一个工作区:含一个合法 source、过期/损坏条目,以及在 root/sessions/.credential-cache.json(沙箱外的遍历目标)植入的一个 secret:

  • ✅ PR readCredentialCache(root, "../sessions") 抛出 Invalid source slug: "../sessions" —— 且绝不返回兄弟目录的 secret
  • ✅ PR getCredentialCachePath(root, "../sessions") 也抛错 —— 不安全路径根本没被构造出来
  • 完全相同的 base 原始拼接 join(root,'sources','../sessions',…) 规整成 root/sessions/.credential-cache.json,会读到植入的 SIBLING-SECRET —— 被堵住的漏洞,具体演示出来
  • ✅ 行为矩阵:合法 → token · 缺失 → null(不打日志)· 过期 → null · 损坏 JSON → null + 一条 Failed to read credential cache for source "corrupt" 诊断 · 只有损坏那次打了日志

所以非法 slug 现在表现为一个区别于 cache miss 的校验错误(正是 PR 的意图),且 secret 永远无法经 credential-cache 路径触达。

4. 无回归

session-mcp-server 只有一个测试文件(就是新增的)—— 5/5;从 index.ts 移出的代码(现在用 logger 调 readCredentialCache)编译 + 打包都干净。

给维护者的备注

  • packages/desktop 是独立的 Bun workspace,被排除在 root npm-workspace 的 PR CI 之外,所以这些 bun test 不会在 PR 的 required checks 里跑 —— 这次本地运行才是这个改动的真实覆盖。依赖 CI 绿时请留意。
  • ../sessions 在 macOS 经 / 遍历(上面已验证);..\sessions 在 Windows 会遍历。正则两者都拒,所以这个防护与平台无关。
  • slug 校验器(assertValidSourceSlug/getSourcePath)本来就在 session-tools-core;本 PR 的贡献是把剩下这一个 credential-cache 调用点也接到它上面(Harden remaining slug-to-path call sites and invalid slug diagnostics #5909 指出的那个缺口)—— 最小、低风险的改动。
  • 仅在 macOS 验证(与 PR Tested on 表一致)。
  • 合并显示 BLOCKED = REVIEW_REQUIRED(需维护者 approve);mac/win/tmux-testing 这些 required check 是 SKIPPED 占位,不是失败 —— 与代码正交。

@wenshao

wenshao commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

✅ Maintainer local verification — PASS (with one important merge caveat)

I built and ran this PR locally on a clean worktree at the PR head (ca167ce) on macOS, including an adversarial mutation test to confirm the guard is load-bearing. Everything matches the PR's claims. There is one blocker for the merge order, not the code: this PR overlaps and conflicts with #5914 — only one of the two can land. Details at the bottom.

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. Reviewer Test Plan — all reproduce

Check Result
bun test src/credential-cache.test.ts 5 pass, 0 fail (11 expects) ✅
bun run build pass (bundled 391 modules) ✅
bunx tsc -p tsconfig.json --noEmit --allowImportingTsExtensions clean (exit 0)
git diff --check HEAD no whitespace errors

2. Adversarial mutation test — the guard is load-bearing

A passing test only matters if it fails when the fix is removed. I reverted getCredentialCachePath to the old direct join (join(workspaceRootPath, 'sources', sourceSlug, …), bypassing getSourcePath). The traversal test then fails — the path resolves out of the sources/ directory to where the planted secret lives, and no longer throws:

expect(() => getCredentialCachePath(root, '../sessions')).toThrow('Invalid source slug…')
  →  Received function did not throw
  →  Received value: ".../session-credential-cache-UAvlF4/sessions/.credential-cache.json"   // escaped sources/

Restoring the getSourcePath() call makes it throw again. So routing through the validated helper is what actually stops the traversal. ✅

I also confirmed getSourcePath (in session-tools-core/src/source-helpers.ts) calls assertValidSourceSlug (regex /^[a-z0-9]+(?:-[a-z0-9]+)*$/), so reusing it — instead of re-implementing the guard — is correct and keeps this call site consistent with getSourceConfigPath/getSourceGuidePath.

3. The "throw on invalid slug" design is safe

This PR intentionally lets an invalid slug throw (the getCredentialCachePath call sits outside the try), so getToken/hasValidCredentials propagate it rather than returning null. I traced the blast radius:

  • The throw flows into source-oauth.ts (ctx.credentialManager.getToken, lines 169/256/334), which has no local catch
  • …but the top-level CallToolRequestSchema dispatch wraps every handler in try/catch (index.ts:525) and converts any throw into a graceful errorResponse("Tool '…' failed: Invalid source slug: …"). No subprocess crash, no unhandled rejection.
  • In normal flow source.config.slug is already validated at source-load time (sources load through the same getSourcePath-based helpers), so this throw is a defense-in-depth backstop, not a hot path.

Net: invalid slugs surface as a loud, diagnostic tool error instead of looking like "no cached credential". That is a defensible choice (see comparison below).

4. Small positives worth noting

  • Replacing existsSync + readFileSync with a single readFileSync + ENOENT check (isMissingFileError) removes a TOCTOU window and one syscall — missing files stay silent, only corrupt/unreadable content logs via the injected onReadError.
  • Test coverage is genuinely broad for this slice: valid / missing / expired / empty / corrupt-JSON / traversal, all asserted.

⚠️ 5. Merge caveat — this PR conflicts with #5914 (pick one)

Both PRs target #5909 and create the same new file session-mcp-server/src/credential-cache.ts. A real merge test (git merge-tree) confirms 3 conflicts:

CONFLICT (add/add):  credential-cache.ts
CONFLICT (add/add):  credential-cache.test.ts
CONFLICT (content):  index.ts

They are mutually exclusive — whichever lands first forces the other to be reworked. They also differ in philosophy and scope:

Aspect #5913 (this PR) #5914
Scope credential-cache call site only credential-cache + validator path sanitization + source-permissions RPC diagnostics + Bun test-isolation fix
Invalid slug → throws → surfaces as a tool error (loud/diagnostic) returns null (treated as a missing credential, quiet)
Guard reuses getSourcePath() calls assertValidSourceSlug() directly
Cache-read diagnostics injected onReadError logger; ENOENT-vs-corrupt split; logs corrupt JSON plain catch → null (no logger)
Tests on this file valid/missing/expired/empty/corrupt/traversal valid/traversal/null-on-invalid
Issue link "Refs #5909" "Closes #5909"

Both are correct and safe — I verified each independently (the #5914 report is on that PR). My suggestion for the maintainer:

CI / merge state

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

Verdict

On its own merits, #5913 is correct, safe, well-tested, and ready to merge. The only decision required is #5913 vs #5914 — they cannot both land. 👍

🇨🇳 中文版(完整对应)

✅ 维护者本地验证 —— 通过(但有一个重要的合并提醒)

我在 PR head(ca167ce)的干净 worktree 上、在 macOS 本地构建并运行了本 PR,并做了对抗性变异测试确认 guard 是有效负载。所有结果都与 PR 声明一致。有一个阻塞项,针对的是合并顺序而不是代码本身:本 PR 与 #5914 重叠并冲突——两者只能合一个。详见末尾。

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

1. Reviewer Test Plan —— 全部复现

检查 结果
bun test src/credential-cache.test.ts 5 通过,0 失败(11 expects)✅
bun run build 通过(打包 391 模块)✅
bunx tsc -p tsconfig.json --noEmit --allowImportingTsExtensions 干净(exit 0)
git diff --check HEAD 无空白字符错误

2. 对抗性变异测试 —— guard 是有效负载

测试通过只有在“把修复删掉后会失败”时才有意义。我把 getCredentialCachePath 回退成旧的直接拼接(join(workspaceRootPath, 'sources', sourceSlug, …),绕过 getSourcePath)。穿越测试随即失败——路径解析到了 sources/ 目录之外(即植入 secret 的位置),并且不再抛错:

expect(() => getCredentialCachePath(root, '../sessions')).toThrow('Invalid source slug…')
  →  Received function did not throw
  →  Received value: ".../session-credential-cache-UAvlF4/sessions/.credential-cache.json"   // 逃出了 sources/

恢复 getSourcePath() 调用后又会抛错。所以“走已校验的 helper”才是真正挡住穿越的东西。✅

我还确认了 getSourcePath(在 session-tools-core/src/source-helpers.ts)会调用 assertValidSourceSlug(正则 /^[a-z0-9]+(?:-[a-z0-9]+)*$/),所以复用它(而不是重写一遍 guard)是正确的,并且让这个调用点与 getSourceConfigPath/getSourceGuidePath 保持一致。

3. “非法 slug 抛错”的设计是安全的

本 PR 有意让非法 slug 抛错getCredentialCachePath 调用放在 try 外面),因此 getToken/hasValidCredentials 会把它向上抛而不是返回 null。我追踪了影响范围:

  • 抛错流入 source-oauth.tsctx.credentialManager.getToken,169/256/334 行),那里没有局部 catch……
  • ……但顶层 CallToolRequestSchema 派发用 try/catch 包住了每个 handlerindex.ts:525),把任何抛错转成优雅的 errorResponse("Tool '…' failed: Invalid source slug: …")不会让子进程崩溃,也不会有 unhandled rejection。
  • 正常流程中 source.config.slug 在 source 加载时就已校验(source 走的是同一套基于 getSourcePath 的 helper),所以这个抛错是纵深防御的兜底,不是热路径。

结论:非法 slug 会表现为一个清晰、可诊断的工具错误,而不是被当成“没有缓存凭证”。这是一个站得住脚的选择(见下方对比)。

4. 几个值得肯定的小点

  • 用单次 readFileSync + ENOENT 判断(isMissingFileError)替代 existsSync + readFileSync,消除了一个 TOCTOU 窗口、少一次系统调用——缺失文件保持静默,只有损坏/不可读内容才通过注入的 onReadError 记日志。
  • 对这个切片来说测试覆盖确实全面:valid / missing / expired / empty / 损坏 JSON / 路径穿越,都有断言。

⚠️ 5. 合并提醒 —— 本 PR 与 #5914 冲突(二选一)

两个 PR 都针对 #5909,并且创建了同一个新文件 session-mcp-server/src/credential-cache.ts。真实合并测试(git merge-tree)确认了 3 处冲突

CONFLICT (add/add):  credential-cache.ts
CONFLICT (add/add):  credential-cache.test.ts
CONFLICT (content):  index.ts

它们互斥——谁先合,另一个就得返工。两者在理念和范围上也不同:

维度 #5913(本 PR) #5914
范围 仅 credential-cache 调用点 credential-cache + validator 路径脱敏 + source-permissions RPC 诊断 + Bun 测试隔离修复
非法 slug → 抛错 → 表现为工具错误(清晰/可诊断) 返回 null(当成缺失凭证,安静)
guard 复用 getSourcePath() 直接调 assertValidSourceSlug()
cache 读取诊断 注入 onReadError logger;区分 ENOENT 与损坏;记录损坏 JSON 简单 catch → null(无 logger)
该文件的测试 valid/missing/expired/empty/损坏/穿越 valid/穿越/非法返回 null
issue 链接 “Refs #5909 “Closes #5909

两者都正确且安全——我分别独立验证过(#5914 的报告在那个 PR 上)。给维护者的建议:

CI / 合并状态

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

结论

就其自身而言,#5913 正确、安全、测试充分,可以合并。 唯一需要决策的是 #5913 vs #5914 —— 两者不能都合。👍

@tt-a1i

tt-a1i commented Jun 27, 2026

Copy link
Copy Markdown
Contributor Author

Looks like #5914 has landed and covers the same credential-cache path. Closing this one to avoid keeping a conflicting duplicate open. Thanks for the verification.

@tt-a1i tt-a1i closed this Jun 27, 2026
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.

3 participants