ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

OpenChamber 隔离空间 Dispatcher 识别机制:为何选择客户端前缀寻址而非服务端嗅探 AI Agent人工智能代码智能体交互助手【免费下载链接】openchamberAgentic 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理由可以压缩成三条只匹配一个东西Dispatcher 唯一识别依据是 URL 前缀/api/spaces/id/。它从不读目录、不读 body、不读 id 来决定请求去向。A 作为“整体转发”根本不成立终端 socket 是宿主与所有空间共用的单一多路复用 socketdev tunnel 只携带端口号大约十个路由族要么把目录放在 JSON body 里、要么完全不携带。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的createRuntimeOpencodeClientruntime 切换时重建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两者都空则省略?directoryquery不带 directory 参数的 SDK 调用auth.set/remove、global.*、experimental.controlPlane.moveSession、provider 页面无无Gitpackages/ui/src/lib/gitApiHttp.ts#L135-L144唯一buildUrl助手68 处调用总是?directory文件web RuntimeAPIpackages/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有?pathbody 是流式application/octet-stream二进制终端 HTTPpackages/ui/src/lib/terminalApi.tscreate 用 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无一个 socketterminal id 在每个帧内部全局事件 WebSocket 与 SSEpackages/ui/src/sync/event-pipeline.tsn/a所有目录共用一条流每个事件自带 directory 字段Dev tunnelpackages/ui/src/lib/browser/devTunnel.ts#L39-L40无仅?portDev 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?portdevTunnel.ts服务器连接127.0.0.1:portserver/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 的成本每一条都在源码中得到印证要读四处位置。Dispatcher 必须同时读 querydirectory/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 意味着“先解析再代理再重新序列化”这已经不是“整体转发”了。终端 socket 无法整体转发。一个 socket 同时承载 Host 与 space 终端的帧帧格式见 packages/ui/src/lib/terminalApi.ts#L11-L14attach/write帧内s: sessionId。Dispatcher 必须按 terminal id 拆分帧并维护一个 terminal→space 索引。session/terminal 键路由需要 id→space 的 Host 索引。这个索引本质是“不可信空间数据的缓存”一旦过期或缺失Dispatcher 只能猜恶意 space 还能冒用别的 space 的 session id。dev tunnel 与/api/dev-servers没有任何可嗅探的东西。它们需要新增参数——而那本身就是方案 B。歧义案例成堆。文件 API 的 header 是环境目录可能与path不一致packages/web/src/api/files.ts#L64-L67x-opencode-directory的编码方式有三种——SDK 侧 URI 编码、files API 原始编码、非 Latin-1 字符由 packages/ui/src/lib/runtime-fetch.ts#L131-L157 做标记编码补x-opencode-directory-encoding: uri而且没有目录时 Host 会静默回退到settings.lastDirectorypackages/web/server/lib/opencode/project-directory-runtime.js#L91-L103所以“没有目录”等于“上次浏览过什么就用什么”——这是方案 A 最致命的一条一个空间路径在 Host 上根本不存在validateDirectoryPathpackages/web/server/lib/opencode/project-directory-runtime.js#L36会先 stat 目录space 请求必须在它之前被转发DESIGN.md。需要嗅探的挂载点散落四处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或什么都不返回。它只应用在三个地方runtimeFetch接受directory选项在buildRuntimeFetchUrlpackages/ui/src/lib/runtime-fetch.ts#L95和extractRelayPathpackages/ui/src/lib/runtime-fetch.ts#L184之前把/api/x重写为/api/spaces/id/x。网络分支与 relay 分支必须使用同一条被重写后的路径。SDK fetch 包装器packages/ui/src/lib/opencode/client.ts#L290-L355读取 SDK 自己放到请求上的目录GET 用?directory其余用x-opencode-directory。这是SDK 契约而不是猜测因此作用域客户端与约 40 个逐调用传目录的站点零改动自动覆盖。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.buildUrl68 处调用一处编辑packages/ui/src/lib/gitApiHttp.ts#L135-L144files.ts11 处调用从path推导目录packages/web/src/api/files.ts#L64-L67terminalApi.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-L28tunnel 原样传递pathname?searchpackages/ui/src/lib/relay/tunnel-payloads.ts#L32-L36shouldResolveApiPath接受/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-L40ALLOWED_WS_PATHStunnel-host.js#L30-L40isUrlAuthWebSocketPathserver/lib/ui-auth/ui-auth.jsisUrlAuthReadableHttpPath为/api/spaces/id/fs/raw准备ui-auth.jsElectron 实时代理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/reposymlink、规范路径未验证——整个 UI 映射依赖这一点packages/ui/src/stores/globalSessionStructure.ts。150 个调用点数量是按文件统计的估算不是全量审计守卫 2 的存在就是为了兜住漏网之鱼。SDK 的/api/session/...风格端点 GET 时用location[directory]SDKclient.js未逐一核对 UI 用非 GET 方法且不带 header 调用了哪些。重写带流式 body 的Request需要duplex: halfpackages/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.mdgit 走 exec 控制通道。整个特性的阶段计划见 STAGES.md测试规范见 TESTING.md。赞分享AI Agent人工智能代码智能体交互助手【免费下载链接】openchamberAgentic Development Environment based on OpenCode AI agent项目地址https://gitcode.com/gh_mirrors/op/openchamber点击查看免费下载相关推荐LMMS完全指南免费开源音乐制作软件终极教程LMMS完全指南免费开源音乐制作软件终极教程 还在为昂贵的音乐制作软件而犹豫吗LMMS这款跨平台免费开源数字音频工作站让你零成本开启专业音乐创作之旅。作为桌面应用音频处理MOSS-VL-Base-0708环境配置指南在Linux系统上部署11B参数模型的完整步骤MOSS VL Base 0708环境配置指南在Linux系统上部署11B参数模型的完整步骤 MOSS VL Base 0708是OpenMOSS生态系统中用Paper2GUI 前端框架为什么选择Naive UI而非Element PlusPaper2GUI 前端框架为什么选择Naive UI而非Element Plus Paper2GUI作为一个致力于将AI论文转化为图形界面GUI的开源项人工智能AI 应用桌面应用上一篇FinalBurn Neo终极指南开启你的复古街机游戏之旅下一篇claw-code 插件子系统完全指南从 .claude-plugin/plugin.json 清单解析到 Hook 执行、权限与生命周期管理创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进