Skip to content

fix(core): Sanitize internal daemon secrets from shell subprocess environments#6606

Open
jadelike-wine wants to merge 21 commits into
QwenLM:mainfrom
jadelike-wine:main
Open

fix(core): Sanitize internal daemon secrets from shell subprocess environments#6606
jadelike-wine wants to merge 21 commits into
QwenLM:mainfrom
jadelike-wine:main

Conversation

@jadelike-wine

@jadelike-wine jadelike-wine commented Jul 9, 2026

Copy link
Copy Markdown

What this PR does

Adds a shared environment sanitization utility for child processes and applies it to model-controlled execution paths.

The shell execution path now removes Qwen internal daemon secrets, such as QWEN_SERVER_TOKEN, before spawning child processes.

The sanitization scope intentionally only covers internal Qwen credentials. User-provided credentials required for normal workflows, such as GitHub, AWS, and npm authentication variables, remain available to shell commands.

Why it's needed

Shell subprocesses previously inherited the full daemon process environment through process.env.

This allowed model-controlled shell commands to access daemon-level credentials that should never be exposed outside the Qwen Code process boundary.

For example, QWEN_SERVER_TOKEN could be read from shell commands executed by the agent.

This change closes that defense-in-depth gap while preserving existing user workflows that rely on inherited third-party credentials.

Reviewer Test Plan

How to verify

  1. Set a fake daemon secret:

export QWEN_SERVER_TOKEN=test-secret-token

  1. Start Qwen Code and run a shell command:

printenv QWEN_SERVER_TOKEN

Expected:
The variable should not be visible in the shell subprocess environment.

  1. Verify user workflow credentials are preserved:

export GH_TOKEN=test-github-token

Run:

printenv GH_TOKEN

Expected:
GH_TOKEN should still be available to shell commands.

Evidence (Before & After)

N/A - Non-user-visible security hardening change.

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ✅ tested
🐧 Linux N/A

Environment (optional)

Qwen Code v0.19.8
macOS arm64
Node.js v24.3.0

Verified with local CLI execution and unit tests.

Risk & Scope

  • Main risk or tradeoff:
    The sanitization intentionally only removes internal Qwen daemon secrets. Third-party user credentials remain available to preserve shell workflows such as gh, aws, and npm commands.

  • Not validated / out of scope:
    This PR does not change user credential handling or introduce permission prompts for shell environment access.

  • Breaking changes / migration notes:
    Commands that previously relied on accessing Qwen internal daemon credentials from the shell environment will no longer work.

Linked Issues

Fixes #6601

中文说明

本 PR 做了什么

新增共享环境变量清理逻辑,并应用到受模型控制的子进程执行路径。

Shell 子进程启动前会移除 Qwen 内部 daemon 密钥,例如 QWEN_SERVER_TOKEN。

清理范围仅针对 Qwen 内部凭据,不影响用户正常使用 GitHub、AWS、npm 等需要环境变量认证的工作流。

为什么需要

之前 shell 子进程会完整继承 daemon 的 process.env。

这导致模型执行的 shell 命令可能读取 daemon 级别的认证信息。

例如 QWEN_SERVER_TOKEN 可能通过 shell 命令暴露。

本修改关闭该安全风险,同时保持用户现有工作流兼容。

…tive with ACP child

Extract the env-scrub helper from acp-bridge into core as shared
`scrubChildEnv` and `collectSensitiveShellEnvKeys`, and use both to
strip secret-semantic env vars (API keys, tokens, credentials, passwords,
private keys, …) before spawning the shell subprocess. The model's own
commands run there — including auto-allowed read-only `printenv`/`env` — so
inherititing the daemon's env verbatim would let a `printenv
QWEN_SERVER_TOKEN` exfiltrate tokens straight into the tool result.

The ACP child deliberately keeps a NARROW scrub set because it is itself a
`qwen` agent that MUST inherit provider API keys to call models; the two
paths share one primitive but NOT one denylist — unifying them would
reintroduce the leak or break the child. `collectSensitiveShellEnvKeys`
drives the shell path's BROAD set, parametrized on the live `process.env`.
…scrub primitive(QwenLM#6601)

Drop the broad user-credential denylist (GH_TOKEN, AWS_*, NPM_TOKEN, …) from
child-env scrubbing — those belong to normal shell/MCP workflows and were
wrongly stripped. Now only daemon/internal Qwen keys are stripped, and the
scrub/collect primitive is shared between core (ACP/shell/MCP) and desktop
(session-tools-core / mcp client) subprocess paths.
@jadelike-wine jadelike-wine changed the title Sanitize internal daemon secrets from shell subprocess environments fix(core): Sanitize internal daemon secrets from shell subprocess environments Jul 9, 2026
@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Qwen precheck requires maintainer approval before automated triage/review.

Head SHA: f041bb5768d0dd5e4367f58ceb583cf48ff4eefd

Reason:

  • sensitive_diff:secret_logging
  • secret_value:assignment

A maintainer with write access can inspect the PR and manually request a run with @qwen-code /triage or @qwen-code /review. A new push requires a fresh precheck.

Comment thread packages/core/src/tools/tool-registry.ts
Comment thread packages/desktop/packages/session-tools-core/src/runtime/sandbox-env.ts Outdated
jadelike-wine and others added 2 commits July 11, 2026 12:38
Windows environment variables are case-insensitive, so PATH and Path can both
exist. Normalize them before scrubbing so discovery + execution child
processes receive a single merged PATH and never a split/partial value.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
… subprocess env

Untrusted scripts spawned by the desktop session tools were inheriting
provider API keys. Block LLM/AWS/GitHub/Google/Stripe/NPM credential keys in
the sanitized environment alongside existing daemon-internal scrubbing.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
@jadelike-wine
jadelike-wine requested a review from wenshao July 11, 2026 04:47
Comment thread packages/core/src/services/shellExecutionService.ts
Comment thread packages/core/src/utils/child-env-scrub.ts Outdated
Comment thread packages/desktop/packages/session-tools-core/src/runtime/sandbox-env.ts Outdated
Comment thread packages/desktop/packages/shared/src/mcp/client.ts Outdated
Comment thread packages/desktop/packages/shared/src/mcp/validation.ts Outdated
Comment thread packages/desktop/packages/shared/src/mcp/client.ts Outdated
Comment thread packages/core/src/services/shellExecutionService.ts

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

[Critical] packages/desktop/packages/shared/src/mcp/validation.ts:309 — The production StdioClientTransport at line 309 uses unsanitized { ...processEnv, ...env } while the probe at line 271 (changed by this diff) correctly uses createSanitizedChildEnv. The MCP handshake subprocess inherits QWEN_SERVER_TOKEN.

— qwen3.7-max via Qwen Code /review

Comment thread packages/core/src/utils/child-env-scrub.test.ts
Comment thread packages/acp-bridge/src/spawnChannel.ts
@jadelike-wine
jadelike-wine requested a review from wenshao July 13, 2026 02:16
@wenshao
wenshao requested a review from Copilot July 13, 2026 03:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@wenshao
wenshao requested a review from Copilot July 13, 2026 07:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@wenshao
wenshao requested a review from Copilot July 13, 2026 08:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@yiliang114 yiliang114 added this to the v1.0.0 milestone Jul 13, 2026
Comment thread packages/cli/src/serve/run-qwen-serve.ts
@wenshao
wenshao requested a review from Copilot July 13, 2026 11:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not reviewed: reverse audit of chunks 2, 3, 5, 8, 11 — the auditors failed to execute (subagent execution failed).

Not reviewed: reverse audit — its prompt was built, but no agent was launched with it that opened its brief, so the reverse-audit pass did not run.

[Critical] packages/cli/src/serve/run-qwen-serve.ts:3220 — persistDisabledSkillsFn calls loadSettings(workspace) without credentialStore. This is the only remaining daemon call site that omits it. When loadSettings triggers loadEnvironment, any QWEN_CUSTOM_API_KEY_* in workspace .env files is written back to process.env (since credentialStore is undefined, writeEnvKey falls back to process.env), re-entering the daemon's OS-visible environment after scrubDaemonProcessEnv removed it. Fix: pass { credentialStore } to the loadSettings call.

— qwen-latest-series-invite-beta-v92 via Qwen Code /review

Comment thread packages/desktop/packages/shared/src/agent/__tests__/qwen-agent-models.test.ts Outdated
Comment thread packages/desktop/packages/shared/src/mcp/validation.test.ts
Comment thread packages/cli/src/serve/voice/resolve-voice-config.ts Outdated
Comment thread packages/core/src/providers/install.ts
Comment thread packages/core/src/models/modelsConfig.ts
…ests

- run-qwen-serve.ts: persistDisabledSkillsFn passes credentialStore to
  loadSettings so workspace .env QWEN_CUSTOM_API_KEY_* is not written
  back to process.env after scrubDaemonProcessEnv
- qwen-agent.ts: extract buildQwenAcpEnv() exported helper from private
  SharedQwenAcpProcess.buildEnv so the env-scrub path is unit-testable
- validation.test.ts: preserve spawnSync when mocking child_process by
  spreading realChildProcess before overriding spawn
- resolve-voice-config.ts: drop dead credentialProvider param from
  buildModelsConfig; voice path uses mergeCredentialStore instead
- install.test.ts: cover credential-store redirection and rollback on
  refreshAuth failure
- modelsConfig.ts: debug message references credential provider instead
  of process.env

Addresses wenshao review on PR QwenLM#6606 (2026-07-18).
@jadelike-wine
jadelike-wine requested a review from wenshao July 18, 2026 08:33
@wenshao

wenshao commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

…nv features

Merge origin/main into pr-6606 branch, resolving conflicts in:
- run-qwen-serve.ts: keep both credentialStore (PR) and skillInstallEnv (main)
  properties in all three workspace service creation sites
- workspace-skills-status.ts: combine PR's credentialStore passthrough with
  main's invalidate method on WorkspaceSkillsStatusProvider
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code resolved the merge conflicts and pushed the branch update.

Merge Conflict Resolution Summary — PR #6606

Branch: qwen-resolve/pr-6606 merged with origin/main

Conflicted Files

1. packages/cli/src/serve/run-qwen-serve.ts (3 conflict regions)

Root cause: PR #6606 adds credentialStore as a property on workspace service creation calls (to support credential-store-aware settings loading). Meanwhile, origin/main added skillInstallEnv as a new property on the same calls (to pass the runtime effective env for skill installation). Both changes touch the same object literal locations.

Resolution: Kept both properties in all three workspace service creation sites:

  • Primary workspace (~line 3443): added credentialStore, and skillInstallEnv: runtimeEffectiveEnv,
  • Secondary workspace (~line 3773): kept createWorkspaceSkillsStatusProvider(credentialStore) (PR), added credentialStore, and skillInstallEnv: secondaryEnv.effectiveEnv,
  • Dynamic workspace (~line 4163): kept createWorkspaceSkillsStatusProvider(credentialStore) (PR), added credentialStore, and skillInstallEnv: wsEnv.effectiveEnv,

2. packages/cli/src/serve/workspace-skills-status.ts (1 conflict region)

Root cause: PR #6606 passes credentialStore through createWorkspaceSkillsStatusProvider to buildWorkspaceSkillsStatus (for credential-store-aware disabled-skill reads). Meanwhile, origin/main refactored the return value to attach an invalidate method to the provider (allowing cache invalidation of the skill manager per workspace).

Resolution: Combined both changes — the provider now:

  1. Passes credentialStore through to buildWorkspaceSkillsStatus (PR's intent)
  2. Returns a provider object with the invalidate method (main's intent)

Commit

merge: resolve conflicts between credential store and skill install env features

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

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

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

Qwen Code · serve A/B

@wenshao
wenshao requested a review from Copilot July 18, 2026 16:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@wenshao

wenshao commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code resolved the merge conflicts and pushed the branch update.

Merge Conflict Resolution Summary

PR: #6606 — fix(core): Sanitize internal daemon secrets from shell subprocess environments
Base branch: main
Commit: merge: resolve conflicts between daemon secret sanitization and test runtime isolation

Conflicted file

packages/cli/src/serve/run-qwen-serve.test.ts

Conflict: The vitest import line at the top of the file.

  • HEAD (PR branch): imported beforeEach (added by PR's test cases at lines 8335, 8408) but not afterAll.
  • origin/main: imported afterAll (added by main's test runtime dir isolation cleanup at line 74) but not beforeEach.

Resolution: Combined both sides — the import now includes both beforeEach and afterAll:

import { describe, it, expect, vi, afterEach, beforeEach, afterAll } from 'vitest';

Both symbols are actively used in the file, so neither side could be dropped.

Auto-merged file (no manual intervention needed)

  • packages/cli/src/serve/run-qwen-serve.ts — Git auto-merged cleanly.

Comment thread packages/core/src/utils/editor.ts
Comment thread packages/core/src/utils/editor.ts
Comment thread packages/cli/src/serve/routes/workspace-settings.ts
Comment thread packages/cli/src/serve/workspace-service/index.ts
Comment thread packages/cli/src/serve/server.ts
Comment thread packages/cli/src/serve/workspace-service/index.ts Outdated
Comment thread packages/desktop/packages/shared/src/mcp/client.ts Outdated
Comment thread packages/desktop/packages/shared/src/mcp/validation.ts Outdated
Comment thread packages/core/src/core/baseLlmClient.ts Outdated
Comment thread packages/core/src/lsp/LspServerManager.test.ts
@jadelike-wine
jadelike-wine requested a review from wenshao July 19, 2026 05:59
@wenshao
wenshao requested a review from Copilot July 19, 2026 08:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@wenshao

wenshao commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Round-5 re-verification @ 2de8c5d13

Re-verified all 11 findings from my 2026-07-19 review plus the round-3 items, on Linux against the current head. 10 of 11 are fixed and verified. One Critical from the previous round is still present despite the "Fixed" reply, and qqqys's fast-path bundle blocker is also still present — now reproduced by CI on this exact head. Two blocking items total; details below.

Status of previous findings

# Finding (round 4) Status
1 editor.test.ts not updated → ubuntu CI red ✅ Fixed — 152 tests pass locally; CI test steps now pass on head
2 Terminal-editor spawnSync unscrubbed ✅ Fixed (editor.ts:337)
3 prepareSettingWrite re-leak (no credentialStore) Still present — see Critical A
4 setWorkspaceSkillEnabled re-leak ✅ Fixed (workspace-service/index.ts:708)
5 Qualified voice routes missing store ✅ Fixed — createRuntimeVoiceDeps merges per request
6 Qualified voice WS missing store ✅ Fixed — per-connection merge in server.ts
7 setWorkspaceVoiceSettings validation env ✅ Fixed — store snapshot merged before validateWorkspaceVoiceState
8 Desktop mcp/client.ts drops user config.env credentials ✅ Fixed + test
9 Desktop mcp/validation.ts same override-drop ✅ Fixed + tests (explicit GITHUB_TOKEN covered)
10 Inert ConfigParameters.credentialProvider plumbing ✅ Fixed — removed
11 LspServerManager.test.ts leaked env stubs ✅ Fixed — vi.unstubAllEnvs()

Round-3 Critical (persistDisabledSkillsFn without store) is also fixed, with a non-vacuous test (run-qwen-serve.test.ts writes a workspace .env custom key and asserts process.env stays clean).


[Critical A] prepareSettingWrite still re-leaks custom keys into the daemon's OS-visible env

packages/cli/src/serve/routes/workspace-settings.ts:185 — unchanged from my previous review:

const existing =
  loadSettings(workspace).forScope(scope).settings.mcpServers ?? {};

The reply on that thread says "Fixed in the updated persistence path", but the fix only covered the persistSetting/persistSettings closures in run-qwen-serve.ts. prepareSettingWrite is a different call site and is reached from both the primary route (workspace-settings.ts:391, POST /workspace/settings) and the qualified route (workspace-settings.ts:581) whenever the key is mcpServers.

Failure scenario, unchanged from before: daemon boots → scrubDaemonProcessEnv() removes QWEN_CUSTOM_API_KEY_* from process.env → a client writes the mcpServers setting → loadSettings(workspace) runs loadEnvironment(…, undefined) → for a custom key present in the workspace .env or settings.env (which is exactly where applyProviderInstallPlan persists installed provider keys), readEnvKey sees it unset and writeEnvKey(key, value, undefined) falls back to process.env — the credential re-enters /proc/<pid>/environ / ps eww, defeating the boot scrub.

Fix: thread credentialStore into prepareSettingWrite and call loadSettings(workspace, { credentialStore }) — or { skipLoadEnvironment: true }, since this call only reads mcpServers from settings scopes and doesn't need env application at all.

[Critical B] check:serve-fast-path-bundle fails on this head — qqqys's blocker is unaddressed

packages/cli/src/serve/run-qwen-serve.ts:48-51 still has the module-scope value import:

import {
  createCredentialStore,
  createCredentialProvider,
} from '@qwen-code/qwen-code-core';

run-qwen-serve.ts is one of the checker's SERVE_PRE_LISTEN_ROOTS, and the core public barrel statically re-exports from forbidden modules (e.g. packages/core/src/index.ts:175 re-exports from ./tools/shell.js) and drags the forbidden vendor packages. This is not hypothetical — on this exact head (2de8c5d13), Test (ubuntu-latest, Node 22.x) now gets past all test steps (the editor.test.ts fix worked) and fails at precisely the "Check serve fast-path bundle closure" step:

Serve fast-path bundle closure includes pre-listen runtime modules:
- glob vendor package
  input: node_modules/glob/node_modules/brace-expansion/index.js
  static path: dist/chunks/run-qwen-serve-K6TPFIRT.js -> dist/chunks/chunk-P4NFORE5.js
- @iarna/toml vendor package
  input: node_modules/@iarna/toml/lib/parser.js
  static path: dist/chunks/run-qwen-serve-K6TPFIRT.js -> dist/chunks/chunk-P4NFORE5.js
  ... (5,747,889-byte chunk pulled into the pre-listen closure)

The type-only imports (CredentialStore) are fine — they erase at compile time. The value import is the problem. Since captureDaemonCredentialStore() runs pre-listen, a post-listen dynamic import isn't the right shape either. credential-provider.ts is dependency-free (pure functions over a Map), so the clean fixes are: add a slim subpath export (e.g. @qwen-code/qwen-code-core/credentialsmodels/credential-provider.js) and import from that, or lift the three tiny factories into a module the fast path is allowed to reach.


New findings (this round)

[Important] ide-client.ts:1083 widens the IDE stdio child env instead of narrowing it. On main, new StdioClientTransport({ command, args }) passes no env, so the MCP SDK uses getDefaultEnvironment() — only HOME/LOGNAME/PATH/SHELL/TERM/USER (plus the Windows equivalents), i.e. zero credentials. This PR changes it to the full process.env minus the 4 internal Qwen keys, so the IDE companion child now inherits OPENAI_API_KEY, GH_TOKEN, AWS_*, and everything else it never saw before — the opposite of this PR's goal, and the updated test (ide-client.test.ts asserting OPENAI_API_KEY is preserved) locks the widening in. Fix: drop the env option (the SDK default is already credential-free), or pass scrubChildEnv(getDefaultEnvironment(), …) if you want the scrub to be explicit.

[Suggestion] Primary-workspace voice env is a boot-time snapshot; qualified routes re-merge per request. In createServeApp the primary route gets env: mergeCredentialStore(getRuntimeEffectiveEnv(primaryRuntime.env), deps.credentialStore) evaluated once, and workspace-voice.ts then uses that frozen deps.env for validation (:448) and transcription (:568). A provider installed or a key rotated after boot updates the store but never reaches the primary route's env, while /workspaces/:workspace/voice/* rebuilds via createRuntimeVoiceDeps per request and stays fresh. In practice readApiKey's settings.env fallback rescues installed providers, so this is consistency rather than breakage — but a per-request merge (mirroring createRuntimeVoiceDeps) would remove the asymmetry. Related nit: resolve-voice-config.ts:96 still calls loadSettings(workspaceCwd, options.env ? { skipLoadEnvironment: true } : true) without credentialStore; harmless today because every daemon caller supplies env, but it's the same class as Critical A if a daemon path ever calls it without one.

[Suggestion] Desktop ACP children lose QWEN_CUSTOM_API_KEY_* with no re-injection path. buildQwenAcpEnv (desktop qwen-agent.ts) scrubs the custom-key pattern and scrubChildEnv refuses override re-introduction — but unlike the CLI daemon's ACP spawn (which injects credentialStore.snapshot() into sourceEnv precisely because ACP children are agents that call models), the desktop has no equivalent. The child normally recovers the keys from its own settings.env at boot, so the standard install flow works; a key that exists only in the parent environment (manually exported) now silently breaks custom-provider auth in desktop sessions. Worth a comment in buildQwenAcpEnv documenting the recovery assumption, or aligning with the ACP policy of the CLI path.


Verification evidence

  • Head greps confirming each fixed call site (workspace-service/index.ts:508/515/522/592/669/708, all ~20 loadSettings sites in run-qwen-serve.ts carry credentialStore, qualified voice/WS merges, desktop client/validation override order).
  • Local test runs on the PR head: editor.test.ts 152 ✅, child-env-scrub.test.ts 11 ✅, credential-provider.test.ts 29 ✅, child-env-scrub.integration.test.ts (real node -e spawns through ShellExecutionService, both PTY and child_process paths) 7 ✅, acp-bridge spawnChannel.test.ts 32 ✅.
  • CI on head 2de8c5d13: every test step green; sole failure is the fast-path closure step quoted above.

The scrub direction is right on every other spawn path I audited (each strictly narrows the child env), the two-context ACP-vs-shell policy documentation in spawnChannel.ts is genuinely useful, and the test coverage — Windows case-insensitivity, override-smuggling, real-spawn integration — is exemplary. Once A and B are fixed (both are small, well-localized changes) this is mergeable from my side.

中文摘要

第 5 轮复核(head 2de8c5d13:上一轮 11 条 findings 中 10 条已修复并验证通过(含 editor 测试、终端编辑器 scrub、语音 qualified 路由、桌面端 MCP override 保留等)。仍有 2 条阻塞项

  1. [Critical A] workspace-settings.ts:185prepareSettingWrite 仍以无 credentialStore 的方式调用 loadSettings(workspace)。回复说"已修复",但实际只修了 run-qwen-serve.ts 里的 persistSetting 闭包,这个调用点没改。写入 mcpServers 设置(主路由 :391 与 qualified 路由 :581 均可触发)会经 loadEnvironment(…, undefined) 把 workspace .env / settings.env 中的 QWEN_CUSTOM_API_KEY_* 写回守护进程的 process.env,重新暴露给 ps eww / /proc/<pid>/environ,绕过本 PR 的启动时清理。修复:传 { credentialStore }{ skipLoadEnvironment: true }

  2. [Critical B] qqqys 指出的 fast-path 阻塞项未处理:run-qwen-serve.ts:48-51 仍在模块顶层以值导入方式引入 core barrel。当前 head 的 CI 已实际复现:Test (ubuntu) 所有测试步骤通过后,恰好在 "Check serve fast-path bundle closure" 一步失败(run-qwen-serve-K6TPFIRT.js 把 glob、@iarna/toml 等禁止的 vendor 包拖进 5.7MB 的 pre-listen chunk)。credential-provider.ts 本身零依赖,建议增加 slim 子路径导出(如 @qwen-code/qwen-code-core/credentials)或把这三个小工厂函数移到 fast path 允许到达的模块。

新发现:[Important] ide-client.ts:1083 把 IDE stdio 子进程环境从 SDK 默认的最小集合(约 6 个无凭据变量)扩大为完整 process.env 减 4 个内部键 —— 与本 PR 目标相反,OPENAI_API_KEY/GH_TOKEN/AWS_* 等首次暴露给该子进程;建议去掉 env 选项或改为 scrub getDefaultEnvironment()。另有两条 Suggestion:主 workspace 语音路由的 env 是启动时快照(qualified 路由是每请求合并,存在不一致);桌面端 ACP 子进程剥离 QWEN_CUSTOM_API_KEY_* 且无重注入机制(正常安装流程可经 settings.env 恢复,仅手动 export 的场景会静默失效)。

本地验证:editor 152 / scrub 11 / credential-provider 29 / 真实 spawn 集成 7 / spawnChannel 32 全部通过。A、B 两处改动都很小,修复后我这边即可合入。

Allow the serve layer to pass a credentialStore into loadSettings so
workspace-scoped MCP server configuration can resolve stored credentials
instead of relying on the default resolution path. Adds the /credentials
subpath export on @qwen-code/qwen-code-core for the serve fast-path bundle,
and drops the now-unused env scrubbing from IdeClient's stdio transport.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
@jadelike-wine

Copy link
Copy Markdown
Author

Addressed the remaining Round-5 blockers in e0293d2:

  • Critical A: prepareSettingWrite now receives the daemon credentialStore and calls loadSettings(workspace, { credentialStore }); both the primary and qualified workspace settings routes supply it. Added a regression test for the MCP-settings read path.
  • Critical B: moved the two runtime credential factories off the core public barrel to the new dependency-free @qwen-code/qwen-code-core/credentials subpath, with explicit type/import exports.
  • IDE stdio finding: removed the explicit env from StdioClientTransport, restoring the MCP SDK default minimal environment. Updated both file-config and environment-config stdio tests.

Verified locally:

  • packages/cli: workspace-settings.test.ts — 15 passed
  • packages/core: ide-client.test.ts — 65 passed
  • git diff --check

I could not complete the local full typecheck / serve-fast-path bundle run because the installed local ink package is missing getFrameController and related exports used by the existing CLI selection UI. This is outside the changed paths; CI will provide the clean-environment bundle verification.

@yiliang114

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code resolved the merge conflicts and pushed the branch update.

Merge conflict resolution for PR #6606

Root cause

PR #6606 (fix(core): Sanitize internal daemon secrets from shell subprocess environments) introduced a scrubChildEnv + collectSensitiveShellEnvKeys API in child-env-scrub.ts and threaded credentialStore/credentialProvider through the daemon's settings-loading and workspace-service construction paths. Meanwhile, origin/main independently introduced a parallel but simpler sanitizeChildEnv API in sanitize-child-env.ts (commit for issue #6601), plus a large workspace-trust feature (workspaceTrusted, generationGuard, assertGenerationOpen, loadSettingsForPersistence, trust-gated routes) that touched many of the same files.

The two changes collided on 28 files: every spawn site, every loadSettings() call in the serve layer, and every interface/type that carries daemon configuration.

Textual or semantic

Both textual and semantic. The core env-sanitization conflicts are semantic — both sides replace the same env construction at spawn sites but with different APIs:

// PR (#6606): parameterized denylist + anti-reintroduction guard
env: scrubChildEnv(
  normalizePathEnvForWindows(process.env),
  collectSensitiveShellEnvKeys(process.env),
  { QWEN_CODE: '1', TERM: 'xterm-256color', ...overrides },
),

// Base (#6601): simple delete-list spread
env: {
  ...normalizePathEnvForWindows(sanitizeChildEnv(process.env)),
  QWEN_CODE: '1', TERM: 'xterm-256color', ...overrides,
},

Resolution: kept the PR's scrubChildEnv form everywhere (6 spawn sites in shellExecutionService.ts, hookRunner.ts, mcp-client.ts, monitor.ts, tool-registry.ts). The PR's API is strictly more defensive — scrubChildEnv prevents overrides from re-introducing scrubbed keys, which the base's spread-based approach cannot guarantee.

The remaining ~22 conflicts are textual — both sides added new fields to the same interfaces or new parameters to the same function calls (credentialStore from the PR, workspaceTrusted/generationGuard/etc. from the base). All were resolved by keeping both sets of additions.

What is load-bearing

  1. scrubChildEnv must be used at every spawn site, not sanitizeChildEnv. The PR's child-env-scrub.ts covers a broader denylist (including QWEN_CODE_SIMPLE and QWEN_CUSTOM_API_KEY_* regex) and enforces anti-reintroduction. If any spawn site regresses to sanitizeChildEnv, custom API keys could leak to subprocesses.

  2. credentialStore must be threaded alongside workspaceTrusted in every loadSettings() call. The loadSettingsForPersistence helper in run-qwen-serve.ts was updated to include both. If credentialStore is dropped from any persistence path, QWEN_CUSTOM_API_KEY_* values will be written to process.env instead of the store.

  3. createSpawnChannelFactory sourceEnv must remain a callback (() => ({...env, ...credentialStore.snapshot()})), not a static object. ACP children need fresh credential snapshots on each spawn.

  4. package.json exports: ./credentials (PR) must coexist with ./transcriptRecords, ./goalWire, ./package.json, ./dist/*, ./src/* (base). All six entries are present.

What I could not verify

No build, typecheck, lint, or tests were run. Specific risks:

  • loadSettings option type: the merged code passes credentialStore alongside skipLoadEnvironment, skipWorkspaceSettings, and workspaceTrusted in a single options object. If the LoadSettingsOptions interface doesn't accept all these fields simultaneously, TypeScript will reject it. The interface was updated in settings.ts to include all fields, but the implementation may have validation logic that rejects unknown combinations.

  • createWorkspaceSkillsStatusProvider signature: the PR passes credentialStore as a direct argument while the base passes { workspaceTrusted }. The merged code passes { credentialStore, workspaceTrusted } — if the function only accepts one shape, this will fail.

  • Test files (hookRunner.test.ts, mcp-client.test.ts, workspace-settings.test.ts): the merged tests assert both QWEN_SERVER_TOKEN and QWEN_DAEMON_TOKEN are scrubbed while GH_TOKEN passes through. If the test mocks don't match the current scrubChildEnv behavior for these specific keys, tests will fail.

  • Non-conflicted callers: files that import sanitizeChildEnv from origin/main but were NOT in conflict (e.g., packages/core/src/index.ts re-exports) may still reference the old sanitize-child-env module alongside the PR's child-env-scrub module. Both files exist in the working tree.

中文说明

PR #6606 合并冲突解决

根因:PR #6606 引入了 scrubChildEnv + collectSensitiveShellEnvKeys API 并在守护进程的设置加载和工作区服务构造中传递 credentialStore/credentialProvider。与此同时,origin/main 独立引入了一个更简单的 sanitizeChildEnv API(#6601),以及一个大型工作区信任功能(workspaceTrustedgenerationGuard 等),涉及许多相同文件。共 28 个文件冲突。

解决方式

  • 所有 spawn 站点保留 PR 的 scrubChildEnv(更安全的参数化拒绝列表 + 防重新引入)
  • 所有 loadSettings() 调用同时保留 credentialStoreworkspaceTrusted 等新选项
  • 接口/类型同时保留两侧新增字段
  • 测试合并两侧断言

关键约束scrubChildEnv 必须在每个 spawn 站点使用;credentialStore 必须与 workspaceTrusted 一起传递;createSpawnChannelFactorysourceEnv 必须保持回调形式以获取最新凭据快照。

未验证项:未运行构建/类型检查/测试。LoadSettingsOptions 接口、createWorkspaceSkillsStatusProvider 签名、以及非冲突文件中对 sanitizeChildEnv 的引用可能需要后续检查。

@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Jul 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shell subprocess inherits sensitive environment variables causing credential exposure

7 participants