升级插件安全策略、包管理和运行时能力
This commit is contained in:
@@ -1,63 +1,25 @@
|
||||
# Plugin Specification For AI Implementers
|
||||
# AI Plugin Integration Guide
|
||||
|
||||
This document is the compact contract for generating YMhut Box plugins with another AI agent.
|
||||
Generate manifest v3 WebView plugins by default. Local HTML, CSS, JavaScript, ES Module, Worker, Wasm, Canvas, WebAudio, localStorage and IndexedDB need no client permission. Never request Shell, Script, native process, arbitrary file system or environment access.
|
||||
|
||||
## Build A Minimal Local Package
|
||||
## Required generation rules
|
||||
|
||||
Create exactly the files needed for a runnable local package:
|
||||
|
||||
- `ymhut.plugin.json`
|
||||
- `README.md`
|
||||
- `index.html`
|
||||
- `style.css`
|
||||
- `main.js`
|
||||
|
||||
Keep UI code and core logic local. Do not load remote scripts as runtime dependencies. Remote HTTP APIs are allowed only through declared permissions and graceful failure states.
|
||||
|
||||
## Manifest Rules
|
||||
|
||||
- Use a stable `id` with letters, numbers, `.`, `-`, or `_`.
|
||||
- Do not prefix the id with `plugin:`.
|
||||
- Include at least one `ToolboxTool` or `NavPage` surface.
|
||||
- Include every local file in `resources`, including `README.md`.
|
||||
- Request only permissions the plugin actually uses.
|
||||
- Explain every requested permission in `README.md`.
|
||||
|
||||
## Runtime Bridge
|
||||
|
||||
Use `window.ymhut` for host abilities:
|
||||
|
||||
- `output.*` for reports and summaries.
|
||||
- `storage.*` for plugin-private state.
|
||||
- `http.fetch` for http/https requests.
|
||||
- `network.*` for host network diagnostics.
|
||||
- `clipboard.*` and `file.*` only when clearly user initiated.
|
||||
- `openExternal(url)` for links, which opens the YMhut safe browser by default.
|
||||
- `openExternal(url, { target: "system" })` only for an explicit system-browser action.
|
||||
|
||||
## UI And Window Boundaries
|
||||
|
||||
The plugin page owns only its WebView content area. Do not mimic system title bars, cover host controls, or create invisible click layers. Avoid full-screen fixed overlays; if a modal is necessary, provide a visible close control and restore focus.
|
||||
|
||||
Design for both embedded and independent-window use. Use responsive grids, readable card density, clear loading states, empty states, and error states. The host output area should not be used as the primary UI.
|
||||
|
||||
## Security Constraints
|
||||
|
||||
Do not modify or override:
|
||||
|
||||
- `server/`
|
||||
- built-in app assets
|
||||
- developer/about identity
|
||||
- built-in tool IDs
|
||||
- paths outside the plugin directory
|
||||
|
||||
All plugin resources must resolve inside the plugin folder. File access must go through host file pickers; never assume arbitrary filesystem access.
|
||||
- Include `manifestVersion: 3`, `apiVersion: "2"`, `runtime: "WebView"`, README and at least one surface.
|
||||
- Add a non-empty `permissionReasons` entry for every permission.
|
||||
- Put only launch-critical permissions in `security.requiredPermissions`.
|
||||
- For `Http`, declare exact public HTTPS/WSS origins with no path or wildcard.
|
||||
- For external links and tools, declare exact `openExternalOrigins` and `runToolIds`.
|
||||
- Keep scripts, styles, fonts and application logic local. Remote code, iframe and navigation are prohibited.
|
||||
- Handle Bridge errors by `error.code`; do not retry permission or scope errors automatically.
|
||||
- Do not generate native binaries, package managers, build-on-first-run behavior or commands that invoke PowerShell, Node or Python.
|
||||
|
||||
## Acceptance Checklist
|
||||
|
||||
- Plugin scans without validation errors.
|
||||
- README explains features, permissions, boundaries, and known failures.
|
||||
- Main UI runs without network and shows a useful degraded state.
|
||||
- Output writes do not hide the main UI.
|
||||
- Links open in the safe browser by default.
|
||||
- No remote scripts, no unbounded z-index overlays, no hidden click blockers.
|
||||
- Manifest passes `docs/plugins/ymhut.plugin.schema.json`.
|
||||
- Every entry/resource stays inside the plugin directory and the package contains no links or junctions.
|
||||
- Zero-permission mode still renders and its browser-private storage remains functional.
|
||||
- Optional permission denial produces a clear UI state.
|
||||
- Direct fetch/WebSocket and `ymhut.http.fetch` use only declared public origins.
|
||||
- No remote script, iframe, popup, download, browser permission, `file://` or Tauri global API is used.
|
||||
- Layout works in narrow and wide embedded surfaces with no full-screen transparent overlay.
|
||||
- Logs and output do not include secrets, tokens or local filesystem paths.
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# 从旧清单迁移到 manifest v3
|
||||
|
||||
1. 添加 `manifestVersion: 3`、`apiVersion: "2"` 和 `runtime: "WebView"`。
|
||||
2. 为 `permissions` 中每项权限添加非空 `permissionReasons`。
|
||||
3. 将启动必需权限放入 `security.requiredPermissions`,其余保持可选。
|
||||
4. 为 `Http` 添加精确公网 HTTPS/WSS `network.allowedOrigins`。
|
||||
5. 为外链和内置工具分别添加 `openExternalOrigins` 与 `runToolIds`。
|
||||
6. 移除 Shell/Script、原生可执行文件、任意文件路径、`file://`、远程脚本、任意目标探测和 WebRTC/STUN 绕过。
|
||||
7. 使用浏览器 `localStorage`/IndexedDB 保存纯 Web 私有状态;需要宿主 KV 时声明 `Storage`。
|
||||
8. 在插件页重新审阅权限并启用。旧授权不会自动继承到新策略指纹。
|
||||
+53
-52
@@ -1,8 +1,8 @@
|
||||
# YMhut Box 插件开发说明
|
||||
# YMhut Box manifest v3 插件
|
||||
|
||||
YMhut Box 插件是本地 WebView 插件包,用来扩展工具箱工具或插件页。插件运行在宿主隔离的 WebView 中,通过 `window.ymhut` Bridge 请求能力;插件不能直接访问应用核心资源、任意文件路径或系统浏览器。
|
||||
本地 HTML/CSS/JavaScript 是默认插件运行时。DOM、ES Module、Worker、Wasm、Canvas、WebAudio、`localStorage` 和 IndexedDB 不需要客户端权限,但仅存在于该插件自己的虚拟 HTTPS 原点中。客户端、系统、文件选择器、宿主存储、网络代理和外部服务能力只能通过 `window.ymhut` Bridge 使用。
|
||||
|
||||
## 最小插件包
|
||||
## 最小包
|
||||
|
||||
```text
|
||||
my-plugin/
|
||||
@@ -13,78 +13,79 @@ my-plugin/
|
||||
main.js
|
||||
```
|
||||
|
||||
`README.md`、`README.txt` 或 `说明.md` 必须存在。`entry`、`resources` 和 surface 入口都必须留在插件目录内,不能使用 `../` 逃逸。
|
||||
插件包不能包含符号链接、目录联接或原生可执行文件。`entry`、surface 入口和资源必须位于包目录内。
|
||||
|
||||
## manifest 核心字段
|
||||
## manifest v3
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "../../../docs/plugins/ymhut.plugin.schema.json",
|
||||
"manifestVersion": 3,
|
||||
"apiVersion": "2",
|
||||
"id": "hello-tools",
|
||||
"name": "Hello Tools",
|
||||
"version": "1.0.0",
|
||||
"author": "you",
|
||||
"description": "A local YMhut Box plugin",
|
||||
"description": "A local WebView plugin",
|
||||
"entry": "index.html",
|
||||
"permissions": ["Output", "Log", "Storage", "OpenExternal"],
|
||||
"surfaces": [
|
||||
{
|
||||
"kind": "ToolboxTool",
|
||||
"id": "hello",
|
||||
"name": "Hello",
|
||||
"description": "Toolbox entry",
|
||||
"entry": "index.html",
|
||||
"category": "plugin"
|
||||
}
|
||||
],
|
||||
"runtime": "WebView",
|
||||
"requirements": {
|
||||
"minimumClientVersion": "2.0.6.2",
|
||||
"minimumWindowsBuild": 17763,
|
||||
"architectures": ["X64", "Arm64"]
|
||||
},
|
||||
"permissions": ["Http", "Output"],
|
||||
"permissionReasons": {
|
||||
"Http": "Read data from the declared public API.",
|
||||
"Output": "Write the user-requested result to the host output panel."
|
||||
},
|
||||
"security": { "requiredPermissions": ["Http"] },
|
||||
"network": {
|
||||
"allowedOrigins": ["https://api.example.com"],
|
||||
"openExternalOrigins": [],
|
||||
"runToolIds": []
|
||||
},
|
||||
"surfaces": [{
|
||||
"kind": "ToolboxTool",
|
||||
"id": "hello",
|
||||
"name": "Hello",
|
||||
"description": "Toolbox entry",
|
||||
"entry": "index.html",
|
||||
"category": "plugin"
|
||||
}],
|
||||
"resources": ["index.html", "style.css", "main.js", "README.md"]
|
||||
}
|
||||
```
|
||||
|
||||
`id` 只允许字母、数字、点、短横线和下划线,不能以 `plugin:` 开头。工具箱挂载后的工具 ID 由宿主生成,格式是 `plugin:<pluginId>:<surfaceId>`。
|
||||
每项权限必须同时出现在 `permissions` 和 `permissionReasons`。`security.requiredPermissions` 中的权限未授权时插件不能启用;其余权限为可选权限,未授权时 Bridge 返回稳定错误码。权限用途、必需状态、范围、运行时或插件版本改变会使旧授权指纹失效。
|
||||
|
||||
## 权限
|
||||
|
||||
权限默认关闭。manifest 只声明插件需要什么,用户仍要在插件页启用插件并授予权限。
|
||||
|
||||
- `Input`:读取或写入插件输入。
|
||||
- `Output`:写入宿主输出区。
|
||||
- `Log`:写入 `plugin:<pluginId>` 日志。
|
||||
- `Storage`:访问插件私有 key-value 状态。
|
||||
- `Http`:通过宿主请求 http/https。
|
||||
- `Clipboard`:读写剪贴板文本。
|
||||
- `FilePicker`:通过系统选择器打开或保存文件。
|
||||
- `RunTool`:调用允许的内置工具。
|
||||
- `OpenExternal`:打开 http/https 外链,默认进入安全浏览器。
|
||||
- `NetworkDiagnostics`:请求本机网络诊断能力。
|
||||
`Http` 必须声明精确 `network.allowedOrigins`;仅允许公网 `https://` 或 `wss://` 原点,不允许路径、通配符、HTTP、localhost、局域网或私网。`OpenExternal`/`OpenSystemBrowser` 使用 `openExternalOrigins`,`RunTool` 使用 `runToolIds`。
|
||||
|
||||
## Bridge
|
||||
|
||||
```js
|
||||
await window.ymhut.output.set("report");
|
||||
await window.ymhut.storage.set("lastRun", JSON.stringify(data));
|
||||
await window.ymhut.http.fetch({ url: "https://example.com/api" });
|
||||
await window.ymhut.openExternal("https://example.com");
|
||||
await window.ymhut.openExternal("https://example.com", { target: "system" });
|
||||
const response = await window.ymhut.http.fetch({
|
||||
url: "https://api.example.com/data",
|
||||
method: "GET"
|
||||
});
|
||||
await window.ymhut.output.set(response.content);
|
||||
```
|
||||
|
||||
普通外链默认进入 YMhut Box 安全浏览器。系统浏览器只作为显式动作使用,并继续受 `OpenExternal` 权限控制。
|
||||
协议为 `PluginHostProtocol v2`。每个 surface 会话绑定插件 ID、surface ID、虚拟原点和一次性令牌。插件脚本无法选择或伪造令牌。单消息上限 256 KiB,每会话最多 64 个并发调用,统一超时 30 秒;宿主 HTTP 响应上限 2 MiB。
|
||||
|
||||
## 窗口与输出
|
||||
常用稳定错误码:`permission_not_declared`、`permission_not_granted`、`permission_scope_denied`、`network_denied`、`session_invalid`、`payload_too_large`、`concurrency_limit`、`timeout`。
|
||||
|
||||
插件内容区承载主 UI;宿主输出区用于报告、日志摘要、复制结果和调试信息。不要用输出区做主交互,也不要在插件页面使用全屏 fixed 遮罩、透明点击层或超高 z-index 覆盖宿主控件。
|
||||
## Web 安全边界
|
||||
|
||||
插件需要适配主窗口内嵌和独立窗口内容区。建议使用响应式网格、可滚动表格和清晰空态;不要假设窗口固定尺寸。
|
||||
- 每个 surface 使用独立的 `https://p-<hash>.plugin.ymhut.invalid` 原点和独立 WebView2 数据目录。
|
||||
- 本地脚本、内联脚本、ES Module、Worker、Blob 和 Wasm 可用。
|
||||
- 远程脚本、样式、字体、iframe、对象、导航、新窗口、下载、外部拖放和浏览器敏感权限被拒绝。
|
||||
- 页面直接 `fetch`/WebSocket 与 `ymhut.http.fetch` 使用同一来源白名单;宿主 fetch 可兼容无 CORS API。
|
||||
- 系统浏览器需要 `OpenSystemBrowser`,且每次调用都由宿主确认。
|
||||
- Shell、Script、PowerShell、Node、Python 和插件自带原生程序不受支持。
|
||||
|
||||
## 安全边界
|
||||
## 运行时与独立窗口
|
||||
|
||||
插件 WebView 只允许加载插件目录内本地资源。非本地导航会被拦截并交给安全浏览器。插件不能覆盖内置工具 ID、应用图标、开发者信息、关于页核心身份或内置 `Assets` 路径。
|
||||
`WebView` 是默认和正式运行时。Tauri 独立窗口仅使用应用自带宿主,默认关闭;它要求插件开发者模式、`ExternalRuntime` 授权和按插件版本保存的二次确认。Tauri 插件内容运行在独立的 `https://p-<hash>.localhost` 原点和插件私有 WebView2 数据目录中,应用宿主页只通过一次性、当前用户命名管道把 Bridge 请求转发给同一个权限宿主。会话令牌仅存在于原生宿主内,不进入插件 JavaScript。插件包不能携带或构建原生可执行文件,也不能访问 Tauri 全局 API。
|
||||
|
||||
网络结果、排行榜和第三方数据都应标明不确定性,并在失败时显示降级状态。
|
||||
|
||||
## 常见问题
|
||||
|
||||
- 加载失败:检查 manifest、README、entry 和 resources 是否存在且路径合法。
|
||||
- 权限拒绝:检查 manifest 是否声明权限,以及插件页是否已授权。
|
||||
- 外链打不开:只支持绝对 http/https URL,默认安全浏览器。
|
||||
- 输出区遮挡:将输出区用于报告,不要用它承载主 UI。
|
||||
- 独立窗口异常:不要调用浏览器弹窗 API 创建系统浏览器窗口。
|
||||
旧清单自动降级为 `LegacyWebOnly`:本地 Web 内容可运行,Bridge、远程连接和外接运行时全部关闭。迁移步骤见 [MIGRATION-v3.md](MIGRATION-v3.md),完整边界见 [SECURITY.md](SECURITY.md),TypeScript 声明见 [ymhut.d.ts](ymhut.d.ts)。
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# 插件安全边界
|
||||
|
||||
插件是“不可信 Web 内容”,不是客户端扩展进程。宿主只承诺本地 Web 平台能力和经授权的 Bridge;插件目录、脚本、远程响应和显示内容都不应被当作可信客户端代码。
|
||||
|
||||
宿主在注册、授权、会话和调用四层检查清单声明、用途、精确范围、用户授权与策略指纹。UI 进程只执行宿主批准的剪贴板、文件选择器和外链动作。页面卸载、窗口关闭或 WebView 进程异常会关闭 Bridge 会话。
|
||||
|
||||
禁止的能力包括任意文件系统、环境变量、进程创建、Shell/Script、Tauri 全局 API、插件自带原生二进制、跨插件资源读取、HTTP/私网连接、远程代码、插件创建的 iframe、页面导航、下载、摄像头、麦克风、定位和通知。
|
||||
|
||||
受控 Tauri 宿主不把插件文件加载到应用原点。应用自有页面通过跨原点 sandbox iframe 承载插件自定义 HTTPS 协议;插件原点按插件和 surface 唯一,浏览器配置目录独立。Bridge 只接受同源 POST,由原生宿主持有一次性会话令牌并通过当前用户命名管道转发。插件页面即使构造原始 Bridge 请求,也只能调用清单已声明、当前已授权且范围匹配的能力。
|
||||
|
||||
浏览器私有存储按虚拟原点与 WebView2 数据目录隔离。清除插件数据或回收式卸载会删除宿主 KV 和对应浏览器配置;卸载前插件目录会移入应用数据下的可恢复回收目录。
|
||||
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
export {};
|
||||
|
||||
type BridgeErrorCode = "invalid_request" | "session_invalid" | "plugin_unavailable" | "legacy_bridge_disabled" | "permission_not_declared" | "permission_not_granted" | "permission_scope_denied" | "payload_too_large" | "concurrency_limit" | "timeout" | "network_denied" | "unsupported" | "host_failure";
|
||||
|
||||
interface BridgeError extends Error { code: BridgeErrorCode; }
|
||||
interface HttpRequest { url: string; method?: string; headers?: Record<string, string>; body?: string; }
|
||||
interface HttpResponse { status: number; ok: boolean; content: string; headers: Record<string, string>; }
|
||||
|
||||
interface YmhutBridge {
|
||||
input: { get(): Promise<unknown>; set(value: unknown): Promise<boolean>; onInputChanged(handler: (value: unknown) => void): void };
|
||||
output: { set(value: unknown): Promise<boolean>; append(value: unknown): Promise<boolean>; clear(): Promise<boolean> };
|
||||
log: { info(message: string, detail?: string): Promise<boolean>; warn(message: string, detail?: string): Promise<boolean>; error(message: string, detail?: string): Promise<boolean> };
|
||||
storage: { get(key: string): Promise<string | null>; set(key: string, value: string): Promise<boolean>; remove(key: string): Promise<boolean>; list(): Promise<Record<string, string>> };
|
||||
http: { fetch(request: HttpRequest): Promise<HttpResponse> };
|
||||
network: { diagnostics(): Promise<unknown>; ping(request: unknown): Promise<unknown>; dnsLookup(request: unknown): Promise<unknown>; traceRoute(request: unknown): Promise<unknown> };
|
||||
clipboard: { readText(): Promise<string>; writeText(text: string): Promise<boolean> };
|
||||
file: { openPicker(): Promise<{ name: string; content: string } | null>; savePicker(name: string, value: string): Promise<{ name: string } | null> };
|
||||
tool: { run(toolId: string, input: string): Promise<unknown> };
|
||||
openExternal(url: string, options?: { target?: "safe" | "system" }): Promise<boolean>;
|
||||
}
|
||||
|
||||
declare global { interface Window { ymhut: YmhutBridge; } }
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://ymhut.local/schemas/ymhut.plugin.v3.json",
|
||||
"title": "YMhut Box Plugin Manifest v3",
|
||||
"type": "object",
|
||||
"required": [ "manifestVersion", "apiVersion", "id", "name", "version", "author", "description", "entry", "runtime", "permissions", "permissionReasons", "security", "surfaces", "resources" ],
|
||||
"properties": {
|
||||
"manifestVersion": { "const": 3 },
|
||||
"apiVersion": { "const": "2" },
|
||||
"id": { "type": "string", "pattern": "^[A-Za-z0-9._-]{1,64}$" },
|
||||
"name": { "type": "string", "minLength": 1 },
|
||||
"version": { "type": "string", "minLength": 1 },
|
||||
"author": { "type": "string" },
|
||||
"description": { "type": "string" },
|
||||
"entry": { "$ref": "#/$defs/relativePath" },
|
||||
"runtime": { "enum": [ "WebView", "Tauri", "Shell", "Script" ] },
|
||||
"builtIn": { "type": "boolean", "default": false },
|
||||
"permissions": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/$defs/permission" } },
|
||||
"permissionReasons": { "type": "object", "additionalProperties": { "type": "string", "minLength": 1 } },
|
||||
"security": { "type": "object", "required": [ "requiredPermissions" ], "properties": { "requiredPermissions": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/$defs/permission" } } } },
|
||||
"requirements": { "type": "object", "properties": { "minimumClientVersion": { "type": "string" }, "minimumWindowsBuild": { "type": "integer", "minimum": 0 }, "architectures": { "type": "array", "uniqueItems": true, "items": { "enum": [ "X64", "X86", "Arm64" ] } } } },
|
||||
"network": { "type": "object", "properties": { "allowedOrigins": { "type": "array", "uniqueItems": true, "items": { "type": "string", "pattern": "^(https|wss)://[^/?#*]+$" } }, "openExternalOrigins": { "type": "array", "uniqueItems": true, "items": { "type": "string", "pattern": "^https://[^/?#*]+$" } }, "runToolIds": { "type": "array", "uniqueItems": true, "items": { "type": "string", "pattern": "^[A-Za-z0-9._-]{1,64}$" } } } },
|
||||
"surfaces": { "type": "array", "minItems": 1, "items": { "type": "object", "required": [ "kind", "id", "name", "description" ], "properties": { "kind": { "enum": [ "ToolboxTool", "NavPage" ] }, "id": { "type": "string", "pattern": "^[A-Za-z0-9._-]{1,64}$" }, "name": { "type": "string", "minLength": 1 }, "description": { "type": "string" }, "entry": { "$ref": "#/$defs/relativePath" }, "category": { "type": "string" }, "keywords": { "type": "array", "items": { "type": "string" } }, "iconGlyph": { "type": "string" } } } },
|
||||
"resources": { "type": "array", "items": { "$ref": "#/$defs/relativePath" } }
|
||||
},
|
||||
"$defs": {
|
||||
"relativePath": { "type": "string", "minLength": 1, "not": { "pattern": "(^[\\/]|(^|[\\/])\\.\\.([\\/]|$)|:)" } },
|
||||
"permission": { "enum": [ "Input", "Output", "Log", "Storage", "Http", "Clipboard", "FilePicker", "RunTool", "OpenExternal", "OpenSystemBrowser", "ExternalRuntime", "NetworkDiagnostics" ] }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user