From e7365f2354ed872a44e525a91369570ef8352b34 Mon Sep 17 00:00:00 2001 From: Greg Anderson Date: Wed, 12 Aug 2026 21:14:23 -0600 Subject: [PATCH] chore(hardening): make telemetry opt-in and give the tips fetch a kill switch Telemetry ran unless the config explicitly said telemetry = false, which also meant a config that failed to parse was treated as consent. Enable it only when the config opts in, so an install that never made a choice, or whose config could not be read, reports nothing. KIMI_DISABLE_TELEMETRY still works as a hard override. Add KIMI_CODE_NO_TIPS for the startup tips fetch. It was the only launch network call with no way to turn it off, so an install otherwise configured for no outbound traffic still beaconed on every run. Co-Authored-By: Claude Opus 4.8 --- .changeset/hardening-egress-defaults.md | 13 ++++++++++ apps/kimi-code/src/cli/telemetry.ts | 6 +++-- .../src/tui/banner/banner-provider.ts | 11 ++++++++ apps/kimi-code/test/cli/telemetry.test.ts | 25 +++++++++++++++++-- .../test/tui/banner/banner-provider.test.ts | 18 ++++++++++++- 5 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 .changeset/hardening-egress-defaults.md diff --git a/.changeset/hardening-egress-defaults.md b/.changeset/hardening-egress-defaults.md new file mode 100644 index 00000000..5357c822 --- /dev/null +++ b/.changeset/hardening-egress-defaults.md @@ -0,0 +1,13 @@ +--- +'@moonshot-ai/kimi-code': minor +--- + +Telemetry is opt-in in this fork. It previously ran unless the config said +`telemetry = false`, including when the config could not be read at all, so an +install that never made a choice still reported. It now runs only when the +config opts in; an unreadable config is treated as no consent rather than as +consent. `KIMI_DISABLE_TELEMETRY` continues to work as a hard override. + +`KIMI_CODE_NO_TIPS=1` disables the startup tips fetch, which was the one +network call at launch with no way to turn it off — so an install configured +for no outbound traffic still beaconed on every run. diff --git a/apps/kimi-code/src/cli/telemetry.ts b/apps/kimi-code/src/cli/telemetry.ts index fefec09e..12a6eac3 100644 --- a/apps/kimi-code/src/cli/telemetry.ts +++ b/apps/kimi-code/src/cli/telemetry.ts @@ -51,7 +51,8 @@ export function initializeCliTelemetry(options: InitializeCliTelemetryOptions): initializeTelemetry({ homeDir: options.harness.homeDir, deviceId: options.bootstrap.deviceId, - enabled: options.config.telemetry !== false, + // Opt-in in this fork: telemetry only runs when the user turns it on. + enabled: options.config.telemetry === true, appName: CLI_USER_AGENT_PRODUCT, version: options.version, uiMode: options.uiMode, @@ -100,7 +101,8 @@ export function initializeServerTelemetry( initializeTelemetry({ homeDir: bootstrap.homeDir, deviceId: bootstrap.deviceId, - enabled: config.telemetry !== false, + // Opt-in in this fork: telemetry only runs when the user turns it on. + enabled: config.telemetry === true, appName: CLI_USER_AGENT_PRODUCT, version: options.version, uiMode: WEB_UI_MODE, diff --git a/apps/kimi-code/src/tui/banner/banner-provider.ts b/apps/kimi-code/src/tui/banner/banner-provider.ts index daf54f95..66dfad00 100644 --- a/apps/kimi-code/src/tui/banner/banner-provider.ts +++ b/apps/kimi-code/src/tui/banner/banner-provider.ts @@ -312,6 +312,14 @@ export function selectDisplayableBanner({ return pickRandomCandidate(candidates, random); } +/** + * `KIMI_CODE_NO_TIPS` disables the startup tips fetch. Same truthy values as + * the other kill switches (`1` / `true` / `yes` / `on`). + */ +export function isTipsBannerDisabled(env: NodeJS.ProcessEnv = process.env): boolean { + return ['1', 'true', 'yes', 'on'].includes((env['KIMI_CODE_NO_TIPS'] ?? '').trim().toLowerCase()); +} + export class BannerProvider { constructor( private readonly clientVersion: string, @@ -322,6 +330,9 @@ export class BannerProvider { fetchImpl: typeof fetch = fetch, options: BannerProviderLoadOptions = {}, ): Promise { + // The banner is the one startup fetch with no way to turn it off, which + // makes an otherwise offline-configured install still beacon on every run. + if (isTipsBannerDisabled()) return null; try { const controller = new AbortController(); const timeout = setTimeout(() => { diff --git a/apps/kimi-code/test/cli/telemetry.test.ts b/apps/kimi-code/test/cli/telemetry.test.ts index 490aa83e..8d610d1e 100644 --- a/apps/kimi-code/test/cli/telemetry.test.ts +++ b/apps/kimi-code/test/cli/telemetry.test.ts @@ -104,7 +104,27 @@ describe('initializeServerTelemetry', () => { ); }); - it('degrades to enabled with no model when config is unreadable', async () => { + it('stays disabled when the config does not opt in', async () => { + mocks.loadRuntimeConfigSafe.mockReturnValue({ config: {}, fileError: undefined }); + const { initializeServerTelemetry } = await import('#/cli/telemetry'); + initializeServerTelemetry({ version: '1.2.3' }); + + expect(mocks.initializeTelemetry).toHaveBeenCalledWith( + expect.objectContaining({ enabled: false }), + ); + }); + + it('enables only when the config opts in', async () => { + mocks.loadRuntimeConfigSafe.mockReturnValue({ config: { telemetry: true }, fileError: undefined }); + const { initializeServerTelemetry } = await import('#/cli/telemetry'); + initializeServerTelemetry({ version: '1.2.3' }); + + expect(mocks.initializeTelemetry).toHaveBeenCalledWith( + expect.objectContaining({ enabled: true }), + ); + }); + + it('degrades to disabled with no model when config is unreadable', async () => { mocks.loadRuntimeConfigSafe.mockReturnValue({ config: {}, fileError: new Error('bad toml'), @@ -112,8 +132,9 @@ describe('initializeServerTelemetry', () => { const { initializeServerTelemetry } = await import('#/cli/telemetry'); initializeServerTelemetry({ version: '1.2.3' }); + // Telemetry is opt-in, so a config we could not read is not consent. expect(mocks.initializeTelemetry).toHaveBeenCalledWith( - expect.objectContaining({ enabled: true, model: undefined }), + expect.objectContaining({ enabled: false, model: undefined }), ); }); }); diff --git a/apps/kimi-code/test/tui/banner/banner-provider.test.ts b/apps/kimi-code/test/tui/banner/banner-provider.test.ts index b21de59d..b9fac8e7 100644 --- a/apps/kimi-code/test/tui/banner/banner-provider.test.ts +++ b/apps/kimi-code/test/tui/banner/banner-provider.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { selectBannerState, @@ -6,6 +6,7 @@ import { shouldDisplayBanner, } from '#/tui/banner/banner-provider'; import type { BannerState } from '#/tui/types'; +import { BannerProvider } from '#/tui/banner/banner-provider'; describe('selectBannerState', () => { const now = new Date('2026-06-15T12:00:00+08:00'); @@ -679,3 +680,18 @@ describe('selectDisplayableBanner', () => { expect(result).toMatchObject({ key: 'fallback-always', display: 'always' }); }); }); + +describe('tips banner kill switch', () => { + it('skips the fetch entirely when KIMI_CODE_NO_TIPS is set', async () => { + vi.stubEnv('KIMI_CODE_NO_TIPS', '1'); + try { + const fetchImpl = vi.fn(); + const provider = new BannerProvider('1.0.0'); + + await expect(provider.load(fetchImpl)).resolves.toBeNull(); + expect(fetchImpl).not.toHaveBeenCalled(); + } finally { + vi.unstubAllEnvs(); + } + }); +});