diff --git a/src/crates/adapters/ai-adapters/src/diagnostics.rs b/src/crates/adapters/ai-adapters/src/diagnostics.rs index aa2ff27f74..bfb8c5419a 100644 --- a/src/crates/adapters/ai-adapters/src/diagnostics.rs +++ b/src/crates/adapters/ai-adapters/src/diagnostics.rs @@ -1,6 +1,6 @@ use std::sync::atomic::{AtomicBool, Ordering}; -static INCLUDE_SENSITIVE_DIAGNOSTICS: AtomicBool = AtomicBool::new(true); +static INCLUDE_SENSITIVE_DIAGNOSTICS: AtomicBool = AtomicBool::new(false); pub fn set_include_sensitive_diagnostics(enabled: bool) { INCLUDE_SENSITIVE_DIAGNOSTICS.store(enabled, Ordering::Relaxed); diff --git a/src/crates/contracts/config-contracts/src/types.rs b/src/crates/contracts/config-contracts/src/types.rs index 6e36c568ea..930caf5031 100644 --- a/src/crates/contracts/config-contracts/src/types.rs +++ b/src/crates/contracts/config-contracts/src/types.rs @@ -388,7 +388,7 @@ pub struct AppLoggingConfig { /// Allowed values: trace, debug, info, warn, error, off. pub level: String, /// Whether diagnostic logs may include sensitive troubleshooting payloads. - #[serde(default = "default_true")] + #[serde(default)] pub include_sensitive_diagnostics: bool, /// Whether the local UI records detailed Flow Chat viewport diagnostics. #[serde(default)] @@ -1946,7 +1946,7 @@ impl Default for AppLoggingConfig { Self { // Set to Debug in early development for easier diagnostics level: "debug".to_string(), - include_sensitive_diagnostics: true, + include_sensitive_diagnostics: false, flow_chat_diagnostics: false, model_exchange_tracing: ModelExchangeTracingConfig::default(), } @@ -3743,13 +3743,13 @@ mod tests { } #[test] - fn app_logging_defaults_to_sensitive_diagnostics_enabled() { + fn app_logging_defaults_to_sensitive_diagnostics_disabled() { let config: AppLoggingConfig = serde_json::from_value(serde_json::json!({ "level": "trace" })) .expect("logging config without sensitive preference should deserialize"); - assert!(config.include_sensitive_diagnostics); + assert!(!config.include_sensitive_diagnostics); assert!(!config.flow_chat_diagnostics); assert_eq!( config.model_exchange_tracing.mode, diff --git a/src/web-ui/src/infrastructure/config/AGENTS.md b/src/web-ui/src/infrastructure/config/AGENTS.md index 8ec95b019f..6fba23aea8 100644 --- a/src/web-ui/src/infrastructure/config/AGENTS.md +++ b/src/web-ui/src/infrastructure/config/AGENTS.md @@ -16,6 +16,15 @@ Follow `src/web-ui/AGENTS.md` for shared frontend rules. ## Focused verification +For logging preference changes and sensitive-data redaction before startup reads finish: + +```bash +pnpm --dir src/web-ui run test:run src/infrastructure/config/services/FrontendLogLevelSync.test.ts src/infrastructure/config/services/FrontendLogLevelSync.privacy.test.ts src/shared/utils/logger.test.ts src/flow_chat/services/EventBatcher.test.ts +``` + +These fixtures exercise the real frontend logger and event batcher with delayed +configuration reads; they do not establish live remote transport behavior. + For loading states, warm snapshots, asynchronous reads, and device isolation: ```bash diff --git a/src/web-ui/src/infrastructure/config/services/FrontendLogLevelSync.privacy.test.ts b/src/web-ui/src/infrastructure/config/services/FrontendLogLevelSync.privacy.test.ts new file mode 100644 index 0000000000..f7e5e06213 --- /dev/null +++ b/src/web-ui/src/infrastructure/config/services/FrontendLogLevelSync.privacy.test.ts @@ -0,0 +1,90 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const configApiMocks = vi.hoisted(() => ({ + getConfigs: vi.fn(), + getRuntimeLoggingInfo: vi.fn(), +})); + +vi.mock('@/infrastructure/api/service-api/ConfigAPI', () => ({ + configAPI: configApiMocks, +})); + +vi.mock('@tauri-apps/plugin-log', () => ({ + trace: vi.fn(async () => {}), + debug: vi.fn(async () => {}), + info: vi.fn(async () => {}), + warn: vi.fn(async () => {}), + error: vi.fn(async () => {}), +})); + +vi.mock('@/infrastructure/diagnostics/flowChatDiagnostics', () => ({ + setFlowChatDiagnosticsEnabled: vi.fn(), +})); + +describe('FrontendLogLevelSync startup privacy', () => { + beforeEach(() => { + vi.resetModules(); + vi.resetAllMocks(); + vi.stubGlobal('__OPENBITFUN_BOOTSTRAP_LOG_LEVEL__', 'trace'); + vi.stubGlobal('requestAnimationFrame', () => 1); + vi.stubGlobal('cancelAnimationFrame', () => {}); + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it.each([undefined, false, true])( + 'redacts startup trace logs until saved preference %s is loaded', + async (savedPreference) => { + const traceOutput = vi.spyOn(console, 'debug').mockImplementation(() => {}); + configApiMocks.getConfigs.mockResolvedValue({ + 'app.logging.level': 'trace', + ...(savedPreference === undefined + ? {} + : { 'app.logging.include_sensitive_diagnostics': savedPreference }), + }); + let finishRuntimeRead!: (value: { effectiveLevel: string }) => void; + configApiMocks.getRuntimeLoggingInfo.mockReturnValue(new Promise((resolve) => { + finishRuntimeRead = resolve; + })); + + const { initializeFrontendLogLevelSync } = await import('./FrontendLogLevelSync'); + const { EventBatcher } = await import('@/flow_chat/services/EventBatcher'); + const initialization = initializeFrontendLogLevelSync(); + const batcher = new EventBatcher({ onFlush: vi.fn() }); + const flushToolEvent = () => { + batcher.add('tool:params:session:call:none', { + toolEvent: { + event_type: 'ParamsPartial', + params: 'private-source-content', + }, + }); + batcher.flushNow(); + }; + + try { + flushToolEvent(); + expect(traceOutput).toHaveBeenCalled(); + expect(JSON.stringify(traceOutput.mock.calls)).not.toContain('private-source-content'); + + finishRuntimeRead({ effectiveLevel: 'trace' }); + await initialization; + traceOutput.mockClear(); + flushToolEvent(); + expect(traceOutput).toHaveBeenCalled(); + const output = JSON.stringify(traceOutput.mock.calls); + if (savedPreference === true) { + expect(output).toContain('private-source-content'); + } else { + expect(output).not.toContain('private-source-content'); + } + } finally { + finishRuntimeRead({ effectiveLevel: 'trace' }); + await initialization; + batcher.destroy(); + } + }, + ); +}); diff --git a/src/web-ui/src/infrastructure/config/services/FrontendLogLevelSync.test.ts b/src/web-ui/src/infrastructure/config/services/FrontendLogLevelSync.test.ts index dde14d5fd4..a6634f48a9 100644 --- a/src/web-ui/src/infrastructure/config/services/FrontendLogLevelSync.test.ts +++ b/src/web-ui/src/infrastructure/config/services/FrontendLogLevelSync.test.ts @@ -6,6 +6,10 @@ const configApiMocks = vi.hoisted(() => ({ getRuntimeLoggingInfo: vi.fn(), })); +const configManagerMocks = vi.hoisted(() => ({ + onConfigChange: vi.fn(), +})); + const loggerMocks = vi.hoisted(() => ({ getLevel: vi.fn(), setLevel: vi.fn(), @@ -20,6 +24,10 @@ vi.mock('@/infrastructure/api/service-api/ConfigAPI', () => ({ configAPI: configApiMocks, })); +vi.mock('./ConfigManager', () => ({ + configManager: configManagerMocks, +})); + vi.mock('@/shared/utils/logger', () => ({ LogLevel: { TRACE: 0, @@ -99,4 +107,40 @@ describe('FrontendLogLevelSync startup reads', () => { expect(loggerMocks.setIncludeSensitiveDiagnostics).toHaveBeenCalledWith(true); expect(loggerMocks.setFlowChatDiagnosticsEnabled).toHaveBeenCalledWith(false); }); + + it('defaults sensitive diagnostics to off when the saved value is missing', async () => { + configApiMocks.getConfigs.mockResolvedValueOnce({ + [LOGGING_LEVEL_PATH]: 'debug', + [FLOW_CHAT_DIAGNOSTICS_PATH]: false, + }); + configApiMocks.getRuntimeLoggingInfo.mockResolvedValueOnce({ effectiveLevel: 'warn' }); + + const { initializeFrontendLogLevelSync } = await importSyncModule(); + await initializeFrontendLogLevelSync(); + + expect(loggerMocks.setLevel).toHaveBeenCalledWith(1); + expect(loggerMocks.setIncludeSensitiveDiagnostics).toHaveBeenCalledWith(false); + expect(loggerMocks.setFlowChatDiagnosticsEnabled).toHaveBeenCalledWith(false); + }); +}); + +describe('FrontendLogLevelSync config changes', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it.each([undefined, null, 'true', false, true])( + 'requires an explicit boolean opt-in for changed value %s', + async (newValue) => { + const { installFrontendLogLevelConfigWatcher } = await importSyncModule(); + await installFrontendLogLevelConfigWatcher(); + + const onConfigChange = configManagerMocks.onConfigChange.mock.calls[0][0]; + onConfigChange(LOGGING_INCLUDE_SENSITIVE_PATH, true, newValue); + + expect(loggerMocks.setIncludeSensitiveDiagnostics).toHaveBeenCalledExactlyOnceWith( + newValue === true, + ); + }, + ); }); diff --git a/src/web-ui/src/infrastructure/config/services/FrontendLogLevelSync.ts b/src/web-ui/src/infrastructure/config/services/FrontendLogLevelSync.ts index 299142a24a..6bde875372 100644 --- a/src/web-ui/src/infrastructure/config/services/FrontendLogLevelSync.ts +++ b/src/web-ui/src/infrastructure/config/services/FrontendLogLevelSync.ts @@ -102,7 +102,7 @@ async function resolveInitialLoggingSettings(): Promise includeSensitiveDiagnostics: typeof configs[LOGGING_INCLUDE_SENSITIVE_PATH] === 'boolean' ? configs[LOGGING_INCLUDE_SENSITIVE_PATH] - : true, + : false, flowChatDiagnostics: configs[FLOW_CHAT_DIAGNOSTICS_PATH] === true, }; } @@ -115,7 +115,7 @@ async function resolveInitialLoggingSettings(): Promise includeSensitiveDiagnostics: typeof configs[LOGGING_INCLUDE_SENSITIVE_PATH] === 'boolean' ? configs[LOGGING_INCLUDE_SENSITIVE_PATH] - : true, + : false, flowChatDiagnostics: configs[FLOW_CHAT_DIAGNOSTICS_PATH] === true, }; } @@ -125,7 +125,7 @@ async function resolveInitialLoggingSettings(): Promise includeSensitiveDiagnostics: typeof configs[LOGGING_INCLUDE_SENSITIVE_PATH] === 'boolean' ? configs[LOGGING_INCLUDE_SENSITIVE_PATH] - : true, + : false, flowChatDiagnostics: configs[FLOW_CHAT_DIAGNOSTICS_PATH] === true, }; } @@ -163,7 +163,7 @@ export async function installFrontendLogLevelConfigWatcher(): Promise { } if (path === LOGGING_INCLUDE_SENSITIVE_PATH) { - setIncludeSensitiveDiagnostics(typeof newValue === 'boolean' ? newValue : true); + setIncludeSensitiveDiagnostics(newValue === true); return; } diff --git a/src/web-ui/src/shared/utils/logger.ts b/src/web-ui/src/shared/utils/logger.ts index e7ef8a16bf..26f46c504b 100644 --- a/src/web-ui/src/shared/utils/logger.ts +++ b/src/web-ui/src/shared/utils/logger.ts @@ -34,7 +34,7 @@ const isTauri = typeof window !== 'undefined' && '__TAURI__' in window; const isDev = import.meta.env?.DEV ?? process.env.NODE_ENV === 'development'; const CONSOLE_FORWARD_INSTALLED = '__openbitfun_console_forward_installed__'; -let includeSensitiveDiagnostics = true; +let includeSensitiveDiagnostics = false; declare global { // Injected by the desktop WebView initialization script before the frontend bundle runs.