news 2026/9/25 2:53:28

OpenChamber 隔离空间 Dispatcher 识别机制:为何选择客户端前缀寻址而非服务端嗅探

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenChamber 隔离空间 Dispatcher 识别机制:为何选择客户端前缀寻址而非服务端嗅探
  • AI Agent
  • 人工智能
  • 代码智能体
  • 交互助手

【免费下载链接】openchamber

Agentic Development Environment based on OpenCode AI agent

项目地址:https://gitcode.com/gh_mirrors/op/openchamber
点击查看免费下载

OpenChamber 的“隔离空间(isolated spaces)”功能让 Agent 在一个容器副本中工作,而 Host 与空间之间的所有请求都由一层薄薄的Dispatcher转发。本文基于仓库docs/isolated-spaces/stage-0/e2-dispatcher-recognition.md这份 2026-09-19 的纯代码阅读实验(stage-0 实验 E2)展开,完整盘点 UI 到服务器之间的每一条请求通道,论证为什么“服务端读请求内容来猜归属”(方案 A)不可行,并给出最终采纳的“客户端在 URL 上加/api/spaces/<id>/前缀”(方案 B)的落地规则、两条服务端守卫与特殊情形处理表。读完后你将对 Dispatcher 的寻址边界、UI 侧runtimeFetch与 SDK 包装器的改造点,以及“守卫只拒绝、永不路由”这一安全原则形成完整认识。


一、背景:Dispatcher 要解决什么问题

在 OpenChamber 的隔离空间设计中(见 DESIGN.md),每个 space 是一个独立容器,内部运行与 Host 相同的一对进程:OpenChamber server + OpenCode。Host 与 space 之间只通过 space 内的 OpenChamber server 通信,空间内的代码路径在两侧保持一致,形如/spaces/<id>/<repo>。

Dispatcher是位于现有服务器前的一层薄代理(DESIGN.md):它把“属于某个 space 的请求”原样转发给该 space 内的服务器。于是最核心的问题随之而来——Dispatcher 如何识别一个请求属于哪个 space?

本实验(stage-0 的 E2)只做代码阅读、没有运行任何东西,结论基于对仓库全量请求通道的盘点。文档约定的路径缩写沿用如下:ui/指packages/ui/src/,server/指packages/web/server/;SDK 行号针对@opencode-ai/sdk1.18.31 的dist/v2/client.js。

二、两个候选方案:服务端识别(A)与客户端寻址(B)

  • 方案 A:服务端识别——Dispatcher 像中间人一样嗅探请求里的 query、header、JSON body、id,从而推断请求该发给哪个 space,然后“整体转发(forward whole)”。
  • 方案 B:客户端寻址——由客户端(UI)在调用时自行把目录映射成 URL 前缀/api/spaces/<id>/,Dispatcher 只认这一个前缀。

实验的Verdict明确选择 B,理由可以压缩成三条:

  1. 只匹配一个东西:Dispatcher 唯一识别依据是 URL 前缀/api/spaces/<id>/。它从不读目录、不读 body、不读 id 来决定请求去向。
  2. A 作为“整体转发”根本不成立:终端 socket 是宿主与所有空间共用的单一多路复用 socket;dev tunnel 只携带端口号;大约十个路由族要么把目录放在 JSON body 里、要么完全不携带。
  3. B 的成本极低:因为 UI 的每一个请求都已经经过两个接缝(runtimeFetch与 runtime URL 解析器),ui/与packages/web/src中不存在裸写的fetch('/api...')。服务器端只需加两条小型守卫,任何漏网的调用点都会响亮地失败,绝不会到达错误的地方。

三、请求通道完整盘点(Inventory)

实验对 UI 到服务器的每一条通道逐一登记,核心结论是:目录信息如何传递,各通道并不一致。下表是文档的完整盘点(路径已转为仓库根相对路径):

通道位置调用点是否知道目录当前如何传递目录
SDK 客户端工厂packages/ui/src/lib/opencode/client.ts#L285-L355,所有调用都以Request形式经过runtimeFetchn/abase URL 在客户端创建时固定(client.ts的createRuntimeOpencodeClient),runtime 切换时重建
SDK 作用域客户端packages/ui/src/lib/opencode/client.ts#L543-L553总是SDK 设置x-opencode-directory头,URI 编码(SDKclient.js对应逻辑;本仓库包装器在client.ts中以encodeURIComponent写入,见 packages/ui/src/lib/opencode/client.ts#L269)。GET/HEAD 时 SDK 将其移入?directory=
无作用域客户端 + 每次调用传directory约 40 处client.ts调用点、packages/ui/src/sync/session-actions.ts尽力而为:requestDirectory ?? currentDirectory,两者都空则省略?directory=query
不带 directory 参数的 SDK 调用auth.set/remove、global.*、experimental.controlPlane.moveSession、provider 页面无无
Gitpackages/ui/src/lib/gitApiHttp.ts#L135-L144(唯一buildUrl助手,68 处调用)总是?directory=
文件(web RuntimeAPI)packages/web/src/api/files.ts#L64-L67 及各方法路径总是有;header 是环境当前目录(packages/web/src/api/index.ts)?path=或 JSON bodypath,外加原始(未编码)x-opencode-directory
文件上传packages/web/src/api/files.ts#L233-L246有?path=,body 是流式application/octet-stream二进制
终端 HTTPpackages/ui/src/lib/terminalApi.ts(create 用 bodycwd、list 用?cwd=、/api/terminal/:id/...、touch的 body 是 id 列表)UI store 以目录为键(packages/ui/src/stores/useTerminalStore.ts)混杂:body、query、仅 id
终端 WebSocketpackages/ui/src/lib/terminalApi.ts#L185,单例TerminalTransport无一个 socket,terminal id 在每个帧内部
全局事件 WebSocket 与 SSEpackages/ui/src/sync/event-pipeline.tsn/a所有目录共用一条流,每个事件自带 directory 字段
Dev tunnelpackages/ui/src/lib/browser/devTunnel.ts#L39-L40无仅?port=
Dev servers、probe、空闲端口packages/ui/src/lib/browser/devServers.ts、packages/ui/src/lib/detectDevServer.ts无无
听写、guests、通知、OpenChamber 事件dictation-client.ts、guests/surface-client.ts、useWebNotificationStream.ts、openchamberEvents.tsn/aHost 服务,从不属于 space
认证资源packages/ui/src/apps/MobileFilesSurface.tsx 等(/api/fs/raw、/api/fs/serve/...)、markdownImageAssets.ts、packages/ui/src/lib/runtime-url.ts#L157路径总是有?path=或路径段,外加oc_url_token
以 session 为键的 OpenChamber 路由见下一节来自 session 记录URL 路径中的 id

盘点总数:378 个runtimeFetch(调用分布在 107 个文件中;直接使用解析器的开启者有 5 个 WebSocket、2 个 SSE、5 个资源 URL 构造器。这个“所有请求都经过runtimeFetch”的事实是方案 B 之所以便宜的根本前提——我在源码中确认,packages/ui/src/lib/runtime-fetch.ts#L263-L329 的runtimeFetch是唯一的统一出口,且installRuntimeFetchBridge甚至把window.fetch也收编进来(packages/ui/src/lib/runtime-fetch.ts#L333-L399)。

四、没有目录信息的请求:问题清单

表三列出的请求族,是方案 A 必须逐个“猜”的对象,也是文档最想暴露的脆弱点:

请求证据当前靠什么标识目标
/api/terminal/wsterminalApi.ts;帧attach、write携带s: sessionId每帧 terminal id。Host 与 space 的终端共用同一 socket
/api/terminal/:id/resize、appearance、restart、DELETE、force-kill、touchterminalApi.tsterminal id。touch发送的批次可跨越 Host 与多个 space
/api/dev-tunnel?port=devTunnel.ts;服务器连接127.0.0.1:port(server/lib/dev-tunnel/runtime.js)端口。端口 3000 可能同时存在于 Host 和每个 space
/api/dev-servers、/api/system/probe-urldevServers.ts无
目录未知时的 session 调用session-actions.ts回退到当前目录;getSessionReplyClient回退到无作用域客户端;rejectQuestion只使用环境目录猜测。猜错今天就会打到 Host 上
权限与提问回复session-actions.ts请求目录 → session 目录 → 当前目录
消息队列ui/stores/messageQueueStore.ts(/api/message-queue/sessions/:id、全局快照)session id。目录只在 JSON 项内部
自动批准ui/stores/permissionStore.tssession id,目录只在 JSON body 中
Goals、message-sent、图片授权sessionGoalActions.ts、session-ui-store.ts、markdownImageAssets.tssession id
批量归档ui/sync/session-archive-batch.ts目录只在 JSON body 中
Host 级快照/api/sessions/status、/api/session-activity、GET /api/message-queue、GET /api/permission-auto-accept无。这些是聚合数据,不是 space 请求

注意最后一行划出的界限:聚合型快照不是 space 请求,它们永远留在 Host,由 Host 在会话合并层处理(见 DESIGN.md 的“Dispatcher, sessions, events”一节)。

五、为什么方案 A(服务端嗅探)不可行

文档逐条列出了方案 A 的成本,每一条都在源码中得到印证:

  1. 要读四处位置。Dispatcher 必须同时读 query(directory/path/cwd)、header、JSON body 和 id。OpenChamber 自有路由挂载了最大50 MB的express.json解析器(packages/web/server/lib/opencode/core-routes.js#L1088-L1117),而 OpenCode 代理路由会跳过解析器让 body 直接流式转发(packages/web/server/lib/opencode/core-routes.js#L1118-L1119)。嗅探 body 意味着“先解析再代理再重新序列化”,这已经不是“整体转发”了。
  2. 终端 socket 无法整体转发。一个 socket 同时承载 Host 与 space 终端的帧(帧格式见 packages/ui/src/lib/terminalApi.ts#L11-L14,attach/write帧内s: sessionId)。Dispatcher 必须按 terminal id 拆分帧,并维护一个 terminal→space 索引。
  3. session/terminal 键路由需要 id→space 的 Host 索引。这个索引本质是“不可信空间数据的缓存”,一旦过期或缺失,Dispatcher 只能猜;恶意 space 还能冒用别的 space 的 session id。
  4. dev tunnel 与/api/dev-servers没有任何可嗅探的东西。它们需要新增参数——而那本身就是方案 B。
  5. 歧义案例成堆。文件 API 的 header 是环境目录,可能与path不一致(packages/web/src/api/files.ts#L64-L67);x-opencode-directory的编码方式有三种——SDK 侧 URI 编码、files API 原始编码、非 Latin-1 字符由 packages/ui/src/lib/runtime-fetch.ts#L131-L157 做标记编码(补x-opencode-directory-encoding: uri);而且没有目录时 Host 会静默回退到settings.lastDirectory(packages/web/server/lib/opencode/project-directory-runtime.js#L91-L103),所以“没有目录”等于“上次浏览过什么就用什么”——这是方案 A 最致命的一条:一个空间路径在 Host 上根本不存在,validateDirectoryPath(packages/web/server/lib/opencode/project-directory-runtime.js#L36)会先 stat 目录,space 请求必须在它之前被转发(DESIGN.md)。
  6. 需要嗅探的挂载点散落四处:HTTP 入口(validateDirectoryPath之前,6 个文件 9 个调用点)以及四个独立的 upgrade 处理器(server/lib/terminal/runtime.js、dev-tunnel/runtime.js、dictation/runtime.js、realtime-proxy.js)。

六、方案 B:一条前缀规则的三处落点

方案 B 的核心是一个纯函数spaceRoute(directory),放在新的ui/lib/spaces/模块中:把路径与 space 根目录比对、把 id 对照 known-spaces store 校验,返回/api/spaces/<id>或什么都不返回。它只应用在三个地方:

  1. runtimeFetch接受directory选项,在buildRuntimeFetchUrl(packages/ui/src/lib/runtime-fetch.ts#L95)和extractRelayPath(packages/ui/src/lib/runtime-fetch.ts#L184)之前把/api/x重写为/api/spaces/<id>/x。网络分支与 relay 分支必须使用同一条被重写后的路径。
  2. SDK fetch 包装器(packages/ui/src/lib/opencode/client.ts#L290-L355)读取 SDK 自己放到请求上的目录:GET 用?directory=,其余用x-opencode-directory。这是SDK 契约而不是猜测,因此作用域客户端与约 40 个逐调用传目录的站点零改动自动覆盖。
  3. spacePath(path, directory)助手,供 WebSocket 与资源 URL 的调用方在调用解析器之前使用。

运行时切换规则:什么都不缓存

前缀只是路径上的一个段,base URL 仍在调用时由解析器解析;known-spaces store 以 runtime key 为键,在 runtime 切换流程中重置。这一点与现有代码完全合拍:scopedClients本就在重连时被清空——packages/ui/src/lib/opencode/client.ts#L522-L535 的reconnectToRuntimeBaseUrl重建客户端的同时执行this.scopedClients.clear()。

需要改动的调用点

约 150 个(378 个中的一部分,涉及约 25 个文件)会碰目录或 session。助手把改动量压到最低:

  • gitApiHttp.buildUrl:68 处调用,一处编辑(packages/ui/src/lib/gitApiHttp.ts#L135-L144);
  • files.ts:11 处调用,从path推导目录(packages/web/src/api/files.ts#L64-L67);
  • terminalApi.command助手;
  • 已经传x-opencode-directory的 config stores。

Host-only 路由族完全不用改:GitHub、Linear、客户端认证、语音、tunnel 设置、guests、主题、passkeys。

终端改造

把现在的单例TerminalTransport(默认 socket 地址见 packages/ui/src/lib/terminalApi.ts#L185)替换为按目标(Host 或某个 space id)各建一个TerminalTransport,目标来自终端所属目录(useTerminalStore的目录键)。touch按目标拆分。

传输层零改造

  • relay HTTP 白名单接受任何/api/路径(packages/web/server/lib/relay/tunnel-host.js#L23-L28);
  • tunnel 原样传递pathname?search(packages/ui/src/lib/relay/tunnel-payloads.ts#L32-L36);
  • shouldResolveApiPath接受/api/(packages/ui/src/lib/runtime-fetch.ts#L11-L13)。

所以 HTTP 与 SSE 在 web、Electron、hosted mobile、Capacitor 上都不需要传输层改动。唯一要动的是白名单。

WebSocket 与 URL-token 白名单

这些路径是精确匹配列表,新前缀必须按“形状”加入它们(就像GUEST_SURFACE_WS_PATH的正则形状一样,见 packages/web/server/lib/relay/tunnel-host.js#L37-L40):

  • ALLOWED_WS_PATHS(tunnel-host.js#L30-L40);
  • isUrlAuthWebSocketPath(server/lib/ui-auth/ui-auth.js);
  • isUrlAuthReadableHttpPath,为/api/spaces/<id>/fs/raw准备(ui-auth.js);
  • Electron 实时代理(server/lib/realtime-proxy.js)。

真正需要加前缀形状的只有三条路径:terminal/ws、dev-tunnel、fs/raw。

认证顺序

Host 先认证用户,然后 Dispatcher 剥掉 cookies、bearer 与oc_url_tokenquery 参数,再换上该 space 的 token。这不破坏 relay 规则:relay Host 依旧不注入任何凭据,Dispatcher 是认证之后的独立一跳。

七、推荐规则与两条服务端守卫

文档给出的实施规则一锤定音:

一个请求属于某个 space,当且仅当它的路径以/api/spaces/<id>/开头,且<id>在 space manager 的 label 派生列表中。Dispatcher 认证用户、剥掉前缀与用户的凭据、换上该 space 的 token,然后原封不动流式转发其余部分。除此之外它什么都不读。UI 侧,每个目录作用域或 session 作用域的请求都要点名自己的目录,由一个函数在调用时把目录变成前缀。

守卫只拒绝、永不路由:

  • 守卫 1:剥掉前缀后,如果?directory=或x-opencode-directory仍然存在,且不在该 space 根目录之下,回答400。
  • 守卫 2:一个没有前缀的请求,如果它的directoryquery 或 header 位于 spaces 根目录之下,在validateDirectoryPath之前、在lastDirectory回退之前,得到一个稳定的 4xx。

两条守卫共同兜住了“约 150 个调用点是估算而非全量审计”这个缺口:任何漏改的调用点都会以明确失败暴露,而不是悄悄打到错误的目标。

八、特殊情形处理表

情形处理
终端 socket 与/api/terminal/:id/*每目标一个传输。socket 位于/api/spaces/<id>/terminal/ws。目标来自useTerminalStore中终端的目录
Dev tunnel、dev servers、probe调用方传发起面板的目录。/api/spaces/<id>/dev-tunnel?port=。Electronrelay-dev-tunnel.mjs与server/lib/dev-tunnel/client.js需要同样的参数
session 键路由(queue、auto-accept、goals、message-sent、图片授权、回复)目录取自 session 记录。没有服务器确认的目录就令该动作失败;只要存在任何 space,就不得回退到当前目录
全局事件、session 列表、/api/sessions/status、queue 与 auto-accept 快照不分发。由 Host 按 DESIGN.md 合并。Host 必须丢弃目录在/spaces/<那个 id>/之外的任何 space 记录或事件,绝不允许 space 覆盖 Host 的 session id
前缀下的/api/fs/serve与/api/preview/proxyDispatcher 中直接拒绝。space 的文件不得在应用源下渲染成页面。fs/raw以nosniff和附件或仅图片 content-type 转发
跨越边界的moveSession、worktree 与 git-integrate 动作源与目标 target 不同时拒绝
auth.set、provider 页面、settings、GitHub、Linear、voice、guests仅 Host,永不加前缀
/api/projects/:id/*Host settings 路由。space 项目的图标发现是后续决策
VS Code没有 space manager。助手返回 “unsupported”,UI 隐藏 spaces(这是 DESIGN.md 产品决策 16 的落地:VS Code 永不获得该特性)

九、安全性质:目标由 URL 命名,而不是由响应决定

文档给出了该方案的安全论证,值得原样保留:

目标由已认证用户的客户端写在 URL 中,id 对照运行时 labels 校验,space token 只由该 id 决定。space 返回的任何东西都不能改变请求的去向,因为 Dispatcher 不读 id、不读 body。一条说谎的 session 记录最多让 UI 去调用产生它的那个 space,或触发守卫 1 失败。

也就是说,这个设计把“信任”压缩到了 URL 前缀 + 认证用户 + label 列表三个点上,空间内部的任何输出(事件、session 记录、JSON 响应)都不具备路由能力。

十、对 DESIGN.md 的相应修改

文档明确了本次实验要回写 DESIGN.md 的变更:

  • “Dispatcher, sessions, events”第一条:把“整体转发匹配请求”替换为上述前缀规则;删除“没有目录的请求逐个处理”,它们同样走前缀。
  • 新增:UI 用一个函数把目录解析为 space;终端传输按目标划分;WebSocket 与 URL-token 白名单加入前缀形状。
  • 第二条:把oc_url_token加入被剥掉的凭据清单。
  • session 条:新增合并规则——space 只能上报自己根目录之下的目录,以及 id 冲突规则。
  • LESSONS.md 的 “The existing server” 一节:补充lastDirectory回退与单一多路复用终端 socket 两个事实。

十一、未验证项:诚实标注的边界

文档最后明确列出未验证内容,这本身就是工程严谨性的体现:

  • 本次实验什么都没运行。relay、Capacitor、Electron 行为是从代码推断的;relay 技能需要真实 relay 测试来验证新 WebSocket 路径。
  • 空间内 OpenCode 是否把session.directory原样报告为/spaces/<id>/<repo>(symlink、规范路径)未验证——整个 UI 映射依赖这一点(packages/ui/src/stores/globalSessionStructure.ts)。
  • 150 个调用点数量是按文件统计的估算,不是全量审计;守卫 2 的存在就是为了兜住漏网之鱼。
  • SDK 的/api/session/...风格端点 GET 时用location[directory](SDKclient.js),未逐一核对 UI 用非 GET 方法且不带 header 调用了哪些。
  • 重写带流式 body 的Request需要duplex: 'half'(packages/ui/src/lib/runtime-fetch.ts#L211-L215 有同款注释)。SDK body 目前都是字符串,上传走字符串路径分支,应当安全,但未实测。
  • VS Code webview 的 fetch 路由(packages/vscode/webview/main.tsx)未读。
  • Host 与 space 之间 session id 的格式与碰撞概率未检查。

延伸阅读:本实验属于 docs/isolated-spaces/stage-0/ 的四个 stage-0 实验之一,与之并列的还有 e1-server-in-stock-image.md(服务器在基础镜像中运行)、e3-model-window-and-short-token.md(模型窗口与短令牌)、e4-git-over-exec.md(git 走 exec 控制通道)。整个特性的阶段计划见 STAGES.md,测试规范见 TESTING.md。

  • AI Agent
  • 人工智能
  • 代码智能体
  • 交互助手

【免费下载链接】openchamber

Agentic Development Environment based on OpenCode AI agent

项目地址:https://gitcode.com/gh_mirrors/op/openchamber
点击查看免费下载
上一篇:FinalBurn Neo终极指南:开启你的复古街机游戏之旅
下一篇:claw-code 插件子系统完全指南:从 `.claude-plugin/plugin.json` 清单解析到 Hook 执行、权限与生命周期管理

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 2:50:47

百托帮GEO服务在全国市场的表现如何

顺应流量迁徙趋势&#xff0c;锚定行业发展使命随着数字经济的深度渗透&#xff0c;线上获客已经成为企业经营发展的核心命题。从早期的搜索引擎营销&#xff0c;到短视频时代的内容种草&#xff0c;再到当下AI搜索的异军突起&#xff0c;用户获取信息与商业服务的路径正在发生…

作者头像 李华
网站建设 2026/9/25 2:50:44

坚瓷建材性价比怎么样

装修过房子的人&#xff0c;大概都记得这样的时刻&#xff1a;瓷砖铺完了&#xff0c;缝隙却成了心病。浅色美缝用了半年&#xff0c;阳台一晒就泛黄;师傅施工到一半&#xff0c;发现一组料只能打十几米&#xff0c;耗材一加再加;出了问题想找厂家&#xff0c;电话那头却始终无…

作者头像 李华
网站建设 2026/9/25 2:50:16

KatelyaTV配置文件详解:从基础到增强版94个片源配置指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华