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
22 changes: 22 additions & 0 deletions desktop/renderer/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
13 changes: 13 additions & 0 deletions desktop/renderer/src/browser-openclaw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
22 changes: 18 additions & 4 deletions desktop/renderer/src/i18n/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand All @@ -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.",
Expand Down
18 changes: 16 additions & 2 deletions desktop/renderer/src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -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":
Expand Down
76 changes: 70 additions & 6 deletions desktop/renderer/src/views/SettingsView.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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),
Expand All @@ -216,6 +229,13 @@ describe("SettingsView", () => {
shell: {
openExternal,
},
plugin: {
weixin: {
getRemoteApprovalStatus,
setRemoteApprovalEnabled,
revokeRemoteApproval,
},
},
windowsNodeMxc: {
getStatus: getWindowsNodeMxcStatus,
preflight: preflightWindowsNodeMxc,
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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 () => {
Expand Down
Loading
Loading