diff --git a/desktop/renderer/env.d.ts b/desktop/renderer/env.d.ts index e4a4126..1fff6ed 100644 --- a/desktop/renderer/env.d.ts +++ b/desktop/renderer/env.d.ts @@ -323,6 +323,28 @@ interface OpenClawAPI { timeoutMs?: number; }): Promise<{ connected: boolean; message: string; accountId?: string }>; disconnect(params?: { accountId?: string }): Promise<{ ok: boolean; error?: string }>; + getRemoteApprovalStatus(): Promise<{ + experimental: true; + enabled: boolean; + configured: boolean; + owner: { + contract: "microclaw.windows-weixin-remote-approval.v1"; + accountId: string; + userId: string; + boundAt: string; + } | null; + availableOwners: Array<{ + contract: "microclaw.windows-weixin-remote-approval.v1"; + accountId: string; + userId: string; + boundAt: string; + }>; + blockedReason: "mxc-off" | "owner-ambiguous" | null; + }>; + setRemoteApprovalEnabled( + enabled: boolean, + ): Promise<{ ok: boolean; error?: string; cancelled?: boolean }>; + revokeRemoteApproval(): Promise<{ ok: boolean }>; onLoginOutput(callback: (text: string) => void): () => void; onLoginDone(callback: (result: { code: number | null }) => void): () => void; }; diff --git a/desktop/renderer/src/browser-openclaw.ts b/desktop/renderer/src/browser-openclaw.ts index 2001e01..4327d57 100644 --- a/desktop/renderer/src/browser-openclaw.ts +++ b/desktop/renderer/src/browser-openclaw.ts @@ -344,6 +344,19 @@ export function createBrowserOpenClawMock(): OpenClawAPI { message: "Unavailable in browser development", }), disconnect: async () => ({ ok: true }), + getRemoteApprovalStatus: async () => ({ + experimental: true as const, + enabled: false, + configured: false, + owner: null, + availableOwners: [], + blockedReason: "mxc-off" as const, + }), + setRemoteApprovalEnabled: async () => ({ + ok: false, + error: "Unavailable in browser development", + }), + revokeRemoteApproval: async () => ({ ok: true }), onLoginOutput: noopSub, onLoginDone: noopSub, }, diff --git a/desktop/renderer/src/i18n/en-US.ts b/desktop/renderer/src/i18n/en-US.ts index 85a148f..cb89f12 100644 --- a/desktop/renderer/src/i18n/en-US.ts +++ b/desktop/renderer/src/i18n/en-US.ts @@ -295,6 +295,22 @@ export default { "settings.windowsNodeMxc": "Experimental bundled Windows Node + MXC", "settings.windowsNodeMxcExperimental": "App-owned headless node and pinned official MXC 0.7 runtime (security framework #202)", + "settings.experimental": "Experimental", + "settings.weixinRemoteApproval": "WeChat remote approval (Experimental)", + "settings.weixinRemoteApprovalRisk": + "Trusts normalized account, sender, direct-chat, message, and cursor metadata from the allowlisted openclaw-weixin plugin and Gateway as authentic. A compromised phone, WeChat session, plugin, or Gateway could authorize local command execution.", + "settings.weixinRemoteApprovalOwner": "Bound owner", + "settings.weixinRemoteApprovalUnbound": "Not bound", + "settings.weixinRemoteApprovalRequiresMxc": + "Enable Windows Node + MXC before enabling remote approval.", + "settings.weixinRemoteApprovalRequiresOwner": + "Exactly one authenticated WeChat account whose owner is in the explicit sender allowlist is required.", + "settings.weixinRemoteApprovalOnceOnly": + "Replies approve only the currently listed request(s) once. They never create durable approvals, folder grants, or future approval. Secret-bearing or unclassified free-text command fields stay local-only.", + "settings.weixinRemoteApprovalEnable": "Enable and bind locally", + "settings.weixinRemoteApprovalDisable": "Disable", + "settings.weixinRemoteApprovalRevoke": "Revoke owner binding", + "settings.weixinRemoteApprovalFailed": "Could not update WeChat remote approval", "settings.windowsNodeMxcModeFull": "Full containment", "settings.windowsNodeMxcFullBadge": "Hard boundary", "settings.windowsNodeMxcModeFullDesc": @@ -319,12 +335,10 @@ export default { "Before MicroClaw changes any setting or lifecycle state, review what {mode} makes unavailable, pauses, and keeps available with restrictions.", "settings.windowsNodeMxcConsent.group.disabled": "Disabled while MXC is requested", "settings.windowsNodeMxcConsent.group.paused": "Paused while MXC is active", - "settings.windowsNodeMxcConsent.group.available-restricted": - "Available with restrictions", + "settings.windowsNodeMxcConsent.group.available-restricted": "Available with restrictions", "settings.windowsNodeMxcConsent.item.gateway-native-filesystem": "Gateway-native filesystem tools are disabled: read, write, edit, and apply_patch.", - "settings.windowsNodeMxcConsent.item.browser-automation": - "Browser automation is disabled.", + "settings.windowsNodeMxcConsent.item.browser-automation": "Browser automation is disabled.", "settings.windowsNodeMxcConsent.item.mcp": "MCP is disabled.", "settings.windowsNodeMxcConsent.item.screen": "Screen access is disabled.", "settings.windowsNodeMxcConsent.item.camera": "Camera access is disabled.", diff --git a/desktop/renderer/src/i18n/zh-CN.ts b/desktop/renderer/src/i18n/zh-CN.ts index 37899d1..2b702e4 100644 --- a/desktop/renderer/src/i18n/zh-CN.ts +++ b/desktop/renderer/src/i18n/zh-CN.ts @@ -277,6 +277,21 @@ export default { "settings.windowsNodeMxc": "实验性内置 Windows Node + MXC", "settings.windowsNodeMxcExperimental": "应用自有无界面节点与固定版本的官方 MXC 0.7 运行时(安全框架议题 #202)", + "settings.experimental": "实验性", + "settings.weixinRemoteApproval": "微信远程审批(实验性)", + "settings.weixinRemoteApprovalRisk": + "本功能将白名单 openclaw-weixin 插件和 Gateway 提供的标准化账户、发送者、私聊、消息及游标元数据视为真实输入。手机、微信会话、插件或 Gateway 被控制时,攻击者可能授权本机命令执行。", + "settings.weixinRemoteApprovalOwner": "绑定所有者", + "settings.weixinRemoteApprovalUnbound": "未绑定", + "settings.weixinRemoteApprovalRequiresMxc": "请先启用 Windows Node + MXC。", + "settings.weixinRemoteApprovalRequiresOwner": + "必须且只能存在一个已登录微信账户,且其所有者必须位于显式发送者白名单中。", + "settings.weixinRemoteApprovalOnceOnly": + "回复仅一次性批准当前列出的请求;绝不会创建永久审批、文件夹授权或未来请求授权。包含秘密或无法分类的自由文本命令字段仅允许本地审批。", + "settings.weixinRemoteApprovalEnable": "在本机确认启用并绑定", + "settings.weixinRemoteApprovalDisable": "禁用", + "settings.weixinRemoteApprovalRevoke": "撤销所有者绑定", + "settings.weixinRemoteApprovalFailed": "无法更新微信远程审批", "settings.windowsNodeMxcModeFull": "完整隔离", "settings.windowsNodeMxcFullBadge": "强边界", "settings.windowsNodeMxcModeFullDesc": @@ -309,8 +324,7 @@ export default { "settings.windowsNodeMxcConsent.item.location": "禁用位置访问。", "settings.windowsNodeMxcConsent.item.tts": "禁用文本转语音(TTS)。", "settings.windowsNodeMxcConsent.item.stt": "禁用语音转文本(STT)。", - "settings.windowsNodeMxcConsent.item.gateway-memory": - "禁用默认网关内存插件和内存槽位。", + "settings.windowsNodeMxcConsent.item.gateway-memory": "禁用默认网关内存插件和内存槽位。", "settings.windowsNodeMxcConsent.item.cron": "保留但暂停 Cron 和计划任务;隔离期间错过的任务不会补跑。", "settings.windowsNodeMxcConsent.item.hooks-webhooks": diff --git a/desktop/renderer/src/views/SettingsView.test.ts b/desktop/renderer/src/views/SettingsView.test.ts index de77201..e8ebd65 100644 --- a/desktop/renderer/src/views/SettingsView.test.ts +++ b/desktop/renderer/src/views/SettingsView.test.ts @@ -27,6 +27,9 @@ describe("SettingsView", () => { const getUserDirs = vi.fn(); const validateFolderPolicy = vi.fn(); const applyFolderPolicy = vi.fn(); + const getRemoteApprovalStatus = vi.fn(); + const setRemoteApprovalEnabled = vi.fn(); + const revokeRemoteApproval = vi.fn(); const localNode = { id: "node-local", @@ -196,6 +199,16 @@ describe("SettingsView", () => { getUserDirs.mockReset().mockResolvedValue({ rw: [], ro: [] }); validateFolderPolicy.mockReset().mockImplementation(async (draft) => draft); applyFolderPolicy.mockReset(); + getRemoteApprovalStatus.mockReset().mockResolvedValue({ + experimental: true, + enabled: false, + configured: false, + owner: null, + availableOwners: [], + blockedReason: "owner-ambiguous", + }); + setRemoteApprovalEnabled.mockReset().mockResolvedValue({ ok: true }); + revokeRemoteApproval.mockReset().mockResolvedValue({ ok: true }); window.openclaw = { config: { read: vi.fn().mockResolvedValue(null), @@ -216,6 +229,13 @@ describe("SettingsView", () => { shell: { openExternal, }, + plugin: { + weixin: { + getRemoteApprovalStatus, + setRemoteApprovalEnabled, + revokeRemoteApproval, + }, + }, windowsNodeMxc: { getStatus: getWindowsNodeMxcStatus, preflight: preflightWindowsNodeMxc, @@ -314,6 +334,52 @@ describe("SettingsView", () => { expect(setWindowsNodeMxcEnabled).not.toHaveBeenCalled(); }); + it("shows the default-off experimental WeChat remote approval owner and controls", async () => { + routeState.section = "security"; + getRemoteApprovalStatus.mockResolvedValueOnce({ + experimental: true, + enabled: false, + configured: true, + owner: { + contract: "microclaw.windows-weixin-remote-approval.v1", + accountId: "bot@im.wechat", + userId: "owner@im.wechat", + boundAt: "2026-10-14T00:00:00.000Z", + }, + availableOwners: [], + blockedReason: null, + }); + const wrapper = shallowMount(SettingsView, { + global: { plugins: [createPinia()] }, + }); + await flushPromises(); + + const card = wrapper.find('[data-testid="weixin-remote-approval"]'); + expect(card.text()).toContain("WeChat remote approval (Experimental)"); + expect(card.text()).toContain("owner@im.wechat"); + expect(card.text()).toContain("allowlisted openclaw-weixin plugin and Gateway"); + expect(card.text()).toContain("never create durable approvals"); + expect(card.text()).toContain("unclassified free-text command fields stay local-only"); + await wrapper.find('[data-testid="weixin-remote-approval-enable"]').trigger("click"); + await flushPromises(); + expect(setRemoteApprovalEnabled).toHaveBeenCalledWith(true); + }); + + it("discloses the trusted plugin and Gateway metadata assumption in Chinese", async () => { + routeState.section = "security"; + setLocale("zh-CN"); + const wrapper = shallowMount(SettingsView, { + global: { plugins: [createPinia()] }, + }); + await flushPromises(); + + const card = wrapper.find('[data-testid="weixin-remote-approval"]'); + expect(card.text()).toContain("微信远程审批(实验性)"); + expect(card.text()).toContain("白名单 openclaw-weixin 插件和 Gateway"); + expect(card.text()).toContain("绝不会创建永久审批"); + expect(card.text()).toContain("无法分类的自由文本命令字段仅允许本地审批"); + }); + it("keeps technical MXC details collapsed on a fresh Settings view", async () => { routeState.section = "security"; getWindowsNodeMxcStatus.mockResolvedValueOnce({ @@ -640,9 +706,9 @@ describe("SettingsView", () => { await flushPromises(); await wrapper.find('[data-testid="mxc-select-limited"]').trigger("click"); - expect( - wrapper.findComponent(MxcCapabilityConsentDialog).props("targetMode"), - ).toBe("limited-dacl"); + expect(wrapper.findComponent(MxcCapabilityConsentDialog).props("targetMode")).toBe( + "limited-dacl", + ); expect(preflightWindowsNodeMxc).not.toHaveBeenCalled(); expect(setWindowsNodeMxcEnabled).not.toHaveBeenCalled(); wrapper.findComponent(MxcCapabilityConsentDialog).vm.$emit("accept"); @@ -782,9 +848,7 @@ describe("SettingsView", () => { expect(wrapper.findComponent(MxcCapabilityConsentDialog).props("modelValue")).toBe(false); expect(preflightWindowsNodeMxc).not.toHaveBeenCalled(); expect(setWindowsNodeMxcEnabled).not.toHaveBeenCalled(); - expect(wrapper.find('[data-testid="mxc-status-summary"]').text()).toContain( - "Full containment", - ); + expect(wrapper.find('[data-testid="mxc-status-summary"]').text()).toContain("Full containment"); }); it("disables unavailable Full containment and shows exact availability guidance", async () => { diff --git a/desktop/renderer/src/views/SettingsView.vue b/desktop/renderer/src/views/SettingsView.vue index 30f2b45..a2b648f 100644 --- a/desktop/renderer/src/views/SettingsView.vue +++ b/desktop/renderer/src/views/SettingsView.vue @@ -601,6 +601,68 @@ +
+
+ {{ t("settings.weixinRemoteApproval") }} +
+

{{ t("settings.weixinRemoteApprovalRisk") }}

+
+ {{ t("settings.weixinRemoteApprovalOwner") }} + {{ + weixinRemoteApprovalStatus.owner?.userId ?? + t("settings.weixinRemoteApprovalUnbound") + }} +
+
+ {{ + weixinRemoteApprovalStatus.blockedReason === "mxc-off" + ? t("settings.weixinRemoteApprovalRequiresMxc") + : t("settings.weixinRemoteApprovalRequiresOwner") + }} +
+

{{ t("settings.weixinRemoteApprovalOnceOnly") }}

+
+ + {{ t("settings.weixinRemoteApprovalEnable") }} + + + {{ t("settings.weixinRemoteApprovalDisable") }} + + + {{ t("settings.weixinRemoteApprovalRevoke") }} + +
+