From 2543cfb6371c28200712731cc337a89665dae719 Mon Sep 17 00:00:00 2001 From: Copilot App <223556219+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:25:57 +0800 Subject: [PATCH] Simplify privacy protection modes Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1fe75171-fea2-45ef-9401-bf8a4ea0f2ba --- desktop/renderer/env.d.ts | 2 +- desktop/renderer/src/i18n/en-US.ts | 15 +--- desktop/renderer/src/i18n/zh-CN.ts | 15 +--- desktop/renderer/src/main.ts | 2 +- desktop/renderer/src/stores/chat.test.ts | 28 ++++++- desktop/renderer/src/stores/chat.ts | 16 ++-- desktop/renderer/src/views/SettingsView.vue | 85 ++++++--------------- desktop/src/main.ts | 33 +++----- 8 files changed, 75 insertions(+), 121 deletions(-) diff --git a/desktop/renderer/env.d.ts b/desktop/renderer/env.d.ts index 529fb47..2c13db0 100644 --- a/desktop/renderer/env.d.ts +++ b/desktop/renderer/env.d.ts @@ -22,7 +22,7 @@ interface AppSettings { startMinimized: boolean; themeMode: string; accentColor: string; - privacyLevel: string; + privacyLevel: "basic" | "strict"; } interface ChatAttachment { diff --git a/desktop/renderer/src/i18n/en-US.ts b/desktop/renderer/src/i18n/en-US.ts index df394eb..c97c928 100644 --- a/desktop/renderer/src/i18n/en-US.ts +++ b/desktop/renderer/src/i18n/en-US.ts @@ -267,18 +267,11 @@ export default { "settings.privacyProtectionDesc": "Control how MicroClaw handles your sensitive data before it's sent to AI models.", "settings.privacyBasic": "Basic", - "settings.privacyBasicDesc1": "Allow reading all accessible files", - "settings.privacyBasicDesc2": "No PII detection or filtering", - "settings.privacyBasicDesc3": "Full file content sent to model", - "settings.privacyBalanced": "Balanced", - "settings.privacyBalancedDesc1": "Warn before reading sensitive files (.env, keys, certs)", - "settings.privacyBalancedDesc2": "Detect & highlight PII (ID numbers, phone, bank cards)", - "settings.privacyBalancedDesc3": "Chat history saved, one-click clear", + "settings.privacyBasicDesc1": "No PII detection or filtering", + "settings.privacyBasicDesc2": "Messages are sent without automatic redaction", "settings.privacyStrict": "Strict", - "settings.privacyStrictDesc1": "Only allow reading whitelisted directories", - "settings.privacyStrictDesc2": "Auto-redact PII before sending to model", - "settings.privacyStrictDesc3": "Some features may be limited", - "settings.privacyRecommended": "Recommended", + "settings.privacyStrictDesc1": "Warn before reading sensitive files (.env, keys, certs)", + "settings.privacyStrictDesc2": "Detect and auto-redact PII before sending to model", "settings.piiDetection": "PII Detection", "settings.piiDetectionDesc": "Scan outgoing messages for personal information before they're sent to the AI model.", diff --git a/desktop/renderer/src/i18n/zh-CN.ts b/desktop/renderer/src/i18n/zh-CN.ts index 73934b8..116c197 100644 --- a/desktop/renderer/src/i18n/zh-CN.ts +++ b/desktop/renderer/src/i18n/zh-CN.ts @@ -252,18 +252,11 @@ export default { "settings.privacyProtection": "隐私防护", "settings.privacyProtectionDesc": "控制 MicroClaw 在将数据发送给 AI 模型前如何处理你的敏感信息。", "settings.privacyBasic": "基本", - "settings.privacyBasicDesc1": "允许读取所有可访问的文件", - "settings.privacyBasicDesc2": "不检测或过滤个人信息", - "settings.privacyBasicDesc3": "完整文件内容发送给模型", - "settings.privacyBalanced": "均衡", - "settings.privacyBalancedDesc1": "读取敏感文件时预警(.env、密钥、证书)", - "settings.privacyBalancedDesc2": "检测并高亮个人信息(身份证号、手机号、银行卡)", - "settings.privacyBalancedDesc3": "聊天记录保存,支持一键清除", + "settings.privacyBasicDesc1": "不检测或过滤个人信息", + "settings.privacyBasicDesc2": "消息发送前不会自动脱敏", "settings.privacyStrict": "严格", - "settings.privacyStrictDesc1": "仅允许读取白名单目录", - "settings.privacyStrictDesc2": "自动脱敏个人信息后再发送给模型", - "settings.privacyStrictDesc3": "部分功能可能受限", - "settings.privacyRecommended": "推荐", + "settings.privacyStrictDesc1": "读取敏感文件时预警(.env、密钥、证书)", + "settings.privacyStrictDesc2": "检测个人信息并自动脱敏后再发送给模型", "settings.piiDetection": "个人信息检测", "settings.piiDetectionDesc": "在发送给 AI 模型前,扫描传出消息中的个人信息。", "settings.piiPhone": "手机号码", diff --git a/desktop/renderer/src/main.ts b/desktop/renderer/src/main.ts index fe645ee..e2e2904 100644 --- a/desktop/renderer/src/main.ts +++ b/desktop/renderer/src/main.ts @@ -43,7 +43,7 @@ if (isBrowserDev) { startMinimized: false, themeMode: "light", accentColor: "#1e1f25", - privacyLevel: "balanced", + privacyLevel: "basic", }), set: noopAsync, }, diff --git a/desktop/renderer/src/stores/chat.test.ts b/desktop/renderer/src/stores/chat.test.ts index deebd02..6b11055 100644 --- a/desktop/renderer/src/stores/chat.test.ts +++ b/desktop/renderer/src/stores/chat.test.ts @@ -7,6 +7,7 @@ const mockSendMessage = vi.fn().mockResolvedValue(undefined); const mockAbort = vi.fn().mockResolvedValue(undefined); const mockDeleteSession = vi.fn().mockResolvedValue(undefined); const mockIsConnected = vi.fn().mockResolvedValue(false); +const mockGetSettings = vi.fn().mockResolvedValue({}); Object.defineProperty(globalThis, "window", { value: { @@ -32,7 +33,7 @@ Object.defineProperty(globalThis, "window", { onWsDisconnected: vi.fn(), restart: vi.fn(), }, - settings: { get: vi.fn().mockResolvedValue({}) }, + settings: { get: mockGetSettings }, sandbox: { onPermissionRequest: vi.fn() }, skills: { pendingIntegrityResult: vi.fn().mockResolvedValue(null) }, cron: { list: vi.fn().mockResolvedValue({ jobs: [] }) }, @@ -918,6 +919,31 @@ describe("useChatStore — attachments", () => { }); }); +describe("useChatStore — privacy", () => { + beforeEach(() => { + Object.keys(storage).forEach((k) => delete storage[k]); + setActivePinia(createPinia()); + mockSendMessage.mockReset().mockResolvedValue(undefined); + }); + + it("sends PII unchanged when privacy defaults to basic", async () => { + const store = useChatStore(); + + await store.sendMessage("phone 13812345678"); + + expect(mockSendMessage).toHaveBeenCalledWith("main", "phone 13812345678", undefined); + }); + + it("redacts PII before sending in strict mode", async () => { + mockGetSettings.mockResolvedValueOnce({ privacyLevel: "strict" }); + const store = useChatStore(); + + await store.sendMessage("phone 13812345678"); + + expect(mockSendMessage).toHaveBeenCalledWith("main", "phone 138****5678", undefined); + }); +}); + describe("useChatStore — session deletion", () => { beforeEach(() => { Object.keys(storage).forEach((k) => delete storage[k]); diff --git a/desktop/renderer/src/stores/chat.ts b/desktop/renderer/src/stores/chat.ts index 341b63c..6cbe09c 100644 --- a/desktop/renderer/src/stores/chat.ts +++ b/desktop/renderer/src/stores/chat.ts @@ -838,17 +838,12 @@ export const useChatStore = defineStore("chat", () => { lastError.value = null; let optimisticTimestamp: number | undefined; try { - // Privacy protection: scan for PII based on privacy level + // Strict privacy mode redacts PII before sending. let finalMsg = msg; const privacySettings = await window.openclaw.settings.get(); - const privacyLevel = privacySettings?.privacyLevel ?? "balanced"; - if (privacyLevel !== "basic") { - const piiMatches = scanPii(msg); - if (privacyLevel === "strict" && piiMatches.length > 0) { - // Auto-redact in strict mode - finalMsg = redactPii(msg); - } - // In balanced mode, piiMatches are available for UI warning (future) + const privacyLevel = privacySettings?.privacyLevel ?? "basic"; + if (privacyLevel === "strict" && scanPii(msg).length > 0) { + finalMsg = redactPii(msg); } // Optimistic: add user message locally @@ -1088,7 +1083,8 @@ export const useChatStore = defineStore("chat", () => { const { phase, name, toolCallId, meta } = payload.data; const args = (payload.data as Record).args as - Record | undefined; + | Record + | undefined; if (phase === "start") { // Build a descriptive display name combining tool name + primary argument let displayName = name; diff --git a/desktop/renderer/src/views/SettingsView.vue b/desktop/renderer/src/views/SettingsView.vue index 42c5acc..8c2d9d8 100644 --- a/desktop/renderer/src/views/SettingsView.vue +++ b/desktop/renderer/src/views/SettingsView.vue @@ -86,7 +86,8 @@
{{ t("settings.totalSpend") }} {{ t("settings.currencySymbol") }}{{ toCny(usageData.totalSpend).toFixed(2) }}{{ t("settings.currencySymbol") + }}{{ toCny(usageData.totalSpend).toFixed(2) }}
@@ -94,7 +95,8 @@
{{ t("settings.currencySymbol") }}{{ toCny(usageData.totalSpend).toFixed(2) }} / - {{ t("settings.currencySymbol") }}{{ toCny(usageData.maxBudget).toFixed(2) }}
- @@ -316,9 +317,7 @@ @@ -337,11 +336,7 @@ {{ t("settings.port") }}
ws://127.0.0.1 : - +
@@ -668,23 +663,6 @@
  • {{ t("settings.privacyBasicDesc1") }}
  • {{ t("settings.privacyBasicDesc2") }}
  • -
  • {{ t("settings.privacyBasicDesc3") }}
  • -
-
-
-
- ⚖️ - {{ t("settings.privacyBalanced") }} - {{ t("settings.privacyRecommended") }} -
-
    -
  • {{ t("settings.privacyBalancedDesc1") }}
  • -
  • {{ t("settings.privacyBalancedDesc2") }}
  • -
  • {{ t("settings.privacyBalancedDesc3") }}
  • {{ t("settings.privacyStrictDesc1") }}
  • {{ t("settings.privacyStrictDesc2") }}
  • -
  • {{ t("settings.privacyStrictDesc3") }}
  • @@ -1157,16 +1134,16 @@ const settings = reactive({ autoStart: false, startMinimized: false, themeMode: "light", - privacyLevel: "balanced" as "basic" | "balanced" | "strict", - fileAccessAudit: true, + privacyLevel: "basic" as "basic" | "strict", + fileAccessAudit: false, }); const piiToggles = reactive({ - phone: true, - idCard: true, - bankCard: true, - email: true, - apiKey: true, + phone: false, + idCard: false, + bankCard: false, + email: false, + apiKey: false, }); // --- Models & API state --- @@ -1397,7 +1374,7 @@ watch( }, ); -function setPrivacyLevel(level: "basic" | "balanced" | "strict") { +function setPrivacyLevel(level: "basic" | "strict") { settings.privacyLevel = level; window.openclaw.settings.set("privacyLevel", level); // Auto-configure PII toggles based on level @@ -1408,13 +1385,6 @@ function setPrivacyLevel(level: "basic" | "balanced" | "strict") { piiToggles.email = false; piiToggles.apiKey = false; settings.fileAccessAudit = false; - } else if (level === "balanced") { - piiToggles.phone = true; - piiToggles.idCard = true; - piiToggles.bankCard = true; - piiToggles.email = true; - piiToggles.apiKey = true; - settings.fileAccessAudit = true; } else { piiToggles.phone = true; piiToggles.idCard = true; @@ -1552,7 +1522,11 @@ onMounted(async () => { settings.autoStart = saved.autoStart ?? false; settings.startMinimized = saved.startMinimized ?? false; settings.themeMode = saved.themeMode ?? "light"; - settings.privacyLevel = (saved.privacyLevel ?? "balanced") as "basic" | "balanced" | "strict"; + const savedPrivacyLevel: string | undefined = saved.privacyLevel; + settings.privacyLevel = savedPrivacyLevel === "strict" ? "strict" : "basic"; + if (savedPrivacyLevel === "balanced") { + await window.openclaw.settings.set("privacyLevel", "basic"); + } // Init PII toggles based on loaded privacy level if (settings.privacyLevel === "basic") { piiToggles.phone = false; @@ -1576,7 +1550,6 @@ onMounted(async () => { // Load web search provider configuration loadSearchConfig(config); - }); onUnmounted(() => { @@ -1693,11 +1666,9 @@ async function removeCustomModel(idx: number) { async function disconnectGitHubCopilot() { if (switchingModelRef.value || removingModelRef.value || copilotDisconnecting.value) return; try { - await ElMessageBox.confirm( - t("settings.copilotDisconnectConfirm"), - t("settings.confirm"), - { type: "warning" }, - ); + await ElMessageBox.confirm(t("settings.copilotDisconnectConfirm"), t("settings.confirm"), { + type: "warning", + }); } catch { return; } @@ -2638,7 +2609,7 @@ async function clearChatHistory() { /* ── Privacy Protection ── */ .privacy-levels { display: grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(2, 1fr); gap: 12px; } @@ -2679,16 +2650,6 @@ async function clearChatHistory() { color: var(--text-primary); } -.privacy-badge-recommended { - font-size: 10px; - font-weight: 600; - padding: 2px 8px; - border-radius: 10px; - background: rgba(212, 168, 67, 0.15); - color: var(--accent-selected); - margin-left: auto; -} - .privacy-card-list { list-style: none; padding: 0; diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 0fb2c09..e5980a5 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -179,8 +179,8 @@ const settingsStore = new Store<{ sandboxUserDirsRO: string[]; /** All directories we've ever granted AC ACL to. Used to detect stale ACLs on startup. */ sandboxGrantHistory: string[]; - /** Privacy protection level: basic, balanced, strict */ - privacyLevel: string; + /** Privacy protection level. */ + privacyLevel: "basic" | "strict"; }>({ name: "settings", defaults: { @@ -203,7 +203,7 @@ const settingsStore = new Store<{ sandboxUserDirsRW: [], sandboxUserDirsRO: [], sandboxGrantHistory: [], - privacyLevel: "balanced", + privacyLevel: "basic", }, }); @@ -1427,9 +1427,7 @@ async function addCatalogAgent( } if (restartAttempted && managedGateway) { try { - await restartManagedGatewayAndRequireReady( - `Rolling back failed agent addition ${agentId}`, - ); + await restartManagedGatewayAndRequireReady(`Rolling back failed agent addition ${agentId}`); } catch (rollbackError) { rollbackErrors.push(rollbackError); } @@ -1489,9 +1487,7 @@ async function removeCatalogAgent( } if (restartAttempted && managedGateway) { try { - await restartManagedGatewayAndRequireReady( - `Rolling back failed agent removal ${agentId}`, - ); + await restartManagedGatewayAndRequireReady(`Rolling back failed agent removal ${agentId}`); } catch (rollbackError) { rollbackErrors.push(rollbackError); } @@ -3674,24 +3670,15 @@ function registerIpcHandlers(): void { }); if (result.canceled) return { attachments: [], rejections: [] }; const currentTotalBytes = - typeof params?.currentTotalBytes === "number" && - Number.isFinite(params.currentTotalBytes) + typeof params?.currentTotalBytes === "number" && Number.isFinite(params.currentTotalBytes) ? Math.max(0, params.currentTotalBytes) : 0; - return prepareChatAttachments( - result.filePaths, - undefined, - undefined, - currentTotalBytes, - ); + return prepareChatAttachments(result.filePaths, undefined, undefined, currentTotalBytes); }); ipcMain.handle( "chat:send-message", - async ( - _event, - params: { sessionKey: string; message: string; attachments?: unknown }, - ) => { + async (_event, params: { sessionKey: string; message: string; attachments?: unknown }) => { if (!gwClient?.connected) throw new Error("Gateway not connected"); // Mark that the latest input is from the local desktop UI. lastInputFromRemote = false; @@ -4167,9 +4154,7 @@ function registerIpcHandlers(): void { ipcMain.handle("model:github-copilot:status", () => { const client = gwClient; - const queryGateway = client?.connected - ? () => client.request("models.authStatus") - : undefined; + const queryGateway = client?.connected ? () => client.request("models.authStatus") : undefined; return getGitHubCopilotAuthStatus(resolveGitHubCopilotAuthRuntime(), queryGateway); });