自托管 Kuru:容器化智能体平台拆解
Kuru 是 MIT 许可的自托管智能体平台,支持独立容器、多智能体协作、定时任务、技能和 MCP 工具,并提供可接管的桌面。文章结合实际搭建与源码阅读,介绍其服务端、桌面镜像和移动端组织方式。
AI 深度解读
Kuru 把聊天 Agent、独立 Linux 容器、手机远程桌面和 MCP 工具接到了一起。作者花了几周阅读和使用它,最认可的是三个工程选择:危险操作在聊天里审批,桌面通过核心服务代理,MCP 凭证留在核心服务。
这篇值得读的地方,是这些选择背后的失败案例:审批后重试为什么总对不上、人类介入时浏览器为什么不能回收、工具密钥为什么不该交进 Agent 容器。但它也有明确前提:面向个人自托管,仍处于实验阶段。
一次审批,怎样准确对应一次操作
Kuru 使用 MIT 许可证,在自己的 Linux 机器上通过 Docker 部署。它的 monorepo 分成 apps/core、apps/desktop、apps/mobile 和 packages/agent-tools,分别负责 Node/Express + Postgres 服务、桌面镜像、Expo/React Native 手机应用和工具定义。
其中最值得拆的是审批链路。
当 Agent 尝试执行自身目录外的 rm -rf、dd if=、git push --force、curl | sh,或删数据库、删 YouTube 视频等操作时,系统先拦截调用,再在聊天里展示准确的工具名和参数。用户可以批准、拒绝,或者选择「Always allow」。
apps/core/src/turn/auto-review.ts 大约 650 行,约三分之一是测试和说明性注释。入口 classifyRisk() 用 DESTRUCTIVE_BASH 正则及工具规则表判断风险;没命中就直接执行,命中则把调用摘要存入 tool_approvals,状态设为 pending,再通过 saveSendMessage 插入审批卡片并发送推送。
难点在于:用户批准之后,怎样确认模型重试的还是同一件事?
下面这段代码生成调用的 SHA-256 摘要,用来匹配审批和重试:
export function hashToolInput(name: string, input: Record<string, unknown>): string {
const copy: Record<string, unknown> = {};
for (const [key, value] of Object.entries(input)) {
if (key === "requestApproval" || key === "approvalId") continue;
// 卡片上的说明文字不属于动作本身;模型重试时可能改写它,
// 导致已批准的卡片无法匹配,出现三张卡片却始终没删除的情况。
if (key === "reason") continue;
// 稳定排序,让同一批例程的删除请求在批准后仍能匹配。
if (key === "routineIds" && Array.isArray(value)) {
copy[key] = [...value].map(String).sort();
continue;
}
copy[key] = value;
}
return createHash("sha256").update(`${name}:${JSON.stringify(copy)}`).digest("hex");
}
重点看排除 reason 的那一行。模型可能把「删除旧任务」改写成「清理不用的任务」,动作没变,解释变了。如果把解释也算进摘要,用户批准第一张卡片,重试却会生成第二张。
routineIds 排序同样是在处理动作等价性:同一批 ID 换个排列,不应被判为新请求。不过,这段代码没有展示对所有对象键进行递归排序,不能据此认定它解决了任意 JSON 输入的规范化问题。
模型收到的是阻塞结果,要求等待,并携带审批 ID 重试原调用。普通批准记录通过 usedAt 保证只能使用一次;原文没有展开「Always allow」如何持久化、如何撤销,不能把两者混为一谈。
后台 worker 也经历过修正:以前审批阻塞会导致任务停止,批准后重新启动却丢失上下文。现在通过 APPROVAL_WAIT_MS 暂停等待 30 分钟,再继续原动作。
审批是否可用,取决于能否把用户同意准确绑定到后续执行。
手机接管桌面,关键在连接和生命周期
每个 Agent 都有自己的 Linux 容器,里面运行 X、浏览器和可见的窗口管理器。手机可以通过 VNC 观看桌面,也可以接管鼠标和键盘,处理 Agent 卡住的弹窗。
连接统一经过 core 和 Cloudflare Tunnel,不要求开放入站端口或配置静态 IP。容器的 VNC 端口不对公网暴露。
apps/core/src/http/screen-proxy.ts 只有 78 行。它在 core 截获 WebSocket upgrade,通过 session cookie 或手机端的短期签名 token 验证身份,再用 profileOnAccount() 检查桌面归属。随后启动桌面,重写 HTTP 请求头,通过 socat 接到容器的 noVNC 端口。
客户端拿不到容器端口。对使用者来说,观看和接管都走原来的服务入口。
另一个细节更贴近日常开发:代理调用 screenOpened(),防止有人观看时浏览器因为空闲而被回收。对 Agent 来说可能暂时没有操作,对人来说却正在排查问题。资源生命周期需要同时考虑这两种状态。
桌面工具位于 packages/agent-tools/src/definitions.ts,包括 computer_screenshot、computer_mouse、computer_click、computer_type、computer_key,以及浏览器工具。作者举出的场景很具体:凌晨 1 点,Agent 卡在 cookie 横幅上,用户从手机接进去处理。
不过,78 行只是代理模块的规模。它背后仍依赖认证、隧道、桌面启动和容器管理,不能据此推导整套远程桌面系统很轻。
MCP 凭证留在哪里,比接入多少工具更重要
Kuru 把 MCP 客户端放在 apps/core/src/mcp/。其中 client.ts 为 259 行,是手写的 MCP HTTP 客户端,协议版本固定为 2025-06-18,处理会话,并配合 mcp-oauth.ts 刷新 OAuth token。
这里的职责划分很清楚:core 列出和调用 MCP 工具,Agent 只接收规范化的工具 schema 和执行结果。按原文描述,Agent 容器既拿不到 bearer token,也不能直接访问 MCP 服务;core 发出的请求只能到公共地址。
这能减少一类直接风险:Agent 即使执行了读取容器文件的命令,也无法从那里读出由 core 持有的 MCP token。但它仍然能申请调用获准暴露的工具,所以凭证隔离并不等于消除了工具滥用。
实现里还有两处限额。分页最多读取 20 页,防止服务端重复返回游标,把一轮任务拖住;工具描述通过 catalog-budget.ts 和内置 skills 共用预算,避免接入 40 个 MCP 服务后,把 200k tokens 的说明全部塞进上下文。
这里的 40 和 200k 是作者用来解释预算必要性的例子,不是压测结果。原文也没有提供工具裁剪后的任务成功率或 token 节省数据。
我的判断
我认可它把复杂问题落到小机制上的做法:审批用表和摘要,桌面复用 core 入口,MCP 在服务端保管凭证。对个人自托管,这比先搭权限 DSL、角色系统和独立网关更容易理解,也更容易追踪故障。
但它的简洁依赖部署前提。Kuru 的账户模型面向单人实例,没有多租户能力;每个 Agent 有容器,不代表它已经支持团队权限隔离。换成不同成员拥有不同审批范围的团队,账户层和授权模型都需要重新考虑。
审批正则也只能提供部分拦截。变量、混淆写法或 find -delete 都可能绕过规则;deletesOnlyOwnFiles() 对自身目录的判断再仔细,也不能代替容器隔离。真正限制破坏范围的是执行环境。
凭证留在 core、动作经过审批、程序运行在容器里,解决的是三个不同层面的问题。
部署成本同样不能忽略:Docker、Cloudflare 域名、Google OAuth 都要配置,手机端还要通过 EAS 或本地构建 Expo 应用。模型需自备 OpenAI、Anthropic、xAI 或 OpenAI-compatible endpoint 的 API key,按账户加密存进 Postgres。作者把它称为周末项目,而非 20 分钟安装,这属于体验判断。
项目还处于 pre-1.0,README 提醒配置、API 和移动端体验可能随提交变化。固定 tag 能减少升级变数,追踪 main 则要接受破坏性变化。原文没有性能、维护成本或同类产品的对照数据,因此「更省代码、更好维护」目前应看作作者的工程经验。
能拿来做什么
- 给 Node 工具执行层增加审批时,可以借鉴「风险判断、存储动作摘要、展示卡片、原调用重试」的流程,并验证说明文字改写、批量 ID 换序和审批重复使用这些情况。
- 开发浏览器 Agent 时,可以提供观看和人工接管入口,同时让观看状态参与空闲回收判断,避免用户排查到一半会话被关闭。
- 接入 MCP 时,可以让服务端统一持有 token,给 Agent 只暴露工具 schema 和结果,再分别限制分页次数与工具描述预算。
- 评估 Kuru 时,先确认自己是否接受单人自托管、移动端自行构建和 pre-1.0 变化;阅读代码可从
apps/core/src/turn/auto-review.ts开始,重点看审批失败后的恢复路径。