A composition is a file, but "edit it on the filesystem" is not a browser affordance. The roster gains `read`/`write`/`remove` beside `select`, and the browser gains a settings section over them: the presets as rows, one composition open in a YAML editor at a time, and per-row default, duplicate, and delete. All four authoring methods are loopback-pinned. A composition names the plugins a session runs, so reading one is reconnaissance, writing one is arbitrary capability, and selecting one can move a session onto a preset that edits the live runtime. `agentPreset.list` deliberately stays ordinary and now reports `authorable`, so a surface knows whether creating is possible at all rather than offering a button whose save always fails. Authoring starts by duplicating: a shipped preset opens read-only because the deployment's copy is what a broken local one is compared against. Ids are contained before they become directory names, and the text is parsed with the loader's own schema, so a save cannot leave a file no session could load. Fixes a defect the real-composition test found: a preset written under the user's home could never mount, because the loader resolves a row against the composition's own directory and Node's `node_modules` walk from there never reaches the installed harness. The mount now records the host base and sends bare specifiers there, leaving relative paths resolving from the preset. Also closes the coverage the earlier surfaces in this stack shipped without — the General row, the composer seat, and the plugin halves now have tests.
4.8 KiB
@deepseek-ai/dsh-client-connection
English | 中文
协议消费层:客户端插件的 apply 会挂载 ctx.connection(共享 API 客户端 + 当前页面的 loopback 状态 + 单消费方流循环启动器);导出表层携带协议契约类型、AbstractApiClient seam,以及循环的 sink/配置类型。浏览器载体以 HTTP POST 发送 unary/respond,并为 events.mux 与 events.host 各开一条只下行的 WebSocket;进程内载体满足同一双流抽象。Loopback hostname 判定逻辑留在包内部:/api Host fence 与 WebSocket upgrade 会直接使用它,其他客户端插件则消费派生的 ctx.connection.isLoopback 状态。node 半侧的 /api 路由让特权方法集(host.pickDirectory、host.openPath,以及整个配置面——settings.describe/openDocument/update/replace/mutate 与 credentials.describe/set/unset;读取与原生操作也在内,因为 describe 会返回已暴露的配置、打开操作会作用于 Host 桌面,而探测任意引用会报出某条凭据来自何处——以及 agent preset 的创作面 agentPreset.select/read/write/remove,因为组装指明了一个会话所运行的插件,读取它是侦察,写入它是任意能力,而选择它可以把会话切到一个能编辑活动运行时的 preset 上;agentPreset.list 不在其中,它只携带 id 与信任级别)以空信任表过信任 fence,从而钉在回环——已声明的 trustedHosts 授权可达其余全部方法,而这些方法在真正的认证层出现之前仍只限回环本机。平台载体与 ConnectionController 循环属于包内部;apply 负责选择并驱动它们。下行边界见 WebSocket 下行载体 Agent Note;协议契约见 api-contracts v3 §3。
/api 浏览器信任栅栏
node 半侧在桥接或 upgrade 前守卫 /api 下的每个入口(src/api-request-trust.ts)。每个请求——无论是否带浏览器标记——Host 都必须是回环地址权威,或与某个 trustedHosts 条目匹配:带端口的 host:port 条目精确匹配,不带端口的条目匹配任意端口,两侧均经 WHATWG 归一化后比较(DNS rebinding 防御)。刻意不为无浏览器标记的 HTTP 请求开捷径:明文 HTTP 下浏览器的图片与导航读取既不带 Origin 也不带 Fetch-Metadata,因此无标记请求仍可能是被重绑页面发起的、响应可被读走的读取,而 Host 是重绑唯一伪造不了的请求头;WebSocket 浏览器握手会带 Origin 并通过同一道比较。非浏览器客户端经由回环地址、CLI 推导的 LAN IP 字面量或已声明的权威通过同一道栅栏。当标记存在时,Origin 必须与 Host 权威完全一致;显式的 sec-fetch-site: cross-site 标记一律拒绝。不是纯的、规范形 host[:port] 权威的 trustedHosts 条目——即 WHATWG 解析读回后与原文不完全一致的——会让插件加载大声失败:否则解析会悄悄授权 harness.internal/path 这类笔误里的 hostname,或把悬空冒号、补零端口放大成任意端口授权。HTTP 失败在任何 RPC 分发之前以纯 403 应答,upgrade 失败在启动任何 event stream 前拒绝握手。因此非回环(--host 0.0.0.0)部署需要让自己的服务权威被信任:dsh CLI 会自行推导本机的 LAN IP 字面量,其 --trusted-host flag 用于声明具名权威,所以 cordis.yml 中的 trustedHosts 面向 CLI 不参与引导的组合。这道栅栏是可达性策略,而不是认证;Web 载体不提供认证层。决策记录:api 浏览器信任边界 Agent Note。
/api WebSocket 下行
/api/events.mux 与 /api/events.host 各接受一条 WebSocket upgrade,并只向浏览器发送对应的 ServerRequest text message;客户端不会在这些 socket 上发送业务数据。任一 socket 结束都会使当前 connection generation 失败并重建两条流,连接就绪仍要求两条 socket open 且 host.describe HTTP 调用成功。Host teardown 会终止两条 socket、中止各自的 source,并等待 source 清理完成后再返回。普通网络 GET 这些路径会返回 426,不保留 SSE 回退;toFetchHandler 的 SSE 编解码只服务进程内同构载体。
模型体验
无。协议消费层只在浏览器与主机之间搬运已经组合好的消息;这里没有任何内容进入模型请求。
KV Cache 影响
无;该包既不组装也不发送提供方请求。
已知限制与暂缓事项
- History 会恢复未附加的会话:打开 history 可能创建宿主侧 agent,并增加首次打开的延迟;没有仅从持久化读取的路径。