给智能体配一台真电脑,却不让它把公司点着:OpenBot 架构解剖与从入门到精通实战指南
装好一套智能体(Agent)系统之后,千万别急着问它“今天天气怎么样”或者“帮我写首诗”。那种测试说明不了任何问题——官网的演示视频永远丝滑,README 里的截图永远漂亮。
目录
- 💥 一、先剥掉外壳:OpenBot 到底是什么?
- 🏗️ 二、全局架构鸟瞰:四块积木与一条铁律
- 🧠 核心设计思想一:治理骑在协议上,而不绑在框架上
- 🚀 三、【入门篇】五分钟点火起飞与三次“破壁实验”
- 🛠️ 路径 A:单容器两条命令极速升空
- 💻 路径 B:源码克隆全功能开发栈(推荐进阶玩家)
- 🧪 入门必做的三次“亲手验证实验”
- 🧬 四、【进阶篇】配置即同事:写二十行 YAML 办入职,顺便治好提示词幻觉
- 🦜 一个价值千金的“反自欺”提示词现场
- 🌪️ Skill 的双重真相:操作手册 + 工具降噪漏斗
- 🛡️ 五、【核心设计思想】拆解那道“一夫当关”的安全网关
- 机关一:零 DOM 污染快照与四层反 DNS 重绑定
- 机关二:非对称故障闭合的 CEL 策略引擎
- 机关三:无审计,不动作(Write-before-Act)与防 TRUNCATE 铁账本
- 机关四:密码盲填与三重视觉打码——“机密永不入上下文”
- 机关五:抢方向盘为什么“直接拒绝”而不“排队等待”?
- 🔌 六、【精通开发篇】零私有 SDK 接入任意智能体与生成式 UI 实战
- 🧑💻 实战一:30 行 Python 写出一个全受控的 Pydantic AI 数字同事
- 🧑💻 实战二:TypeScript + LangGraph 图内分流(部署工具自调 vs 界面工具让渡)
- 🎨 实战三:用组件回答,且不给大模型编造报表数字的机会
- 🌐 七、【大师拓扑篇】全端矩阵与“一张镜像的三重变形记”
- 📱 藏在仓库里的三件跨端奇兵
- 🚢 一张镜像的三重变形记(防坑内幕全公开)
- 🎯 终章:从入门到精通的五步通关清单
- 📚 参考资料与核心源码索引
装好一套智能体(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/ | 3001 | Bun + 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 – 4214 | TS (Bun/Node) 与 Python 3.12 (FastAPI) | 各路大脑:从零框架手写 SSE 到 LangGraph、CrewAI、Pydantic AI、Claude SDK 等 15 套标准 AG-UI 智能体样板。 |
toolkit/ & shared/ | 库(无端口) | TypeScript + Zod | shared 是跨端零机密契约;toolkit 仅供 server 内部使用,处理无头回合、会话锁与工具挂起续跑。 |
desktop/ mobile/ extension/ | 客户端 | Tauri v2 (Rust) / Expo RN / Chrome MV3 | 桌面一键容器编排与本机文件夹沙箱审批、手机端随时看屏幕过验证码、浏览器操作轨迹录制教学。 |
PostgreSQL + Intelligence | 5432 + 外部/本地 | PG 16 + CopilotKit Intelligence | 双账本分离:策略、授权、加密凭据与不可篡改审计流存你自己的 PG;持久化会话线程与记忆由 Intelligence 承载。 |
🧠 核心设计思想一:治理骑在协议上,而不绑在框架上
很多团队做智能体平台,第一步就是写一个巨厚的基类 BaseCompanyAgent,逼着所有业务工程师继承它写代码。三个月后,新框架出来了,没人愿意用你的老古董基类,治理体系当场报废。
OpenBot 聪明在哪?它根本不管你的智能体肚子里装的是什么框架,它只认一个开放协议——AG-UI。
💡 核心概念注释:AG-UI(Agent-User Interaction Protocol)
AG-UI 是一套开放的智能体与用户界面流式通信协议。入站是一个标准 JSON 请求(RunAgentInput,装着会话消息messages、本轮可用工具的 JSON Schematools和附加属性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 图表组件,而非干巴巴的文字。
https://httpbin.org/forms/post。接着打开 /admin/boundaries(边界策略页),添加一条禁止在该域名点击提交或输入的规则,回聊天框让它再试一次,随后打开 /admin/audit(审计页)。
- 看什么:智能体第二次动作被当场拦停,审计表里清清楚楚躺着一条
computer.action_refused,不仅记下了谁在什么时候干的,还指名道姓写出了是哪一条 CEL 规则拦住了它。
- 看什么:你在网页画面里直接动鼠标、敲键盘,跟操作远程桌面一样顺滑;而在你握着方向盘的这几分钟里,智能体哪怕想动一下手指都会收到
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{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]和 NAT6464: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{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(未命中放行!) 处理,绝不开门!
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"。 - 人类掌盘期间,智能体发来的任何改变状态请求——不仅包括点网页,连同底下的
/execBash 命令和/files/write写文件也全部拦死(防止你在浏览器前台输银行卡号,智能体在容器后台跑脚本偷窥!)——在微秒级直接抛回409 Conflict (HUMAN_HAS_CONTROL)。 - 人类点“交还控制权”时,服务端立刻清空旧快照,并置
resumeSnapshotRequired = true——智能体接回方向盘后的第一步必须先重拍快照看清眼前的新世界,否则休想动一根手指头。
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 个独立外部样板工程)。先扫一眼这份豪华全家桶,你会惊讶于它的包容度:
| 语言阵营 | 仓库内置样板目录(端口) | 亮点与特殊本领 |
|---|---|---|
| TypeScript | agent-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.12 | agent-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 原生工作流。 |
package.json 和 pyproject.toml——它们没有一个安装了 @openbot/toolkit 或任何 OpenBot 私有 SDK!为什么不用装私有 SDK 就能无缝调受控浏览器、调企业 MCP 插件、弹审批卡片?因为 OpenBot 把工具闭环修成了两条极其清爽的高速公路:
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,适合复杂多步状态图):
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)沙箱 | 无需发版,在线写 HTML/CSS/JS 点 Publish 即刻全网热生效!强制 custom_ 前缀防覆盖核心组件,沙箱内零主站 Cookie、零 /api/* 访问权。 |
| 3. 自由生成式 UI | 环境变量 OPENBOT_GENERATIVE_UI 控制 | 沙箱 / A2UI 1.0 白名单原语 | 即席生成轻量交互界面(A2UI 刻意剔除了 Favicon 等组件,防止浏览器自动请求外部图标服务泄露内网域名!)。 |
server/src/components/functions.ts)。想想看,如果你让大模型画一张“过去 7 天各智能体被网关拦截的审计统计报表”,传统做法是让模型调工具查出几百行数字,再由模型把这些数字抄进图表组件参数里吐给前端——这不仅白白浪费几千个输出 token,更要命的是模型抄数字的时候一旦手滑产生幻觉,老板在图表上看到的就是假账!
看看 app/src/components/gallery/activity.tsx(showActivityReport)是怎么破局的:
1. 模型只传意图,不碰数据:大模型调用 showActivityReport 时,传的参数仅仅是 { report: "refusals", days: 7 }。大模型自己的上下文里从头到尾根本看不见查询出来的真实业务数字!
2. 双重动态验权:浏览器挂载该组件前,先敲 POST /api/components/showActivityReport/decision 确认管理员此刻没禁用它;挂载后,组件直接由前端敲 POST /api/components/showActivityReport/call 请求服务端数据函数 recentRefusals。
3. 组件级细粒度授权:服务端连过三道关——当前用户能不能用这个 Bot、这个 Bot 能不能用这张组件卡、以及这张组件卡在 component_functions 表里有没有被单独授权调用 recentRefusals 函数!全过之后,数据库真实聚合结果直达前端画布,顺手写下一条 component.function_called 审计记录。
模型看不见数字,就绝无可能篡改或编造数字。 把大模型擅长理解意图的长处和确定性代码擅长算数画表的长处切得干干净净,这才是生成式 UI 的正确打开方式。
🌐 七、【大师拓扑篇】全端矩阵与“一张镜像的三重变形记”
走到这一步,你已经能定制团队、编写安全策略并接入自研智能体了。最后一章,我们看看 OpenBot 怎么跨出网页浏览器,以及同一套代码如何从笔记本电脑一路变形到企业级 Kubernetes 集群。
📱 藏在仓库里的三件跨端奇兵
很多人只盯着 app/ 看,却漏掉了仓库里另外三个工程量惊人的客户端:
1. 桌面端 desktop/(Tauri v2 + Rust 30 余模块):
- 零 IPC 泄露的 PKCE 登录(
organization_auth.rs):唤起系统浏览器完成企业 SSO 登录后,Rust 原生宿主拿令牌向本机 Server 换取一次性短效ticket,直接导航 WebView 种下HttpOnlyCookie——前端渲染进程的 JS 和 IPC 管道里全程摸不到半个长期会话令牌! - 宿主机文件夹的“铁笼审批”(
host_access.rs):智能体想读改你电脑桌面上的真文件夹?Rust 宿主会弹出操作系统级原生阻塞弹窗(写文件限制在 24 行、2000 字符以内,完整摊在弹窗里给你过目,拒绝暗箱截断)。你点同意后,它绝不在你的宿主机裸跑命令,而是拉起一个--network none --read-only --cap-drop ALL --pids-limit 128的断网只读一次性容器,只把你亲手选定的那个文件夹非递归挂载进去跑完即焚!
mobile/(React Native 0.83 + Expo SDK 55):
- 当后台运行的智能体撞上验证码或高危审批,你的手机会收到原生推送;点开
computer.tsx屏幕,手机直连 WebSocket 实时播放智能体浏览器的 JPEG 画面流,你直接用手指在手机屏幕上点选验证码、代填一次性口令,完事一键把方向盘丢回给智能体继续加班!
extension/(Chrome MV3 OpenBot Recorder):
- 有些业务流程写提示词太费劲,不如“你做一遍给它看”。装上扩展后,你在真浏览器里的点击、导航和麦克风语音旁白会被实时脱敏(
recorder-masking),不经 OpenBot Server 落盘,直接通过短期通行证连上 Phoenix WebSocket 推进你个人的专属学习空间(容器 ID 由user-加上用户 ID 的 SHA-256 前 32 位派生)。经 Automatic Learning 分析发布后,下一次运行就会按 “Bot 专属习得技能 > 你个人的习得技能 > 团队公开共享技能” 三级优先级自动注入智能体脑中!
🚢 一张镜像的三重变形记(防坑内幕全公开)
最体现 OpenBot 架构功底的,是它在三种部署尺度下迥然不同却严丝合缝的隔离手段:
| 部署形态 | 进程编排方式 | 浏览器电脑隔离粒度 | 数据库与网络防逃逸杀手锏 |
|---|---|---|---|
| 1. 本地混合开发 ( scripts/start.sh) | 宿主机 Bun 热跑 server/worker/app,Docker 跑其余基础设施 | supervisor 按人(或 Isolated Bot)动态起容器,仅绑 127.0.0.1 随机端口 | docker-compose.yml 拆出 data 与 default 双隔离网桥:postgres 只进 data 网,agent-computer 只进 default 网,物理断绝智能体 Shell 直连数据库。 |
| 2. 单镜像一体机 ( Dockerfile + s6) | s6-overlay v3 依赖图同容器拉起 PG + 迁移 + Computer + API | 全员共用容器内那一个 agent-computer(仍按人/按 Bot 分目录与 Profile) | 同容器“一室三户”四道铁闸(见下方拆解),绝不让 pwuser 偷看隔壁 postgres 和 apiuser 的家底! |
| 3. 企业级 K8s ( charts/openbot) | 同一镜像靠环境变量开关分饰 API Deployment、CronJob 与沙箱 Pod | 对接 K8s SIG agents.x-k8s.io/v1beta1 Sandbox CRD,支持 gVisor / Kata | 零 Docker Socket!空闲电脑自动 Suspended 缩容归零(Scale-to-Zero) 保卷省钱,下次发消息唤醒瞬间顺手用 JSON Patch 热升级镜像;全覆盖 NetworkPolicy 封死内网与元数据网段。 |
1. 单容器里怎么防智能体“偷家”?
在单容器模式下,PostgreSQL、API Server 和智能体能跑 Bash 的 agent-computer 挤在同一个 Linux 网络命名空间里,127.0.0.1:5432 就在手边!要是像普通教程那样把本地 PG 设成 trust 免密登录,智能体在终端敲一句 psql -h 127.0.0.1 -U openbot 就能把整座数据库搬空。
看看 Dockerfile 和 docker/s6/ 设下的四道同室防线:
- 三权分立 UID:严禁 root 跑业务,数据库归
postgres,API 归apiuser(UID1002,禁止登录 Shell),浏览器与智能体终端归pwuser(UID1001)。 - 随机 SCRAM 数据库密码:每次初始化从
/dev/urandom抽 32 字节随机密码锁死scram-sha-256,密码文件权限0600只给postgres和 s6 环境目录(0700)。 env -i剃刀级环境清空:s6 启动computer服务时,先用env -i把继承自容器的所有环境变量彻底清零,再只挑出COMPUTER_TOKEN(每次启动现场生成的 64 位随机十六进制串)等几个无害参数传进去——pwuser的内存和/proc/self/environ里压根不存在DATABASE_URL和KEY_ENCRYPTION_KEY!- 精确到二进制的
sudoers:为了允许智能体装工具,/etc/sudoers.d/pwuser只精确放行/usr/bin/apt-get、apt、dpkg、apt-key、apt-cache五个包管理器,想sudo cat偷读别人的文件?门儿都没有!
--entrypoint sh!
因为这张镜像的默认入口是 s6 的 /init。如果你不覆盖入口直接跑 docker run openbot sh -c "... migrate.ts",s6 会先把整套 API Server 和 Chromium 顶着还没迁移的旧数据库拉起来,最后才跑你的迁移命令——本来想避免的并发抢跑事故,全让你这一条命令亲手制造出来了!正确姿势永远是:docker run --rm --env-file .env --entrypoint sh openbot -c "cd /app/server && bun scripts/migrate.ts"。
3. K8s 电脑 Pod 为什么刻意不配 livenessProbe,以及 spire/ 是干什么的?
- 在
charts/openbot/templates/computer/statefulset.yaml里,作者只配了startupProbe和readinessProbe,偏偏不写livenessProbe。原因极其实在:Chromium 渲染超重网页时偶尔会卡顿几秒,要是配了存活探针,kubelet 一看超时直接把容器杀掉重启——正在上面工作的所有人瞬间掉线登出!读懂工具的真实脾气,才不会搞出形式主义的“货物崇拜运维”。 - 至于根目录下的
spire/(CNCF SPIFFE/SPIRE 配置),则是给离线内网或高安全多容器环境准备的密码学身份证局:spire-agent直接从宿主机内核/proc/反查容器的真实 Docker 标签,确认是由/cgroup supervisor合法创建的容器后,才给它签发有效期仅 1 小时的 X.509-SVID 证书(spiffe://openbot.local/)。不靠云厂商 IAM,在纯断网机房里也能做到零信任工作负载见证。/
🎯 终章:从入门到精通的五步通关清单
合上引擎盖之前,送你一张可以立刻照着打勾的实战路线图:
- [ ] 第 1 关(点火验真):用
scripts/start.sh或单容器镜像跑起环境,让智能体打开一个真实外部网页并用图表组件汇报,再去/admin/audit找到它刚刚留下的每一条足迹。 - [ ] 第 2 关(立规矩):去
/admin/boundaries写下第一条带initiator.kind != "person"的 CEL 拒绝规则,体验把无人值守风险关进笼子的踏实感。 - [ ] 第 3 关(招同事):复制
examples/fintech为你自己的租户目录,在agents/下写一个 20 行 YAML 的专职同事,并用“可观测条件”打磨它的系统指令,再跟通用助手对话用/skill-creator造出团队第一个/技能。 - [ ] 第 4 关(接外脑):用上面那 30 行 Python
FastAPI或 TypeScriptLangGraph跑起一个外部 AG-UI 服务,配好AGENT_ENDPOINT_ALLOWED_HOSTS注册进/bots,验证零私有 SDK 受控工具调用。 - [ ] 第 5 关(上生产):关掉
OPENBOT_SINGLE_USER,配好企业 Google / Microsoft / Okta 或 SAML/OIDC 登录与INITIAL_ADMIN_EMAILS,换上真随机KEY_ENCRYPTION_KEY,在 K8s 上开启SandboxScale-to-Zero 与NetworkPolicy(单机部署别忘了加一条定时清理未发送草稿附件的bun scripts/cull-staged-attachments.ts任务)。
掀开盖子,齿轮都在那儿明明白白转着。现在,这台机器归你了。就这么回事。
📚 参考资料与核心源码索引
- 项目仓库与架构总览:CopilotKit OpenBot Repository (
README.md,docs/architecture.md,docs/deployment.md,docs/routines.md,docs/automatic-learning.md) - AG-UI 开放协议规范:Agent-User Interaction Protocol (
https://github.com/ag-ui-protocol/ag-ui,@ag-ui/core&@ag-ui/encoder) - 单一网关与非对称故障闭合 CEL 引擎:
server/src/computer/gateway.ts,server/src/computer/policy.ts,server/src/computer/target.ts,server/src/computer/snapshot-store.ts - 受控沙箱电脑、DNS Pinning 出口代理与机密打码:
agent-computer/src/index.ts,agent-computer/src/egress.ts,agent-computer/src/aria-snapshot.ts,agent-computer/src/secret-masking.ts,agent-computer/src/action-barrier.ts,agent-computer/src/shell.ts,agent-computer/src/workspace.ts - 容器隔离、一室三户 s6 编排与工作负载身份见证:
supervisor/src/docker.ts,supervisor/src/identity.ts,spire/server.conf,Dockerfile,docker/s6/,charts/openbot/ - 防篡改审计触发器与分布式定时任务队列:
server/drizzle/0012_truncate_is_not_a_way_around_append_only.sql,server/src/work/queue.ts,server/src/routines/schedule.ts,server/src/routines/sweep.ts