- 人工智能
- AI 应用
- 桌面应用
- 交互助手
【免费下载链接】ClawX
ClawX is a desktop app that provides a graphical interface for OpenClaw AI agents. It turns CLI-based AI orchestration into a desktop experience without using the terminal. China website is https://clawx.com.cn.
本文是 ClawX 开源仓库中规则文档 web-browser-security-and-lifecycle.md 的深度技术解读。该规则定义了桌面应用如何安全地预览 Agent(OpenClaw)生成的 HTML 文件:把一切 Agent 产物视为不可信输入,通过单一专用 webview、专用 Session、严格 URL 白名单与多路导航拦截,把"本地文件预览"与"通用浏览器"彻底隔离。读完本文,你将掌握 ClawX 本地 HTML 预览的完整安全模型、生命周期管理与源码级实现原理,并可直接对照仓库代码验证每一道防线。
一、背景:为什么 Agent 生成的 HTML 必须被当作不可信代码
在 ClawX 的会话工作流中,Agent(OpenClaw)会在会话期间生成.html/.htm文件,并在"产物面板(Artifact Panel)"中以 HTML 预览的形式呈现给用户。这类文件的特殊性在于:
- 内容完全由模型生成,可能包含任意脚本、链接、表单、弹窗或网络请求意图;
- 它运行在 Electron 渲染进程附近,若赋予 Node 能力或 ClawX 桥接权限,等于把本机能力直接交给不可信内容;
- 它天然具备"像网页一样展示"的需求,却又绝不能退化为"一个内置浏览器"。
因此,规则文档开宗明义:Treat agent-produced HTML as untrusted(把 Agent 产出的 HTML 视为不可信输入),并用一条"专用会话(dedicated-session)webview"承载全部预览逻辑。该 webview 不允许存在 preload 脚本、Node 集成、插件、不安全内容加载、弹窗能力或 ClawX 桥接,且强制开启沙箱(sandbox)、上下文隔离(context isolation)与 Web 安全(web security)。这些约束在源码中都有逐项落地的硬性实现(详见下文第三、四节)。
二、核心安全模型:专用 Session + 严格 URL 白名单
2.1 专用分区与确定性身份
所有 HTML 预览共用一个独立的 Electron 持久化分区,常量定义在 shared/web-browser.ts:
export const WEB_BROWSER_PARTITION = 'persist:clawx-web-browser' as const; export const WEB_BROWSER_INITIAL_URL = 'about:blank' as const; export const WEB_BROWSER_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.7559.236 Electron/40.8.4 Safari/537.36' as const;WEB_BROWSER_PARTITION:预览 webview 的partition属性值,唯一且固定;WEB_BROWSER_INITIAL_URL:webview 初始加载地址固定为about:blank,不预载任何内容;WEB_BROWSER_USER_AGENT:固定 UA,保证身份可判定。
2.2 URL 白名单:仅允许纯净的本地 HTML 文件
规则文档明确:应用发起的导航只能加载 hostless、无 query、无 fragment 的file:///URL,且路径必须以.html或.htm结尾。这条规则在normalizeWebBrowserHtmlFileUrl中被实现为一道严格的"归一化校验器":
export function normalizeWebBrowserHtmlFileUrl(input: string): string | null { const trimmed = input.trim(); if (!trimmed.startsWith('file:///')) { return null; } let parsed: URL; try { parsed = new URL(trimmed); } catch { return null; } if ( parsed.protocol !== 'file:' || parsed.hostname !== '' || parsed.username !== '' || parsed.password !== '' || parsed.search !== '' || parsed.hash !== '' || !/\.html?$/i.test(parsed.href) ) { return null; } return parsed.href; }它同时校验六类约束(shared/web-browser.ts):
| 约束 | 含义 | 目的 |
|---|---|---|
必须file:///前缀 | 拒绝http(s)://等一切网络协议 | 禁绝远程内容与 SSRF 类风险 |
hostname必须为空 | 拒绝file://localhost/...等带主机形式 | 消除身份歧义与主机注入 |
无username/password | 拒绝内嵌凭据 | 防止凭据外泄 |
无search(query) | 拒绝?query | 防止把参数伪装进本地文件导航 |
无hash(fragment) | 拒绝#fragment | 防止页内锚点被利用 |
扩展名/\.html?$/i | 仅.html/.htm | 只允许 HTML 主文档 |
只要任意一项不满足,函数返回null,上层即拒绝该导航。
2.3 渲染进程不得自行构造 URL,必须走类型化 Host API
规则文档要求:渲染进程必须从"已经过验证的附件(attachment)或工作区(Workspace)引用"派生 URL,并通过类型化 Host API 调用主进程。这一约束有两层实现:
- URL 只能由已验证引用派生。渲染端工具函数 local-html-browser.ts 的
localHtmlBrowserUrl只接受两类输入:{ kind: 'workspace'; ref: WorkspaceFileRef }:由workspaceRoot + relativePath拼出本地绝对路径;{ kind: 'attachment'; ref: AttachmentFileRef }:仅当uri为file:协议、且 hostname 为空或为localhost时才转换为本地文件 URL,否则返回null。- 最终通过
htmlPreviewFileUrl依据产物面板中的attachmentFileRef/workspaceFileRef/ 已存在文件路径派生 URL(local-html-browser.ts)。
- 必须经由类型化 Host API。契约定义在 shared/host-api/contract.ts:
webBrowser: { navigate: (payload: WebBrowserNavigatePayload) => void; openExternal: (payload: WebBrowserNavigatePayload) => void; };其中WebBrowserNavigatePayload = { url: string }取自 shared/web-browser.ts。渲染进程(如 WebBrowserHost.tsx)调用hostApi.webBrowser.navigate(url)后,主进程侧 web-browser-api.ts 会再次执行requireAllowedUrl(即normalizeWebBrowserHtmlFileUrl)校验,并对ERR_ABORTED(errno === -3,因旧导航被取消而产生的预期中断)做静默吞除,其余错误原样抛出。派生 + 传输 + 主进程复验三道关卡,确保到达 webview 的 URL 必然是白名单内的本地 HTML。
三、webview 防护:偏好加固 + 身份门控 + 导航全拦截
3.1 偏好加固:从 WebPreferences 上删除一切危险能力
规则文档要求 webview 无 preload、无 Node 集成、无插件、无 insecure content、无 ClawX bridge,并强制沙箱、上下文隔离、Web 安全。对应实现是hardenWebBrowserPreferences(web-browser-policy.ts):
export function hardenWebBrowserPreferences(preferences: WebPreferences): void { delete preferences.preload; preferences.nodeIntegration = false; preferences.nodeIntegrationInSubFrames = false; preferences.nodeIntegrationInWorker = false; preferences.plugins = false; preferences.allowRunningInsecureContent = false; preferences.contextIsolation = true; preferences.sandbox = true; preferences.webSecurity = true; }注意delete preferences.preload是"主动删除"而非仅置空,即使渲染进程或其它代码传入了 preload 路径也会被抹掉,从根上杜绝 ClawX bridge 注入。
3.2 身份门控:单 guest 注册表 + 精确附件身份匹配
规则文档要求"保持单一 guest 注册表与精确附件身份门控"。WebBrowserGuestRegistry(web-browser-policy.ts)是主进程维护的"唯一存活 webview 登记簿":
beginAttachment():在will-attach-webview阶段预占一个名额,若已有 pending 或已有 guest 则拒绝(防止第二个 webview 挂载);completeAttachment(guest):在did-attach-webview阶段正式登记,并监听destroyed自动清空;current()/owns()/hasLiveGuest():供后续导航与 API 判断当前唯一 guest 是否存活。
而isExpectedWebBrowserAttachment(web-browser-policy.ts)校验 webview 挂载参数必须与预期完全一致:
export function isExpectedWebBrowserAttachment(params: Record<string, unknown>): boolean { return params.partition === WEB_BROWSER_PARTITION && params.src === WEB_BROWSER_INITIAL_URL && params.useragent === WEB_BROWSER_USER_AGENT && params.allowpopups !== true && params.preload === ''; }即:分区必须正确、初始src必须是about:blank、UA 必须一致、禁止弹窗(allowpopups不为 true)、禁止 preload。对应测试 web-browser-policy.test.ts 覆盖了"错误分区、非初始 src、带 preload、开启弹窗"四种被拒场景。
3.3 导航全拦截:五路独立防线
installWebBrowserGuestPolicy(web-browser-policy.ts)在 guest 挂载后叠加了五路互不依赖的拦截器:
| 拦截点 | 事件 | 行为 |
|---|---|---|
| 页面级导航 | will-frame-navigate | 一律preventDefault()并告警 |
| 非法程序化导航 | did-start-navigation | 仅当主框架且目标 URL 未通过归一化校验时guest.stop() |
| 重定向 | will-redirect | 一律preventDefault() |
| 弹窗 | setWindowOpenHandler | 返回{ action: 'deny' } |
| 页内导航 | did-navigate-in-page | 回退(loadURL恢复)到已提交的 HTML URL |
其中"页内导航回退"(restoreAfterInPageNavigation)专门处理location.hash/ 锚点跳转等不触发will-frame-navigate的导航:guest 记录上一次通过校验的已提交 URL(rememberCommittedHtml),一旦发现页内跳到了别的地址,立即loadURL拉回原预览文件。这与规则文档"独立阻止所有 will-frame-navigate、重定向、非法程序化、页内、表单和脚本导航"逐条对应。
此外,makeLinksVisuallyInert在did-finish-load时向页面注入 user-origin CSS(web-browser-policy.ts):
a, area { color: inherit !important; cursor: inherit !important; pointer-events: none !important; text-decoration: none !important; }让所有链接/热点区域在视觉与交互上完全失效(惰性链接),配合 JS 级导航拦截形成"CSS 视觉禁用 + 事件层拦截"的双保险。测试 web-browser-policy.test.ts 验证了will-frame-navigate对https://example.com/的阻止与windowOpenHandler返回deny。
四、专用 Session 的全局封锁:权限、下载、网络请求
规则文档要求:拒绝每个弹窗与每个权限、取消下载、在专用 Session 中阻止 HTTP(S)/WebSocket 及其它网络请求、拒绝非 HTML 主文档。configureWebBrowserSession(web-browser-session.ts)在 Session 层面完成了这套"默认全拒":
browserSession.setPermissionCheckHandler(() => false); browserSession.setPermissionRequestHandler((_contents, _permission, callback) => { callback(false); }); browserSession.setDevicePermissionHandler(() => false); browserSession.setDisplayMediaRequestHandler((_request, callback) => { callback({}); }); browserSession.webRequest.onBeforeRequest( { urls: ['file://*/*', 'http://*/*', 'https://*/*', 'ws://*/*', 'wss://*/*'] }, (details, callback) => { const isNetworkRequest = /^(?:https?|wss?):/i.test(details.url); const isInvalidMainDocument = details.resourceType === 'mainFrame' && normalizeWebBrowserHtmlFileUrl(details.url) === null; callback({ cancel: isNetworkRequest || isInvalidMainDocument }); }, );- 权限四连拒:权限检查、权限请求、设备权限、屏幕共享(
setDisplayMediaRequestHandler回调空对象)全部拒绝; - 网络硬拦截:
webRequest.onBeforeRequest监听file/http/https/ws/wss,凡https?/wss?网络请求一律cancel; - 主文档白名单:
mainFrame资源类型的请求若未通过normalizeWebBrowserHtmlFileUrl校验(即非本地 HTML),同样被cancel,从网络层杜绝远程主文档与任意文件加载; - 下载禁用:通过
will-download事件preventDefault()取消一切下载,并用WeakSet保证每个 Session 只挂载一次监听器。
同时 Session 固定设置与 webview 一致的 UA(browserSession.setUserAgent(WEB_BROWSER_USER_AGENT)),保证身份可判定。
五、生命周期:guest 是 Preview 的实现细节,绝无"浏览器"外观
5.1 不暴露浏览器 UI
规则文档强调:guest 是 Preview 的实现细节——不提供 Web Browser 标签页、首页、地址栏、历史控件、站点数据控件、通用 HTTP 导航或"空 guest 入口"。因此用户看到的只有一个"跟随预览锚点几何位置的透明宿主",没有任何浏览器 chrome。渲染端WebBrowserHost(WebBrowserHost.tsx)的核心设计:
- 路由稳定:组件始终挂载(
if (!previewUrl) return null仅在无预览目标时卸载),不随标签切换卸载重建; - 几何跟随:用
ResizeObserver+requestAnimationFrame持续测量htmlPreviewAnchor(由 WebBrowserAnchor.tsx 注册的锚点元素)的 bounding rect,将 webview 以position: fixed精确叠加其上; - 隐身状态:仅当"面板打开且当前标签为 preview 且锚点存在"时才
visible;否则visibility: 'hidden'、pointerEvents: 'none'、aria-hidden、HTMLinert,与规则文档"不可见、pointer-inert、无障碍隐藏、不能接收焦点"完全一致; - 崩溃自愈:监听
render-process-gone进入 crashed 态,展示可重试界面,通过递增key={generation}重建 webview(WebBrowserHost.tsx); - 初始源固定:webview 的
src恒为WEB_BROWSER_INITIAL_URL(about:blank),真实内容由主进程导航注入,渲染进程不直接设置文件 URL。
5.2 主进程组装:注册表 → Session → 策略
主进程在 electron/main/index.ts 完成三件套组装:
- 创建
WebBrowserGuestRegistry单例; - 调用
configureWebBrowserSession得到加固后的专用 Session; - 通过
installWebBrowserGuestPolicy把"身份门控 + 偏好加固 + 导航拦截"绑定到主窗口的will-attach-webview/did-attach-webview事件。
之后渲染进程的hostApi.webBrowser.navigate / openExternal通过 ipc-handlers.ts 进入createWebBrowserApi(web-browser-api.ts),其navigate先取注册表中的唯一存活 guest,再做一次 URL 归一化校验后才loadURL;openExternal则把"明确提供、独立复验过的本地 HTML 文件 URL"交给shell.openExternal(默认实现)在系统默认浏览器中打开——这正是规则文档所说"Main 可加载已验证 HTML 文件,或通过shell.openExternal打开明确提供的本地 HTML URL"的唯一两个出口,不存在任何通用 web URL 或站点数据管理 API。
六、安全边界小结:从规则到实现的七道防线对照
| 规则要求(harness/specs/rules/web-browser-security-and-lifecycle.md) | 实现位置 | 机制 |
|---|---|---|
| 视 Agent HTML 为不可信,专用 webview | web-browser-policy.ts | persist:clawx-web-browser专用分区 + 单 guest |
| 无 preload / Node / 插件 / 桥接,强制沙箱 | web-browser-policy.ts | hardenWebBrowserPreferences删除并置假全部危险项 |
仅 hostless、无 query/fragment 的file:///本地 HTML | shared/web-browser.ts | normalizeWebBrowserHtmlFileUrl六维校验 |
| 渲染进程从已验证引用派生 URL 并走类型化 Host API | shared/host-api/contract.ts、local-html-browser.ts | webBrowser.navigate/openExternal契约 + 引用派生 |
| 链接惰性化、拦截全部导航/弹窗/重定向/页内跳转 | web-browser-policy.ts | user-origin CSS + 五路事件拦截 + 页内回退 |
| 拒绝权限/下载/网络请求/非 HTML 主文档 | web-browser-session.ts | 权限全拒 +webRequest硬拦截 +will-download取消 |
| 不暴露浏览器 UI,仅 Preview 实现细节 | WebBrowserHost.tsx | 锚点跟随、隐身/pointer-inert/aria-hidden/inert |
| 全部文案走四语 locale 与设计 token | i18n 资源 | t('filePreview.html.crashed')等键位覆盖中英日俄 |
七、给二次开发者的实践建议
- 新增预览入口时先过
normalizeWebBrowserHtmlFileUrl:任何要送进预览 webview 的 URL,必须先经过归一化校验,再走webBrowser.navigate,不要自行拼接file://字符串; - 不要往预览 webview 上挂 preload 或
nodeIntegration:主进程会在will-attach-webview阶段强制删除/覆盖这些偏好,预期之外的挂载参数会直接被isExpectedWebBrowserAttachment拒绝并在日志中告警([WebBrowser] Rejected webview attachment with unexpected identity); - 扩展导航能力前先扩展拦截面:目前
will-frame-navigate、will-redirect、did-start-navigation、setWindowOpenHandler、did-navigate-in-page五路拦截各自独立,任何新导航通道都应在 Session 的webRequest与 guest 策略中同步加固; - 保持"单 guest + 精确身份"不变量:
WebBrowserGuestRegistry.beginAttachment会拒绝并发挂载,破坏该不变量会同时破坏身份门控与导航回退的正确性; - 文案必须四语齐备:新增任何可见标签或失败提示(如
filePreview.html.crashed、filePreview.errors.htmlLoadFailed)都要补全英文、中文、日文、俄文资源,并沿用项目设计 token,否则会破坏 i18n-locale-parity.test.ts 所守护的 locale 一致性约束。
ClawX 的本地 HTML 预览功能证明了"桌面应用内的本地文件预览"可以既贴近真实浏览器体验、又不承担真实浏览器的任何风险——全部功劳都来自这套"URL 白名单 + 偏好加固 + 导航拦截 + Session 封锁"的纵深防御体系。理解并遵守 web-browser-security-and-lifecycle.md 中的规则,是安全扩展该能力的前提。
- 人工智能
- AI 应用
- 桌面应用
- 交互助手
【免费下载链接】ClawX
ClawX is a desktop app that provides a graphical interface for OpenClaw AI agents. It turns CLI-based AI orchestration into a desktop experience without using the terminal. China website is https://clawx.com.cn.
相关推荐
Nuxt.js 生命周期深度解析:从构建到渲染的全过程
Nuxt.js 生命周期深度解析:从构建到渲染的全过程 前言 理解框架的生命周期是掌握其核心机制的关键。本文将深入剖析 Nuxt.js 的生命周期流程,帮助开发
Nuxt.js 生命周期深度解析:从构建到渲染的全过程
Nuxt.js 生命周期深度解析:从构建到渲染的全过程 前言 理解框架的生命周期是掌握其核心机制的关键。本文将深入剖析 Nuxt.js 的生命周期,帮助开发者构
EmDash Bot 状态机架构:issue 生命周期与 Agent 运行生命周期的完整设计解析
EmDash Bot 状态机架构:issue 生命周期与 Agent 运行生命周期的完整设计解析 本篇技术指南以 BOT_STATE_MACHINE.md ht
CMS后端前端插件系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考