一名宅。 - Rust
type: channel
link: https://zhaiyiming.com/tag/Rust/
items: Tokimo 存储系统重构：从分布式自治到集中式管控
items: 把一个 OS 子系统拆成独立进程：video app 的解耦实录
items: 浏览器里的 AI agent 工具调用：从前端按钮到 microVM PTY 的完整链路
items: 为什么我们给 AI 沙箱手写了一套跨平台用户态 TCP/IP 栈
items: 在浏览器里给 AI 跑命令：Tokimo sandbox 三平台 backend 的现状切片

Tokimo 存储系统重构：从分布式自治到集中式管控
type: item
date: 2026-06-02T17:02:35+08:00
description: 本文记录 Tokimo（Web Desktop OS）blob 存储系统的完整重构过程——从各 app 独立管理存储到主进程统一管控、app 通过 RPC 读写的架构演进。涵盖重构动因、两阶段实施路径及最终架构设计。 背景与问题 Tokimo 采用 pnpm monorepo + Rust sidecar 架构，系统中包含多个独立 app（音乐、视频、照片等），每个 app 均需持久化 blob 数据（专辑封面、艺术家头像、视频海报、缩略图等）。 重构前的存储架构 ┌─────────────────────────────────────────────────────────────────┐ │ Tokimo 主进程 │ │ │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ │ Music App │ │ Video App │ │ Photo App │ ← 各 sidecar │ │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ │ Storage │ │ Storage │ │ Storage │ ← 各自持有 │ │ │ Provider │ │ Provider │ │ Provider │ StorageProvider │ │ │ (OpenDAL) │ │ (OpenDAL) │ │ (OpenDAL) │ + 独立实例 │ │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │ │ │ │ │ │ │ └──────────────┼──────────────┘ │ │ ▼ │ │ ┌─────────────────┐ │ │ │ .data/storage/ │ ← 共享物理目录 │ │ │ music/ │ │ │ │ video/ │ │ │ │ tmdb-images/ │ │ │ └─────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ 每个 app 持有独立的 StorageProvider 实例（基于 OpenDAL），均指向同一物理目录 {DATA_LOCAL_PATH}/storage。从功能层面看，该设计能够正常工作——音乐 app 将封面写入 music/ 前缀，视频 app 将海报写入 video/ 前缀，各自维护独立的文件命名空间。 然而，该架构在 sidecar 拆分后暴露了三个结构性缺陷： 问题一：Sidecar 进程间的存储路径不一致 当音乐 app 从主进程内嵌模块拆分为独立 sidecar 进程后，DATA_LOCAL_PATH 环境变量在进程间传递出现偏差。Sidecar 通过 tokimo-app.toml 声明并由主进程 spawn，其工作目录与主进程不同，导致 DATA_LOCAL_PATH 解析结果不一致： 主进程: DATA_LOCAL_PATH = /home/lab/.data/tokimo Sidecar: DATA_LOCAL_PATH = /home/lab/.data/tokimo/apps/music ← 解析错误 结果是专辑封面被写入 sidecar 的 cwd 而非主进程的存储目录，前端请求 GET /storage/music/ab/cd/uuid/cover.jpg 时无法定位文件，所有图片资源返回 404。 问题二：Key 格式泄露内部 app 结构 原有的 key 格式为 {app_id}/{path}/{filename}： music/library-images/music/abc123/cover.jpg video/tmdb-images/poster/550.jpg 该 key 直接暴露在 URL 中（GET /storage/music/library-images/...）。若后续接入 CDN 或启用公开分享，URL 将泄露内部 app 的组织结构和文件命名规则。 问题三：缺乏写隔离，存在跨 app 数据篡改风险 所有 app 共享同一存储目录，且 key 格式未做强制校验。恶意或有缺陷的 app 可以构造其他 app 命名空间下的 key 来删除或覆盖文件，系统缺乏基于 app 维度的访问控制。 Phase 1：Storage Bus Service 识别出上述问题后，核心重构目标明确：将存储访问从 app 自治模式转为主进程集中…

把一个 OS 子系统拆成独立进程：video app 的解耦实录
type: item
date: 2026-05-21T09:18:03+08:00
description: TL;DR 把一个浏览器桌面 OS 里最复杂的子系统从主进程里拆出来跑成独立进程，听起来像"挪个目录"，实际要重新回答四个问题：谁负责接 HTTP，谁负责排任务，DB 怎么分家，前端怎么跨 bundle 借能力。这篇讲我们怎么回答这四个问题，以及拆离过程踩到的三个真问题。 目录 TL;DR 目录 Part 1 · video 从内置域模型迁移为独立 app §1.1 项目定位与职责边界变化 §1.2 video 的职责归属冲突 §1.3 video 是最关键的一次抽离 Part 2 · 设计约束 Part 3 · 四条核心架构边界 §3.1 Bus 通信架构 §3.2 Job 系统架构 §3.3 Player 能力注册架构 §3.4 数据库边界：从隐式漂移到 multiSchema Part 4 · 抽离后的目标架构与可观测收益 Part 5 · 验收方法论 Part 1 · video 从内置域模型迁移为独立 app §1.1 项目定位与职责边界变化 tokimo 立项时更像媒体管理系统：视频库、刮削、转码、字幕、播放状态，是产品的主轴。随后系统逐步加入文件管理、SSH、AI、dashcam、docs、自动化、数据库会话等能力，形态从“一个媒体产品”变成“浏览器里的桌面 OS”。在这个语境里，OS 的职责不再是理解每一种内容业务，而是提供窗口、身份、任务、文件、播放、通知、事件这些可复用能力，让 app 在上面运行。 身份漂移的关键不是功能数量变多，而是“谁为谁服务”的方向反了。媒体系统里，文件系统、任务队列、播放器都是为了 video 服务；webOS 里，video 只是众多 app 之一，反过来消费 OS 提供的通用能力。只要主 server 仍然把 TMDB、剧集、字幕当作内置领域，其他 app 就只能围着 video 的历史结构让路，OS 的抽象也会不断被媒体业务拉偏。 flowchart LR A[媒体管理系统] --> B[文件 / SSH / AI] B --> C[dashcam / docs / automation] C --> D[浏览器桌面 OS] D --> E[30+ apps] A -.早期 core.-> V[video] D -.运行于 OS 之上的 app.-> V 维度 媒体管理系统语境 webOS 语境 video 的角色 核心域模型 第三方 app 能力 OS 应该知道什么 TMDB、字幕、剧集、刮削 streamUrl、窗口、任务、权限 失败影响面 产品核心失败 单 app 降级 发布节奏 跟主 server 一起发 可独立迭代 §1.2 video 的职责归属冲突 video 又早又重：它是项目最早、最复杂的子系统之一，占主仓 Rust 代码显著比例，并留下约 101 处 raw SQL。这样的复杂度在“媒体系统”里合理，因为它就是中心；但在“webOS”里会变成反向依赖：OS 需要硬编码 TMDB 刮削、NFO、HLS、字幕、人物关系、剧集季集等业务概念。更直接的问题是，任何 video 崩溃都可能污染主 server 的心智模型：故障归因无法区分 OS 故障与 app 故障。如果回答不清，抽离边界就还没建立。 尴尬还体现在 API 归属上。/api/apps/video/* 从 URL 看已经像 app，但实现却长期散落在主仓 handler、queue handler、前端 app registry、播放器上下文里。URL 先 app 化，进程却仍 monolith，这会制造一种危险错觉：调用者以为边界已经存在，维护者却仍能在主仓里随手 import video 内部类型。抽离 sidecar 后，URL 语义与运行时边界保持一致。 Before monorepo server ├─ auth / window / vfs / jobs ├─ video: tmdb / hls / subtitle / scrape / raw sql └─ other apps After main server = OS capabilities video sidecar = business domain + own router + own workers 判断题 留在 OS 放入 video sidecar 用户身份、会话、窗口容器 ✅ ❌ TMDB lookup / NFO parse ❌ ✅ HLS 字节流反代能力 ✅ 数据面 ✅ 业务 handler 字幕选择器 UI ❌ ✅ Media Session / controls ✅ ❌ §1.3 video 是最关键的一次抽离 dashcam-archive、docs 等 app 已经证明“sidecar 运行模型可用”。…

浏览器里的 AI agent 工具调用：从前端按钮到 microVM PTY 的完整链路
type: item
date: 2026-05-13T00:11:54+08:00
description: 浏览器里的 AI agent 工具调用：从前端按钮到 microVM PTY 的完整链路 目录 为什么一次「看目录」不只是一次命令执行 链路全景：从 React 到 guest-agent 前端：按钮、SSE、WS 与 typed client 的边界 后端：Axum 把聊天流和工具执行拆开 sandbox：为什么是 vsock，不是 TCP PTY：为什么不是直接 exec 错误与回流：stdout、exit、error 都是协议 业界正在做什么 一个可复用的工程框架 为什么一次「看目录」不只是一次命令执行 用户在浏览器 chat 输入「帮我看一下当前目录」，表面上只是 pwd && ls。但在 agent 产品里，它同时穿过四个边界：浏览器进程、Rust server、sandbox runtime、microVM guest。每一层都可能丢事件、阻塞、错认工作目录，或者把错误包装成一段模型看不懂的文本。 真正难的不是执行命令，而是让一次工具调用具备这些性质：用户能看到流式反馈；模型拿到结构化结果；命令运行在隔离环境；刷新页面后状态还能恢复；超时、退出码、stderr 都能被解释；交互式 shell 和一次性命令不能互相污染。 这里还有一个容易被忽略的产品问题：用户以为自己在和「一个助手」对话，但系统实际由很多异步参与者组成。LLM 可能还在思考，SSE 已经断线；命令可能已经退出，消息缓存还没 flush；sandbox 可能刚被重建，旧的 PTY tab 还在尝试写 stdin；用户切换 agent 后，工作目录、技能开关、模型参数都要跟着换。如果这些边界没有清晰协议，最后出现的 bug 往往不是「命令跑错了」，而是「结果跑对了，但出现在错误的会话、错误的窗口、错误的工作目录」。 难点 表面现象 工程解法 事件先后 POST 成功但前端没收到首个 token 先建 SSE 订阅，再触发 send 执行边界 模型命令误跑在 host executor 显式携带 sandbox handle 工作目录 多轮 Bash cwd 漂移 执行后回读 pwd -P 并更新状态 页面刷新 terminal 输出丢失 PTY registry 保存 history 并支持 attach 错误表达 stderr 被当普通文本 exit/error/stderr 分帧再映射 ToolOutput ┌──────────────┐ POST /chat/send ┌──────────────┐ │ Browser Chat │ ───────────────────▶ │ Axum AI API │ │ React + SSE │ ◀── data: AgentEvent │ ChatService │ └──────┬───────┘ └──────┬───────┘ │ │ tool: Bash │ WS: app events / terminal ▼ │ ┌──────────────┐ │ │ AgentSandbox │ │ │ per-agent cwd│ │ └──────┬───────┘ │ │ spawn_shell │ ▼ │ ┌──────────────┐ └────────────────────────────▶ │ microVM guest│ │ vsock + PTY │ └──────────────┘ Agent 工程的本质，是把「模型想做一件事」变成跨进程、跨协议、跨权限边界的可恢复事务。 链路全景：从 React 到 guest-agent 当前代码里，AI chat 的入口主要在 packages/web/src/apps/ai-chat/。AiChatContext.tsx 维护 agent、conversation、panel 状态；useSendMessage.ts 把编辑器内容、附件和模型选择组装成发送输入；useChatSSE.ts 负责先连 /api/apps/ai/chat/{conversationId}/events，再 POST /api/apps/ai/chat/send，避免服务端广播时前端还没订阅。 服务端路由集中在 packages/rust-server/src/apps/ai/router.rs：stream::chat_events 提供 SSE，stream::chat_send 触发 ChatService::stream_completion。工具层落在 packages/rust-server/src/apps/ai/tools/，其中 Bash 工具最终会走 LocalToolExecutor::exec_bash_streaming；正常情况下 …

为什么我们给 AI 沙箱手写了一套跨平台用户态 TCP/IP 栈
type: item
date: 2026-05-12T13:30:00+08:00
description: 为什么我们给 AI 沙箱手写了一套跨平台用户态 TCP/IP 栈 目录 问题为什么难 为什么不是容器 / userns 业界正在做什么 网络方案怎么选 现在的架构 性能目标不是越高越好 我们诚实承认的取舍 AI sandbox 的网络问题，表面上只是"让 guest 能访问外网"。但我担心的不是能不能 ping 通，而是：未知代码会在高频网络路径里做什么，以及我们能不能在事后解释它为什么能做。 AI agent 跑的是用户给的仓库、脚本、二进制和临时命令。它可能 curl | sh，可能跑包管理器，也可能启动一个开发服务器。网络是高频路径：DNS、TLS、包管理器、Git、模型下载、webhook、local service probe，全部都会从这里经过。如果这些流量直接进宿主内核网络栈，我们很难在跨 Linux / macOS / Windows 的语义上稳定地做审计、DLP、egress allowlist，甚至很难解释一次连接到底从哪里来、为什么被允许。 所以 Tokimo 最后选择了一条看起来更重、但边界更清楚的路：guest 里只放 tokimo-tun-pump 搬运 Ethernet frame，host 侧自己跑基于 smoltcp 的 userspace netstack。guest 可以像平常一样发包，但真正决定 flow 怎么建、怎么路由、怎么记账、怎么被策略拦截的地方，回到了 Tokimo 自己的 Rust 代码里。 问题为什么难 AI sandbox 的网络需求和传统 VM 网络不同，面临三个同时成立的约束： ┌──────────────────────────────────────────────────────────┐ │ 约束 1: 未知代码 │ │ Guest 运行任意用户仓库 / 脚本 / 二进制 │ │ 网络行为不可预测，必须从外部可解释 │ └──────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────┐ │ 约束 2: 三平台一致性 │ │ Linux Auto (CH → bwrap) / macOS (VZ) / Windows (Hyper-V) │ │ 策略语义必须统一，不能每 backend 各写一套 │ └──────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────┐ │ 约束 3: 可审计 / 可解释 │ │ 每次连接需能追溯到 session、task、进程 │ │ flow log 要产品侧可控，而非依赖宿主平台状态 │ └──────────────────────────────────────────────────────────┘ 这三个约束加在一起，就排除了大部分"接通即可"的常规方案。传统的平台原生网络（nftables / pf / WFP）能解决单平台的某个约束，但无法同时满足三个。 AI sandbox 网络的真正难题不是吞吐，而是同时满足跨平台一致性和完整的流量可解释性。 为什么不是容器 / userns 容器路线我们不是没想过。userns、Landlock、seccomp、cgroup 全部拼起来，看起来像一个便宜的 sandbox，而且启动快、镜像生态成熟、调试体验也好。问题在于它们共享宿主内核，爆炸半径不够小。CVE-2022-0185、CVE-2024-1086 这种例子不需要过度展开；结论就是，只要未知代码还能打到同一个内核，AI shell 的风险就没有被切开。 对传统 CI 来说，容器可能是足够好的折中；对一个会让用户把任意仓库、任意命令、任意 agent loop 丢进去的产品来说，我们不想把"请相信 namespace 组合得足够好"当成核心安全边界。尤其网络审计还要和进程、文件、会话身份绑定时，共享内核会让很多边界变成策略约定，而不是故障域隔离。 于是主路线切到 VM：Linux 优先 Cloud Hypervisor，macOS 用 Virtualization.framework，Windows 用 Hyper-V。VM 至少给了一个硬边界：guest 用户态和宿主内核不是同一个故障域。但 Linux 现在还有一个必须诚实写出的可用性兜底：当 CH 不可用时，Auto backend 会退到 bubblewrap。bwrap 是 namespaces-based …

在浏览器里给 AI 跑命令：Tokimo sandbox 三平台 backend 的现状切片
type: item
date: 2026-05-11T18:00:00+08:00
description: 在浏览器里给 AI 跑命令：Tokimo sandbox 三平台 backend 的现状切片 目录 问题为什么难 一张总图：三平台不是三套产品 Linux / Auto backend：Cloud Hypervisor 优先，bubblewrap 自动兜底 macOS / Virtualization.framework：原生 VM，协议尽量不特殊化 Windows / Hyper-V：service、镜像打包和诊断链路正在持续推进 共同基础：tun-pump、vsock 抽象与 guest-agent 协议 为什么不直接 Docker / WSL2 业界做法 我们诚实承认的缺口 问题为什么难 在浏览器里让 AI agent 跑命令，难点不在“把一串 argv 丢给 shell”。难点是：这串命令可能来自模型、用户、网页和历史上下文的混合产物，我们无法提前审查它会不会 curl | sh、跑包管理器、开端口、扫内网、读工作区外的文件，或者把一台开发机拖进 fork bomb。 传统 Web 应用把不可信输入关在进程边界里；AI coding agent 则反过来，主动要求我们给它一个“像真机器一样”的 shell。它需要 PTY、文件系统、网络、DNS、包管理器、长进程、信号和退出码。这里的工程悖论是：越像真机器，隔离越难；越像沙箱，工具越不像真机器。 Tokimo sandbox 当前的答案不是押一个平台原语，而是把 Linux、macOS、Windows 各自最靠谱的 VM / namespace 能力收敛到同一组语义：启动 sandbox、打开 shell、转发 stdio/PTY、挂载工作区、给网络加策略、最后销毁会话状态。 金句：AI shell 的安全边界不是一条 syscall 黑名单，而是一台可预测、可销毁、可观测的小机器。 一张总图：三平台不是三套产品 flowchart TD Browser[Browser / AI tool call] --> Server[tokimo-server] Server --> API[tokimo-package-sandbox API] API --> Auto[Linux: Auto backend] Auto -->|preferred| CH[Cloud Hypervisor microVM] Auto -->|fallback| BW[bubblewrap namespaces] API --> VZ[macOS: Virtualization.framework] API --> HV[Windows: Hyper-V HCS + service] CH --> GA[tokimo-guest-agent / sandbox-init] BW --> GA VZ --> GA HV --> GA GA --> Shell[exec / PTY / shell] GA --> FS[workspace mount] GA --> Pump[tokimo-tun-pump] Pump --> Netstack[host smoltcp netstack] Netstack --> Policy[policy / audit hooks] Policy --> Internet[(network)] 跨平台一致性视图 ┌─────────────── public API ───────────────┐ │ configure / create / start / shell / stop│ └───────────────┬───────────────┬──────────┘ │ │ ┌───────▼────────┐ ┌────▼──────┐ ┌────────▼────────┐ │ Linux Auto │ │ macOS VZ │ │ Windows Hyper-V │ │ ch → bwrap │ │ virtio-vsock │ HvSocket + pipe │ └───────┬────────┘ └────┬──────┘ └────────┬────────┘ └───────────────▼─────────────────┘ guest-agent protocol + netstack 最近的主仓和 sandbox 子包 commit 说明这条线已经发生了两类重要变化。第一，Linux 产品路径从“只讲 CH”变成 Auto：优先 Cloud Hypervisor，遇到无 KVM、无 vhost-vsock、缺 CH binary 等条件时自动降级到 bubblewrap（886048c5、c2b4979）。第二，Windows…