Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion desktop/renderer/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ interface AppSettings {
minimizeToTray: boolean;
themeMode: string;
accentColor: string;
privacyLevel: string;
privacyLevel: "basic" | "strict";
privacyControls?: {
phone: boolean;
idCard: boolean;
bankCard: boolean;
email: boolean;
apiKey: boolean;
};
}

interface ChatAttachment {
Expand Down
2 changes: 1 addition & 1 deletion desktop/renderer/src/browser-openclaw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export function createBrowserOpenClawMock(): OpenClawAPI {
minimizeToTray: false,
themeMode: "light",
accentColor: "#1e1f25",
privacyLevel: "balanced",
privacyLevel: "basic",
}),
set: noopAsync,
},
Expand Down
21 changes: 5 additions & 16 deletions desktop/renderer/src/i18n/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,19 +269,12 @@ 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.piiDetection": "PII detection",
"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.",
"settings.piiPhone": "Phone numbers",
Expand All @@ -293,10 +286,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.fileAccessAuditToggle": "Log file access",
"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": "Tool sandbox (AppContainer)",
"settings.externalApps": "Apps allowed outside the sandbox",
Expand Down
22 changes: 5 additions & 17 deletions desktop/renderer/src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,18 +254,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": "手机号码",
Expand All @@ -276,10 +269,6 @@ export default {
"settings.sensitiveFiles": "敏感文件保护",
"settings.sensitiveFilesDesc": "智能体读取匹配以下模式的文件前,需要用户确认。",
"settings.sensitiveFilePatterns": ".env, *_key*, *.pem, *.p12, *.pfx, id_rsa, credentials",
"settings.fileAccessAudit": "文件访问审计",
"settings.fileAccessAuditToggle": "记录文件访问",
"settings.fileAccessAuditDesc":
"记录智能体在对话中读取的所有文件。审计日志保存在工作区数据目录中。",
"settings.security": "安全沙箱",
"settings.sandboxEnabled": "工具沙箱(AppContainer)",
"settings.externalApps": "允许在沙箱外运行的应用",
Expand Down Expand Up @@ -347,8 +336,7 @@ export default {
"settings.customModelDeleted": "自定义模型已删除",
"settings.copilotDisconnected": "GitHub Copilot 已断开连接",
"settings.copilotDisconnectFailed": "无法断开 GitHub Copilot:{error}",
"settings.copilotDisconnectedRestartFailed":
"GitHub Copilot 已断开,但网关重启失败:{error}",
"settings.copilotDisconnectedRestartFailed": "GitHub Copilot 已断开,但网关重启失败:{error}",
"settings.copilotConfigUnavailable": "无法读取当前模型配置",
"settings.searchSettingsSaved": "联网搜索设置已保存",
"settings.searchConfigured": "已配置",
Expand Down
63 changes: 62 additions & 1 deletion desktop/renderer/src/stores/chat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const mockAbort = vi.fn().mockResolvedValue(undefined);
const mockDeleteSession = vi.fn().mockResolvedValue(undefined);
const mockGenerateSessionTitle = vi.fn().mockResolvedValue("Summary title");
const mockIsConnected = vi.fn().mockResolvedValue(false);
const mockGetSettings = vi.fn().mockResolvedValue({});

Object.defineProperty(globalThis, "window", {
value: {
Expand All @@ -34,7 +35,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: [] }) },
Expand Down Expand Up @@ -926,6 +927,66 @@ 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);
});

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,
},
});
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", () => {
beforeEach(() => {
Object.keys(storage).forEach((k) => delete storage[k]);
Expand Down
17 changes: 7 additions & 10 deletions desktop/renderer/src/stores/chat.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -839,17 +840,13 @@ 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") {
const controls = hydratePrivacyControls("strict", privacySettings.privacyControls);
finalMsg = redactPii(msg, privacyControlsToScanOptions(controls));
}

// Optimistic: add user message locally
Expand Down
61 changes: 61 additions & 0 deletions desktop/renderer/src/utils/privacy-settings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { describe, expect, it } from "vitest";
import { hydratePrivacyControls, privacyControlsToScanOptions } 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,
});
});

it("defaults every control on for Strict", () => {
expect(hydratePrivacyControls("strict")).toEqual({
phone: true,
idCard: true,
bankCard: true,
email: true,
apiKey: true,
});
});

it("preserves explicit Strict choices and defaults only missing fields", () => {
expect(
hydratePrivacyControls("strict", {
phone: false,
email: false,
}),
).toEqual({
phone: false,
idCard: true,
bankCard: true,
email: false,
apiKey: true,
});
});

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,
}),
).toEqual({
phone: false,
idCard: true,
bankCard: false,
email: true,
apiKey: false,
});
});
});
35 changes: 35 additions & 0 deletions desktop/renderer/src/utils/privacy-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export type PrivacyLevel = "basic" | "strict";

export interface PrivacyControls {
phone: boolean;
idCard: boolean;
bankCard: boolean;
email: boolean;
apiKey: boolean;
}

export function hydratePrivacyControls(
level: PrivacyLevel,
saved?: Partial<PrivacyControls>,
): 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,
};
}

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";
2 changes: 1 addition & 1 deletion desktop/renderer/src/views/SettingsView.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe("SettingsView", () => {
autoStart: false,
minimizeToTray: false,
themeMode: "light",
privacyLevel: "balanced",
privacyLevel: "basic",
}),
set: vi.fn().mockResolvedValue(undefined),
},
Expand Down
Loading
Loading