Repository navigation
fix(core): honor usage-statistics opt-out for extension lifecycle events - #12789
Conversation
ExtensionManager built its throwaway telemetry Config with only the telemetry settings, dropping the resolved usageStatisticsEnabled and proxy. Since Config defaults usageStatisticsEnabled to true and QwenLogger.getInstance is the only opt-out gate, extension install / uninstall / update / enable / disable events were always enqueued and flushed to RUM after 60s, even with privacy.usageStatisticsEnabled=false or QWEN_USAGE_STATISTICS_ENABLED=0; uploads also bypassed the configured proxy. Pass the resolved usageStatisticsEnabled and proxy through ExtensionManagerOptions into the throwaway Config, and wire every mutation-capable construction site: the main session Config, the extensions CLI commands, and the serve daemon's extension controller. The opt-out resolution now shares resolveUsageStatisticsEnabled with the main session chain (env ?? settings ?? true) so they cannot drift. Fixes #12770 Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-issue-patrol/jmuiitwvb1m
The workspace extensions controller now resolves the proxy for extension lifecycle telemetry from the workspace settings first and then from the daemon process proxy environment (#12770), so the serve process.env guard needs the matching documented allowance. The three CLI call sites that read the same variables live outside the scanned serve/ root. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-conflict/jmuinh3e57o
…tation The Config constructor normalizes the proxy eagerly and throws for values a session would reject at startup (e.g. SOCKS). Since #12789 started forwarding the resolved proxy into the throwaway telemetry Config, that throw could escape getTelemetryConfig and fail a user-facing extension command -- in enableExtension after the store mutation had already committed, and regardless of the usage-statistics opt-out, so even users who turned telemetry off could have extension commands broken by a proxy value telemetry would never use. Pre-normalize the proxy in getTelemetryConfig and drop values the session layer rejects, so the upload falls back to a direct connection (the pre-#12789 behavior for that case) instead of aborting the mutation. Addresses the qwen-code-ci-bot stage-3 blocking review finding on #12789. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-issue-patrol/jmuivovhy1q
|
Addressed the stage-3 blocking finding in 40f203c. Throwaway telemetry Verified with a red→green test at the exact call path from the review: On the two non-blocking notes:
Also noting for the record: the |
Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmuj74eda8m
The workspace extension controller resolved its telemetry proxy from the daemon's shared ambient process.env, and the loadSettings() call it hoisted loaded with no skipLoadEnvironment, so a workspace's own .env / settings.env values were written into process.env for the lifetime of the daemon. One daemon hosts every workspace, so a repo-shipped .qwen/.env in workspace A could flip workspace B's usage-statistics opt-out and route B's extension lifecycle uploads through a proxy B never configured, undoing the #12770 contract this PR exists to enforce. - Load settings once per createExtensionManager call, trust-gated and with skipLoadEnvironment, and thread that merged view into resolveExtensionLocale, so the locale, the trust fallback and the telemetry options all read one consistent view of the same workspace file. This also removes the second full loadSettings() per call. - Resolve the telemetry proxy from the workspace settings only. Drop the ambient HTTPS_PROXY/HTTP_PROXY fallback and the process-env-guard allowance entry it required rather than widening the guard. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmuj74eda8m
…ambient env R1 review repair for #12789: - Config gains installProxyDispatcher (default true); the throwaway telemetry Config passes false so building it no longer installs a process-global undici dispatcher / runtime-fetch proxy on every extension lifecycle event, while getProxy() still feeds the RUM logger's own agent (R1-1, R1-13). - The serve extension controller loads settings once, gated on the resolved workspace trust with skipLoadEnvironment, and resolves the usage-statistics consent and telemetry proxy from the owning runtime's injected env instead of the daemon's ambient process.env (R1-2, R1-11). - The five-term proxy precedence chain is extracted into resolveExtensionTelemetryProxy beside resolveUsageStatisticsEnabled and shared by all four call sites (R1-4). - Wiring tests now pin the forwarded usageStatisticsEnabled/proxy at the utils/install/uninstall construction sites, the serve controller, and the core Config -> ExtensionManager handoff (R1-5). The QwenLogger first-writer-wins proxy behavior is documented on the option (R1-3). Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-issue-patrol/jmuj8ju831v
Converge with the parallel closeout commit 93529a0 (R1-2/R1-11): take its settings-only trust-gated controller load wholesale and keep this repair's R1-1 installProxyDispatcher fix, R1-4 proxy-chain helper for the CLI commands, R1-5 wiring tests, R1-3 documentation, and the single-loadSettings-per-manager assertion. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-issue-patrol/jmuj8ju831v
|
R1 repair round landed in
Verification: |
`QwenLogger.getProxyAgent()` pins an `HttpsProxyAgent`, which has no notion of `NO_PROXY`, while the session's own egress installs an `EnvHttpProxyAgent` precisely so `NO_PROXY` is honored. Extension lifecycle telemetry now forwards a resolved proxy into its throwaway Config, so a user with `NO_PROXY=*` or `NO_PROXY=.aliyuncs.com` would have lifecycle events POSTed through a proxy they explicitly excluded; before that forwarding the upload went direct. Apply undici's matching rule (lowercase `no_proxy` precedence, bare `*`, exact host, `.suffix`, port-qualified entries) at the single point where the agent is chosen, evaluated against the RUM upload host. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmujbepw18u
…etry-optout Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmujbepw18u
|
CI attribution for the red The delivered commit is not the cause. The deterministic half was already red one head earlier. On On It is not a main-side break either. Where that points. This branch does change three serve files — Runner load was moderate, not the overload band: Left to whoever owns the serve-side changes on this branch. |
… merge The merge commit 38bd5f6 resolved packages/cli/src/serve/server.ts by keeping the branch side and dropping main's a480bc7 (#12776) form: isChannelLive: () => bridge.isWorkspaceControlLive?.() ?? bridge.isChannelLive() With the workspace-control check gone, /workspace/acp/status reports channelLive: true for a paired Legacy channel that is not in control, so two tests in packages/cli/src/serve/paired-engine-workspace-runtime.test.ts failed on the Test (ubuntu-latest, Node 22.x) job of run 36296096909. Restore the upstream line verbatim; server.ts is now byte-identical to main at the merge base, so this PR no longer changes that file at all. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmujfp1ev92
|
Root cause for the red The merge commit dropped a main-side line. - isChannelLive: () =>
- bridge.isWorkspaceControlLive?.() ?? bridge.isChannelLive(),
+ isChannelLive: () => bridge.isChannelLive(),Both parents confirm it: Without Fix: restore the upstream line verbatim. The third failure stays classified as timing, not code. Local verification was skipped this round under the patrol memory guardrail (719 MB available, two other lanes running vitest/tsc). Evidence here is the CI job log plus the parent-tree comparison above; |
qqqys
left a comment
There was a problem hiding this comment.
Critical-only review at d6f637c2 — approving
Base 81c260bb. Eighteen files, +721/-74. This is a privacy-boundary fix, so I verified the two historical Criticals in code at this head and then checked the one completeness question that decides whether the fix actually holds: whether any construction site can still emit a lifecycle event past the opt-out.
Both historical Criticals are fixed at this head
R1-1 [fails-closed] [regression] — the process-global dispatcher leak. Forwarding proxy into the throwaway telemetry Config made its constructor call setGlobalDispatcher, which nothing ever restores, so every extension lifecycle event re-routed the host process's plain fetch — and in qwen serve leaked one workspace's proxy into every other workspace the daemon hosts. The author initially declined this as needing a maintainer decision; it has since been fixed, and I verified the mechanism rather than the intent. ConfigParameters gains installProxyDispatcher?: boolean (config/config.ts:1169) and the constructor's gate is now:
const proxyUrl = this.getProxy();
if (proxyUrl && (params.installProxyDispatcher ?? true)) {That wraps the whole setGlobalDispatcher + setResolvedProxyUrlForRuntimeFetch block, and the ?? true default means every existing construction site behaves exactly as before — only the telemetry-only Config opts out, at extensionManager.ts:601. getProxy() still returns the value on that Config, so QwenLogger.getProxyAgent() can pin its own agent and the RUM upload still honors the proxy: the global side effect is gone without losing the behavior the forwarding was for. The same method also wraps normalizeProxyUrl(this.proxy) in a try/catch and drops an unsupported value, so a SOCKS proxy cannot make the constructor throw and abort the extension mutation the throwaway exists to record.
R1-2 [certifies-falsely] [new-surface] — ambient env in the workspace-scoped controller. Verified fixed at workspace-extensions-controller.ts:314-340. loadSettings is now a single call carrying skipLoadEnvironment: true, plus skipWorkspaceSettings: !workspaceTrusted and workspaceTrusted when known, so a workspace's .env / settings.env is no longer written into the daemon's shared process.env and the trust value resolved above is the one the load actually uses. The telemetry proxy is settings.proxy alone; the ambient HTTPS_PROXY / https_proxy / HTTP_PROXY / http_proxy fallback is gone, with the multi-workspace reason recorded in the comment. The asymmetry with the CLI command sites is deliberate and correct — those are one-shot processes where ambient env is the right source, while the daemon hosts many workspaces in one process.
The standing CHANGES_REQUESTED is the bot's at the older head 40f203c0; both findings it rests on are closed at this one.
The completeness question
A consent fix only holds if no site can bypass it, so I checked whether the PR's claim — every mutation-capable construction site is wired — survives contact with the tree. A repo-wide search for new ExtensionManager( returns three production sites this diff does not touch: commands/mcp/list.ts, commands/mcp/reconnect.ts and serve/workspace-skills-status.ts, plus a bench script. None of the three contains a single call to installExtension, uninstallExtension, updateExtension, enableExtension or disableExtension.
And every logExtension* call in extensionManager.ts sits inside a mutation method: logExtensionEnable in enableExtension (:772), logExtensionDisable in disableExtension (:823), the install and update events in installExtensionInternal (:2835, :2847, :2938, :2950) and commitPreparedExtensionInternal (:3032, :3044, :3089, :3101), and logExtensionUninstall in uninstallExtensionPolicy (:3324). So a read-only consumer cannot reach a lifecycle event, and the five wired sites — main-session Config, extensions/install.ts, extensions/uninstall.ts, extensions/utils.ts and the daemon controller — are all of the mutation-capable ones. The claim holds.
The shared helper is what keeps them from drifting: resolveUsageStatisticsEnabled is parseBooleanEnvFlag(env['QWEN_USAGE_STATISTICS_ENABLED']) ?? settingsValue ?? true, and cli/src/config/config.ts now calls it instead of carrying its own copy of that chain.
Bonus fix worth naming
qwen-logger.ts gains isExcludedByNoProxy, mirroring undici's EnvHttpProxyAgent#shouldProxy — lowercase no_proxy wins, a bare * disables proxying, entries split on comma or whitespace, a leading . or *. is stripped for subdomain matching, and a port-qualified entry only matches that port. HttpsProxyAgent has no such notion, so without this the newly forwarded proxy would have routed RUM uploads through it even for hosts an operator explicitly excluded. That closes the substance of R1-7 and makes the proxy forwarding safe to have added.
No Critical found. All eight threads are resolved; the six Suggestions are not mine to adjudicate under this scan.
Non-blocking
resolveExtensionTelemetryProxy starts at settings.proxy and omits the leading argv.proxy term the canonical session chain carries, so qwen --proxy … extensions install would upload through the env or settings proxy rather than the flag. Telemetry-only, one-shot command, no consent implication.
CI
Healthy at this head: 22 checks pass with zero failures, including Test (ubuntu-latest, Node 22.x) and Lint & Static, which carry the new regression tests. review-pr and web-shell E2E Smoke are still queued; neither is a code-quality signal and I did not wait on them.
chiga0
left a comment
There was a problem hiding this comment.
Review at head d6f637c2 -- approving
Base 81c260bb. Eighteen files, +721/-74.
Tier: Standard (privacy boundary).
What I checked
Root cause -- getTelemetryConfig was a module-level function that built a throwaway Config without usageStatisticsEnabled, so the default true was always used regardless of the user's opt-out setting. QwenLogger.getInstance(config) is the sole opt-out gate; it stays closed only when config.getUsageStatisticsEnabled() returns false. The fix: ExtensionManager now accepts usageStatisticsEnabled and proxy, and getTelemetryConfig is promoted to a private method that reads them.
resolveUsageStatisticsEnabled -- Canonical resolver in telemetry/config.ts with the same precedence as the main session: QWEN_USAGE_STATISTICS_ENABLED env, then settings.privacy.usageStatisticsEnabled, then default true. Tests pin the env-override direction and the unrecognized-value (parseBooleanEnvFlag) fallback to false.
resolveExtensionTelemetryProxy -- Returns the raw first match (settings.proxy, then HTTPS_PROXY etc.) without normalizing. Normalization happens in getTelemetryConfig inside a try-catch: a SOCKS proxy or other unsupported value is silently dropped so telemetry never aborts the extension mutation.
installProxyDispatcher: false -- The old getTelemetryConfig never installed the global undici dispatcher. The new path would install it on every lifecycle event in qwen serve, re-routing the daemon's plain fetch through whichever workspace's proxy happened to log last. installProxyDispatcher: false prevents this. The test "forwards the resolved proxy so RUM uploads use it" asserts the global dispatcher is untouched 50ms after the event.
CLI vs daemon asymmetry -- CLI commands (install.ts, uninstall.ts, utils.ts) call resolveExtensionTelemetryProxy(settings.proxy), which falls back to ambient env. The daemon's extension controller uses settings.proxy directly with skipLoadEnvironment: true. One daemon hosts every workspace; the daemon's ambient HTTPS_PROXY is process-global and must not become a workspace-specific telemetry proxy. The test "resolves telemetry proxy and consent from the workspace settings only" pins that no ambient-env leak occurs even when HTTPS_PROXY is set in the daemon's environment.
loadSettings consolidation -- One trust-gated load per createExtensionManager call, shared by locale resolution, trust status check, and telemetry options. Test "loads settings once per manager" pins this at exactly 1 call.
Taxonomy classes checked
Auth/credentials (usage-statistics opt-out gate, proxy routing), contract asymmetry (CLI env fallback vs daemon settings-only -- documented intentional), error handling (SOCKS proxy dropped in getTelemetryConfig), test validity (env teardown in uninstall/utils tests saves and restores all five proxy env keys).
No findings. Ready to merge.
qwen-code-dev-bot
left a comment
There was a problem hiding this comment.
Approved at head d6f637c.
Required CI is complete and green, all threads are resolved, and I verified both round-1 Criticals against this head rather than against the resolved flags. The review that requested changes is bound to an older commit.
- R1-2, consent resolved from the wrong place: the workspace controller now takes one trust-gated loadSettings per call and shares it across the locale, the trust fallback and the telemetry options, and resolves consent through resolveUsageStatisticsEnabled, whose precedence (env flag, then the privacy setting, then true) is the same chain the session config uses. Standalone entrypoints that build no session Config now have one shared resolver to route through, which is the right shape for an opt-out that has to reach the logger.
- That shared load also carries skipLoadEnvironment, with the reason recorded: one daemon hosts every workspace, so writing the env of this workspace into the shared process environment would leak the values of one repo into the resolution of every other workspace.
- The proxy is scoped twice over. Only the resolved settings of this workspace may pick the telemetry proxy, because the ambient proxy env of the daemon is process-global and falling back to it would route the RUM uploads of this workspace through a proxy it never configured. And the throwaway Config is built with installProxyDispatcher false: the RUM logger pins its own agent from getProxy at upload time, so the proxy stays visible without installing a process-global undici dispatcher that nothing restores, which would otherwise re-route the plain fetch of the host process on every lifecycle event and leak the proxy of one workspace into every other workspace the daemon hosts.
- R1-1, the throwaway Config aborting the mutation it was built for: normalizeProxyUrl now runs inside a try and an unsupported value such as SOCKS is dropped to undefined, so the constructor cannot throw and the extension mutation always proceeds, degrading to the direct connection that was the behaviour before this PR. Telemetry losing a proxy is the right thing to sacrifice; a user-visible install or uninstall is not.
- The QwenLogger singleton limitation is disclosed rather than papered over: the docblock states that the logger binds to the first Config that opens its gate, so in a multi-workspace daemon the proxy of whichever workspace logs first wins for the process lifetime, and calls this pre-existing behaviour newly visible now that the proxy differs per workspace.
No new Critical found.
The daemon's extensions controller resolved the usage-statistics opt-in and the telemetry proxy against the process-wide `process.env`, and the trust-free extensions status route loaded the bound workspace's settings without `skipLoadEnvironment`, so one GET wrote that workspace's `.env` / `settings.env` into the shared env for the process lifetime. One daemon hosts every workspace, so workspace A's `QWEN_USAGE_STATISTICS_ENABLED=1` could outrank workspace B's own `privacy.usageStatisticsEnabled: false` and re-open the `QwenLogger.getInstance` opt-out gate that #12770 closes. - thread the primary runtime's resolved env (`primaryRuntimeEffectiveEnv`, the carrier `resolveSetupGithubProxy(boundWorkspace, deps.env, ...)` already uses) into the extensions controller, and resolve consent and proxy against it: settings first, then that runtime's env, never an ambient value another workspace published - `skipLoadEnvironment: true` on the status route's settings load, matching the guarded load in `createExtensionManager` - pin the default arm of `installProxyDispatcher`, which both existing callers pass `false`, so inverting it can no longer leave the suite green while session traffic silently stops honouring `settings.proxy` - isolate the ambient telemetry env and `QWEN_HOME` in the two controller tests whose assertions the runner's own environment could decide Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmujqeubd9m
e771234
|
Recording a drift follow-up here so it is not lost, and one line in this PR's description that needs reconciling. No change requested to this PR's code. 1. A second hand-written copy of the precedence chain now exists. #12857 (the 2. This PR's out-of-scope line is now stale for one of the sites it names. It reads: "read-only For context on the mechanism, since it differs from this PR's: |
doudouOUC
left a comment
There was a problem hiding this comment.
Re-reviewed at head 6981ff3d. Both historical Criticals are verifiably fixed in code — the throwaway telemetry Config no longer installs the process-global dispatcher (config.ts:3583, extensionManager.ts:601), SOCKS values can no longer abort an extension mutation (extensionManager.ts:586-591), and the daemon controller now resolves consent and proxy from the owning runtime's env with trust-gated, skipLoadEnvironment / consumeCorruptionEnvVars: false settings loads (workspace-extensions-controller.ts:328-362, :1168-1191). Wiring completeness re-verified: of the 12 non-test new ExtensionManager( sites, the 5 mutation-capable ones are wired and the 7 untouched ones cannot reach any lifecycle emission.
One discrepancy to clean up before the R2-6 thread is resolved: the closeout reply (16:50:48Z) says a new test does not let an untrusted workspace pick the status locale was added in 6981ff3dd8 and mutation-verified, but no such test exists at this head — the name appears nowhere in the tree, and workspace-extensions-controller.test.ts has no zh-CN assertion. The code fix itself is present and correct (workspace-extensions-controller.ts:1184-1191); only the pinning test for the untrusted re-load branch is missing. Could you either add the test as described or amend the reply so the thread state matches the tree?
The two items deliberately left open (R2-10/R2-11, split to #12852) and the documented first-writer-wins proxy note (R1-3) are fine as-is from my side.
CI at this head: all code checks green, including Test (ubuntu-latest) which carries the new suites; the only red is the review-pr automation job (review timeout, not a code signal).
在 head 6981ff3d 重新审过。两个历史 Critical 在代码层面均已确认修复——临时遥测 Config 不再安装进程全局 dispatcher(config.ts:3583、extensionManager.ts:601),SOCKS 值不会再中断扩展变更操作(extensionManager.ts:586-591),守护进程 controller 现在从所属 runtime 的 env 解析 consent 与 proxy,且 settings 加载带信任门控、skipLoadEnvironment 与 consumeCorruptionEnvVars: false(workspace-extensions-controller.ts:328-362、:1168-1191)。接线完整性也已复核:12 处非测试 new ExtensionManager( 中,5 处可产生变更的都已接线,未动的 7 处到不了任何生命周期事件发触点。
R2-6 线程在 resolve 前有一处出入需要清理:关闭回复(16:50:48Z)称 6981ff3dd8 新增了 does not let an untrusted workspace pick the status locale 测试并做过变异验证,但该测试在当前 head 不存在——整个代码树里搜不到这个用例名,workspace-extensions-controller.test.ts 里也没有任何 zh-CN 断言。代码修复本身已到位且正确(workspace-extensions-controller.ts:1184-1191),缺的只是不受信任分支重解析的钉住测试。麻烦补上该测试,或修正回复,让线程状态与实际代码树一致。
有意保持开放的两项(R2-10/R2-11,已拆到 #12852)以及已文档化的 first-writer-wins proxy 说明(R1-3),我这边没有异议。
当前 head 的 CI:所有代码检查全绿,包括承载新增测试套的 Test (ubuntu-latest);唯一红色是 review-pr 自动化评审 job(评审超时,非代码信号)。
…etry-optout Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-issue-patrol/jmum3fjj72y
The 16:50:48Z R2-6 closeout reply claimed this test landed in 6981ff3 and was mutation-verified, but it was never in the tree. Add it for real: with general.language=zh-CN in the workspace settings, an untrusted workspace must resolve the status locale from the gated re-load (skipWorkspaceSettings), while a trusted workspace keeps its choice. Mutation-verified: pointing the untrusted branch back at probeSettings fails this test. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-issue-patrol/jmum3fjj72y
|
Re: the re-review at
The earlier 16:50:48Z reply claimed this test existed in |
|
@qwen-code /review Re-triggering the round-3 re-review: run 36518175249 died on a model-API transport error ( |
|
Qwen Code review request accepted. Review is queued for an available runner; follow the workflow run for progress. A command-triggered review is not listed under the checks of this PR; the result is posted here as a review when it finishes. |
… untrusted settings
Three review-found defects in the daemon's extensions controller:
- `createExtensionManager` read `deps.env` unconditionally. That delegate
throws `WorkspaceGenerationClosedError` whenever the primary entry is not
`active`, which the trust reconciler does in normal operation, so extension
routes 503'd for unrelated workspaces whose own entry was fine. It also
attributed the primary runtime's consent and proxy to managers built for
other directories. The term is now bound to `workspaceDir === boundWorkspace`
and copied inside a `try`, so a property read cannot throw out of a route.
- `buildLocalExtensionsStatus` probed settings ungated, so for an UNTRUSTED
bound workspace it parsed that workspace's own `.qwen/settings.json` and then
discarded the result. Parsing runs the migration / corruption-recovery path,
which REWRITES the file (`$version` injected, or invalid JSON reset to `{}`
beside a `.corrupted` sibling): a trust-free, read-only-by-contract status
poll was mutating the workspace it reports on. The probe now runs only where
it is read, so that arm performs one load instead of two.
- The `workspaceTrusted === undefined` arm of the manager load had no test, so
reverting it to base's `loadSettings(dir, true)` -- which publishes the
workspace `.env` into the daemon's shared `process.env` and spends the
one-shot corruption markers `acpAgent.ts` reports from -- kept the suite
green. It is now pinned.
Comments and JSDoc that claimed a per-workspace env scoping the code did not
implement are corrected to describe what it actually does.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmumj5dw0er
…etry-optout Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmumj5dw0er
…etry-optout Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmumj5dw0er
…eir own PRs The status-route settings-load hardening (trust-gated locale, one-shot corruption markers, workspace-env isolation) and the QwenLogger NO_PROXY handling are independent of the telemetry opt-out fix and move to separate PRs. This branch keeps only the extension lifecycle telemetry wiring: usageStatisticsEnabled/proxy forwarding, the shared resolvers, and the per-runtime env attribution.
… state (QwenLM#13022) Extension-related settings loads in the serve daemon ran with default side effects: they published the workspace's own .env/settings.env into the shared process.env every hosted workspace resolves against, and spent the one-shot settings-corruption marker pair without surfacing it. The trust-free status route could also parse (and via corruption recovery, rewrite) an untrusted workspace's settings.json, and the unresolved-trust arm of the locale resolve used the legacy ungated load form. Make each load explicit: one trust-gated load per createExtensionManager call shared by locale and trust, skipLoadEnvironment everywhere, consumeCorruptionEnvVars: false so the marker pair survives until a path that reports it, and no settings probe at all when trust is an authoritative false. Split out of QwenLM#12789.
Resolve the two-file conflict in workspace-extensions-controller against main's 2b162b3 (#13022), which is the status-route hardening this PR deliberately split out. - controller.ts: keep main's hardened buildLocalExtensionsStatus (single gated load, no untrusted-workspace parse, unspent corruption markers) and keep this PR's createExtensionManager telemetry additions (deps.env, runtimeEnv binding, usageStatisticsEnabled/proxy) on top of the settings load both sides introduced identically. - controller.test.ts: union of both sides' disjoint test sets -- main's 5 status-poll tests plus this PR's 5 telemetry tests, 35 passing. Dedupe the loadSettings import and unify the env-pin helper on main's AMBIENT_ENV_KEYS/pinAmbientEnvCleared naming. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-conflict/jmumtibtcfa
… not choose
`createExtensionManager` wrapped the `deps.env` read so a non-active runtime
yields `undefined` instead of throwing, but `undefined` is exactly what makes
`resolveUsageStatisticsEnabled` fall back to its `env = process.env` default.
Every manager with no attributable runtime env therefore resolved the
usage-statistics consent term from the daemon's shared ambient environment,
while the proxy line directly below already passed `runtimeEnv ?? {}`.
Ambient env is not neutral: `loadEnvironment` writes parsed `.env` keys into
`process.env` in no-override mode and never restores them,
`canApplyParsedEnvKey` has no trust gate, and
`QWEN_USAGE_STATISTICS_ENABLED` is in neither `DEFAULT_EXCLUDED_ENV_VARS` nor
`PROJECT_ENV_HARDCODED_EXCLUSIONS`. So one hosted repo publishing
`QWEN_USAGE_STATISTICS_ENABLED=1` re-opened the gate for a different workspace
whose own `settings.json` opts out, and that workspace's extension lifecycle
events would upload despite its opt-out (#12770).
Narrow the fallback in one direction only: keep an ambient opt-out able to
close the gate (that one is an operator decision about this daemon), refuse an
ambient opt-in this workspace did not choose. Also correct the three comments
that asserted the opposite, and pin the `env` delivery hop in `controllerDeps`,
whose removal silently returns consent to ambient env while the suite stays
green.
Tests: packages/cli workspace-extensions-controller.test.ts 37 passed
(35 baseline + 2 new); typecheck clean.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmun8vb7vg3
…e process.env guard workspace-extensions-controller.ts reads process.env['QWEN_USAGE_STATISTICS_ENABLED'] twice to apply the daemon-wide opt-out without inheriting another hosted workspace's opt-in. The serve process.env guard requires each access to be registered with a reason, so the Test lane failed on the unregistered pair. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmund5mtcgb
) QwenLogger picks an HttpsProxyAgent for the RUM endpoint, which has no NO_PROXY notion, so uploads ignore an explicit exclusion even though the session's undici egress (EnvHttpProxyAgent) honors it. Mirror undici's shouldProxy rules — lowercase no_proxy wins, bare '*' disables proxying, leading '.'/'*.' suffixes match the host and its subdomains, and a port-qualified entry only matches that port. Split out of QwenLM#12789.
chiga0
left a comment
There was a problem hiding this comment.
Review at head 24433f2e — approving (round 2)
Base e767e223. 19 files, +1175/-61 (scope-narrowed at head relative to earlier revisions). Privacy fix: extension lifecycle telemetry now honors usageStatisticsEnabled opt-out and routes uploads through the configured proxy.
Round 1 reviewed at d6f637c2 (same scope minus the daemon env isolation added since). Both historical Criticals (R1-1 dispatcher leak, R1-2 ambient env in daemon controller) were confirmed fixed at that head. This round re-verifies both at the current head and audits the new daemon env isolation logic.
What I checked at current head
R1-1 — process-global dispatcher leak. Verified fixed: extensionManager.ts passes installProxyDispatcher: false to the throwaway telemetry Config, and config.ts gates the setGlobalDispatcher + setResolvedProxyUrlForRuntimeFetch block on params.installProxyDispatcher ?? true. getProxy() still returns the value on that Config so QwenLogger.getProxyAgent() can pin its own agent — the proxy is routed correctly without installing a process-global side effect on every lifecycle event.
R1-2 — daemon controller ambient env. Verified fixed at current head. The new runtimeEnv path: workspaceDir === boundWorkspace && deps.env → snapshot { ...deps.env } in a try-catch (handles non-active runtime throwing on property access). The consentEnv construction is correctly one-directional: an ambient opt-out (QWEN_USAGE_STATISTICS_ENABLED=0/false) is preserved for any workspace the daemon hosts; an ambient opt-in is refused for non-bound workspaces (where the value may be contamination from another repo's settings load). This matches the documented intent — an operator's daemon-wide opt-out closes every gate, while a per-workspace opt-in is not inherited by other workspaces.
Proxy scoping. For a non-bound workspace or a non-active runtime: runtimeEnv ?? {} = {}, so resolveExtensionTelemetryProxy(settings.proxy, {}) returns settings.proxy only — no ambient proxy env leaks into that workspace's telemetry uploads. Correct.
normalizeProxyUrl try-catch. An unsupported proxy (SOCKS) drops to undefined rather than throwing and aborting the extension mutation. This is the correct tradeoff (telemetry falls back to direct; the mutation succeeds).
resolveUsageStatisticsEnabled shared resolver. The chain parseBooleanEnvFlag(env['QWEN_USAGE_STATISTICS_ENABLED']) ?? settingsValue ?? true matches the main session chain exactly. loadCliConfig now calls this helper, so the two cannot drift. Tested independently in config.test.ts.
CLI sites. install.ts, uninstall.ts, utils.ts all call resolveUsageStatisticsEnabled(settings.privacy?.usageStatisticsEnabled) and resolveExtensionTelemetryProxy(settings.proxy) with default env = process.env. For CLI commands this is correct — they are short-lived processes attributed to one workspace, and ambient env is their own.
Cross-check. Prior approvals from qqqys and chiga0 (me, round 1) at d6f637c2 confirmed both Criticals fixed. doudouOUC's review at 6981ff3dd8 confirmed the same and raised a discrepancy about a missing test for the untrusted-locale branch — that branch was subsequently moved to a separate PR (scope narrowed at 7d002fe42a), which resolves the discrepancy. Remaining open suggestions: R1-3 (QwenLogger singleton first-writer-wins proxy — disclosed in JSDoc, pre-existing limitation); R1-4 (proxy-chain duplication, tracked in follow-up #12857). Neither is a blocker.
No blocking findings. Approval blockers: none.
Checked: R1-1 dispatcher gate at current head; R1-2 daemon env isolation logic (runtimeEnv, consentEnv, one-directional narrowing); proxy scoping for non-bound workspaces; shared resolver chain; CLI wiring.
Not run: local vitest — CI green at this head.
Reviewed with AI assistance.
qqqys
left a comment
There was a problem hiding this comment.
Reviewed at head 24433f2e49da1134653ecb8daeb7fae3ece774a4.
Verdict: APPROVE
Every Critical ever filed on this PR is fixed at this head — verified by reading the current code and its call chain, not by trusting thread-resolution flags — and an independent Critical-only scan of the diff found none.
Historical blockers, re-checked against this head
R1-1 — throwaway telemetry Config rewrote process-global network state and nothing restored it. Fixed. ConfigParameters gains installProxyDispatcher?: boolean, and the constructor's block is now gated: if (proxyUrl && (params.installProxyDispatcher ?? true)) before the dynamic import that calls setGlobalDispatcher(new EnvHttpProxyAgent(…)) and setResolvedProxyUrlForRuntimeFetch(proxyUrl). ExtensionManager.getTelemetryConfig passes installProxyDispatcher: false while still passing proxy, so getProxy() keeps feeding the RUM logger's own agent without the host process's plain fetch being re-routed on every lifecycle event. Default true leaves every session Config behaving exactly as before.
R1-2 — the workspace controller resolved consent and proxy from the daemon's ambient process.env, and a hoisted loadSettings wrote the workspace's .env into that shared env. Fixed on both halves. All three loadSettings sites in workspace-extensions-controller.ts now pass skipLoadEnvironment: true (:346/:348, :1238, :1257), so no workspace .env / settings.env reaches the daemon's shared env. And resolveUsageStatisticsEnabled no longer falls through to its env = process.env default: it receives consentEnv, which is runtimeEnv when the workspace in hand is the bound one, otherwise only the ambient opt-out term, otherwise {}. A hosted repo publishing QWEN_USAGE_STATISTICS_ENABLED=1 therefore cannot re-open the gate for a different workspace whose own settings opt out, while an operator's daemon-wide opt-out still closes every gate. Proxy is resolveExtensionTelemetryProxy(settings.proxy, runtimeEnv ?? {}) — settings first, and no ambient proxy for a workspace that never configured one.
R1-2 (fix-induced) — reading deps.env coupled every secondary workspace's extension routes to the primary runtime's lifecycle, since the delegate's get trap calls requirePrimaryRuntime() and throws once that entry leaves active. Fixed. runtimeEnv is built in an IIFE that returns undefined when workspaceDir !== boundWorkspace || !deps.env, and otherwise wraps { ...deps.env } in try/catch returning undefined. A property read on the live delegate can no longer throw out of a route serving a different workspace; the undefined then degrades to settings-only resolution rather than to the ambient env.
R2-1 (fix-induced) — the probe load parsed an untrusted workspace's own .qwen/settings.json, and parsing runs the migration / corruption-recovery path, which rewrites that file. Fixed. buildLocalExtensionsStatus now short-circuits the probe on an authoritative false from deps.isWorkspaceTrusted?.() (probeSettings = undefined), so an untrusted workspace performs exactly one load — the gated one, which passes skipWorkspaceSettings: true, workspaceTrusted: false. A trust-free, read-only-by-contract status poll reachable with a single GET can no longer mutate the workspace it reports on.
The Critical about the throwaway Config aborting an extension mutation on a proxy a session would reject at startup is also handled: normalizeProxyUrl(this.proxy) runs inside try/catch and an unsupported value drops to undefined, restoring the direct-connection behaviour rather than failing the install or uninstall.
Independent scan of the current diff
- The change is additive at the core boundary.
ExtensionManagerOptionsgains two optional fields, both stored and read only by the new privategetTelemetryConfig. The module-level helper it replaces tooktelemetrySettingsalone; existing constructions that pass neither field behave as before. - The three CLI entrypoints are single-process, so ambient env is correct there.
install.ts,uninstall.tsandutils.tseach hoistloadSettings(workspaceDir).mergedinto onesettingsconst — removing a duplicate load, not adding one — and passresolveUsageStatisticsEnabled(settings.privacy?.usageStatisticsEnabled)andresolveExtensionTelemetryProxy(settings.proxy).qwen extensions …exits beforeloadCliConfig, so the omitted leading CLI-flag proxy term genuinely cannot reach these handlers. loadCliConfig's rewrite is behaviour-preserving.resolveUsageStatisticsEnabled(settings.privacy?.usageStatisticsEnabled)with the defaultenv = process.envexpands to exactly theparseBooleanEnvFlag(…) ?? settings.privacy?.usageStatisticsEnabled ?? truechain it replaced.- Env scoping is guarded twice, not once.
workspace-extensions.tsforwardsdeps.envonly whenws === boundWorkspace && deps.env, and the controller re-checksworkspaceDir === boundWorkspaceper manager it builds. A secondary workspace therefore resolves nothing from the primary runtime's env. installProxyDispatcheris not a dead switch. It is declared, read in the constructor's gate, and set tofalseby the one caller that needs it; no other construction site passes it, which is the intended default-truepath.- The disclosed residual is pre-existing and documented, not introduced here:
QwenLoggeris a process-wide singleton bound to the first Config that opens its opt-in gate, so in a multi-workspace daemon the first workspace's proxy wins for the process lifetime. The option comment says so plainly.
The three threads still open are all [Suggestion] — the ambient opt-out term being repository-writable (its effect is to close the gate, the fail-closed direction), the unpinned proxy precedence at the controller call site, and six of the seven converted getTelemetryConfig call sites having no witness asserting the Config argument. Per this channel's Critical-only policy they do not gate the verdict, and this PR is well past the review-round count at which suggestions defer to a follow-up.
I also note the review I submitted earlier on this PR was dismissed at d6f637c2ec; the head has moved 15 commits since, so nothing in this verdict relies on it — the invariants above were re-read at 24433f2e.
CI
At 24433f2e: 22 checks succeeded, 8 skipped, and only web-shell E2E Smoke and review-pr were still running, with no failures. Nothing attributable to this PR. Per the review policy, pending checks were not waited on.
What this PR does
Extension lifecycle events (install / uninstall / update / enable / disable) are logged through a throwaway
Configbuilt insideExtensionManager. That config only carriedtelemetrysettings, so the resolved usage-statistics opt-out and proxy never reached it:ConfigdefaultsusageStatisticsEnabledtotrue, andQwenLogger.getInstance(config)— the only opt-out gate on this path — always opened. In any process that lives longer than the 60s RUM flush interval (interactive session, ACP,qwen servedaemon), extension events were queued and uploaded even withprivacy.usageStatisticsEnabled: falseorQWEN_USAGE_STATISTICS_ENABLED=0, and the upload bypassed the configured proxy.This PR adds
usageStatisticsEnabledandproxytoExtensionManagerOptionsand forwards both into the throwaway telemetryConfig(now a privategetTelemetryConfigmethod). Every mutation-capable construction site is wired: the main-sessionConfigpasses its already-resolved values, and the standalone entrypoints (extensions CLI commands, the serve daemon's extension controller) resolve the opt-out via a new sharedresolveUsageStatisticsEnabledhelper — the exact sameQWEN_USAGE_STATISTICS_ENABLEDenv ??settings.privacy.usageStatisticsEnabled??truechain the main session uses (which now calls the helper too, so the two cannot drift). Proxy is passed raw (settings +HTTPS_PROXY-family env, mirroring the main chain minusargv.proxy), keeping normalization lazy insideConfig.getProxy()exactly as before.Why it's needed
With the opt-out in effect, no extension event may reach the RUM endpoint — that is the contract every other usage-statistics event honors. Managed installations disable usage statistics by policy; events that bypass the opt-out are unexpected outbound calls, and because they also bypass the configured proxy they are invisible to network egress controls.
Reviewer Test Plan
How to verify
packages/core/src/extension/extensionManager.test.ts("telemetry config for lifecycle events"): withusageStatisticsEnabled: false, the config handed to the lifecycle loggers reportsgetUsageStatisticsEnabled() === false, soQwenLogger.getInstancereturnsundefinedand nothing is enqueued; with the option omitted the default staystrue; a passedproxyis visible viagetProxy(). Before the fix, the first and third of these tests fail (opt-out ignored, proxy dropped); after the fix all four pass.resolveUsageStatisticsEnabledinpackages/core/src/telemetry/config.test.tslock the env-over-settings-over-default precedence.cd packages/core && npx vitest run src/extension/extensionManager.test.ts src/telemetry/config.test.ts src/telemetry/loggers.test.ts src/telemetry/qwen-logger/qwen-logger.test.tsandcd packages/cli && npx vitest run src/config/config.test.ts src/commands/extensions src/serve/routes/workspace-extensions-controller.test.ts.Evidence (Before & After)
N/A (non-UI). Before: 2 of the 4 new tests red (
expected true to be false,expected undefined to be 'http://127.0.0.1:7890'). After: all green; the 7installExtensiongit-mock failures inextensionManager.test.tsreproduce identically on pristinemainin this environment and are unrelated.Tested on
Environment (optional)
Unit tests only (vitest, packages/core + packages/cli).
tsc --noEmitclean for packages/core; packages/cli typecheck errors are confined tosrc/serve/*and reproduce identically on pristine main (stale workspace-dist artifacts), zero errors in files touched here.Risk & Scope
Confignow receives a proxy, so its constructor installs the global proxy dispatcher (same code path the main session already runs with the same value). For one-shot extension CLI commands this also means subsequent undici fetches honor the configured proxy, which is the intended behavior change. A SOCKS proxy value surfaces the repo's existing actionable "SOCKS proxy is not supported" error at mutation time instead of being silently ignored — consistent with main-session behavior.ExtensionManagerconsumers (mcp list/reconnect, ACP settings dump, skills status) never build a telemetry config and were left untouched; a broader "reuse the host Config" refactor was deliberately not done.Linked Issues
Fixes #12770
Update (2026-09-29)
Changes since the description above, at head
7d002fe42a:process.env.qwen servepasses the primary runtime's resolved environment to the primary workspace's extensions controller, which applies it only to managers built for that workspace (workspaceDir === boundWorkspace). A secondary hosted workspace resolves consent and proxy from its own settings; its own runtime env is not visible to the controller. Reading the live env delegate is wrapped so a non-active runtime cannot make an unrelated workspace's extension route fail (8d6a328d09).skipLoadEnvironmentso a workspace's.env/settings.envis not written into the daemon's sharedprocess.env, andconsumeCorruptionEnvVars: falseso the one-shot corruption markers stay for the load that reports them.7d002fe42a): the status-route settings hardening (trust-gated locale, corruption markers, env isolation on that route) and the QwenLoggerNO_PROXYhandling moved to their own PRs. This PR keeps only the extension lifecycle telemetry wiring:usageStatisticsEnabled/proxyforwarding, the shared resolvers, and the per-runtime env attribution.中文说明
本 PR 做了什么
扩展生命周期事件(install / uninstall / update / enable / disable)通过
ExtensionManager内部构造的临时Config记录。该临时 config 此前只携带telemetry设置,导致已解析的 usage-statistics 退出开关和 proxy 都没有传进去:Config对usageStatisticsEnabled默认兜底为true,而QwenLogger.getInstance(config)是这条路径上唯一的 opt-out 闸口,因此闸口恒开。在任何存活超过 60s RUM flush 周期的进程里(交互式会话、ACP、qwen serve守护进程),即使设置了privacy.usageStatisticsEnabled: false或QWEN_USAGE_STATISTICS_ENABLED=0,扩展事件仍会入队并上传,且上传绕过已配置的代理。本 PR 在
ExtensionManagerOptions上新增usageStatisticsEnabled与proxy,并将两者透传进临时遥测Config(现为私有方法getTelemetryConfig)。所有会产生变更事件的构造点都已接线:主会话Config直接传入自己已解析的值;独立入口(extensions CLI 命令、serve 守护进程的扩展控制器)通过新增的共享辅助函数resolveUsageStatisticsEnabled解析 opt-out —— 与主会话完全相同的QWEN_USAGE_STATISTICS_ENABLEDenv ??settings.privacy.usageStatisticsEnabled??true优先级链(主会话链路本身也改为调用该 helper,两边不会再漂移)。proxy 以原始值传入(settings +HTTPS_PROXY系列环境变量,对齐主链、仅缺argv.proxy),归一化保持像以前一样在Config.getProxy()内惰性发生。为什么需要
opt-out 生效时,任何扩展事件都不应到达 RUM 端点 —— 这是其他所有 usage-statistics 事件都遵守的契约。托管环境按策略禁用使用统计;绕过 opt-out 的事件是非预期的外发调用,且因为同时绕过已配置代理,对网络出口管控也不可见。
评审验证计划
如何验证
packages/core/src/extension/extensionManager.test.ts新增回归测试("telemetry config for lifecycle events"):当usageStatisticsEnabled: false时,传给生命周期 logger 的 config 满足getUsageStatisticsEnabled() === false,因此QwenLogger.getInstance返回undefined、事件不再入队;未传该选项时默认仍为true;传入的proxy可通过getProxy()读到。修复前这 4 个测试中的第 1、3 个失败(opt-out 被忽略、proxy 丢失),修复后全部通过。packages/core/src/telemetry/config.test.ts为resolveUsageStatisticsEnabled新增单测,锁定 env > settings > 默认值 的优先级。cd packages/core && npx vitest run src/extension/extensionManager.test.ts src/telemetry/config.test.ts src/telemetry/loggers.test.ts src/telemetry/qwen-logger/qwen-logger.test.ts,以及cd packages/cli && npx vitest run src/config/config.test.ts src/commands/extensions src/serve/routes/workspace-extensions-controller.test.ts。证据(前后对比)
N/A(非 UI)。修复前:4 个新测试中 2 个红(
expected true to be false、expected undefined to be 'http://127.0.0.1:7890')。修复后:全绿;extensionManager.test.ts中 7 个installExtensiongit-mock 失败在本环境的 pristinemain上同样复现,与本改动无关。测试平台
环境(可选)
仅单元测试(vitest,packages/core + packages/cli)。
tsc --noEmit在 packages/core 全绿;packages/cli 的 typecheck 错误全部位于src/serve/*且在 pristine main 上同样复现(工作区 dist 过期导致),本改动涉及的文件零错误。风险与范围
Config现在会携带 proxy,其构造函数会安装全局 proxy dispatcher(与主会话用同一个值走同一段代码)。对一次性扩展 CLI 命令而言,这也意味着后续 undici 请求会走已配置代理 —— 这正是预期的行为变化。SOCKS 代理值会在变更操作时抛出仓库既有的可操作错误("SOCKS proxy is not supported"),而不是被静默忽略 —— 与主会话行为一致。ExtensionManager使用方(mcp list/reconnect、ACP settings 导出、skills status)从不构造遥测 config,保持不动;未做「复用宿主 Config」的更大重构。关联 Issue
Fixes #12770
本次更新(2026-09-29)
相对上文描述的变化,对应 head
7d002fe42a:process.env读取用量统计开关与代理。qwen serve把主 runtime 解析后的环境传给主 workspace 的扩展控制器,只作用于为该 workspace 构建的管理器(workspaceDir === boundWorkspace)。secondary workspace 从它自己的设置解析开关与代理,控制器看不到它自己 runtime 的环境。读取实时环境时做了保护,不活跃的 runtime 不会让其他 workspace 的扩展路由失败(8d6a328d09)。skipLoadEnvironment,不把 workspace 的.env/settings.env写进守护进程共享的process.env;并使用consumeCorruptionEnvVars: false,一次性的损坏标记留给真正上报它的那次读取。7d002fe42a):status 路由的设置加固(受信任门控的语言、损坏标记、该路由的环境隔离)和 QwenLogger 的NO_PROXY处理已拆到各自的 PR。本 PR 只保留扩展生命周期遥测的接线:usageStatisticsEnabled/proxy透传、共享的解析函数,以及按 runtime 归属的环境。