feat(plugins): bridge clipboard reads with consent gate

This commit is contained in:
Jinpy authored and GitHub committed 2026-09-24 16:16:49 +08:00
1 parent 1335ec032c
commit 92f92f6cdb
18 files changed
+424 -12

No files matched your search

@@ -2,6 +2,7 @@
import { computed, defineComponent, h, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { ArrowUp, BadgeCheck, Check, ChevronRight, CircleAlert, Download, ExternalLink, FileUp, FolderTree, Globe, Info, LayoutGrid, Link2, List, Loader2, PackageCheck, Pencil, Pin, PinOff, Plus, RefreshCw, RotateCcw, Search, Settings2, ShieldCheck, Store, Trash2 } from "@lucide/vue";
import { Badge } from "@/components/ui/badge";
import { isSensitivePluginPermission } from "@/lib/plugins/pluginPermissions";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -1039,7 +1040,16 @@ onBeforeUnmount(() => {
</div>
<div class="mt-3 flex flex-wrap gap-1.5">
<Badge v-for="tag in listing.plugin.tags.slice(0, 3)" :key="tag" variant="outline" class="h-5 px-1.5 text-[10px]">{{ tag }}</Badge>
<Badge v-if="listing.plugin.permissions.length" variant="outline" class="h-5 px-1.5 text-[10px]">{{ t("pluginPlatform.permissionsCount", { count: listing.plugin.permissions.length }) }}</Badge>
<!-- Real permission strings, not a count badge: sensitive ones (clipboard read, …)
highlight in the destructive variant so a user sees the risk surface before
installing; the rest stay muted. -->
<Tooltip :delay-duration="300">
<TooltipTrigger as-child>
<Badge v-if="listing.plugin.permissions.length" variant="outline" class="h-5 px-1.5 font-mono text-[10px]">{{ listing.plugin.permissions.join(" · ") }}</Badge>
</TooltipTrigger>
<TooltipContent side="bottom" class="max-w-md break-all font-mono text-[11px]">{{ listing.plugin.permissions.join("\n") }}</TooltipContent>
</Tooltip>
<Badge v-for="permission in listing.plugin.permissions" :key="permission" v-show="isSensitivePluginPermission(permission)" variant="destructive" class="h-5 px-1.5 font-mono text-[10px]" :data-sensitive-permission="permission">{{ permission }}</Badge>
</div>
<Tooltip :delay-duration="700">
<TooltipTrigger as-child>
@@ -3,7 +3,7 @@ import { computed, inject, nextTick, onBeforeUnmount, onMounted, ref, watch } fr
import { AlertTriangle, Loader2 } from "@lucide/vue";
import * as api from "@/lib/backend/api";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { copyToClipboard } from "@/lib/common/clipboard";
import { copyToClipboard, readTextFromClipboard } from "@/lib/common/clipboard";
import {
PluginHostBridge,
pluginSandboxDocument,
@@ -404,6 +404,15 @@ function createBridge() {
downloadFile: isTauriRuntime() ? downloadPluginFile : undefined,
cancelDownload: isTauriRuntime() ? cancelPluginDownload : undefined,
copyText: (_pluginId, text) => copyToClipboard(text),
// Permission-gated in the bridge (host.clipboard:read); the helper
// prefers the Tauri clipboard plugin and falls back to the Web Clipboard.
clipboardRead: (_pluginId) => readTextFromClipboard(),
// Session consent for the first clipboard read: a native ask dialog naming
// the plugin, so reads always have a human in the loop. On the web host
// (no dialog surface) the callback is omitted and the bridge denies.
confirmClipboardRead: isTauriRuntime()
? (_pluginId, pluginName) => import("@tauri-apps/plugin-dialog").then(({ ask }) => ask(t("pluginPlatform.clipboardReadConsent", { name: pluginName }), { title: t("pluginPlatform.clipboardReadConsentTitle"), kind: "warning" }).then((allowed) => allowed === true))
: undefined,
pickFiles: (pluginId, options) => pickPluginFiles(pluginId, options),
readFileChunk: (pluginId, handleId, offset, length) => readPluginFileChunkById(pluginId, handleId, offset, length),
beginFileSave: (pluginId, request) => beginPluginFileSave(pluginId, request),