Files
boss/local-agent/desktop-dialog-guard.mjs

203 lines
6.0 KiB
JavaScript

import { createHash } from "node:crypto";
const SAFE_DISMISS_BUTTONS = [
"稍后",
"跳过",
"以后再说",
"不,谢谢",
"Not now",
"Skip",
"Later",
"Cancel",
"Maybe later",
"No thanks",
];
const BLOCKED_TEXT_PATTERNS = [
/screen recording/i,
/accessibility/i,
/input monitoring/i,
/full disk access/i,
/keychain/i,
/administrator/i,
/apple id/i,
/user account control/i,
/make changes to your device/i,
/屏幕录制/,
/辅助功能/,
/输入监控/,
/完整磁盘访问/,
/钥匙串/,
/管理员密码/,
/用户帐户控制/,
/用户账户控制/,
];
export function normalizeDialogText(value) {
return String(value || "").replace(/\s+/g, " ").trim();
}
export function normalizeDialogSnapshot(input = {}) {
const buttons = Array.isArray(input.buttons)
? input.buttons.map(normalizeDialogText).filter(Boolean)
: [];
const appName = normalizeDialogText(input.appName || input.app || "Unknown App");
return {
platform: normalizeDialogText(input.platform || process.platform || "unknown"),
deviceId: normalizeDialogText(input.deviceId || "unknown-device"),
appName,
appBundleId: normalizeDialogText(input.appBundleId || input.appId || appName || "unknown-app"),
title: normalizeDialogText(input.title),
text: normalizeDialogText(input.text),
buttons,
raw: input.raw,
};
}
function parseSnapshotJson(raw, sourceName) {
const value = String(raw || "").trim();
if (!value) {
return undefined;
}
const parsed = JSON.parse(value);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error(`INVALID_DIALOG_GUARD_SNAPSHOT:${sourceName}`);
}
return parsed;
}
export function readDialogSnapshotFromEnv(env = process.env, platform = process.platform) {
const normalizedPlatform = normalizeDialogText(platform || process.platform || "unknown");
const platformSnapshotKey =
normalizedPlatform === "darwin"
? "BOSS_MAC_DIALOG_GUARD_SNAPSHOT_JSON"
: normalizedPlatform === "win32"
? "BOSS_WINDOWS_DIALOG_GUARD_SNAPSHOT_JSON"
: "";
const parsed =
parseSnapshotJson(env.BOSS_DIALOG_GUARD_SNAPSHOT_JSON, "BOSS_DIALOG_GUARD_SNAPSHOT_JSON") ||
(platformSnapshotKey ? parseSnapshotJson(env[platformSnapshotKey], platformSnapshotKey) : undefined);
if (!parsed) {
return undefined;
}
return normalizeDialogSnapshot({
...parsed,
platform: parsed.platform || normalizedPlatform,
});
}
function hash(value) {
return createHash("sha256").update(String(value || "")).digest("hex").slice(0, 16);
}
export function createDialogSignature(snapshotInput = {}) {
const snapshot = normalizeDialogSnapshot(snapshotInput);
const titleHash = hash(snapshot.title.toLowerCase());
const textHash = hash(snapshot.text.toLowerCase());
const buttonHash = hash(snapshot.buttons.join("|").toLowerCase());
return {
id: hash([
snapshot.platform,
snapshot.deviceId,
snapshot.appBundleId,
titleHash,
textHash,
buttonHash,
].join("|")),
scopeKey: [snapshot.platform, snapshot.deviceId, snapshot.appBundleId].join(":"),
platform: snapshot.platform,
deviceId: snapshot.deviceId,
appBundleId: snapshot.appBundleId,
titleHash,
textHash,
buttonHash,
};
}
function textMatchesAny(text, patterns) {
return patterns.some((pattern) => pattern.test(text));
}
function findSafeDismissButton(buttons) {
return buttons.find((button) =>
SAFE_DISMISS_BUTTONS.some((candidate) => candidate.toLowerCase() === button.toLowerCase()),
);
}
function isBlockedPrompt(snapshot) {
const combined = `${snapshot.title} ${snapshot.text}`;
return textMatchesAny(combined, BLOCKED_TEXT_PATTERNS);
}
export function evaluateDialogSnapshot(snapshotInput = {}) {
const snapshot = normalizeDialogSnapshot(snapshotInput);
const signature = createDialogSignature(snapshot);
if (isBlockedPrompt(snapshot)) {
return {
disposition: "needs_user_action",
kind: "permission_required",
risk: "high",
action: "pause_for_user",
signature,
};
}
const safeButton = findSafeDismissButton(snapshot.buttons);
if (safeButton) {
return {
disposition: "auto_action",
kind: "safe_dismiss",
risk: "low",
action: "click_button",
button: safeButton,
signature,
};
}
return {
disposition: "needs_user_action",
kind: "unknown_dialog",
risk: "medium",
action: "pause_for_user",
signature,
};
}
export function buildDialogAuditEntry({ requestId, snapshot: snapshotInput, decision, handledAt = new Date().toISOString() }) {
const snapshot = normalizeDialogSnapshot(snapshotInput);
const signature = decision?.signature || createDialogSignature(snapshot);
return {
kind: "desktop_dialog_guard",
requestId: requestId || undefined,
handledAt,
platform: snapshot.platform,
appName: snapshot.appName,
dialogId: signature.id,
risk: decision?.risk || "medium",
disposition: decision?.disposition || "unknown",
action: decision?.action || "pause_for_user",
button: decision?.button || undefined,
policyKind: decision?.kind || "unknown_dialog",
};
}
export function buildDialogInterventionResult({ requestId, snapshot: snapshotInput, decision }) {
const snapshot = normalizeDialogSnapshot(snapshotInput);
const signature = decision?.signature || createDialogSignature(snapshot);
const blocked = decision?.risk === "high";
return {
status: "needs_user_action",
requestId: requestId || undefined,
kind: "dialog_intervention_required",
dialogId: signature.id,
risk: decision?.risk || "medium",
summary: `${snapshot.appName} 弹窗需要确认:${snapshot.title || snapshot.text || "未知弹窗"}`,
recommendedAction: blocked ? "handled_on_device" : "allow_once",
availableActions: blocked
? ["handled_on_device", "cancel_task"]
: ["allow_once", "allow_for_device_dialog", "deny"],
platform: snapshot.platform,
appName: snapshot.appName,
};
}