Skip to content
Open
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
2 changes: 1 addition & 1 deletion desktop/renderer/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface AppSettings {
startMinimized: boolean;
themeMode: string;
accentColor: string;
privacyLevel: string;
privacyLevel: "basic" | "strict";
}

interface ChatAttachment {
Expand Down
15 changes: 4 additions & 11 deletions desktop/renderer/src/i18n/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
15 changes: 4 additions & 11 deletions desktop/renderer/src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": "手机号码",
Expand Down
2 changes: 1 addition & 1 deletion desktop/renderer/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ if (isBrowserDev) {
startMinimized: false,
themeMode: "light",
accentColor: "#1e1f25",
privacyLevel: "balanced",
privacyLevel: "basic",
}),
set: noopAsync,
},
Expand Down
28 changes: 27 additions & 1 deletion desktop/renderer/src/stores/chat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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: [] }) },
Expand Down Expand Up @@ -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]);
Expand Down
16 changes: 6 additions & 10 deletions desktop/renderer/src/stores/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1088,7 +1083,8 @@ export const useChatStore = defineStore("chat", () => {

const { phase, name, toolCallId, meta } = payload.data;
const args = (payload.data as Record<string, unknown>).args as
Record<string, unknown> | undefined;
| Record<string, unknown>
| undefined;
if (phase === "start") {
// Build a descriptive display name combining tool name + primary argument
let displayName = name;
Expand Down
85 changes: 23 additions & 62 deletions desktop/renderer/src/views/SettingsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,17 @@
<div class="card-row" :class="{ 'no-border': !usageData.maxBudget }">
<span class="row-label">{{ t("settings.totalSpend") }}</span>
<span class="row-value usage-spend"
>{{ t("settings.currencySymbol") }}{{ toCny(usageData.totalSpend).toFixed(2) }}</span
>{{ t("settings.currencySymbol")
}}{{ toCny(usageData.totalSpend).toFixed(2) }}</span
>
</div>
<div v-if="usageData.maxBudget" class="card-row no-border">
<span class="row-label">{{ t("settings.budget") }}</span>
<div class="budget-bar-wrapper">
<span class="row-value"
>{{ t("settings.currencySymbol") }}{{ toCny(usageData.totalSpend).toFixed(2) }} /
{{ t("settings.currencySymbol") }}{{ toCny(usageData.maxBudget).toFixed(2) }}</span
{{ t("settings.currencySymbol")
}}{{ toCny(usageData.maxBudget).toFixed(2) }}</span
>
<div class="budget-bar">
<div
Expand Down Expand Up @@ -170,7 +172,6 @@
</div>
</div>
</template>

</template>

<div class="section-footer">{{ t("settings.usageFooter") }}</div>
Expand Down Expand Up @@ -316,9 +317,7 @@
<span
class="status-indicator"
:class="
gateway.status === 'running'
? 'status-indicator--ok'
: 'status-indicator--error'
gateway.status === 'running' ? 'status-indicator--ok' : 'status-indicator--error'
"
>
<span class="status-dot"></span>
Expand All @@ -337,11 +336,7 @@
<span class="row-label">{{ t("settings.port") }}</span>
<div class="port-input-group">
<span class="port-prefix">ws://127.0.0.1 :</span>
<el-input
v-model="gatewayPort"
style="width: 80px"
@change="saveGatewayPort"
/>
<el-input v-model="gatewayPort" style="width: 80px" @change="saveGatewayPort" />
</div>
</div>
</div>
Expand Down Expand Up @@ -668,23 +663,6 @@
<ul class="privacy-card-list">
<li>{{ t("settings.privacyBasicDesc1") }}</li>
<li>{{ t("settings.privacyBasicDesc2") }}</li>
<li>{{ t("settings.privacyBasicDesc3") }}</li>
</ul>
</div>
<div
class="privacy-card"
:class="{ active: settings.privacyLevel === 'balanced' }"
@click="setPrivacyLevel('balanced')"
>
<div class="privacy-card-header">
<span class="privacy-card-icon">⚖️</span>
<span class="privacy-card-title">{{ t("settings.privacyBalanced") }}</span>
<span class="privacy-badge-recommended">{{ t("settings.privacyRecommended") }}</span>
</div>
<ul class="privacy-card-list">
<li>{{ t("settings.privacyBalancedDesc1") }}</li>
<li>{{ t("settings.privacyBalancedDesc2") }}</li>
<li>{{ t("settings.privacyBalancedDesc3") }}</li>
</ul>
</div>
<div
Expand All @@ -699,7 +677,6 @@
<ul class="privacy-card-list">
<li>{{ t("settings.privacyStrictDesc1") }}</li>
<li>{{ t("settings.privacyStrictDesc2") }}</li>
<li>{{ t("settings.privacyStrictDesc3") }}</li>
</ul>
</div>
</div>
Expand Down Expand Up @@ -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 ---
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -1576,7 +1550,6 @@ onMounted(async () => {

// Load web search provider configuration
loadSearchConfig(config);

});

onUnmounted(() => {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down
Loading