Skip to content

fix(cli): honor the usage-statistics opt-out under --bare - #12604

Merged
yiliang114 merged 3 commits into
mainfrom
fix/bare-usage-statistics-opt-out
Sep 24, 2026
Merged

yiliang114 merged 3 commits into
mainfrom
fix/bare-usage-statistics-opt-out

Conversation

@yiliang114

Copy link
Copy Markdown
Collaborator

What this PR does

With --bare, a privacy.usageStatisticsEnabled value set in system-defaults, user or system settings is now honored. Bare mode already reads those three files to keep the operator sandbox policy; the same read now also carries the usage-statistics choice. It is resolved with normal settings precedence (system over user over system defaults), and the pre-v2 top-level usageStatisticsEnabled key is accepted too, because bare mode reads the files without migrating them. Workspace settings and every other setting stay ignored.

Why it's needed

Bare mode replaced all settings with the sandbox policy only, so a user who had opted out of usage statistics fell back to the default (on). Every --bare run then sent the session-start usage beacon. Only the QWEN_USAGE_STATISTICS_ENABLED=0 environment variable could stop it. Automation that uses --bare, such as the harness-perf-benchmark qwen-code demo reported in #12405, made this external call on every run.

Reviewer Test Plan

How to verify

  1. Put {"privacy": {"usageStatisticsEnabled": false}} in ~/.qwen/settings.json.
  2. Run qwen --bare -p hi while watching outbound traffic. Before: a POST to the RUM endpoint. After: none.
  3. Unit tests in execution-sandbox-settings.test.ts: the opt-out survives bare mode in both the privacy.* shape and the legacy top-level shape; scope precedence matches a normal load; a non-boolean value is ignored.

Evidence (Before & After)

The before behavior was reported with an isolated HOME in #12405. After: covered by the unit tests above. No end-to-end network capture was made from this change.

Tested on

OS Status
🍏 macOS ⚠️
🪟 Windows ⚠️
🐧 Linux ⚠️

The unit tests were written but not run locally; CI runs them.

Environment (optional)

N/A

Risk & Scope

  • Main risk or tradeoff: bare mode now respects one more non-sandbox setting. true is carried through as well as false, so a system-level true overrides a user-level false, exactly as it does without --bare.
  • Not validated / out of scope: the other telemetry settings (--no-telemetry already governs OpenTelemetry separately).
  • Breaking changes / migration notes: none.

Linked Issues

Closes #12600. Part of #12405.

中文说明

本 PR 的改动

带 --bare 时,system-defaults、user 或 system settings 里的 privacy.usageStatisticsEnabled 现在会生效。bare 模式本来就会读这三个文件来保留 operator 沙箱策略,现在同一次读取也带上使用统计的选择。优先级与正常 settings 加载相同(system 高于 user 高于 system defaults),同时接受 v2 之前的顶层 usageStatisticsEnabled 键,因为 bare 模式直接读文件、不做迁移。workspace settings 和其他所有设置仍然忽略。

为什么需要这个改动

bare 模式原来用只含沙箱策略的 settings 替换掉全部设置,所以已经关掉使用统计的用户会回落到默认值(开启)。每次 --bare 运行都会发出 session-start 使用统计上报,只有 QWEN_USAGE_STATISTICS_ENABLED=0 环境变量能拦住。使用 --bare 的自动化场景,比如 #12405 里提到的 harness-perf-benchmark qwen-code demo,每次运行都会发出这次外部请求。

Reviewer 测试计划

如何验证

  1. 在 ~/.qwen/settings.json 里写入 {"privacy": {"usageStatisticsEnabled": false}}。
  2. 观察出站流量,同时运行 qwen --bare -p hi。改动前:有一个发往 RUM 端点的 POST。改动后:没有。
  3. execution-sandbox-settings.test.ts 里的单测:privacy.* 写法和旧版顶层写法的 opt-out 在 bare 模式下都会保留;scope 优先级与正常加载一致;非布尔值会被忽略。

证据(改动前后)

改动前的行为已在 #12405 里用隔离 HOME 复现并报告。改动后:由上面的单测覆盖。这次改动没有做端到端的网络抓包。

已测试平台

OS 状态
🍏 macOS ⚠️
🪟 Windows ⚠️
🐧 Linux ⚠️

单测已编写但没有在本地跑,交给 CI。

环境(可选)

N/A

风险与范围

  • 主要风险或取舍:bare 模式现在多遵守一项非沙箱设置。true 和 false 都会带过来,所以 system 级别的 true 会覆盖 user 级别的 false,与不带 --bare 时完全一样。
  • 未验证 / 不在范围内:其他遥测设置(OpenTelemetry 已经由 --no-telemetry 单独控制)。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

Closes #12600。属于 #12405。

Bare mode replaces all settings with the operator sandbox policy, so
privacy.usageStatisticsEnabled: false in user or system settings was
dropped and the usage-statistics default (on) applied: a --bare run
still sent the session-start beacon. Carry the setting through the same
operator read, resolved with normal scope precedence and the pre-v2
top-level key.
@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Sep 24, 2026
@yiliang114
yiliang114 requested review from qqqys and wenshao September 24, 2026 07:47
yiliang114 and others added 2 commits September 24, 2026 17:31
…uts in bare mode

Bare mode read a scope's top-level `usageStatisticsEnabled` regardless of that
file's `$version`, so a stray legacy key in a current-version file — which a
normal load only warns about, because `config.ts` reads `privacy.*` alone —
could outvote a lower scope's explicit opt-out and re-enable the beacon under
`--bare`. Gate the fallback at `$version >= 2`, mirroring the v1->v2
migration's own short-circuit, so only genuinely pre-v2 files contribute it.

Also stop dropping falsy non-boolean values. The normal path coerces with
`?? true`, which keeps `0` and `""` as opt-outs; the strict boolean guard
turned them into "unset" and let `?? true` fire, so `--bare` re-enabled
statistics for a file that suppressed it. Truthy junk (`'no'`) is still
ignored rather than read as an opt-in.

The privacy tests now cross-check `loadSettings()` against
`createMinimalSettings()` on every expectation, and pin the `??` operand order
with a same-file fixture holding both key shapes.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmufdy4iv1n

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

Standard-tier review at 0cde996f. APPROVE.

Triage: privacy opt-out not honored in --bare / stream-json mode. Standard tier — bug fix touching settings merge logic.

What the change does: introduces readBareModeOperatorSettings() which reads both sandbox confinement settings and the privacy.usageStatisticsEnabled opt-out. createMinimalSettings() (used in --bare) switches from readOperatorSandboxSettings() to this new function so a user's opt-out survives the --bare code path.

Key properties verified:

  1. Version gate on legacy key — legacyUsageStatisticsEnabled(scope) returns the top-level usageStatisticsEnabled only when `` is absent or < 2, mirroring the migration short-circuit in `migration/versions/v1-to-v2.ts:75`. A stray top-level key in a ` >= 2` file cannot outvote a `privacy.*` opt-out.

  2. Falsy non-boolean opt-out coercion — the reduce uses return value ? current : false, which keeps any falsy value (0, "", false) as an opt-out and ignores truthy junk. This matches the ?? true coercion in config.ts (nullish coalescing keeps 0 and "" as-is, treating them as opted-out).

  3. Scope precedence — [systemDefaults, user, system] reduce in that order so system (last) wins, consistent with the normal load. Test confirmed with the new normalAndBareMerged() helper asserting both loadSettings() and createMinimalSettings() together.

  4. Bot findings R1-1 through R1-4 — all four were addressed in commit 0cde996f, verified by reading the current-head code.

  5. sandboxSettingsFromScopes refactor — clean extraction; readOperatorSandboxSettings() delegates to the new readOperatorSettingsScopes(), semantically unchanged.

No blockers. Approving.

Reviewed by chiga0 (阿奇 / AI00002265)

@qqqys qqqys 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-only review at 0cde996f, base d0cd622a.

Verdict: APPROVE — no blocking issue was filed here and I found no Critical. For a privacy opt-out the direction of every ambiguous case is what matters, so I traced each one rather than checking only the happy path, and all of them resolve toward not collecting.

The defect being fixed, and why the wiring is real

createMinimalSettings() — the settings object used where a normal load is skipped — previously read only sandbox confinement, so privacy.usageStatisticsEnabled: false was invisible under --bare and the opt-out silently did not apply. The fix adds readBareModeOperatorSettings(), which resolves the same operator scopes and returns the sandbox settings plus the privacy choice, and settings.ts spreads privacy into the resulting Settings.

The shape is the one consumers already read: config.ts coerces with ?? true, and usageStatisticsEnabled is consumed there and in execution-worker.ts and llm-content-generator/index.ts. So this is not a field nothing looks at — populating it changes the decision those readers make. The fourth new test pins the complementary case, that when no scope configures the value the privacy key is omitted entirely rather than written as undefined, so the consumer's own default still applies instead of being shadowed.

Every ambiguous input resolves toward opt-out

The reducer is the privacy-critical part, so I walked it case by case:

if (value === undefined || value === null) return current;
if (typeof value === 'boolean') return value;
return value ? current : false;
  • false → opt-out. true → opt-in. Both as written.
  • Falsy non-booleans (0, "", NaN) → false, i.e. opt-out. A normal load reaches the same verdict, since 0 ?? true is 0 and reads as disabled.
  • Truthy non-booleans → current, i.e. ignored. The comment says this differs from a normal load's mechanism, and it does, but not its outcome: ignoring the value falls through to the consumer's ?? true default, which is the same enabled verdict a normal load reaches by coercing the truthy junk directly. So the two paths agree on every input, and neither can turn a stated opt-out into an opt-in.
  • undefined/null in a scope → that scope expresses no opinion and the previously resolved value stands, so a later scope cannot accidentally clear an earlier opt-out.

The legacy-key handling is gated the same way the migration gates it. legacyUsageStatisticsEnabled returns the pre-v2 top-level usageStatisticsEnabled only when $version is not a number at or above FIRST_NON_V1_SETTINGS_VERSION (2), mirroring the $version >= 2 short-circuit in migration/versions/v1-to-v2.ts: from v2 onward a normal load no longer migrates the v1 location and only warns about a stray key there, so --bare must not honour one either. The two paths can diverge on a malformed $version such as the string "2", where typeof version === 'number' is false and bare mode honours the legacy key while the migration's >= comparison would coerce and short-circuit. That divergence makes bare mode more private, not less, so it is the safe side to be wrong on.

Per-scope resolution prefers privacy.usageStatisticsEnabled over the legacy location via ??, and scope order reuses the existing operator convention in this file — system defaults, then user, then system, reduced so later scopes win — rather than inventing a precedence for this one setting. readOperatorSandboxSettings() is refactored onto the shared readOperatorSettingsScopes() and sandboxSettingsFromScopes() helpers, so existing callers get identical behaviour and the file parse now types the scope as the wider OperatorSettingsScope.

Tests

The four new cases are the decision table rather than a sample: privacy preferred over a stale pre-v2 key in the same file, a stray legacy key ignored in a current-version file, normal scope precedence across files, and the value left unset when no scope configures it. Each corresponds to a branch I traced above.

CI and review state

15 checks pass and 26 are skipped at this head, with only review-pr pending and nothing failing. Test (ubuntu-latest, Node 22.x) and Lint & Static are green, so the new suite ran. No thread is unresolved, and a maintainer approval is on this head.

One observation about the precedence, recorded rather than filed: because operator scopes resolve system-over-user, a system settings file asserting usageStatisticsEnabled: true overrides a user's false here exactly as it does for sandbox confinement. That is the pre-existing model for this settings tier and this PR inherits rather than introduces it, but a privacy opt-out is the one setting where that precedence is worth a deliberate confirmation rather than an analogy to confinement — a user who opted out and is then re-opted-in by a machine-level file will not see anything that tells them so.

@qwen-code-dev-bot qwen-code-dev-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.

Approved at head 0cde996.

Required CI complete and green on every substantive lane (Test ubuntu/macos/windows, Lint & Static, both Integration Tests lanes, Desktop Shell x2, web-shell E2E Smoke, OpenTUI no-flicker gate, TUI parity snapshots). All four round-1 findings were Suggestions and all four are verified fixed at this head; no Critical was ever recorded.

Independent verification, end to end:

  • The fix reaches the actual decision point. config.ts:2504-2507 resolves usageStatisticsEnabled as the env flag, then settings.privacy?.usageStatisticsEnabled, then true, and createMinimalSettings now puts the resolved privacy block into the operator settings it hands to LoadedSettings, so a bare-mode opt-out really suppresses the beacon.
  • The version gate mirrors the migration exactly: v1-to-v2.ts:73-75 short-circuits on typeof version === number and version >= 2, and FIRST_NON_V1_SETTINGS_VERSION expresses the same condition, so a stray top-level key in a current-version file is ignored here just as a normal load ignores it.
  • The falsy coercion matches normal-load semantics case by case. Zero and empty string stay an opt-out on both paths, null falls back to the default on both, and truthy junk is ignored rather than read as an opt-in, which lands on the same enabled outcome as the ?? true chain.
  • Bare mode does not widen. readOperatorSettingsScopes still returns only the system-defaults, user and system scopes, so a project settings file cannot decide telemetry for the user, and a new test asserts confinement stays unset when only the privacy choice is configured.
  • The tests are not tautological: the helper evaluates a real loadSettings alongside createMinimalSettings, the assertions compare the effective ?? true value that config.ts uses rather than the raw setting, and the same-file precedence case deliberately stamps $version 1 because at $version 4 the gate would ignore the legacy key and leave the ?? operand order unpinned.

No new Critical found.

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.

fix(cli): --bare ignores privacy.usageStatisticsEnabled

4 participants