给智能体配一台真电脑,却不让它把公司点着:OpenBot 架构解剖与从入门到精通实战指南

装好一套智能体(Agent)系统之后,千万别急着问它“今天天气怎么样”或者“帮我写首诗”。那种测试说明不了任何问题——官网的演示视频永远丝滑,README 里的截图永远漂亮。

目录
  1. 💥 一、先剥掉外壳:OpenBot 到底是什么?
  2. 🏗️ 二、全局架构鸟瞰:四块积木与一条铁律
  3. 🧠 核心设计思想一:治理骑在协议上,而不绑在框架上
  4. 🚀 三、【入门篇】五分钟点火起飞与三次“破壁实验”
  5. 🛠️ 路径 A:单容器两条命令极速升空
  6. 💻 路径 B:源码克隆全功能开发栈(推荐进阶玩家)
  7. 🧪 入门必做的三次“亲手验证实验”
  8. 🧬 四、【进阶篇】配置即同事:写二十行 YAML 办入职,顺便治好提示词幻觉
  9. 🦜 一个价值千金的“反自欺”提示词现场
  10. 🌪️ Skill 的双重真相:操作手册 + 工具降噪漏斗
  11. 🛡️ 五、【核心设计思想】拆解那道“一夫当关”的安全网关
  12. 机关一:零 DOM 污染快照与四层反 DNS 重绑定
  13. 机关二:非对称故障闭合的 CEL 策略引擎
  14. 机关三:无审计,不动作(Write-before-Act)与防 TRUNCATE 铁账本
  15. 机关四:密码盲填与三重视觉打码——“机密永不入上下文”
  16. 机关五:抢方向盘为什么“直接拒绝”而不“排队等待”?
  17. 🔌 六、【精通开发篇】零私有 SDK 接入任意智能体与生成式 UI 实战
  18. 🧑‍💻 实战一:30 行 Python 写出一个全受控的 Pydantic AI 数字同事
  19. 🧑‍💻 实战二:TypeScript + LangGraph 图内分流(部署工具自调 vs 界面工具让渡)
  20. 🎨 实战三:用组件回答,且不给大模型编造报表数字的机会
  21. 🌐 七、【大师拓扑篇】全端矩阵与“一张镜像的三重变形记”
  22. 📱 藏在仓库里的三件跨端奇兵
  23. 🚢 一张镜像的三重变形记(防坑内幕全公开)
  24. 🎯 终章:从入门到精通的五步通关清单
  25. 📚 参考资料与核心源码索引

智能体的动作排着队穿过青色海关闸门:查目标、记账、放行,绿球过闸抵达受控电脑,红色违规件被弹回,账本指示灯只增不灭

装好一套智能体(Agent)系统之后,千万别急着问它“今天天气怎么样”或者“帮我写首诗”。那种测试说明不了任何问题——官网的演示视频永远丝滑,README 里的截图永远漂亮。

想看清一台机器的底色,最好的办法是 故意干几件坏事,看它拦不拦你。

把 OpenBot 的代码库拉下来之后,我对着它的受控电脑容器连捅了三刀:

  • 第一刀:我让智能体在它自己的 Bash 终端里敲下一行 env,想顺手把同容器里的数据库连接串 DATABASE_URL 和主加密密钥 KEY_ENCRYPTION_KEY 偷出来。结果终端里干干净净,半个机密字节都没有——连上游网络代理地址里的 username:password 都被提前剃掉了。
  • 第二刀:我让智能体在允许读写的 /workspace 目录里建了一个指向 /etc/cron.d/backdoor 的悬空软链接(Dangling Symlink)——目标文件此时还不存在,普通代码调 realpath() 检查路径时会收到“文件不存在”的报错,然后傻乎乎地放行,接着 writeFile 就会顺着软链接直接把文件写进系统敏感目录。结果 OpenBot 当场吐出 403:它在落盘前手写了 lstat 加 readlink 循环,沿着软链接逐跳追踪了最多 32 层。
  • 第三刀:我直接连进开发数据库,敲下一句 TRUNCATE audit_events;,想把刚才干坏事留下的审计记录一把清空。数据库啪地拍回来一行报错:Audit events are append-only。翻开迁移目录,第 12 号 SQL 文件的名字直白得让人想笑:0012_truncate_is_not_a_way_around_append_only.sql。
看到这个文件名的时候,我知道这帮写代码的人是真上过战场、真挨过毒打的。

市面上“能让大模型调工具”的框架多如牛毛;但 “能调你的工具”和“你敢让它靠近公司的生产工具”,中间隔着一整座海关大厦。OpenBot 要解决的,恰恰就是后面这件事。

这篇文章带你把 OpenBot 的引擎盖彻底掀开。我们不背空洞的架构黑话,先看清它每一颗齿轮为什么长成这样,再手把手带你从五分钟点火跑通、零代码捏出数字同事、配置铁血安全护栏,一直走到用 Python 或 TypeScript 接入你自己写的智能体,最后把它稳稳当当地搬上生产集群。


💥 一、先剥掉外壳:OpenBot 到底是什么?

一句话说透:OpenBot 是一套长着 ChatGPT / Claude 交互外壳、跑在你自己机器或内网里、把每个智能体都关进受控独立电脑的企业级智能体底座。

💡 核心概念注释:模板而非产品(A Template, Not a Product)
许多开源项目发一堆 npm 包让你 npm install,自己把内核藏在云端。OpenBot 反其道而行之:整个代码库里所有子工作区都标着 "private": true,官方既不提供托管注册版,也不发 npm 依赖包。它的标准用法就是让你直接把仓库克隆走,换掉 examples/ 下的示例配置包,改造成你们公司自己拥有百分之百控制权的内部平台。

为什么非得大费周章搞这么一套底座?你想想看,把一个大模型放进真实办公环境做“数字同事(Coworker)”,你会遇到四个绕不过去的物理难题:

1. 许多系统压根没有 API,或者验证码拦在门口:报销系统是十年前的老网页,供应商后台每次登录都要短信验证码(2FA)。纯靠调 API 的智能体寸步难行;但如果给它配一个真浏览器,遇到登录墙谁来输密码?密码输进聊天框,岂不是直接喂给了大模型训练语料和后台日志? 2. 智能体框架半年换一茬:风控组用 Python 的 LangGraph,客服组喜欢 Pydantic AI,前端组在玩 TypeScript 的 Mastra,还有人直接调 Claude Agent SDK。公司总不能给每个框架各造一套权限后台、聊天界面和审计账本吧? 3. 大模型面对三十个工具就会眼花:接一个 Slack 有 167 个动作,接上 Google Drive 和 Notion 瞬间突破两百个。把两百个工具定义一股脑塞进上下文窗口,模型选对工具的概率会断崖式跳水,顺便把你的 token(模型处理文本的计量单位)预算烧出窟窿。 4. 半夜三点没人盯着屏幕:你白天在聊天框里看着智能体干活,它点歪了你能立马喊停;可如果你设了个每天凌晨三点自动跑的定时任务,趁你睡觉时它收到了网页里夹带的恶意提示词注入(Prompt Injection),转头去删你的云盘文件,谁来踩刹车?

记住这四个问题。OpenBot 仓库里那三十多个文件夹、七十个数据库迁移脚本,全是为了把这四颗钉子一颗一颗钉死而长出来的。


🏗️ 二、全局架构鸟瞰:四块积木与一条铁律

先别一头扎进几万行源码里迷了路。站在三百米高空往下看,整座 OpenBot 城池其实只由 四块积木 拼成:交互表面(Surfaces)、控制平面与网关(Server & Gateway)、可插拔大脑(AG-UI Bots),以及受控手脚(Computers & Supervisor)。

[ 你:Web / 桌面端 / 手机 / 浏览器扩展 ]
                 │  1. 发起对话 / 实时看屏幕 / 随时抢方向盘
                 ▼
┌─────────────────────────────────────────────────────────┐
│               server (:3001) 控制平面                    │
│  身份认证 · 频道与同事名录 · 五维上下文装配 · CopilotKit │
│  ┌───────────────────────────────────────────────────┐  │
│  │ 🚧 单一网关 (The Gateway) —— 唯一合法的动作出口     │  │
│  │ ① 服务端快照解析目标  ② CEL 策略裁决(拒绝优先)      │  │
│  │ ③ 先写 PostgreSQL 审计行  ④ 放行执行(或点名规则拒绝) │  │
│  └─────────┬───────────────────────────────┬─────────┘  │
└────────────┼───────────────────────────────┼────────────┘
    2. AG-UI │ ▲ 3. 工具调用回流              │ 4. 仅放行动作可达
             ▼ │                             ▼
┌──────────────────────────┐   ┌──────────────────────────┐
│    十五路可插拔大脑       │   │  受控电脑 (agent-computer) │
│ agent-langgraph (:4201)  │   │ 有头 Chromium · 独立登录态 │
│ agent-pydantic-ai 等 15种│   │ /workspace 文件 · 净化Bash │
│ 或你自己的任意 AG-UI 服务 │   │ (由 supervisor 按人/按Bot编排)│
└──────────────────────────┘   └──────────────────────────┘

把仓库根目录下的核心组件和端口摊在桌面上,一张表就能对上号:

模块目录 / 服务默认端口技术栈真实职责(说人话版)
app/3010(开发)
3001(单容器)
React 19 + Vite 7 + TanStack Router你的主操作台:频道群聊、数字同事工位、浏览器实时画面、接管方向盘与 /admin 治理后台。
server/3001Bun + Hono + Drizzle + Better Auth海关总署:管登录权限、装配上下文、转发对话,最重要的是卡死单一网关,所有工具调用必先过筛并记账。
agent-computer/4100(仅绑回环)Bun + Playwright + Xvfb + 本地过滤代理受控手脚:装着有头 Chromium 浏览器、/workspace 文件夹和净化版 Bash 的沙箱容器,自己不做任何权限决定。
supervisor/宿主机 4500
容器内 4300
Bun + Hono + Dockerode + SPIRE CLI机房管理员:全系统唯一握有只读 Docker Socket 的进程,负责按需给每个人(或高敏隔离 Bot)发一台独立电脑容器。
worker/无监听端口Bun 轮询器(K8s 下转为 CronJob)打更人:每 30 秒扫一次到期定时例程(Routines),用数据库行锁抢单后递交给 server 执行。
agent-*(共 15 个)4200 – 4214TS (Bun/Node) 与 Python 3.12 (FastAPI)各路大脑:从零框架手写 SSE 到 LangGraph、CrewAI、Pydantic AI、Claude SDK 等 15 套标准 AG-UI 智能体样板。
toolkit/ & shared/库(无端口)TypeScript + Zodshared 是跨端零机密契约;toolkit 仅供 server 内部使用,处理无头回合、会话锁与工具挂起续跑。
desktop/ mobile/ extension/客户端Tauri v2 (Rust) / Expo RN / Chrome MV3桌面一键容器编排与本机文件夹沙箱审批、手机端随时看屏幕过验证码、浏览器操作轨迹录制教学。
PostgreSQL + Intelligence5432 + 外部/本地PG 16 + CopilotKit Intelligence双账本分离:策略、授权、加密凭据与不可篡改审计流存你自己的 PG;持久化会话线程与记忆由 Intelligence 承载。

🧠 核心设计思想一:治理骑在协议上,而不绑在框架上

很多团队做智能体平台,第一步就是写一个巨厚的基类 BaseCompanyAgent,逼着所有业务工程师继承它写代码。三个月后,新框架出来了,没人愿意用你的老古董基类,治理体系当场报废。

OpenBot 聪明在哪?它根本不管你的智能体肚子里装的是什么框架,它只认一个开放协议——AG-UI。

💡 核心概念注释:AG-UI(Agent-User Interaction Protocol)
AG-UI 是一套开放的智能体与用户界面流式通信协议。入站是一个标准 JSON 请求(RunAgentInput,装着会话消息 messages、本轮可用工具的 JSON Schema tools 和附加属性 forwardedProps);出站是一条标准的 SSE(Server-Sent Events)事件流,顺序吐出 RUN_STARTED、TEXT_MESSAGE_CONTENT、TOOL_CALL_START/ARGS/END、RUN_FINISHED 等事件。只要一个 HTTP 接口能听懂这套输入、吐出这套事件流,它在 OpenBot 眼里就是一个合法的数字同事。

你看这个分工有多干净:

  • 智能体只管“动脑子想下一步干什么”:它收到 server 递过来的消息和工具清单,决定要调用 computer_click 点网页上的第 e12 号按钮,或者调用 showBarChart 画一张柱状图,于是吐出一条工具调用事件。
  • 网关全权接管“能不能干、留什么据、怎么执行”:工具调用流回 server 的单一网关,网关查快照、跑规则、写审计表,通过了才替它去敲 agent-computer 或外部 MCP 服务。
哪怕那个智能体跑在另一台机器上、用完全陌生的语言写成,它也绕不开这道门。治理骑在通信协议上,于是框架彻底自由了。


🚀 三、【入门篇】五分钟点火起飞与三次“破壁实验”

光看图纸不过瘾,我们直接上手跑起来。OpenBot 提供了两条截然不同的启动路径——动手前先选对门,能省下两小时折腾时间:

  • 想最快体验完整功能,或者往云服务器上部署?选路径 A(单容器一体机):一条 docker run 命令包办前端、后端、Chromium 浏览器甚至内置 PostgreSQL 数据库。注意这时候所有服务都在 3001 端口,没有 3010!
  • 想改源码、改租户 YAML 捏自己的同事、或者体验“每人一台独立电脑容器”?选路径 B(源码克隆开发栈):用 scripts/start.sh 拉起混合栈,前端热更新跑在 3010 端口,API 跑在 3001 端口。

🛠️ 路径 A:单容器两条命令极速升空

准备一份最小 .env 文件(只需填大模型密钥、加密主密钥和 CopilotKit Intelligence 项目密钥,本地试用保留 OPENBOT_SINGLE_USER=true 即可免配 OAuth 直接以管理员身份进门):

# 1. 生成一把真正随机的 32 字节 Base64 主加密密钥
openssl rand -base64 32

# 2. 写入最小 .env(把下面两个密钥换成你自己的)
cat << 'EOF' > .env
OPENBOT_SINGLE_USER=true
KEY_ENCRYPTION_KEY=刚才生成的32字节base64字符串
OPENAI_API_KEY=sk-proj-...
INTELLIGENCE_API_URL=https://api.copilotkit.ai
INTELLIGENCE_GATEWAY_WS_URL=wss://gateway.copilotkit.ai
INTELLIGENCE_API_KEY=cpk_...
EOF

# 3. 一键拉起全栈一体机(内置 PostgreSQL + 自动迁移 + API + 前端 + Chromium)
docker run -p 3001:3001 --env-file .env \
  -e EMBEDDED_POSTGRES=on \
  -v openbot-data:/var/lib/postgresql \
  ghcr.io/copilotkit/openbot:latest

打开浏览器访问 http://localhost:3001,齐活!

⚠️ 防踩坑铁律:挂载卷千万别多写一层 /data!
仔细看上面那行 -v openbot-data:/var/lib/postgresql——挂载的是父目录,绝不能挂到 /var/lib/postgresql/data!为什么?因为在绝大多数云主机的 ext4 磁盘卷里,新挂载的根目录天生自带一个 lost+found 文件夹。PostgreSQL 的 initdb 一见目标目录非空就会当场罢工拒建数据库,而外层容器照常启动,云平台还会恭喜你部署成功,点开网页却是一脸懵的 502 Bad Gateway。挂在父目录,让 data 成为它底下的干净子目录,这个暗坑就永远踩不到你。

💻 路径 B:源码克隆全功能开发栈(推荐进阶玩家)

确保本机装好了 Docker、Bun 1.4.3+(package.json 锁了 1.4,1.3 跑部分测试会挂)、openssl 和 Node npx,在仓库根目录依次执行:

# 1. 复制配置模板并安装工作区依赖
cp .env.example .env
bun install

# 2. 登录 CopilotKit(免费开发者计划足够)并自动铸造 openbot 学习容器与运行密钥
npx --yes copilotkit@latest login
npx --yes copilotkit@latest project select
bun scripts/setup-learning.ts

# 3. 在 .env 中填入你的 OPENAI_API_KEY(若用 Anthropic,需同时设 ANTHROPIC_API_KEY、BOT_PROVIDER=anthropic 和 BOT_MODEL=claude-sonnet-4-5)

# 4. 点火启动全套服务(Docker 基础设施 + 数据库迁移 + API + Worker + Vite 前端)
bash scripts/start.sh

看到健康检查全绿后,打开 http://localhost:3010。

这里藏着一个极讲究的安全细节:scripts/start.sh 首次运行时会自动替你生成两把绝不相同的随机密钥写进 .env——MANAGED_AGENT_TOKEN 和 AGENT_TOOL_TOKEN。为什么要两把?

💡 核心概念注释:双向异构鉴权令牌(Dual Asymmetric Tokens)
许多系统图省事,服务端和智能体之间共用同一把共享密钥,这在安全上叫“左右互搏”。在 OpenBot 中,方向是焊死的:MANAGED_AGENT_TOKEN 是 server 向智能体证明“我是合法调度方”;而 AGENT_TOOL_TOKEN(以及后台为每个外部 Bot 单独签发的 obot_agt_...)是 智能体回调 server 网关调工具时证明“我是那个智能体”。两边各执一词,绝不混用;一旦任意一把发生轮换,start.sh 会自动重启旧进程并重建容器,杜绝新旧密钥失配导致静默 401。

🧪 入门必做的三次“亲手验证实验”

服务跑起来了,怎么确认它真通了?别只发一句“你好”,按下面三步做一遍现场实验,你对整套架构的手感会瞬间打通:

1. 实验一(验手脚与画布):进入 /bot,对智能体说:*“打开 news.ycombinator.com,看看排第一的新闻是什么,并画一张前五名得分对比的柱状图给我。”*

  • 看什么:右侧实时屏幕(Live Screen)里真有一个有头 Chromium 正在加载网页;下方的 Activity 面板列出了它的导航与读取动作;聊天框里弹出的是一张带数据溯源角标的交互式 React 图表组件,而非干巴巴的文字。
2. 实验二(验海关与账本):让智能体去填写测试表单 https://httpbin.org/forms/post。接着打开 /admin/boundaries(边界策略页),添加一条禁止在该域名点击提交或输入的规则,回聊天框让它再试一次,随后打开 /admin/audit(审计页)。
  • 看什么:智能体第二次动作被当场拦停,审计表里清清楚楚躺着一条 computer.action_refused,不仅记下了谁在什么时候干的,还指名道姓写出了是哪一条 CEL 规则拦住了它。
3. 实验三(验抢方向盘):让智能体打开一个需要登录的网站,或者直接点击屏幕面板上的 Take control(接管控制)。
  • 看什么:你在网页画面里直接动鼠标、敲键盘,跟操作远程桌面一样顺滑;而在你握着方向盘的这几分钟里,智能体哪怕想动一下手指都会收到 409 Conflict 直接停手。翻回 /admin/audit,交接全过程已被记为 computer.help_requested、computer.control_taken 和 computer.control_released 三条铁证。

🧬 四、【进阶篇】配置即同事:写二十行 YAML 办入职,顺便治好提示词幻觉

把环境跑顺之后,下一步就是把示例公司“Acme Fintech”换成你自己的团队。

打开 examples/fintech/(由环境变量 TENANT_PACKAGE_DIR 指向,相对于 server/ 默认是 ../examples/fintech)。你会发现,OpenBot 里的数字同事、频道、品牌配色和常用技能,全都是声明式 YAML 配置,一行胶水代码都不用写。

租户包文件是否必填作用与玄机
brand.yaml必填定义工作区名字、标语、主色与强调色。
model.yaml必填内置智能体的默认提供商、模型名(支持 ${ENV:-default} 占位符)、超时时限与单轮最大工具步数。
agents.yaml + agents/*.yaml必填在职数字同事花名册。支持 built-in(跑在 server 进程内)和 remote-ag-ui / remote-mastra(连外部端点)。
catalog/*.yaml选填样板人才储备库!里面躺着 10 个写好的专职同事(费用核查、面试纪要、值班交接、发版说明、工单分诊等),默认不加载。
skills.yaml选填部署级 / 技能指令与所需工具声明(如 /check-a-claim、/research-public-web、/skill-creator)。
channels.yaml必填默认团队频道及允许进驻发言的智能体名单(permitted_agents)。
theme.css选填自定义皮肤。加载器 validateThemeCss() 只放行 :root/.dark 与 32 个指定 CSS 变量,严禁 @import 和 url() 防样式数据外带。
想给团队招一个“工单分诊员”?只需一条复制命令:

cp examples/fintech/catalog/ticket-triage.yaml examples/fintech/agents/

重启 server,加载器 loadTenantPackage() 会自动计算全包 SHA-256 校验和并幂等同步进数据库,这位新同事就坐进 /bots 名录里待命了;哪天不需要了,删掉那个 YAML 文件就是解聘。而且 agents.yaml 有个极贴心的裁剪机制:如果某个远端智能体的 endpoint: ${MY_AGENT_URL:-} 环境变量展开后是空字符串,加载器会自动把它静默略过,绝不会在界面上挂一个点进去就报错的死链接。

🦜 一个价值千金的“反自欺”提示词现场

翻开 examples/fintech/agents.yaml 第 29 行,作者在 knowledge(企业知识库同事)上面留了一段长长的注释,记录了一次真实的翻车。每个写系统指令(System Prompt)的人都该读一读这段故事:

最早的时候,系统还没接任何网盘连接器。为了防止大模型胡编乱造公司制度,作者在提示词末尾写了一句看似无比正确的铁律: *“当没有连接知识源时,直截了当地告诉用户没有访问权限。”*

后来,管理员给它接上了 Google Drive MCP 连接器,并且授了权。诡异的事情发生了:用户问公司制度,智能体明明调用了 search_files 工具,工具也明明把真实的文件列表返回到了它的上下文里——结果它扭头对用户说:“抱歉,我没有访问公司文档的权限!”

为什么会这样?因为一句死板的无条件否定指令,在大模型的注意力权重里,压倒了提示词里没点名提过的工具返回结果! 你以为你在教它诚实,其实你在教它闭着眼睛背台词。

看看作者是怎么修好它的(直接摘自现行 agents.yaml 的逻辑精髓): 1. 把判断条件改成模型眼前可观测的事实(Observable Condition):工具返回了文件,那就是已连接的来源,老老实实引用它;压根没有相关工具、或者工具明确返回未连接/报错,才直说没有。 2. 把防编造戒律精准对准真正的漏洞:“绝不要把你自身训练记忆里的东西当成外部来源说出来;也绝不要在工具刚刚返回结果时声称自己没有访问权限。”

就改了这么一个支点,瞎话消失了,装瞎也消失了。写提示词跟做物理实验一个道理:永远用可观测的输入输出下定义,别用模糊的道德口号赌模型的领悟力。

🌪️ Skill 的双重真相:操作手册 + 工具降噪漏斗

在 OpenBot 输入框里敲 /,就能唤出 skills.yaml 或个人创建的 Skill(可复用的任务技能)。甚至你可以直接跟持有 skill-creator 的通用助手聊天,让它采访你的需求、现场彩排一遍步骤,最后弹出一张确认卡片——只有你亲手按下卡片上的保存按钮,这个新 / 命令才会落盘。

但如果你以为 Skill 只是“一段省得每次重敲的提示词模板”,那就只看懂了一半。在 server/src/plugins/selection.ts 里,Skill 还承担着另一个生死攸关的任务:给大模型做运行前工具降噪。

前面算过一笔账:大模型从 10 个工具里挑一个,十拿九稳;从 30 个以上工具里挑,就开始乱点鸳鸯谱。于是当一个智能体被授予的外部工具总数超过门槛(SELECTION_FLOOR = 12)时,每轮对话真正开跑前,server 会先用轻量模型扫一眼用户的最新消息,挑出本轮匹配的已授权技能集合 \(S_{\text{matched}}\),然后按下面这条集合算式决定本轮真正递给大模型的工具清单 \(T_{\text{offered}}\):

\[T_{\text{offered}} = T_{\text{granted}} \cap \left( \bigcup_{s \in S_{\text{matched}}} T_{\text{declared}}(s) \cup T_{\text{unclaimed}} \right)\]

别被符号吓着,拆成大白话就三句铁律:

  • \(T_{\text{granted}}\) 是管理员真正授予该智能体的工具全集,\(T_{\text{declared}}(s)\) 是技能 \(s\) 在 YAML 里声明自己需要的工具,\(T_{\text{unclaimed}}\) 是没被任何技能认领过的独立工具。
  • 最外层那个交集符号 \(\cap\) 是安全底线:你在自己的 Skill 里写上一百个 google-drive/delete_file 也没用——只要管理员没把那个工具放进 \(T_{\text{granted}}\),交集一算就是空集!Skill 只是说明书,绝不授予半点新权限。
  • 窄化层故意设计成“故障开放(Fail-Open)”:万一用户说的话哪个 Skill 都没对上,或者前置筛选模型超时了怎么办?代码直接退回完整的 \(T_{\text{granted}}\)!为什么这里敢故障开放?因为窄化层只管“让模型少看几个干扰项”,它根本不是安全边界;要是窄化失败就锁死,管理员明明授了权的工具就会莫名其妙失灵。真正的安全边界在后面的单一网关里,那里才是寸步不让的“故障闭合(Fail-Closed)”。
分得清哪一层该宽松保可用、哪一层该死硬保安全,这叫架构定力。

💡 核心概念注释:外部应用接入六态机(App Access Six-State Machine)
当智能体通过 Google Drive、Notion 或 Composio(托管了数百款 SaaS 的连接器聚合平台)替你干活时,它默认用的是提问者本人的 OAuth 账号(除非管理员显式配置为团队共享账号 Shared App)。server/src/plugins/app-access.ts 把“某人能否通过某 Bot 用某应用”收敛为唯一的六种确定状态:ready(就绪)、not_connected(需你本人去连接账号)、reconnect(令牌过期需重连)、shared_not_connected、disabled、not_granted(后三项需管理员操作)。遇到缺权限时,系统指令严令智能体直接弹出 show_app_access 申请卡片,绝不允许智能体自作聪明打开无登录态的浏览器去硬闯第三方网站的登录页。

🛡️ 五、【核心设计思想】拆解那道“一夫当关”的安全网关

现在我们走到整座大厦最坚固的金库门口了——server/src/computer/gateway.ts 与 policy.ts。

不管智能体想点一下网页按钮、敲一行 Shell 命令、写一个工作区文件,还是调一次外部 MCP 工具,全都必须从这道网关钻过去。它的动作顺序在代码里是焊死的四步:解析真实目标 \(\to\) 评估 CEL 策略 \(\to\) 写入审计行 \(\to\) 最后才发往执行端。

我们挑里面最精彩、最能体现工程功力的五个机关,挨个拆开看。

机关一:零 DOM 污染快照与四层反 DNS 重绑定

智能体是怎么“看”网页和点按钮的?如果向网页 DOM 里硬塞一堆 data-agent-id="123" 属性,现代 React/Vue 网页一重绘属性就没了,还会瞬间触发各大网站的反爬虫警报。

agent-computer/src/aria-snapshot.ts 走了一条极干净的路:直接调 Playwright 原生的 target.ariaSnapshot({ mode: "ai" }),一次性穿透多层 iframe 提取出无障碍树(ARIA Tree),只筛出 17 种可交互控件(按钮、链接、输入框等,上限 200 个),给每个元素分配一个像 e5 这样的引用号。

关键来了:当智能体喊出“我要点击 e5”时,网关怎么知道 e5 是“查看详情”还是“确认转账一百万”?网关绝不相信智能体嘴里说的按钮名字! 1. 每次拍快照时,网关把 { session, snapshotId, url, elements } 存进服务端 PostgreSQL 的 computer_snapshot 表。 2. 智能体来点 e5 时,网关必须同时核对容器运行纪元 session 和快照代数 snapshotId 完全吻合(差一代立刻抛 409 StaleSnapshotError 逼它重看),然后从服务端自己的数据库里反查出 e5 真正的 role、name 和页面 url,送进策略引擎审判!

那网络层呢?万一智能体访问一个恶意网址 http://evil.example.com,第一秒 DNS 解析出合法的公网 IP 骗过了 server 的 URL 检查,第二秒浏览器真正建连时,攻击者把 DNS 改成 169.254.169.254(云主机元数据地址,能直接偷走 AWS/GCP 宿主机 IAM 密钥)或者 127.0.0.1:5432,这招臭名昭著的 DNS 重绑定(DNS Rebinding) 怎么防?

OpenBot 布下了双层天罗地网:

  • 第一层(server/src/computer/target.ts):硬编码最高优先级黑名单 NEVER_ALLOWED_HOSTNAMES,把 AWS/GCP/阿里云/Fargate 的所有元数据地址(169.254.169.254、169.254.170.2、100.100.100.200、fd00:ec2::254 等)彻底封死——哪怕本地开发开了允许访问私有网段开关也绝不放行!同时手写 embeddedIpv4() 剥开三种 IPv6 伪装壳(如 [::ffff:169.254.169.254] 和 NAT64 64:ff9b::)。
  • 第二层(agent-computer/src/egress.ts 容器内本地代理 + DNS Pinning):容器里的浏览器和 Shell 出网全走本地双端口过滤代理,且未收到 server 推送的出站策略前默认拒绝一切外连。每次建连时,代理只做唯一一次 DNS 解析,把拿到的所有 IP 逐个过一遍黑名单与白名单(只要有一个 IP 越界就整单毙掉);通过后,直接拿校验过的字面量 IP 地址去建立 TCP/TLS 连接,全程绝不触发第二次 DNS 查询!DNS 重绑定在物理连接层被彻底废掉武功。

机关二:非对称故障闭合的 CEL 策略引擎

目标解析清楚了,接下来轮到 server/src/computer/policy.ts 里的 CEL(Common Expression Language)策略引擎 拍惊堂木。

💡 核心概念注释:CEL(通用表达式语言)与意图归一化(Intent Normalization)
CEL 是由 Google 开源的非图灵完备轻量表达式语言,不会写出死循环,求值极快,非常适合做安全规则。OpenBot 在把动作送进 CEL 前,还做了一步极贼的意图归一化(intentOf):如果你写规则只拦 tool.name == "computer_click",狡猾的模型可以在输入框调 computer_type 时传 submit: true,或者直接调 computer_key 敲回车键 Enter / 空格键 Space 照样把表单交出去!因此,policy.ts 把点击、带提交的输入、以及敲 Enter/NumpadEnter/Space 全部统一归类为 intent == "activate"——一网打尽,不留旁门左道。

更绝的是它的求值逻辑。假设管理员配置了一组拒绝规则集合 \(D\) 和一组放行规则集合 \(A\),对于任意动作上下文 \(c\),网关最终放行与否严格遵循下面这条布尔判定式:

\[\text{Permit}(c) = \neg \Big( \bigvee_{d \in D} \text{Eval}_{\text{err}\mapsto\text{true}}(d, c) \Big) \land \Big( \bigvee_{a \in A} \text{Eval}_{\text{err}\mapsto\text{false}}(a, c) \Big)\]

仔细品味下标里的 \(\text{err}\mapsto\text{true}\) 和 \(\text{err}\mapsto\text{false}\)——这就是教科书级的双向非对称故障闭合(Asymmetric Fail-Closed): 1. 拒绝永远排在放行前面算(Deny before Allow);没有任何策略配置时,\(A\) 为空,最右边取或结果为 false,默认全拒。 2. 万一管理员手滑,把一条规则语法写错了,或者忘了写比较符直接填了个字符串 "Delete Account"(求值结果不是严格布尔值),怎么办?

  • 如果写坏的是拒绝规则 \(d\),报错立刻按 true(命中拒绝!) 处理,直接拦停!
  • 如果写坏的是放行规则 \(a\),报错立刻按 false(未命中放行!) 处理,绝不开门!
除此之外,上下文 \(c\) 里还藏着一个杀手级字段:initiator.kind(取值有 person、routine、responsibility、memory、handoff、deployment)。为什么要把它和 actor.id(操作人)分开?因为定时任务 routine 半夜跑的时候,顶着的也是创建者本人的 actor.id 权限,但那时房间里根本没人在看屏幕!有了 initiator.kind,你一行 CEL 就能立下规矩:

// 只要不是真人在屏幕前发起的会话,一律禁止执行任何外部写工具或 Shell 命令!
initiator.kind != "person" && (intent == "write_tool" || intent == "run_command")

把这个填进 /admin/boundaries 的 Deny 列表里,半夜的定时任务就只能老老实实读数据写晨报,天塌下来也删不掉你半行线上记录。

机关三:无审计,不动作(Write-before-Act)与防 TRUNCATE 铁账本

很多系统的日志是在动作执行完之后顺手 logger.info() 打一行——要是执行中途进程崩了,或者数据库满了,动作已经捅出去了,账本上却一片空白。

看一眼 server/src/computer/gateway.ts 第 723 行的 govern() 函数:策略刚算完,在真正向 agent-computer 发起任何网络调用之前,第一件事就是 await write(auditStore, ...) 把决策(允许或拒绝)实打实写进 PostgreSQL 的 audit_events 表!而且这行 await 外面故意不包 try/catch 吞错——如果数据库挂了写不进审计行,异常直接炸穿,后面的动作函数 run(address) 永远不会被调用。要是放行后执行端报错了,外层 catch 会再补写第二条 computer.action_failed 审计行,把“准许过门”和“出门摔跤”分得明明白白。

那落了库的账本怎么防篡改?除了开头提到的同时挂上行级触发器与 BEFORE TRUNCATE 语句级触发器(并在第一行先判 TG_OP 堵死 OLD 为 NULL 的三值逻辑漏洞),连审计后台的 Export CSV(导出 CSV 报表) 都防了一手:凡是以 =、+、-、@ 开头的单元格内容,导出时一律前置单引号 '(防止攻击者故意访问一条写成 =cmd|'/C calc'!A0 的恶意 URL,等安全员导出 CSV 用 Excel 打开时触发公式注入攻击),并且“谁导出了审计表、导出了多少行、用了什么过滤条件”本身也会作为一条 audit.exported 被永远写进审计表!

机关四:密码盲填与三重视觉打码——“机密永不入上下文”

智能体替你干活遇到登录框怎么办?如果你在聊天框里敲:*“我的账号是 alice,密码是 S3cr3t!”*——完了,这串密码瞬间进了大模型提供商的请求体、进了会话持久化历史、还可能在下一次页面快照里被原样读回来。

OpenBot 从物理链路上把大模型彻底踢出了密码环路(server/src/passwords/ + agent-computer/src/secret-masking.ts): 1. 带外私密表单:智能体遇到登录墙只能调 computer_request_sign_in(site, reason) 挂起自己。你在聊天流之外的专属安全卡片里输入密码(或点选 AES-256-GCM 加密保存过的密码库条目),请求直接发往 POST /api/sign-in-requests/:id/submit。 2. 同源锁定防钓鱼:agent-computer 往网页输入框填密码前,现场核对浏览器当前页面的真实 origin 是否与请求域名一字不差;哪怕双步登录输完用户名点下一步后被网页偷梁换柱跳到了钓鱼域名,填表也会当场中止! 3. 模型只收状态回执:填完后,返回给大模型的工具结果只有一句冷冰冰的 { ok: true, signedIn: true, note: "You were not told the credentials and must never ask for them another way." }。 4. 三重防偷窥打码(最容易被忽略的侧信道!):很多人以为不把密码传给模型就安全了,却忘了 Playwright 的无障碍树快照默认会把输入框里的明文 value 读出来!因此,OpenBot 往 DOM 填入机密(或你在接管屏幕期间亲手敲入任何内容)的瞬间,会立刻给该 DOM 节点打上 data-openbot-secret="true" 或 data-openbot-person-filled="true" 标签,并加上 CSS -webkit-text-security: disc !important 变成黑圆点。此后无论智能体调 snapshot、读网页正文还是截屏,所有密码框、验证码框、信用卡框和人类手填框的值,一律被硬替换成 "[hidden: sensitive field]" 或 "[hidden: filled in by the person]"!

大模型连密码有几位都猜不着,想泄密都没素材。

机关五:抢方向盘为什么“直接拒绝”而不“排队等待”?

在 agent-computer/src/action-barrier.ts 文件第一行,写着一句斩钉截铁的设计宣言: “同步准入,异步排空。绝不排队,绝不重放(No work is queued or replayed)。”

你想想看,为什么当人类按下“接管控制”去过验证码或改配置时,不能把智能体发来的动作先放进队列排着,等人类松手再自动执行? 因为人类接管前的页面可能是“购物车预览页”,智能体当时算好的动作是“点击坐标/引用 e8”;等人类操作了三分钟交还方向盘时,页面早就变成了“账户安全设置页”,那里的 e8 说不定变成了“注销账号”!如果后台傻乎乎地把排队的旧动作重放出来,那就是一场灾难。

所以 OpenBot 的动作栅栏 ActionBarrier 根本没有设计队列:

  • 人类一点接管,栅栏先 close() 关门,再 await barrier.drain() 等正在跑的最后一个在途动作收尾,随后把控制权切给 "human"。
  • 人类掌盘期间,智能体发来的任何改变状态请求——不仅包括点网页,连同底下的 /exec Bash 命令和 /files/write 写文件也全部拦死(防止你在浏览器前台输银行卡号,智能体在容器后台跑脚本偷窥!)——在微秒级直接抛回 409 Conflict (HUMAN_HAS_CONTROL)。
  • 人类点“交还控制权”时,服务端立刻清空旧快照,并置 resumeSnapshotRequired = true——智能体接回方向盘后的第一步必须先重拍快照看清眼前的新世界,否则休想动一根手指头。
顺带一提,当你不在电脑前,定时例程(Routines,server/src/routines/)也有三道防呆铁闸护体:
  • 15 分钟最小间隔下限:大模型很容易被你一句话忽悠出“每分钟跑一次”的 Cron。为了防止像 45,55 8 * * *(每天 8:45 和 8:55 连跑两次,仅隔 10 分钟,但在 8:50 创建时看下两次间隔却是 23 小时 50 分!)这种夹带局部高频的表达式蒙混过关,schedule.ts 固定从闰年起点 2024-01-01T00:00:00Z 连续推演 200 次触发,只要任意相邻两次小于 15 分钟当场拒收!
  • 每人最多同时开启 20 个例程:超过一个人脑子能记住的上限,必须先关掉旧的才能开新的。
  • 连续 10 次失败疲劳熔断(Fatigue Rule):瞬时网络抖动归底层的 work_items 队列重试管;但如果你的 Notion 令牌三月份就过期了,每晚试一百次也是白烧大模型 token。第 1 次失败发一条提醒,连续 10 次失败自动把例程开关拨到关闭并发出最终通知,体面止损。

🔌 六、【精通开发篇】零私有 SDK 接入任意智能体与生成式 UI 实战

摸透了底层的治理铁律,现在轮到你自己写代码造大脑了。

翻开 OpenBot 根目录,你会看到整整 **15 个 agent-* 智能体实现目录(外加 examples/ 下的 3 个独立外部样板工程)。先扫一眼这份豪华全家桶,你会惊讶于它的包容度:

语言阵营仓库内置样板目录(端口)亮点与特殊本领
TypeScriptagent-bot (:4200)
agent-langgraph (:4201)
agent-mastra (:4213)
agent-bot 零框架手写原生 fetch + SSE,是看清 AG-UI 裸协议的最佳解剖标本;agent-langgraph 支持在状态图内回调网关工具;agent-mastra 直接跑原生 Mastra Server,由服务端 @ag-ui/mastra 桥接。
Python 3.12agent-crewai (:4202) · agent-agno (:4203)
agent-llamaindex (:4204) · agent-pydantic-ai (:4205)
agent-langgraph-agui (:4206) · agent-strands (:4207)
agent-adk (:4208) · agent-langroid (:4209)
agent-ag2 (:4210) · agent-microsoft (:4211)
agent-claude-sdk (:4212) · agent-antigravity (:4214)
囊括主流 Python 框架!其中 agent-langgraph-agui 支持挂载个人 ChatGPT Plus/Pro 登录态;agent-claude-sdk 内置 Claude Code CLI 并用同步钩子挂起前端工具;agent-antigravity 驱动内置 Go 语言 localharness 跑 Gemini 原生工作流。
最不可思议的一件事是什么?你去翻这 15 个目录的 package.json 和 pyproject.toml——它们没有一个安装了 @openbot/toolkit 或任何 OpenBot 私有 SDK!

为什么不用装私有 SDK 就能无缝调受控浏览器、调企业 MCP 插件、弹审批卡片?因为 OpenBot 把工具闭环修成了两条极其清爽的高速公路:

  • 高速公路 A(零回调客户端工具流,15 个样板里有 13 个纯走这条路!)**:
server 发给你的 AG-UI 请求里,已经把角色系统指令塞进了 messages,把本轮获准使用的全部工具(浏览器 computer_*、MCP 工具、画廊 UI 组件)转成标准 JSON Schema 塞进了 tools。你的智能体只需把 tools 原样喂给大模型;模型想调工具时,你的服务吐出标准 AG-UI TOOL_CALL_* 事件并结束本轮 HTTP 流。 接下来奇迹发生了:如果调的是 MCP 等服务端受控工具,守在 server 里的 HeadlessToolsMiddleware 直接在网关内存里半路截胡,验权限、过 CEL 策略、写审计表(需要人工审批就原地转化为 interrupt 挂起),拿到结果拼成 role: "tool" 消息后,自动再敲一次你的智能体接口接着往下跑!如果调的是浏览器或 UI 组件,事件直达前端执行完再带结果续跑。你的智能体代码里连一行调网关的 fetch 都不用写!
  • 高速公路 B(图内同步回调 /api/agent-tools/call,适合复杂多步状态图):
如果你的 LangGraph 状态图想在自己的一个节点循环里同步拿到部署工具(列在 forwardedProps.openbotDeploymentTools 中,如跨 Bot 派活 message_bot 或向人类提问 ask_person)的结果再走下一个图节点,只需带着请求头 x-openbot-agent-token 和请求体里的 forwardedProps.openbotRun(由 server 用 HMAC-SHA256 签名的 10 分钟防伪运行断言) 回调 POST /api/agent-tools/call。网关会交叉核验“你的静态令牌身份 == 签名断言里的 botId”,杜绝任何智能体伪造操作人或冒领别人权限的企图。

🧑‍💻 实战一:30 行 Python 写出一个全受控的 Pydantic AI 数字同事

口说无凭,直接上真家伙。用 Python FastAPI + pydantic-ai,只需不到 30 行核心代码(提炼自 agent-pydantic-ai/src/main.py),就能造出一个自动继承 OpenBot 浏览器沙箱、CEL 边界策略、MCP 插件与生成式图表的数字同事:

import hmac, os
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse, Response
from pydantic_ai import Agent
from pydantic_ai.ui.ag_ui import AGUIAdapter

# OpenBot Server 调用我们时出示的身份令牌(双向鉴权的第一把锁)
EXPECTED_TOKEN = os.environ.get("MANAGED_AGENT_TOKEN", "").strip()

app = FastAPI()

@app.middleware("http")
async def verify_openbot_caller(request: Request, call_next) -> Response:
    if request.url.path == "/health" or not EXPECTED_TOKEN:
        return await call_next(request)
    offered = request.headers.get("x-openbot-agent-token", "")
    # 常量时间比对,防时序侧信道猜解令牌
    if not hmac.compare_digest(offered, EXPECTED_TOKEN):
        return JSONResponse({"error": "Unauthorized"}, status_code=401)
    return await call_next(request)

@app.get("/health")
async def health():
    return {"ok": True}

@app.post("/ag-ui")
async def run_coworker(request: Request) -> Response:
    agent = Agent("openai:gpt-4o-mini")
    # manage_system_prompt="client" 表示直接采用 OpenBot 网关注入的角色设定、溯源护栏与工具指引
    return await AGUIAdapter.dispatch_request(
        request,
        agent=agent,
        manage_system_prompt="client",
    )

把它跑在比如 http://10.0.0.42:9000/ag-ui 上,怎么接进 OpenBot? 1. 为了防 SSRF,OpenBot 默认拒绝注册内网私有地址的智能体端点。在 .env 里精确点名放行这台主机:AGENT_ENDPOINT_ALLOWED_HOSTS=10.0.0.42:9000(严禁写 * 通配符,写了启动直接报错拦截)。 2. 打开页面 /bots 点击新建同事,填好名字、头衔与职责描述,填入端点 URL 和只写(Write-only)鉴权头,点 Test Connection 绿灯通过后保存。 3. 去 /admin/plugins 给它勾选需要的 MCP 工具。从这一秒起,它说的每一句话、点的每一个网页,全都在你的审计大屏监控之下了!

🧑‍💻 实战二:TypeScript + LangGraph 图内分流(部署工具自调 vs 界面工具让渡)

如果你用 TypeScript 写复杂的 LangGraph,想要自己掌控部署工具回调(路径 B),同时把 UI 图表和浏览器工具让渡给前端表面(路径 A),核心分流条件边(提炼自 agent-langgraph/src/index.ts)就这么几行:

async function callOpenBotGateway(name: string, args: unknown, openbotRun: unknown) {
  const res = await fetch(process.env.OPENBOT_TOOL_URL!, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-openbot-agent-token": process.env.AGENT_TOOL_TOKEN!, // 双向鉴权的第二把锁
    },
    // openbotRun 携带服务端 HMAC 签名的 { botId, actorId, runId, threadId, depth, initiator }
    body: JSON.stringify({ name, args, run: openbotRun }),
  });
  const body = (await res.json()) as { text?: string; error?: string };
  if (!res.ok) throw new Error(body.error ?? `Gateway refused (${res.status})`);
  return body.text ?? "";
}

// 在 LangGraph 的条件边里做精准分流:
graph.addConditionalEdges("agent", (state) => {
  const calls = (state.messages.at(-1) as AIMessage)?.tool_calls ?? [];
  if (calls.length === 0) return END;
  // 若全部属于服务端下发的 openbotDeploymentTools(如 message_bot),进图内 tools 节点回调网关;
  // 一旦包含 UI 组件(如 showBarChart)或浏览器工具(computer_click),立刻结束当前流让渡给前端!
  return calls.every((c) => deploymentTools.has(c.name)) ? "tools" : END;
});

🎨 实战三:用组件回答,且不给大模型编造报表数字的机会

纯文字聊天框适合闲聊,干正事得看图表、填表单、批工单。OpenBot 的“用组件代替长篇大论(Components instead of prose)”分了三层装备:

组件层级代码/编辑位置渲染隔离环境适用场景与治理闸门
1. 编译态画廊组件app/src/components/gallery/*.tsx(内置 16 款)主应用同源 React 树(Zod 强类型校验 + 流式防抖 settledArgs)5 类图表(强制带 provenance 数据来源标签)、7 类信息卡、3 类挂起等待人类拍板的决策卡(askApproval / askChoice / askForm)。
2. 在线沙箱组件/admin/playground 网页实时编写发布独立不透明源(Opaque-Origin)沙箱