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 1/5] 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); }); From a30435cda6c0385c85a556a3961f3c9471cf67e1 Mon Sep 17 00:00:00 2001 From: Copilot App <223556219+Copilot@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:46:28 +0800 Subject: [PATCH 2/5] Fix privacy control rehydration Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1fe75171-fea2-45ef-9401-bf8a4ea0f2ba --- desktop/renderer/src/views/SettingsView.vue | 39 +++++++-------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/desktop/renderer/src/views/SettingsView.vue b/desktop/renderer/src/views/SettingsView.vue index 8c2d9d8..a8ee94d 100644 --- a/desktop/renderer/src/views/SettingsView.vue +++ b/desktop/renderer/src/views/SettingsView.vue @@ -1374,25 +1374,20 @@ watch( }, ); +function applyPrivacyLevelState(level: "basic" | "strict") { + const enabled = level === "strict"; + piiToggles.phone = enabled; + piiToggles.idCard = enabled; + piiToggles.bankCard = enabled; + piiToggles.email = enabled; + piiToggles.apiKey = enabled; + settings.fileAccessAudit = enabled; +} + function setPrivacyLevel(level: "basic" | "strict") { settings.privacyLevel = level; - window.openclaw.settings.set("privacyLevel", level); - // Auto-configure PII toggles based on level - if (level === "basic") { - piiToggles.phone = false; - piiToggles.idCard = false; - piiToggles.bankCard = false; - piiToggles.email = false; - piiToggles.apiKey = false; - settings.fileAccessAudit = false; - } else { - piiToggles.phone = true; - piiToggles.idCard = true; - piiToggles.bankCard = true; - piiToggles.email = true; - piiToggles.apiKey = true; - settings.fileAccessAudit = true; - } + applyPrivacyLevelState(level); + void window.openclaw.settings.set("privacyLevel", level); } // --- Auto-load data when tab is selected --- @@ -1524,18 +1519,10 @@ onMounted(async () => { settings.themeMode = saved.themeMode ?? "light"; const savedPrivacyLevel: string | undefined = saved.privacyLevel; settings.privacyLevel = savedPrivacyLevel === "strict" ? "strict" : "basic"; + applyPrivacyLevelState(settings.privacyLevel); 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; - piiToggles.idCard = false; - piiToggles.bankCard = false; - piiToggles.email = false; - piiToggles.apiKey = false; - settings.fileAccessAudit = false; - } } // Load existing config for models & gateway From 8d51af58413c975607f7d76e91dfb5c12c626ad2 Mon Sep 17 00:00:00 2001 From: Copilot App <223556219+Copilot@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:57:24 +0800 Subject: [PATCH 3/5] Persist privacy control choices Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1fe75171-fea2-45ef-9401-bf8a4ea0f2ba --- desktop/renderer/env.d.ts | 8 +++ .../src/utils/privacy-settings.test.ts | 47 +++++++++++++ .../renderer/src/utils/privacy-settings.ts | 26 +++++++ desktop/renderer/src/views/SettingsView.vue | 68 ++++++++++++++----- desktop/src/main.ts | 9 +++ 5 files changed, 141 insertions(+), 17 deletions(-) create mode 100644 desktop/renderer/src/utils/privacy-settings.test.ts create mode 100644 desktop/renderer/src/utils/privacy-settings.ts diff --git a/desktop/renderer/env.d.ts b/desktop/renderer/env.d.ts index 2c13db0..1759248 100644 --- a/desktop/renderer/env.d.ts +++ b/desktop/renderer/env.d.ts @@ -23,6 +23,14 @@ interface AppSettings { themeMode: string; accentColor: string; privacyLevel: "basic" | "strict"; + privacyControls?: { + phone: boolean; + idCard: boolean; + bankCard: boolean; + email: boolean; + apiKey: boolean; + fileAccessAudit: boolean; + }; } interface ChatAttachment { diff --git a/desktop/renderer/src/utils/privacy-settings.test.ts b/desktop/renderer/src/utils/privacy-settings.test.ts new file mode 100644 index 0000000..8bb6af9 --- /dev/null +++ b/desktop/renderer/src/utils/privacy-settings.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "vitest"; +import { hydratePrivacyControls } from "./privacy-settings"; + +describe("hydratePrivacyControls", () => { + it("defaults every control off for Basic", () => { + expect(hydratePrivacyControls("basic")).toEqual({ + phone: false, + idCard: false, + bankCard: false, + email: false, + apiKey: false, + fileAccessAudit: false, + }); + }); + + it("defaults every control on for Strict", () => { + expect(hydratePrivacyControls("strict")).toEqual({ + phone: true, + idCard: true, + bankCard: true, + email: true, + apiKey: true, + fileAccessAudit: true, + }); + }); + + it("preserves explicit Strict choices and defaults only missing fields", () => { + expect( + hydratePrivacyControls("strict", { + phone: false, + email: false, + fileAccessAudit: false, + }), + ).toEqual({ + phone: false, + idCard: true, + bankCard: true, + email: false, + apiKey: true, + fileAccessAudit: false, + }); + }); + + it("forces controls off when hydrating Basic", () => { + expect(hydratePrivacyControls("basic", { phone: true })).toMatchObject({ phone: false }); + }); +}); diff --git a/desktop/renderer/src/utils/privacy-settings.ts b/desktop/renderer/src/utils/privacy-settings.ts new file mode 100644 index 0000000..f4a310a --- /dev/null +++ b/desktop/renderer/src/utils/privacy-settings.ts @@ -0,0 +1,26 @@ +export type PrivacyLevel = "basic" | "strict"; + +export interface PrivacyControls { + phone: boolean; + idCard: boolean; + bankCard: boolean; + email: boolean; + apiKey: boolean; + fileAccessAudit: boolean; +} + +export function hydratePrivacyControls( + level: PrivacyLevel, + saved?: Partial, +): PrivacyControls { + const defaultEnabled = level === "strict"; + const persisted = level === "strict" ? saved : undefined; + return { + phone: persisted?.phone ?? defaultEnabled, + idCard: persisted?.idCard ?? defaultEnabled, + bankCard: persisted?.bankCard ?? defaultEnabled, + email: persisted?.email ?? defaultEnabled, + apiKey: persisted?.apiKey ?? defaultEnabled, + fileAccessAudit: persisted?.fileAccessAudit ?? defaultEnabled, + }; +} diff --git a/desktop/renderer/src/views/SettingsView.vue b/desktop/renderer/src/views/SettingsView.vue index a8ee94d..b34d44a 100644 --- a/desktop/renderer/src/views/SettingsView.vue +++ b/desktop/renderer/src/views/SettingsView.vue @@ -687,26 +687,43 @@
    {{ t("settings.piiPhone") }} - +
    {{ t("settings.piiIdCard") }} - +
    {{ t("settings.piiBankCard") }}
    {{ t("settings.piiEmail") }} - +
    {{ t("settings.piiApiKey") }} - +
    @@ -739,6 +756,7 @@ @@ -835,6 +853,11 @@ import { removeGitHubCopilotModelReferences, } from "@/utils/auth-managed-models"; import { getManagedModelProvider } from "@/utils/managed-model-providers"; +import { + hydratePrivacyControls, + type PrivacyControls, + type PrivacyLevel, +} from "@/utils/privacy-settings"; const route = useRoute(); const gateway = useGatewayStore(); @@ -1134,7 +1157,7 @@ const settings = reactive({ autoStart: false, startMinimized: false, themeMode: "light", - privacyLevel: "basic" as "basic" | "strict", + privacyLevel: "basic" as PrivacyLevel, fileAccessAudit: false, }); @@ -1374,20 +1397,30 @@ watch( }, ); -function applyPrivacyLevelState(level: "basic" | "strict") { - const enabled = level === "strict"; - piiToggles.phone = enabled; - piiToggles.idCard = enabled; - piiToggles.bankCard = enabled; - piiToggles.email = enabled; - piiToggles.apiKey = enabled; - settings.fileAccessAudit = enabled; +function currentPrivacyControls(): PrivacyControls { + return { + ...piiToggles, + fileAccessAudit: settings.fileAccessAudit, + }; +} + +function applyPrivacyControls(controls: PrivacyControls) { + piiToggles.phone = controls.phone; + piiToggles.idCard = controls.idCard; + piiToggles.bankCard = controls.bankCard; + piiToggles.email = controls.email; + piiToggles.apiKey = controls.apiKey; + settings.fileAccessAudit = controls.fileAccessAudit; +} + +function persistPrivacyControls() { + return window.openclaw.settings.set("privacyControls", currentPrivacyControls()); } -function setPrivacyLevel(level: "basic" | "strict") { +function setPrivacyLevel(level: PrivacyLevel) { settings.privacyLevel = level; - applyPrivacyLevelState(level); - void window.openclaw.settings.set("privacyLevel", level); + applyPrivacyControls(hydratePrivacyControls(level)); + void Promise.all([window.openclaw.settings.set("privacyLevel", level), persistPrivacyControls()]); } // --- Auto-load data when tab is selected --- @@ -1519,7 +1552,8 @@ onMounted(async () => { settings.themeMode = saved.themeMode ?? "light"; const savedPrivacyLevel: string | undefined = saved.privacyLevel; settings.privacyLevel = savedPrivacyLevel === "strict" ? "strict" : "basic"; - applyPrivacyLevelState(settings.privacyLevel); + applyPrivacyControls(hydratePrivacyControls(settings.privacyLevel, saved.privacyControls)); + await persistPrivacyControls(); if (savedPrivacyLevel === "balanced") { await window.openclaw.settings.set("privacyLevel", "basic"); } diff --git a/desktop/src/main.ts b/desktop/src/main.ts index e5980a5..f7eb660 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -181,6 +181,15 @@ const settingsStore = new Store<{ sandboxGrantHistory: string[]; /** Privacy protection level. */ privacyLevel: "basic" | "strict"; + /** Per-control privacy preferences. Missing fields use mode-specific defaults. */ + privacyControls?: { + phone: boolean; + idCard: boolean; + bankCard: boolean; + email: boolean; + apiKey: boolean; + fileAccessAudit: boolean; + }; }>({ name: "settings", defaults: { From db5f0ff5ce7905efc97a9ec59f8a910c335cc35a Mon Sep 17 00:00:00 2001 From: Copilot App <223556219+Copilot@users.noreply.github.com> Date: Wed, 5 Aug 2026 12:07:25 +0800 Subject: [PATCH 4/5] Honor privacy controls during redaction Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1fe75171-fea2-45ef-9401-bf8a4ea0f2ba --- desktop/renderer/src/stores/chat.test.ts | 36 +++++++++++++++++++ desktop/renderer/src/stores/chat.ts | 8 +++-- .../src/utils/privacy-settings.test.ts | 21 ++++++++++- .../renderer/src/utils/privacy-settings.ts | 11 ++++++ 4 files changed, 72 insertions(+), 4 deletions(-) diff --git a/desktop/renderer/src/stores/chat.test.ts b/desktop/renderer/src/stores/chat.test.ts index 6b11055..fc71f50 100644 --- a/desktop/renderer/src/stores/chat.test.ts +++ b/desktop/renderer/src/stores/chat.test.ts @@ -942,6 +942,42 @@ describe("useChatStore — privacy", () => { expect(mockSendMessage).toHaveBeenCalledWith("main", "phone 138****5678", undefined); }); + + it("honors disabled Strict categories while redacting enabled ones", async () => { + mockGetSettings.mockResolvedValueOnce({ + privacyLevel: "strict", + privacyControls: { + phone: false, + idCard: true, + bankCard: true, + email: true, + apiKey: true, + fileAccessAudit: false, + }, + }); + const store = useChatStore(); + + await store.sendMessage("phone 13812345678 email alice@example.com"); + + expect(mockSendMessage).toHaveBeenCalledWith( + "main", + "phone 13812345678 email al***@example.com", + undefined, + ); + }); + + it("defaults missing legacy Strict controls to enabled", async () => { + mockGetSettings.mockResolvedValueOnce({ privacyLevel: "strict", privacyControls: {} }); + const store = useChatStore(); + + await store.sendMessage("phone 13812345678 email alice@example.com"); + + expect(mockSendMessage).toHaveBeenCalledWith( + "main", + "phone 138****5678 email al***@example.com", + undefined, + ); + }); }); describe("useChatStore — session deletion", () => { diff --git a/desktop/renderer/src/stores/chat.ts b/desktop/renderer/src/stores/chat.ts index 6cbe09c..1bffcf5 100644 --- a/desktop/renderer/src/stores/chat.ts +++ b/desktop/renderer/src/stores/chat.ts @@ -1,7 +1,8 @@ import { defineStore } from "pinia"; import { ref, computed } from "vue"; import { useSessionStore } from "./sessions"; -import { scanPii, redactPii } from "@/utils/pii-scanner"; +import { redactPii } from "@/utils/pii-scanner"; +import { hydratePrivacyControls, privacyControlsToScanOptions } from "@/utils/privacy-settings"; /** * Chat store — mirrors the webchat gateway protocol. @@ -842,8 +843,9 @@ export const useChatStore = defineStore("chat", () => { let finalMsg = msg; const privacySettings = await window.openclaw.settings.get(); const privacyLevel = privacySettings?.privacyLevel ?? "basic"; - if (privacyLevel === "strict" && scanPii(msg).length > 0) { - finalMsg = redactPii(msg); + if (privacyLevel === "strict") { + const controls = hydratePrivacyControls("strict", privacySettings.privacyControls); + finalMsg = redactPii(msg, privacyControlsToScanOptions(controls)); } // Optimistic: add user message locally diff --git a/desktop/renderer/src/utils/privacy-settings.test.ts b/desktop/renderer/src/utils/privacy-settings.test.ts index 8bb6af9..a5e38a2 100644 --- a/desktop/renderer/src/utils/privacy-settings.test.ts +++ b/desktop/renderer/src/utils/privacy-settings.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { hydratePrivacyControls } from "./privacy-settings"; +import { hydratePrivacyControls, privacyControlsToScanOptions } from "./privacy-settings"; describe("hydratePrivacyControls", () => { it("defaults every control off for Basic", () => { @@ -44,4 +44,23 @@ describe("hydratePrivacyControls", () => { it("forces controls off when hydrating Basic", () => { expect(hydratePrivacyControls("basic", { phone: true })).toMatchObject({ phone: false }); }); + + it("maps every PII control to typed scanner options", () => { + expect( + privacyControlsToScanOptions({ + phone: false, + idCard: true, + bankCard: false, + email: true, + apiKey: false, + fileAccessAudit: true, + }), + ).toEqual({ + phone: false, + idCard: true, + bankCard: false, + email: true, + apiKey: false, + }); + }); }); diff --git a/desktop/renderer/src/utils/privacy-settings.ts b/desktop/renderer/src/utils/privacy-settings.ts index f4a310a..47073f1 100644 --- a/desktop/renderer/src/utils/privacy-settings.ts +++ b/desktop/renderer/src/utils/privacy-settings.ts @@ -24,3 +24,14 @@ export function hydratePrivacyControls( fileAccessAudit: persisted?.fileAccessAudit ?? defaultEnabled, }; } + +export function privacyControlsToScanOptions(controls: PrivacyControls): ScanOptions { + return { + phone: controls.phone, + idCard: controls.idCard, + bankCard: controls.bankCard, + email: controls.email, + apiKey: controls.apiKey, + }; +} +import type { ScanOptions } from "./pii-scanner"; From a7b160df659871ad0a03db60e7b629843a6ae65f Mon Sep 17 00:00:00 2001 From: Copilot App <223556219+Copilot@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:03:08 +0800 Subject: [PATCH 5/5] Remove unimplemented file access audit UI Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1fe75171-fea2-45ef-9401-bf8a4ea0f2ba --- desktop/renderer/env.d.ts | 1 - desktop/renderer/src/i18n/en-US.ts | 3 --- desktop/renderer/src/i18n/zh-CN.ts | 3 --- desktop/renderer/src/stores/chat.test.ts | 1 - .../src/utils/privacy-settings.test.ts | 5 ----- .../renderer/src/utils/privacy-settings.ts | 2 -- desktop/renderer/src/views/SettingsView.vue | 21 +------------------ desktop/src/main.ts | 1 - 8 files changed, 1 insertion(+), 36 deletions(-) diff --git a/desktop/renderer/env.d.ts b/desktop/renderer/env.d.ts index 1759248..f8d78e6 100644 --- a/desktop/renderer/env.d.ts +++ b/desktop/renderer/env.d.ts @@ -29,7 +29,6 @@ interface AppSettings { bankCard: boolean; email: boolean; apiKey: boolean; - fileAccessAudit: boolean; }; } diff --git a/desktop/renderer/src/i18n/en-US.ts b/desktop/renderer/src/i18n/en-US.ts index c97c928..6a4a415 100644 --- a/desktop/renderer/src/i18n/en-US.ts +++ b/desktop/renderer/src/i18n/en-US.ts @@ -284,9 +284,6 @@ export default { "settings.sensitiveFilesDesc": "Ask for confirmation before the agent reads files that match these patterns.", "settings.sensitiveFilePatterns": ".env, *_key*, *.pem, *.p12, *.pfx, id_rsa, credentials", - "settings.fileAccessAudit": "File Access Audit", - "settings.fileAccessAuditDesc": - "Log every file the agent reads during conversations. Audit logs are saved in the workspace data directory.", "settings.security": "Security Sandbox", "settings.sandboxEnabled": "Enable tool sandbox (AppContainer)", "settings.externalApps": "Sandbox bypass whitelist", diff --git a/desktop/renderer/src/i18n/zh-CN.ts b/desktop/renderer/src/i18n/zh-CN.ts index 116c197..c38a5c6 100644 --- a/desktop/renderer/src/i18n/zh-CN.ts +++ b/desktop/renderer/src/i18n/zh-CN.ts @@ -267,9 +267,6 @@ export default { "settings.sensitiveFiles": "敏感文件守卫", "settings.sensitiveFilesDesc": "Agent 读取匹配以下模式的文件前,需要用户确认。", "settings.sensitiveFilePatterns": ".env, *_key*, *.pem, *.p12, *.pfx, id_rsa, credentials", - "settings.fileAccessAudit": "文件访问审计", - "settings.fileAccessAuditDesc": - "记录 Agent 在对话中读取的所有文件。审计日志保存在工作区数据目录中。", "settings.security": "安全沙箱", "settings.sandboxEnabled": "启用工具沙箱 (AppContainer)", "settings.externalApps": "免沙箱应用白名单", diff --git a/desktop/renderer/src/stores/chat.test.ts b/desktop/renderer/src/stores/chat.test.ts index fc71f50..051d2f6 100644 --- a/desktop/renderer/src/stores/chat.test.ts +++ b/desktop/renderer/src/stores/chat.test.ts @@ -952,7 +952,6 @@ describe("useChatStore — privacy", () => { bankCard: true, email: true, apiKey: true, - fileAccessAudit: false, }, }); const store = useChatStore(); diff --git a/desktop/renderer/src/utils/privacy-settings.test.ts b/desktop/renderer/src/utils/privacy-settings.test.ts index a5e38a2..a452678 100644 --- a/desktop/renderer/src/utils/privacy-settings.test.ts +++ b/desktop/renderer/src/utils/privacy-settings.test.ts @@ -9,7 +9,6 @@ describe("hydratePrivacyControls", () => { bankCard: false, email: false, apiKey: false, - fileAccessAudit: false, }); }); @@ -20,7 +19,6 @@ describe("hydratePrivacyControls", () => { bankCard: true, email: true, apiKey: true, - fileAccessAudit: true, }); }); @@ -29,7 +27,6 @@ describe("hydratePrivacyControls", () => { hydratePrivacyControls("strict", { phone: false, email: false, - fileAccessAudit: false, }), ).toEqual({ phone: false, @@ -37,7 +34,6 @@ describe("hydratePrivacyControls", () => { bankCard: true, email: false, apiKey: true, - fileAccessAudit: false, }); }); @@ -53,7 +49,6 @@ describe("hydratePrivacyControls", () => { bankCard: false, email: true, apiKey: false, - fileAccessAudit: true, }), ).toEqual({ phone: false, diff --git a/desktop/renderer/src/utils/privacy-settings.ts b/desktop/renderer/src/utils/privacy-settings.ts index 47073f1..f21f8a2 100644 --- a/desktop/renderer/src/utils/privacy-settings.ts +++ b/desktop/renderer/src/utils/privacy-settings.ts @@ -6,7 +6,6 @@ export interface PrivacyControls { bankCard: boolean; email: boolean; apiKey: boolean; - fileAccessAudit: boolean; } export function hydratePrivacyControls( @@ -21,7 +20,6 @@ export function hydratePrivacyControls( bankCard: persisted?.bankCard ?? defaultEnabled, email: persisted?.email ?? defaultEnabled, apiKey: persisted?.apiKey ?? defaultEnabled, - fileAccessAudit: persisted?.fileAccessAudit ?? defaultEnabled, }; } diff --git a/desktop/renderer/src/views/SettingsView.vue b/desktop/renderer/src/views/SettingsView.vue index b34d44a..6e54619 100644 --- a/desktop/renderer/src/views/SettingsView.vue +++ b/desktop/renderer/src/views/SettingsView.vue @@ -748,20 +748,6 @@ - -
    {{ t("settings.fileAccessAudit") }}
    -
    -
    - {{ t("settings.fileAccessAudit") }} - -
    -
    - -
    {{ t("settings.chatHistory") }}
    @@ -1158,7 +1144,6 @@ const settings = reactive({ startMinimized: false, themeMode: "light", privacyLevel: "basic" as PrivacyLevel, - fileAccessAudit: false, }); const piiToggles = reactive({ @@ -1398,10 +1383,7 @@ watch( ); function currentPrivacyControls(): PrivacyControls { - return { - ...piiToggles, - fileAccessAudit: settings.fileAccessAudit, - }; + return { ...piiToggles }; } function applyPrivacyControls(controls: PrivacyControls) { @@ -1410,7 +1392,6 @@ function applyPrivacyControls(controls: PrivacyControls) { piiToggles.bankCard = controls.bankCard; piiToggles.email = controls.email; piiToggles.apiKey = controls.apiKey; - settings.fileAccessAudit = controls.fileAccessAudit; } function persistPrivacyControls() { diff --git a/desktop/src/main.ts b/desktop/src/main.ts index f7eb660..e41752a 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -188,7 +188,6 @@ const settingsStore = new Store<{ bankCard: boolean; email: boolean; apiKey: boolean; - fileAccessAudit: boolean; }; }>({ name: "settings",