Repository navigation
fix(cli): honor the usage-statistics opt-out under --bare - #12604
Conversation
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.
…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
left a comment
There was a problem hiding this comment.
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:
-
Version gate on legacy key —
legacyUsageStatisticsEnabled(scope)returns the top-levelusageStatisticsEnabledonly 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. -
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?? truecoercion inconfig.ts(nullish coalescing keeps 0 and "" as-is, treating them as opted-out). -
Scope precedence —
[systemDefaults, user, system]reduce in that order so system (last) wins, consistent with the normal load. Test confirmed with the newnormalAndBareMerged()helper asserting bothloadSettings()andcreateMinimalSettings()together. -
Bot findings R1-1 through R1-4 — all four were addressed in commit
0cde996f, verified by reading the current-head code. -
sandboxSettingsFromScopesrefactor — clean extraction;readOperatorSandboxSettings()delegates to the newreadOperatorSettingsScopes(), semantically unchanged.
No blockers. Approving.
Reviewed by chiga0 (阿奇 / AI00002265)
qqqys
left a comment
There was a problem hiding this comment.
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, since0 ?? trueis0and 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?? truedefault, 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/nullin 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
left a comment
There was a problem hiding this comment.
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.
What this PR does
With
--bare, aprivacy.usageStatisticsEnabledvalue 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-levelusageStatisticsEnabledkey 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
--barerun then sent the session-start usage beacon. Only theQWEN_USAGE_STATISTICS_ENABLED=0environment 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
{"privacy": {"usageStatisticsEnabled": false}}in~/.qwen/settings.json.qwen --bare -p hiwhile watching outbound traffic. Before: a POST to the RUM endpoint. After: none.execution-sandbox-settings.test.ts: the opt-out survives bare mode in both theprivacy.*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
HOMEin #12405. After: covered by the unit tests above. No end-to-end network capture was made from this change.Tested on
The unit tests were written but not run locally; CI runs them.
Environment (optional)
N/A
Risk & Scope
trueis carried through as well asfalse, so a system-leveltrueoverrides a user-levelfalse, exactly as it does without--bare.--no-telemetryalready governs OpenTelemetry separately).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 测试计划
如何验证
~/.qwen/settings.json里写入{"privacy": {"usageStatisticsEnabled": false}}。qwen --bare -p hi。改动前:有一个发往 RUM 端点的 POST。改动后:没有。execution-sandbox-settings.test.ts里的单测:privacy.*写法和旧版顶层写法的 opt-out 在 bare 模式下都会保留;scope 优先级与正常加载一致;非布尔值会被忽略。证据(改动前后)
改动前的行为已在 #12405 里用隔离
HOME复现并报告。改动后:由上面的单测覆盖。这次改动没有做端到端的网络抓包。已测试平台
单测已编写但没有在本地跑,交给 CI。
环境(可选)
N/A
风险与范围
true和false都会带过来,所以 system 级别的true会覆盖 user 级别的false,与不带--bare时完全一样。--no-telemetry单独控制)。关联 Issue
Closes #12600。属于 #12405。