dsh-zen-remote 2.3.3 → 2.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/docs/plugin-bridge.md +14 -7
  4. package/docs/remote-access.en.md +8 -8
  5. package/docs/remote-access.md +10 -10
  6. package/lib/adapters/dsh-better-sidebar/index.js +21 -8
  7. package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
  8. package/lib/adapters/dsh-better-sidebar/surface.js +79 -0
  9. package/lib/adapters/dsh-better-sidebar/surface.js.map +1 -0
  10. package/lib/client.js +1 -1
  11. package/lib/index.js +103 -10
  12. package/lib/index.js.map +1 -1
  13. package/lib/intercept.js +23 -1
  14. package/lib/intercept.js.map +1 -1
  15. package/lib/merge-streams.js +29 -2
  16. package/lib/merge-streams.js.map +1 -1
  17. package/lib/plugin-adapter-core.js +69 -13
  18. package/lib/plugin-adapter-core.js.map +1 -1
  19. package/lib/plugin-bridge.js +179 -12
  20. package/lib/plugin-bridge.js.map +1 -1
  21. package/lib/plugin-route-intercept.js +631 -79
  22. package/lib/plugin-route-intercept.js.map +1 -1
  23. package/lib/plugin-tunnel.js +84 -12
  24. package/lib/plugin-tunnel.js.map +1 -1
  25. package/lib/relay-server.js +65 -47
  26. package/lib/relay-server.js.map +1 -1
  27. package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
  28. package/lib/types/adapters/dsh-better-sidebar/surface.d.ts +23 -0
  29. package/lib/types/adapters/dsh-better-sidebar/surface.d.ts.map +1 -0
  30. package/lib/types/adapters/types.d.ts +14 -3
  31. package/lib/types/adapters/types.d.ts.map +1 -1
  32. package/lib/types/index.d.ts +2 -2
  33. package/lib/types/index.d.ts.map +1 -1
  34. package/lib/types/intercept.d.ts +19 -0
  35. package/lib/types/intercept.d.ts.map +1 -1
  36. package/lib/types/merge-streams.d.ts +12 -0
  37. package/lib/types/merge-streams.d.ts.map +1 -1
  38. package/lib/types/plugin-adapter-core.d.ts +24 -11
  39. package/lib/types/plugin-adapter-core.d.ts.map +1 -1
  40. package/lib/types/plugin-bridge.d.ts +93 -5
  41. package/lib/types/plugin-bridge.d.ts.map +1 -1
  42. package/lib/types/plugin-route-intercept.d.ts +52 -3
  43. package/lib/types/plugin-route-intercept.d.ts.map +1 -1
  44. package/lib/types/plugin-tunnel.d.ts +27 -3
  45. package/lib/types/plugin-tunnel.d.ts.map +1 -1
  46. package/lib/types/relay-server.d.ts +2 -25
  47. package/lib/types/relay-server.d.ts.map +1 -1
  48. package/lib/types/upload-limits.d.ts +67 -0
  49. package/lib/types/upload-limits.d.ts.map +1 -0
  50. package/lib/types/wizard/proxy-snippets.d.ts +4 -3
  51. package/lib/types/wizard/proxy-snippets.d.ts.map +1 -1
  52. package/lib/upload-limits.js +67 -0
  53. package/lib/upload-limits.js.map +1 -0
  54. package/lib/wizard/proxy-snippets.js +5 -4
  55. package/lib/wizard/proxy-snippets.js.map +1 -1
  56. package/package.json +2 -2
  57. package/scripts/check-upload-limits.mjs +167 -0
  58. package/src/adapters/dsh-better-sidebar/index.ts +21 -9
  59. package/src/adapters/dsh-better-sidebar/surface.ts +90 -0
  60. package/src/adapters/types.ts +14 -3
  61. package/src/client-data/wizard.ts +1 -1
  62. package/src/index.ts +99 -12
  63. package/src/intercept.ts +47 -1
  64. package/src/merge-streams.ts +41 -1
  65. package/src/plugin-adapter-core.ts +69 -14
  66. package/src/plugin-bridge.ts +228 -14
  67. package/src/plugin-route-intercept.ts +677 -82
  68. package/src/plugin-tunnel.ts +95 -13
  69. package/src/relay-server.ts +60 -48
  70. package/src/upload-limits.ts +69 -0
  71. package/src/wizard/proxy-snippets.ts +5 -4
package/README.md CHANGED
@@ -66,7 +66,7 @@ dsh plugin --profile web add dsh-zen-remote
66
66
  // ~/.dsh/profiles/desktop/package.json
67
67
  {
68
68
  "dependencies": {
69
- "dsh-zen-remote": "^2.3.3" // for local development: "link:/path/to/dsh-zen-remote"
69
+ "dsh-zen-remote": "^2.3.4" // for local development: "link:/path/to/dsh-zen-remote"
70
70
  }
71
71
  }
72
72
  ```
package/README.zh-CN.md CHANGED
@@ -66,7 +66,7 @@ dsh plugin --profile web add dsh-zen-remote
66
66
  // ~/.dsh/profiles/desktop/package.json
67
67
  {
68
68
  "dependencies": {
69
- "dsh-zen-remote": "^2.3.3" // 本地开发换成 "link:/path/to/dsh-zen-remote"
69
+ "dsh-zen-remote": "^2.3.4" // 本地开发换成 "link:/path/to/dsh-zen-remote"
70
70
  }
71
71
  }
72
72
  ```
@@ -54,10 +54,16 @@ export function apply(ctx: Context) {
54
54
  | `sessionParam` | 可选,缺省 `'sessionId'`;须匹配 `/^[A-Za-z][A-Za-z0-9_]{0,63}$/`——你的接口用别的名字接会话 id 时在这里声明 |
55
55
  | `stream` | 可选,缺省 `false`;标记该路由以流式响应回答 |
56
56
  | `methods` | 可选,缺省 `['GET']`;大写 HTTP 方法数组,只接受 `GET`、`HEAD`、`POST`、`PUT`、`PATCH`、`DELETE`,去重并按字典序排序后进规范化与摘要(2.2 起,写请求) |
57
- | `maxBody` | 可选,缺省 `1048576`(1 MiB);请求体上限(字节),正整数,最高 64 MiB(2.2 起) |
57
+ | `maxBody` | 可选,缺省 `1048576`(1 MiB);请求体上限(字节),正整数,最高 500 MiB(与中继上传上限同源单一定义,PB-20 起) |
58
58
  | `upgrade` | 可选;置 `true` 标记该路由是 WebSocket 升级端点,远程会话里经字节隧道转发(2.2 起,见下文「WebSocket 路由」)。只能为 `true` 或省略;不能与 `stream` 同用;升级路由只有 GET——显式 `methods` 里出现其他方法登记即报错 |
59
+ | `locator` | 可选(2.4 起);会话 id 在请求里的位置,替代 `sessionParam`(两者同写登记即报错):`{ in: 'query', name }` 指定查询参数;`{ in: 'path-segment', index }` 数声明路径之后的第 index 段(0 起);`{ in: 'json-body', fields }` 取 JSON 体的顶层字段(可列多个,出现几个算几个)。`path-segment` 必须配 `prefix: true`;不写 `locator` 时 `sessionParam` 语义原样保留 |
60
+ | `prefix` | 可选(2.4 起);置 `true` 时命中 `path` 本身或其任意子路径(`path/…`,按未解码的原始路径名判断,与适配器同一条规则)。不能与 `upgrade` 同用 |
61
+ | `deny` | 可选(2.4 起);置 `true` 标记路由的数据只在本机有意义:远程会话里命中即拒——请求带虚拟会话 id,或浏览器当前打开的就是远程会话——统一 503 `route-denied`,字节隧道路由同样生效;本地会话完全不受影响 |
62
+ | `rewriteIds` | 可选(2.4 起);两种形态,都把远程转发回来的 JSON 响应缓冲(上限 8 MB,超限原样直通、非 JSON 不动)后再改写。`true`(回显型):对解析后的响应做深扫,凡与「**请求自带的**真实会话 id ∪ 子客户端已知的会话清单」里任一真实 id 确切相等的字符串,换回虚拟形态交还浏览器(2.4 后期扩源,PB-25b:已知清单是本机合并流维护的会话集合——侧边栏当前以 `zr~` 形态持有的每个真实 id,subagents.live 的对象键、workflows.list 的行这类「不在本请求里但在视野里」的 id 也按值改写;清单由各合并流**替换式**上报后取并集,流刷新即失效,两次刷新之间新出现的 id 要等下一次刷新才生效;改道层只读消费,绝不反向修改合并状态)。`{ fields: ['value.childId', …] }`(创建型):响应会诞生**请求与已知清单都从未携带**的全新会话 id(如 sidechat.start 新建的 childId)的创建类路由用这个形态——按位置声明那些 id 在响应里的点路径,每段非空、仅 `[A-Za-z0-9_-]`、不含通配符与数组下标(数组元素场景归按值深扫);改写时逐条路径取值,路径不存在或中途穿过数组、标量即跳过,取到非空且还不是虚拟形态的字符串就地包上 `zr~<服务端>~<id>` 外壳。按值与按位并存,同一字符串只改一次。**`true` 的边界**:请求与已知清单都不含的真实会话 id 仍会原样到达浏览器(浏览器拿真实 id 发后续请求会被判本地、拿到空数据)——那正是 `fields` 形态要接管的场景。`true` 与 `{ fields }` 是不同声明、摘要不同,且 **`fields` 数组的顺序也进摘要**(`["a","b"]` 与 `["b","a"]` 摘要不同);既有声明(含 `true`)摘要逐位不变。不能与 `stream` 同用 |
59
63
 
60
- 声明里出现未知字段直接报错(防拼错)。`methods`、`maxBody` 与 `upgrade` 只在**显式给出**时进摘要——2.1 时代(没有这三个字段)的声明,摘要与当年完全一致,升级不破坏已桥接状态。2.1 只匹配 **exact 路径**:声明的 `path` 与请求 pathname 完全相等才算命中,把 `path` 写成前缀不会覆盖子路径。
64
+ 声明里出现未知字段直接报错(防拼错)。`methods`、`maxBody`、`upgrade` 与 2.4 的四个字段(`locator`/`prefix`/`deny`/`rewriteIds`)只在**显式给出**时进摘要——2.1 时代(没有这些字段)的声明,摘要与当年完全一致,升级不破坏已桥接状态。2.1 只匹配 **exact 路径**;2.4 起声明 `prefix: true` 的路由额外命中其子路径(见上表)。
65
+
66
+ 两个字段同写时 `deny` 优先:远程命中的请求先被 `route-denied` 拒下,`rewriteIds` 的改写不可达(本地请求两边都不触发,响应原样交回)。`upgrade` 路由上的 `rewriteIds` 在隧道半边被忽略——隧道搬运的是不透明字节,无从改写;同一声明路由被当作普通 HTTP 请求命中时照常生效。
61
67
 
62
68
  ## 你的插件要守的约定
63
69
 
@@ -91,12 +97,13 @@ content-type: application/json; charset=utf-8
91
97
  | `server-changed` | 会话 id 指向另一台服务端(切换服务端后页面未刷新) |
92
98
  | `bad-query` | 会话参数重复,或虚拟会话 id 解析失败(浏览器侧缺失 / 为空的请求走本地放行,到不了 503) |
93
99
  | `method-not-bridged` | 请求方法不在该路由声明的 `methods` 内(未声明即只转发 GET) |
100
+ | `route-denied` | 路由声明了 `deny: true`:远程会话(或浏览器当前打开的就是远程会话)命中即拒,本机数据不进远程面板;本地会话不受影响 |
94
101
  | `not-shared` / `busy` / `plugin-unknown` / `plugin-mismatch` / `route-undeclared` / `loopback-auth` / `internal` | 服务端中继路由的对应拒绝原样透传(会话未共享 / 该设备并发超限 / 服务端无此登记 / 版本或摘要不符 / 路径未声明 / 服务端回环鉴权失败 / 服务端内部错误) |
95
102
 
96
103
  ## 限制
97
104
 
98
105
  - 2.1 只转发 **GET**;2.2 起路由可声明 `methods` 开放写方法(POST 等),请求体经中继流式转发,受该路由 `maxBody` 约束(缺省 1 MiB)——超限在本地即回答 413,不落本机、不到服务端。
99
- - 只匹配 **exact 路径**;前缀声明不生效。
106
+ - 2.1 只匹配 **exact 路径**;2.4 起声明 `prefix: true` 的路由额外命中其子路径(见「声明字段表」)。
100
107
 
101
108
  ## WebSocket 路由(2.2 起)
102
109
 
@@ -123,18 +130,18 @@ content-type: application/json; charset=utf-8
123
130
  - **远程判定**:定位到的会话 id 全是 `zr~` 虚拟 id → 远程转发;全不是(或一个都没定位到)→ 本地放行;**混合 → 503 `bad-query`**,绝不落本机。
124
131
  - **两个钩子(可选)**:子客户端侧 `client.request`(拒绝或改写 JSON 体,如在转发前拒绝 `open.external`)与 `client.response`(把响应里的真实会话 id 换回虚拟 id,JSON 响应缓冲上限 8 MB,超限原样直通);主服务端侧 `server.request`(回环前拒绝或剥字段,如剥掉 `cwd`)。
125
132
 
126
- 登记规则:适配器按宿主 `pluginPackages` 服务探测目标插件的**已装版本**,版本不在适配器声明的版本清单里就不登记;登记进与协作声明同一张注册表(版本 = 已装版本,摘要 = 适配器路由表的规范化哈希),握手同一个 `plugins` 字段,整机数据的同意开关逐插件生效(默认关,设置页条目标注「由 zen-remote 适配」)。若某插件既有适配器、自己又登记了协作声明,**以插件自己的声明为准**(适配器让位,记一条日志)。
133
+ 登记规则:适配器按宿主 `pluginPackages` 服务探测目标插件的**已装版本**,版本不在适配器声明的版本清单里就不登记——唯一例外(PB-19):接线层会读已装插件的 `lib/index.js`,用该适配器的纯函数 surface 抽取器把 wire 面还原成摘要(16 位十六进制),摘要命中适配器 `surfaces` 表里的某一代,就按已装版本登记(注册摘要仍是路由表哈希,含版本号)。上游发了 wire 不变的补丁版(如 0.26.1、0.28.2)时 zen-remote 不用发版即可桥接;抽取失败、表不命中都按未登记版本拒绝,只会更严不会更松。登记进与协作声明同一张注册表(版本 = 已装版本,摘要 = 适配器路由表的规范化哈希),握手同一个 `plugins` 字段,整机数据的同意开关逐插件生效(默认关,设置页条目标注「由 zen-remote 适配」)。若某插件既有适配器、自己又登记了协作声明,**以插件自己的声明为准**(适配器让位,记一条日志)。
127
134
 
128
135
  ## 开发一个适配器
129
136
 
130
- 先确认目标插件真的需要适配器:能让作者加几行 `zenRemote.bridge(...)` 就优先走协作声明——适配器只在第三方插件不配合时使用。当前唯一在册的适配器是 **`dsh-better-sidebar`(0.25.0、0.26.0、0.26.1、0.28.0-alpha.0 与 0.28.0 五版)**(`src/adapters/dsh-better-sidebar/`),新适配器照它写。
137
+ 先确认目标插件真的需要适配器:能让作者加几行 `zenRemote.bridge(...)` 就优先走协作声明——适配器只在第三方插件不配合时使用。当前唯一在册的适配器是 **`dsh-better-sidebar`(0.25.0、0.26.0、0.26.1、0.28.0-alpha.0、0.28.0、0.28.1 与 0.28.2 七版)**(`src/adapters/dsh-better-sidebar/`),新适配器照它写。
131
138
 
132
139
  步骤:
133
140
 
134
141
  1. **摸清接口面**:读目标插件的构建产物(`~/.dsh/profiles/<profile>/node_modules/<包>/lib/`),列出全部 HTTP 路由与 WebSocket:每个路由的方法、会话 id 在哪个字段(query / 路径段 / JSON 体)、读还是写、响应里有没有会话 id、有没有不带会话字段的调用。每个结论带行号。带会话 id 的请求体会不会另外携带本机路径(如 `cwd`)也要核实——主服务端要按会话自己解析,客户端传来的路径必须剥掉。
135
- 2. **写适配器**:`src/adapters/<插件名>/` 一个目录,`versions` 只写核对过的确切版本;路由表里**表外方法一律拒绝**(宁可少转发,不可误转发);需要在「无会话字段」的情况下按当前会话拒绝的入口(如 better-sidebar 的 `open.external`)用 `deny` 钩子 + 浏览器上报的当前会话信号。适配器一律按整机数据对待(主人逐插件同意,默认关)。
142
+ 2. **写适配器**:`src/adapters/<插件名>/` 一个目录,`versions` 只写核对过的确切版本;wire 不变的补丁版想让 zen-remote 不发版就放行,再维护一张 `surfaces` 表(用该适配器的 surface 抽取器对各核对版本算摘要,键 = 摘要,值 = 注明来源代际);路由表里**表外方法一律拒绝**(宁可少转发,不可误转发);需要在「无会话字段」的情况下按当前会话拒绝的入口(如 better-sidebar 的 `open.external`)用 `deny` 钩子 + 浏览器上报的当前会话信号。适配器一律按整机数据对待(主人逐插件同意,默认关)。
136
143
  3. **登记**:`src/adapters/index.ts` 的 `ADAPTERS` 加一行,这是整个目录与框架之间的唯一接线。
137
144
  4. **测试**:方法表与目标插件源码集合的对照测试(源码缺失时 skip)、每种定位的改写、响应改写、版本漂移不登记、e2e 至少读写各一条全链路。可插拔的静态扫描与空表回归由 `test/adapters-pluggable.test.cjs` 自动覆盖新适配器。
138
145
  5. **文档**:两份 README 的兼容清单、`docs/interface.md`、安全边界(整机数据时写明等于什么权限)同步更新。
139
146
 
140
- 路由匹配:协作声明只支持 **exact 路径**;适配器路由额外支持**前缀匹配**(`match: 'prefix'`,会话 id 在路径段时按段定位)。
147
+ 路由匹配:2.4 起协作声明与适配器同规则——exact 与 `prefix: true` 前缀都支持,query / 路径段 / JSON 体三种定位两半共用 adapter-core 同一组函数;剩下的真实差别都在适配器一侧:`local: true` 的纯本机路由(声明没有这个字段)、按请求判定的 `deny` 钩子(声明侧是静态 `deny: true` 标记,按「远程上下文」整体拒绝)、`client.request` / `client.response` / `server.request` 三个钩子(声明侧只有 `rewriteIds` 的请求局部改写)、以及多版本 `versions` 清单加 `surfaces` 表(协作声明一次只登记一代)。
@@ -221,7 +221,7 @@ sudo cloudflared service install # then install it as a service
221
221
  - **`Error 1033` / tunnel offline**: `cloudflared` isn't running, or the token expired. `cloudflared tunnel info <name>` shows the connection count; `sudo launchctl list | grep cloudflared` (macOS) or `systemctl status cloudflared` (Linux) shows the service.
222
222
  - **502 Bad Gateway**: the tunnel is up but the origin isn't reachable — either the gateway isn't running, or the Public Hostname URL says `3080` (DSH itself) instead of `3088` (the gateway). Pointing it at 3080 bypasses the entire pairing wall, i.e. **puts DSH bare on the internet**; make sure it says 3088.
223
223
  - **The conversation stream stalls / messages never appear**: WebSockets aren't getting through. Cloudflare's free tier supports them, so usually there's an Access policy on that hostname in Zero Trust blocking the upgrade request, or `disableChunkedEncoding` in the ingress rule.
224
- - **Large uploads fail**: Cloudflare's free tier caps a request body at 100MB. This plugin's 20MB upload default sits below that, so you shouldn't hit it; for genuinely larger files, raise the Cloudflare plan before raising `maxUploadBytes`.
224
+ - **Large uploads fail**: the relay channel's attachment upload cap is 500 MiB, which is past Cloudflare's free-tier per-request body limit of 100MB — through a CF Tunnel, uploads over 100MB hit Cloudflare's wall first (HTTP 413), no matter how the plugin is configured; raise the Cloudflare plan or route large uploads around CF.
225
225
  - **Want one more layer**: you can add an Access policy for this hostname in Zero Trust → Access (email OTP or similar) stacked in front of the pairing code. Not required — the pairing wall is already an authentication step.
226
226
 
227
227
  #### Post-setup self-check (do this for any proxy)
@@ -318,7 +318,7 @@ A desktop client has no DSH login cookie, and the relay deliberately does not fo
318
318
  | `/v1/stream` | POST | Opens one DSH stream subscription. **NDJSON format**: one JSON value per line; `{"type":"ping"}` heartbeat lines (every 15s by default) keep reverse proxies from timing idle streams away, and each one carries the current `serverName` / `deviceName` (a server rename reaches every connected client within one heartbeat); when a share closes or a subscription loses its session the server writes `{"type":"error","error":{"code":"unshared"}}` and ends the response |
319
319
  | `/v1/event-result` | POST | The answer half for approval/question events (`{eventId, result}`); the server matches it against what it forwarded, per device, before handing it over |
320
320
  | `/v1/http` | POST | The plain-HTTP long tail of the changes summary / diff panels: the `{route, query}` body admits only `changes.summary` / `changes.diff` and forwards GETs only (the query string is this call's coordinate, never a request body to forward); the query is **normalized and rebuilt** along an allowlist — only `sessionId` (exactly once, share-checked) and `seq` / `index` (at most once each, decimal non-negative integers) survive, unknown parameters are dropped, and the synthetic URL is composed from the normalized result alone (a control-character parsing differential between the two URL parsers was once an authorization bypass) — then dispatched **in-process** through the host's shared `/api` fetch handler, no loopback HTTP |
321
- | `/v1/upload` | POST | The **non-image attachment** upload of a remote session: the request body is a raw byte stream (not JSON); the query string is normalized and rebuilt along an allowlist — only `sessionId` (exactly once, share-checked, 403 when not shared) and `name` (at most once) survive; the cap is 100 MiB, enforced twice (a `Content-Length` pre-check plus a counting pump over the stream), over-limit answers 413 (the refusal is written before the tail bytes are drained); at most 8 concurrent uploads per device (429 past that); then, like `/v1/http`, dispatched **in-process** through the host's shared `/api` handler — the staged receipt lands under the same session the later `session/prompt` resolves the attachment against |
321
+ | `/v1/upload` | POST | The **non-image attachment** upload of a remote session: the request body is a raw byte stream (not JSON); the query string is normalized and rebuilt along an allowlist — only `sessionId` (exactly once, share-checked, 403 when not shared) and `name` (at most once) survive; the cap is 500 MiB, enforced twice (a `Content-Length` pre-check plus a counting pump over the stream), over-limit answers 413 (the refusal is written before the tail bytes are drained); at most 8 concurrent uploads per device (429 past that); then, like `/v1/http`, dispatched **in-process** through the host's shared `/api` handler — the staged receipt lands under the same session the later `session/prompt` resolves the attachment against |
322
322
  | `/v1/unshare` | POST | The client closes a session's remote access itself (table members only) |
323
323
 
324
324
  **The allowlist** (everything else is `forbidden-method`): session reads and writes (`session/follow`, `session/page`, `session/prompt`, `session/cancel`, `session/rename`, `session/selectModel`, `session/updateQueue`, `session/attachment`, `session/projections`), creation and forking (`session/create`, `session/fork` — results auto-share), subagents (`subagents/prompt`, `subagents/interruptByParent`, judged by the parent), attachments and @ references (`fileUploads/upload`, `fileReferences/list`, `sessionReferenceResolver/candidates`), the panel long tail (`goals/*` five verbs, `commands/list|execute`, `agentPresets/select`, `sessionFeedback/record`), file tree and previews (`workspaceFiles/list|changes|read|readBytes|stat`), terminals (`terminal/*`), jobs (`job/list|follow|kill`), `skills/list`, message feedback (`messageFeedback/*`), `schedule/list`, workspace session ops (`workspace/pinSession` and friends), plus four global reads: the `session/control` stream, `session/list`, the `workspace/follow` stream and `session/modelCatalog`. The first three carry no session argument — their safety is **output filtering**: every frame / every result row is narrowed by the share table before it is written, so an unshared session's row never reaches the client; in `session/list` result rows, a `parentSessionId` naming an inaccessible session is deleted as well (the rest of the row passes through unchanged). `session/modelCatalog` is likewise parameter-less and globally read-only — it is the server's model-provider directory, group and model ids plus display names only, no session data; the server empties the `failures` array in the result (the host's per-group error texts may carry endpoint or credential details the client never shows anyway). `sessionReferenceResolver/candidates` (the @-mention resolver) answers with EVERY server session's title, cwd and a ready-made mention, so it travels through the same row-level filtering — only shared sessions' rows leave the box. Approval/question events ride a `$zr/events` subscription, each forwarded event judged by its `agentId`.
@@ -334,8 +334,8 @@ The `/v1/http` route is a second allowlist, keyed by **route name** (not part of
334
334
  - `dsh-session:` references are scanned too: DSH injects the referenced session's content along **messages** (the content text blocks of `session/prompt` / `subagents/prompt`), **queue edits** (`session/updateQueue`'s edit content) and **slash commands** (EVERY string of a `commands/execute` call — a command handler steers its raw input in as the next user message), with no access check of its own at injection time, so a call whose reference names an unshared session is refused server-side with 403; the @-mention candidates (`sessionReferenceResolver/candidates`) likewise only ever list shared sessions' rows;
335
335
  - Terminals are deliberate (a desktop client is a trusted device): what opens is a **server-side** PTY running as the server user, without the agent sandbox or approval restrictions; closing the session's remote access kills its terminal streams with it;
336
336
  - `workspaceFiles/read` and friends are not contained to the session directory (DSH itself does not contain them) — anything the server process can read is readable, under the same trusted-device premise as terminals;
337
- - The plugin bridge's whole-machine data requires the server owner's per-plugin consent — the toggles in the settings page's Plugin bridge section, everything OFF by default; the loopback carries the operator's cookie, so a bridged plugin sees the same permissions the local browser sees. The bridge's boundary is therefore the DECLARED route set (exact equality) plus the share table — there is **no** parameter-level scanning;
338
- - **Adapters (better-sidebar)**: beyond cooperative declarations, zen-remote ships a built-in adapter for `dsh-better-sidebar` **0.25.0, 0.26.0, 0.26.1, 0.28.0-alpha.0 and 0.28.0** (`src/adapters/` — non-cooperative third-party plugins are bridged by it, with prefix/path-segment route matching and session-id rewriting in JSON bodies and path segments). **Consenting to its whole-machine toggle hands the paired desktop client whole-disk file read/write on the server, git write operations (commit, push, revert, …), and model spend through commit-message generation** — the product decision is "both directions forward, no path fencing", and a server owner should treat that toggle as terminal-grade trust in the paired machine; `open.external` (launching processes on the host) refuses for remote sessions, `settings.*` and `/sidebar/bundle` stay on the sub-client's own machine; the two WebSockets (`/sidebar/ws/fs-watch` file-tree watch, `/sidebar/ws/agent-opens` open push) bridge over the byte tunnel (frames pass through untouched — the browser never reads the session id inside a push); side chat (`sidechat.*`) forwards — its child sessions carry `meta.parentSession`, so they inherit the parent's share through the ancestor chain, and the `childId` in `sidechat.start`'s answer is virtualized before it reaches the browser; 0.28's (alpha.0 and stable) `changes.ops` long-poll (`wait: true`, parks up to 25 s — past the relay's 15 s header wait) has `wait` stripped on the server, so a remote session's "this round's files" view polls on 0.26's 2.5 s cadence; 0.26's `ws/sidechat` push socket is **not** bridged — remote side chat falls back to 0.26's built-in 15s polling, correctness intact, only the push cadence degrades to polling; `sidechat.models` shows the **sub-client's own** model catalog in the picker while the choice goes through `sidechat.model` and takes effect on the **server** — when the two machines' catalogs differ, what the server actually applies is authoritative;
337
+ - The plugin bridge's whole-machine data requires the server owner's per-plugin consent — the toggles in the settings page's Plugin bridge section, everything OFF by default; the loopback carries the operator's cookie, so a bridged plugin sees the same permissions the local browser sees. The bridge's boundary is therefore the DECLARED route set (exact equality, plus a `prefix: true` route's sub-paths under the PB-23b raw-byte tail red line) plus the share table — there is **no** parameter-level scanning;
338
+ - **Adapters (better-sidebar)**: beyond cooperative declarations, zen-remote ships a built-in adapter for `dsh-better-sidebar` **the seven versions 0.25.0 through 0.28.2** (`src/adapters/` — non-cooperative third-party plugins are bridged by it, with prefix/path-segment route matching and session-id rewriting in JSON bodies and path segments). A prefix hit's tail passes the shape red line: `?`/`#`/`..` (encoded forms included) or whitespace answers 400 `bad-request` (PB-23b, the same rule as the cooperative segment). **Consenting to its whole-machine toggle hands the paired desktop client whole-disk file read/write on the server, git write operations (commit, push, revert, …), and model spend through commit-message generation** — the product decision is "both directions forward, no path fencing", and a server owner should treat that toggle as terminal-grade trust in the paired machine; `open.external` (launching processes on the host) refuses for remote sessions, `settings.*` and `/sidebar/bundle` stay on the sub-client's own machine; the two WebSockets (`/sidebar/ws/fs-watch` file-tree watch, `/sidebar/ws/agent-opens` open push) bridge over the byte tunnel (frames pass through untouched — the browser never reads the session id inside a push); side chat (`sidechat.*`) forwards — its child sessions carry `meta.parentSession`, so they inherit the parent's share through the ancestor chain, and the `childId` in `sidechat.start`'s answer is virtualized before it reaches the browser; 0.28's (alpha.0 and stable) `changes.ops` long-poll (`wait: true`, parks up to 25 s — past the relay's 15 s header wait) has `wait` stripped on the server, so a remote session's "this round's files" view polls on 0.26's 2.5 s cadence; 0.26's `ws/sidechat` push socket is **not** bridged — remote side chat falls back to 0.26's built-in 15s polling, correctness intact, only the push cadence degrades to polling; `sidechat.models` shows the **sub-client's own** model catalog in the picker while the choice goes through `sidechat.model` and takes effect on the **server** — when the two machines' catalogs differ, what the server actually applies is authoritative;
339
339
  - Concurrent streams are budgeted per device (32), and so are invokes kept in flight at once — past the per-device cap the invoke answers 429 `too-many-invokes`. Error codes shaped `*/internal` (`gateway/internal` and friends) are returned as the bare code with no message — that kind of message quotes server-side internals. Event answers are matched against the subscription and device they were forwarded to. Approval/question forwarding is **first come, first served**: whichever of the server's own UI and the sub-client answers first wins; the later answer is refused by the gateway and the client syncs to "already handled". The relay **never answers on the client's behalf**: an unshared session's approval/question events are not forwarded at all, and with the server's own UI offline too, the approval simply keeps waiting — the gateway re-delivers still-pending events to the next `$events` subscriber. A wait, never a rejection (an abstention would fail every pending approval of the session at once and break the push → wake → approve flow). Notification-type events (EMIT frames: session-list summaries, account expirations and other server-wide state) of an unshared session are never forwarded.
340
340
 
341
341
 
@@ -362,17 +362,17 @@ A bridged plugin's panel request, rerouted by the sub-client's local backend (th
362
362
  | `x-zr-query` | The query string (no `?`; the session parameter already carries the REAL id) |
363
363
  | `accept`, `range` | The only request headers passed through to the plugin |
364
364
 
365
- **Validation chain** (first failure answers, with the `x-zen-remote-error: <code>` header): gateway secret and markers → body must be empty → header shapes → the plugin is in THIS server's registry → version AND digest equal the local declaration → `x-zr-path` matches a declared route EXACTLY (exact-only in 2.1) → the query is parsed once and the route's declared `sessionParam` appears exactly once, non-empty → the session passes the share table (ancestor chain included) → a `scope:'server'` route requires `pluginBridgeConsent[plugin] === true` (read live off the row) → the per-device budget. On success the query is RE-SERIALIZED from the parsed result via `URLSearchParams` — what was checked is exactly what is sent.
365
+ **Validation chain** (first failure answers, with the `x-zen-remote-error: <code>` header): gateway secret and markers → body must be empty → header shapes → the plugin is in THIS server's registry → version AND digest equal the local declaration → `x-zr-path` matches the declared routes — an exact route by equality alone, a `prefix: true` route also by deeper sub-paths (since PB-23, the adapter segment's rule; multiple hits take the first in declaration order; `x-zr-path` is never decoded), with the prefix tail passing the shape red line (PB-23b, the SAME rule on the adapter segment): the tail after the declared path (RAW bytes) refuses `?`, `#`, `..` (any `%2e`/`%2E` encoding — the mixed `.%2e.` form included) and whitespace → 400 `bad-request`. The tail rides verbatim into the loopback request line: a `?` would break the parse-once query discipline, and whether `..` escapes the declared subtree depends on host URL normalization — a premise this red line refuses to rely on, so the verdict is on the raw bytes, never decode-then-check (`.` is RFC 3986-unreserved; no legitimate encoder emits `%2e`; `%252e` passing is the same design — it is the only legitimate encoding of a literal `%2e` filename, and with the host decoding at most once it resolves to an ordinary segment name) → the query is parsed once and the share wall reads the route's EFFECTIVE locator (since PB-24): the query convention (2.1's `sessionParam`, or an explicit `locator: { in: 'query' }`) requires exactly one non-empty occurrence, and that is the value the share table (ancestor chain included) judges; a v2 locator (path segment / JSON body) RE-LOCATES over the same adapter-core rule the sub-client classified with and share-checks EVERY hit (nothing located → 400 `bad-request`), with a JSON body consumed right there — buffered bounded by the route's `maxBody` (413 past it, 400 `bad-query` on a parse failure) and sent to the loopback as exactly that share-checked buffer → a `scope:'server'` route requires `pluginBridgeConsent[plugin] === true` (read live off the row) → the per-device budget. On success the query is RE-SERIALIZED from the parsed result via `URLSearchParams` — what was checked is exactly what is sent.
366
366
 
367
367
  **Error codes and statuses**:
368
368
 
369
369
  | code | status | condition |
370
370
  | --- | --- | --- |
371
371
  | `unsupported` | 501 | the bridge is not wired on this server |
372
- | `bad-request` | 400 | a body was sent; missing or malformed coordinate headers |
372
+ | `bad-request` | 400 | a body was sent; missing or malformed coordinate headers; a prefix hit's tail carrying `?`/`#`/`..` (encoded forms included) or whitespace (PB-23b) |
373
373
  | `plugin-unknown` | 404 | the plugin is not in the local registry |
374
374
  | `plugin-mismatch` | 409 | version or digest differs from the local declaration |
375
- | `route-undeclared` | 404 | the path matches no declared route exactly |
375
+ | `route-undeclared` | 404 | the path neither equals a declared exact route nor falls under a `prefix: true` route |
376
376
  | `bad-query` | 400 | the session parameter is missing, repeated, or empty |
377
377
  | `not-shared` | 403 | the session is not reachable |
378
378
  | `consent-off` | 403 | a whole-machine-data route without the owner's consent |
@@ -396,7 +396,7 @@ Routes declared `upgrade: true` travel over these three (the declaration side is
396
396
  | `/v1/plugin-tunnel/send` | `x-zr-tunnel` (the tunnel id from the open line), `x-zr-seq` (strictly increasing from 0) | Raw bytes, ≤ 64 KiB | `{"ok":true}` — sent only after the plugin's socket took the bytes (backpressure: a stalled plugin slows THIS send) |
397
397
  | `/v1/plugin-tunnel/close` | `x-zr-tunnel` | none | `{"ok":true}`, idempotent (an unknown id still answers ok) |
398
398
 
399
- The open route's validation chain is **entirely shared** with `/v1/plugin` (registry → version and digest → exact route → parse-once query → share table → consent wall; same order, same codes), plus one wall of its own: the route must ALSO be declared `upgrade: true` — behind an ordinary route there is no WebSocket endpoint to dial, so 404 `route-undeclared`. On success the relay dials the local web server ITSELF (net/tls, loopback only; self-signed https verification is relaxed only for the literal `127.0.0.1`), hand-writes the upgrade request from the four received headers plus the operator's cookie (no Origin), and pumps the socket's bytes into the NDJSON stream line by line:
399
+ The open route's validation chain is **entirely shared** with `/v1/plugin` (registry → version and digest → the declared-route match (exact / prefix) → parse-once query → share table → consent wall; same order, same codes), plus one wall of its own: the route must ALSO be declared `upgrade: true` — behind an ordinary route there is no WebSocket endpoint to dial, so 404 `route-undeclared` (`prefix` cannot be combined with `upgrade`, so prefix sub-paths never reach the tunnel). On success the relay dials the local web server ITSELF (net/tls, loopback only; self-signed https verification is relaxed only for the literal `127.0.0.1`), hand-writes the upgrade request from the four received headers plus the operator's cookie (no Origin), and pumps the socket's bytes into the NDJSON stream line by line:
400
400
 
401
401
  - The first line `{"type":"open","tunnel":"<32 hex chars>"}` is the client's signal to stop HTTP-ing and start reading; a dial failure does not drop the connection — it becomes an `end` line before the stream closes;
402
402
  - `{"type":"data","b64":"…"}` carries upstream bytes (base64), at most 48 KiB of **raw** bytes per line (base64 inflates by 4/3 — the cap is on the raw bytes);
@@ -133,9 +133,9 @@ server {
133
133
 
134
134
  # 必须加:nginx 默认 client_max_body_size 只有 1m,而中继的 JSON
135
135
  # invoke(远程会话带图消息,上限 32MiB)和附件二进制上传
136
- # (relay/v1/upload,上限 100MiB)都远超——缺了这条,文字消息正常、
136
+ # (relay/v1/upload,上限 500MiB)都远超——缺了这条,文字消息正常、
137
137
  # 一发图片就被掐断,子客户端界面报一个没有内容的错误(实测 2026-10-10)。
138
- client_max_body_size 128m;
138
+ client_max_body_size 512m;
139
139
  }
140
140
  }
141
141
  ```
@@ -238,7 +238,7 @@ sudo cloudflared service install # 通了再装成常驻服务
238
238
  - **`Error 1033` / 隧道离线**:`cloudflared` 服务没起来或 token 过期。`cloudflared tunnel info <名字>` 看连接数,`sudo launchctl list | grep cloudflared`(macOS)或 `systemctl status cloudflared`(Linux)看服务状态。
239
239
  - **502 Bad Gateway**:隧道通了但回源失败——网关没在跑,或 Public Hostname 的 URL 写成了 `3080`(DSH 本体)而不是 `3088`(网关)。写 3080 会绕开整个配对墙,**等于把 DSH 裸奔到公网**,务必确认是 3088。
240
240
  - **对话流卡住 / 消息不出来**:WebSocket 没通。Cloudflare 免费版支持 WebSocket,一般是 Zero Trust 里给这个 hostname 挂了 Access 策略拦住了升级请求,或者 ingress 里写了 `disableChunkedEncoding`。
241
- - **上传大文件失败**:Cloudflare 免费版单请求体上限 100MB。本插件默认上传上限 20MB,低于它,正常不会撞上;真要传更大的文件,先调 Cloudflare 套餐再调 `maxUploadBytes`。
241
+ - **上传大文件失败**:中继通道的附件上传上限是 500MiB,已超 Cloudflare 免费版单请求体 100MB 的墙——走 CF Tunnel 时,超过 100MB 的上传会先被 Cloudflare 拒掉(HTTP 413),与插件配置无关;要么调 Cloudflare 套餐,要么让大文件走不经 CF 的通道。
242
242
  - **想再收紧一层**:可以在 Zero Trust → Access 里给这个 hostname 加一条策略(邮箱 OTP 之类),叠在配对码之前。不是必须的,配对墙本身已经是一道认证。
243
243
 
244
244
  #### 配好后自检(任何反代都做一遍)
@@ -355,7 +355,7 @@ sudo cloudflared service install # 通了再装成常驻服务
355
355
  | `/v1/stream` | POST | 打开一条 DSH 流式订阅。**NDJSON 格式**:响应体每行一条 JSON;`{"type":"ping"}` 心跳行(默认 15 秒一条)防止反代把闲置流掐掉,每条都带当下的 `serverName` / `deviceName`(服务端改名约 15 秒内到达所有客户端);共享关闭/订阅失效时补一行 `{"type":"error","error":{"code":"unshared"}}` 再结束 |
356
356
  | `/v1/event-result` | POST | 审批/提问这类「需要应答事件」的应答回传(`{eventId, result}`),服务端按转发记录核对归属后转交 |
357
357
  | `/v1/http` | POST | 改动摘要 / diff 面板的 plain-HTTP 长尾:请求体 `{route, query}` 只登记 `changes.summary` / `changes.diff` 两条、只转发 GET(查询串是这次调用的坐标,不是要转发的请求体);查询串按允许清单**规范化重建**——只保留 `sessionId`(恰好一次,过共享表)与 `seq` / `index`(至多一次、十进制非负整数),未知参数丢弃,合成 URL 只由规范化结果拼成(两次解析对控制字符的处理差异曾是越权读取口子)——随后**进程内**交给宿主 `/api` 共享 fetch handler,不走回环 HTTP |
358
- | `/v1/upload` | POST | 远程会话的**非图片附件**上传:请求体是原始字节流(不是 JSON);查询串按允许清单规范化重建——只保留 `sessionId`(恰好一次、过共享表,未共享 403)与 `name`(至多一次);上限 100 MiB,`Content-Length` 预检加流式计数双保险,超限 413(先回拒绝再排空尾部字节);每设备并发 8(超出 429);随后与 `/v1/http` 一样**进程内**交给宿主 `/api` 共享 handler——上传回执落进的会话与之后 `session/prompt` 解析附件引用的会话是同一个 |
358
+ | `/v1/upload` | POST | 远程会话的**非图片附件**上传:请求体是原始字节流(不是 JSON);查询串按允许清单规范化重建——只保留 `sessionId`(恰好一次、过共享表,未共享 403)与 `name`(至多一次);上限 500 MiB,`Content-Length` 预检加流式计数双保险,超限 413(先回拒绝再排空尾部字节);每设备并发 8(超出 429);随后与 `/v1/http` 一样**进程内**交给宿主 `/api` 共享 handler——上传回执落进的会话与之后 `session/prompt` 解析附件引用的会话是同一个 |
359
359
  | `/v1/unshare` | POST | 子客户端主动关闭某会话的远程(只认共享表内的会话) |
360
360
 
361
361
  **允许清单里的方法**(其余一律 `forbidden-method`):会话读写(`session/follow`、`session/page`、`session/prompt`、`session/cancel`、`session/rename`、`session/selectModel`、`session/updateQueue`、`session/attachment`、`session/projections`)、新建与分叉(`session/create`、`session/fork`,结果自动共享)、子智能体(`subagents/prompt`、`subagents/interruptByParent`,按父会话判定)、附件与 @ 引用(`fileUploads/upload`、`fileReferences/list`、`sessionReferenceResolver/candidates`)、面板长尾(`goals/*` 五个、`commands/list|execute`、`agentPresets/select`、`sessionFeedback/record`)、文件树与预览(`workspaceFiles/list|changes|read|readBytes|stat`)、终端(`terminal/*`)、任务(`job/list|follow|kill`)、`skills/list`、消息反馈(`messageFeedback/*`)、`schedule/list`、工作区会话操作(`workspace/pinSession` 等)、以及四个全局读:`session/control` 流、`session/list`、`workspace/follow` 流与 `session/modelCatalog`。前三个不带会话参数,安全靠**输出过滤**:每一帧/每一行结果先按共享表过滤,未共享会话的行到不了客户端;`session/list` 结果行里指向不可访问会话的 `parentSessionId` 字段也会被删掉(行的其余部分照常透传)。`session/modelCatalog` 同样无参数、全局只读——它就是服务端的模型提供方目录,只有分组与模型 id、显示名,不带会话数据;服务端会把结果里的 `failures` 数组清空(宿主的逐组错误文本可能带端点或凭据细节,客户端本来也不展示它)。`sessionReferenceResolver/candidates`(@ 引用候选)的答案带**每一个**服务端会话的标题、目录与现成 mention,同样走行级输出过滤,只放行已共享会话的行。审批/提问事件经 `$zr/events` 订阅转发,逐事件按 `agentId` 判定可达性;已共享会话的运行状态事件(`api-session/status` emit)也经同一条订阅转发(T75),按 `args[0]` 里的会话 id 判定可达性——这是唯一一种被放行的 emit。
@@ -371,8 +371,8 @@ sudo cloudflared service install # 通了再装成常驻服务
371
371
  - `dsh-session:` 会话引用也会被扫描:DSH 会在**发消息**(`session/prompt` / `subagents/prompt` 的 content 文本块)、**改写排队消息**(`session/updateQueue` 的 edit 内容)与**斜杠命令**(`commands/execute` 参数里的全部字符串——命令处理器会把原始输入拼进下一条用户消息)这三条路径上注入被引用会话的内容,且注入时不再做权限检查,所以引用指向未共享会话的调用在服务端直接 403;@ 引用候选(`sessionReferenceResolver/candidates`)的答案也只放行已共享会话的行;
372
372
  - 终端是有意开放的(桌面应用端是可信设备):开的是**服务端**的 PTY,以服务端用户身份运行、不受智能体沙箱与审批限制;关闭会话的远程时终端流一并终止;
373
373
  - `workspaceFiles/read` 等文件预览接口不限制在会话目录内(DSH 自身就不限制),服务端进程可读的文件都能读——信任前提与终端相同;
374
- - 插件桥的整机数据需要主服务端主人逐插件同意——设置页「插件桥」里的开关,缺省全部关闭;回环执行带操作者 cookie,插件接口看到的权限与本机浏览器相同,因此桥接的边界就是声明登记的路径(exact 相等)加会话共享表,**不做**参数级扫描;
375
- - **适配器(better-sidebar)**:协作声明之外,zen-remote 为 `dsh-better-sidebar` **0.25.0、0.26.0、0.26.1、0.28.0-alpha.0 与 0.28.0** 内置了一个适配器(`src/adapters/`,不配合的第三方插件由它代为桥接,路由按前缀/路径段匹配、会话 id 在 JSON 体与路径段里改写)。**同意它的整机数据开关,等于让已配对的桌面应用端读写主服务端整盘文件、执行 git 写操作(提交、推送、回滚……),并通过提交信息生成消耗主服务端的模型额度**——产品上的决定就是「读写都接、不加路径围栏」,主服务端主人应当把它当成给对方一台本机终端级别的信任来开;`open.external`(在宿主机起进程)对远程会话拒绝,`settings.*` 与 `/sidebar/bundle` 留在子客户端本机;两条 WebSocket(`/sidebar/ws/fs-watch` 文件树监视、`/sidebar/ws/agent-opens` 打开推送)经字节隧道桥接(帧原样透传,浏览器不消费推送里的会话 id);侧聊(`sidechat.*`)转发——侧聊子会话带 `meta.parentSession`,按祖先链自动继承父会话的共享,`sidechat.start` 响应里的 `childId` 会换算成虚拟 id 回给浏览器;0.28(alpha.0 与正式版)的 `changes.ops` 长轮询(`wait: true`,最长挂 25 秒,超过中继 15 秒的响应头等待)在服务端去掉 `wait`,远程会话的「本轮文件」退回 0.26 的 2.5 秒轮询;0.26 新增的 `ws/sidechat` 侧聊推送**不桥接**——远程侧聊走 0.26 自带的 15s 兜底轮询,正确性无损,只是推送降级为轮询节奏;`sidechat.models` 的模型选择器显示的是**子客户端本机**的模型目录,而选择经 `sidechat.model` 作用在**服务端**——两机目录不一致时以服务端实际生效为准;
374
+ - 插件桥的整机数据需要主服务端主人逐插件同意——设置页「插件桥」里的开关,缺省全部关闭;回环执行带操作者 cookie,插件接口看到的权限与本机浏览器相同,因此桥接的边界就是声明登记的路径(exact 相等,`prefix: true` 路由含其子路径,前缀尾巴还要过 PB-23b 的原样字节形状红线)加会话共享表,**不做**参数级扫描;
375
+ - **适配器(better-sidebar)**:协作声明之外,zen-remote 为 `dsh-better-sidebar` **0.25.0 至 0.28.2 的七个版本**内置了一个适配器(`src/adapters/`,不配合的第三方插件由它代为桥接,路由按前缀/路径段匹配、会话 id 在 JSON 体与路径段里改写)。前缀命中的尾巴过形状红线:`?`/`#`/`..`(含编码形态)或空白一律 400 `bad-request`(PB-23b,与协作段同一条)。**同意它的整机数据开关,等于让已配对的桌面应用端读写主服务端整盘文件、执行 git 写操作(提交、推送、回滚……),并通过提交信息生成消耗主服务端的模型额度**——产品上的决定就是「读写都接、不加路径围栏」,主服务端主人应当把它当成给对方一台本机终端级别的信任来开;`open.external`(在宿主机起进程)对远程会话拒绝,`settings.*` 与 `/sidebar/bundle` 留在子客户端本机;两条 WebSocket(`/sidebar/ws/fs-watch` 文件树监视、`/sidebar/ws/agent-opens` 打开推送)经字节隧道桥接(帧原样透传,浏览器不消费推送里的会话 id);侧聊(`sidechat.*`)转发——侧聊子会话带 `meta.parentSession`,按祖先链自动继承父会话的共享,`sidechat.start` 响应里的 `childId` 会换算成虚拟 id 回给浏览器;0.28(alpha.0 与正式版)的 `changes.ops` 长轮询(`wait: true`,最长挂 25 秒,超过中继 15 秒的响应头等待)在服务端去掉 `wait`,远程会话的「本轮文件」退回 0.26 的 2.5 秒轮询;0.26 新增的 `ws/sidechat` 侧聊推送**不桥接**——远程侧聊走 0.26 自带的 15s 兜底轮询,正确性无损,只是推送降级为轮询节奏;`sidechat.models` 的模型选择器显示的是**子客户端本机**的模型目录,而选择经 `sidechat.model` 作用在**服务端**——两机目录不一致时以服务端实际生效为准;
376
376
  - 每台设备并发流数有上限(32)、同时在途的 invoke 数也有上限(32,超出 429 `too-many-invokes`);`*/internal` 形状的错误码(`gateway/internal` 等)只回码不带消息——那类消息引用的是服务端内部细节。事件应答按「转发时的订阅 + 设备」核对归属。审批/提问的转发是**先到先得**:服务端自己的界面和子客户端谁先应答谁生效,后答的一方被网关拒绝、同步成「已处理」。中继**不代答**:未共享会话的审批/提问事件不会被转发,服务端界面也不在线时这个审批就一直等待——网关会把仍未应答的事件转交给下一个连上来的订阅,等,而不是替你拒绝(代答会把整批待审批一次性判死,砸掉「推送 → 唤醒 → 审批」链路)。未共享会话的**通知类**事件(EMIT 帧:会话列表摘要、账号到期之类的服务端全局状态)一律不转发;唯一的例外是已共享会话(含其子会话)的 `api-session/status`——它只描述单个会话的运行/空闲,逐帧按共享表判定,子客户端靠它在主机侧一轮开始或结束时同步运行状态徽标(断线期间的翻转由重连后补拉列表纠正)。
377
377
 
378
378
 
@@ -399,17 +399,17 @@ sudo cloudflared service install # 通了再装成常驻服务
399
399
  | `x-zr-query` | query 字符串(不带 `?`,会话参数已是真实 id) |
400
400
  | `accept`、`range` | 唯二透传给插件的请求头 |
401
401
 
402
- **校验次序**(任一失败即返回,响应带头 `x-zen-remote-error: <code>`):网关密钥与标记 → 请求体必须为空 → 坐标头形状 → 插件在本机注册表 → 版本与摘要都等于本机 → `x-zr-path` 与声明路由**完全相等**(2.1 只有 exact 语义)→ query 只解析一次、会话参数(该路由声明的 `sessionParam`)恰好一个且非空 → 会话过共享表(含祖先链)→ `scope:'server'` 路由需 `pluginBridgeConsent[plugin] === true`(现场读行配置)→ 每设备并发预算。通过后用 `URLSearchParams` 把**解析结果重新序列化**成 query 再发出——校验的与发出去的是同一个字符串。
402
+ **校验次序**(任一失败即返回,响应带头 `x-zen-remote-error: <code>`):网关密钥与标记 → 请求体必须为空 → 坐标头形状 → 插件在本机注册表 → 版本与摘要都等于本机 → `x-zr-path` 与声明路由匹配——exact 路由完全相等,`prefix: true` 路由同时收更深子路径(PB-23 起,与适配器段同规则;多条命中按声明次序取第一条,`x-zr-path` 原样不解码),前缀命中的尾巴还要过形状红线(PB-23b,适配器段同一条):声明路径之后的尾巴(原样字节)出现 `?`、`#`、`..`(含任意 `%2e`/`%2E` 编码点,混合形态 `.%2e.` 一并堵住)或空白 → 400 `bad-request`——尾巴原样进回环请求行,`?` 会破坏 parse-once 纪律,`..` 能否逃出声明子树取决于宿主做不做 URL 规范化(此前提不钉死,故按原样字节出现即拒、不做解码后判:`.` 是无需编码的字符,合法编码器不会产出 `%2e`;`%252e` 放行同理是有意的——它是字面 `%2e` 文件名的唯一合法编码,宿主解码深度 ≤1,解出来就是普通段名)→ query 只解析一次,会话墙按路由的**有效 locator** 判(PB-24 起):query 约定(2.1 的 `sessionParam` 或显式 `locator: { in: 'query' }`)要求该参数恰好一个且非空,过共享表(含祖先链)查的就是它;v2 locator(路径段 / JSON 体)按与子客户端同一条 adapter-core 定位规则**重定位**,逐个 id 过共享表(一个都定位不到 → 400 `bad-request`),JSON 体就在这一步按路由 `maxBody` 限长缓冲(超限 413、解析失败 400 `bad-query`),回环发送的正是这份已校验缓冲 → `scope:'server'` 路由需 `pluginBridgeConsent[plugin] === true`(现场读行配置)→ 每设备并发预算。通过后用 `URLSearchParams` 把**解析结果重新序列化**成 query 再发出——校验的与发出去的是同一个字符串。
403
403
 
404
404
  **错误码与状态**:
405
405
 
406
406
  | code | 状态 | 条件 |
407
407
  | --- | --- | --- |
408
408
  | `unsupported` | 501 | 服务端未接线插件桥 |
409
- | `bad-request` | 400 | 带请求体;坐标头缺失或不成形 |
409
+ | `bad-request` | 400 | 带请求体;坐标头缺失或不成形;前缀命中的尾巴带 `?`/`#`/`..`(含编码形态)或空白(PB-23b) |
410
410
  | `plugin-unknown` | 404 | 本机注册表无此插件 |
411
411
  | `plugin-mismatch` | 409 | 版本或摘要与本机声明不等 |
412
- | `route-undeclared` | 404 | 路径不精确等于任何声明路由 |
412
+ | `route-undeclared` | 404 | 路径既不等于任何 exact 声明,也不是任何 `prefix: true` 声明的子路径 |
413
413
  | `bad-query` | 400 | 会话参数缺失、重复或为空 |
414
414
  | `not-shared` | 403 | 会话不可达 |
415
415
  | `consent-off` | 403 | 整机数据路由且主人未同意 |
@@ -433,7 +433,7 @@ sudo cloudflared service install # 通了再装成常驻服务
433
433
  | `/v1/plugin-tunnel/send` | `x-zr-tunnel`(open 首行给的隧道 id)、`x-zr-seq`(从 0 起严格递增) | 原始字节,≤ 64 KiB | `{"ok":true}`——等插件那条 socket 收下字节才回(背压:插件收不动,这次的 send 就慢) |
434
434
  | `/v1/plugin-tunnel/close` | `x-zr-tunnel` | 无 | `{"ok":true}`,幂等(未知 id 也答 ok) |
435
435
 
436
- open 的校验链与 `/v1/plugin` **完全共享**(注册表 → 版本与摘要 → exact 路由 → parse-once query → 共享表 → 同意墙,次序与错误码一致),外加一条自己的墙:路由还必须声明 `upgrade: true`——普通路由背后没有可拨的 WebSocket 端点,404 `route-undeclared`。通过后中继**自己拨本机 web server**(net/tls、只限回环;自签 https 仅对字面 `127.0.0.1` 放宽证书校验),用收到的四个头与操作者 cookie 手写升级请求(Origin 不写),把 socket 字节逐行泵进 NDJSON 流:
436
+ open 的校验链与 `/v1/plugin` **完全共享**(注册表 → 版本与摘要 → 声明路由匹配(exact / prefix)→ parse-once query → 共享表 → 同意墙,次序与错误码一致),外加一条自己的墙:路由还必须声明 `upgrade: true`——普通路由背后没有可拨的 WebSocket 端点,404 `route-undeclared`(`prefix` 不许与 `upgrade` 同声明,前缀子路径到不了隧道)。通过后中继**自己拨本机 web server**(net/tls、只限回环;自签 https 仅对字面 `127.0.0.1` 放宽证书校验),用收到的四个头与操作者 cookie 手写升级请求(Origin 不写),把 socket 字节逐行泵进 NDJSON 流:
437
437
 
438
438
  - 首行 `{"type":"open","tunnel":"<32 位十六进制>"}`——子客户端收到它就停止 HTTP、开始读流;拨号失败不丢连接,变成一行 `end` 再关闭;
439
439
  - `{"type":"data","b64":"…"}`——上游字节(base64),每行最多 48 KiB **原始**字节(base64 会膨胀 4/3,上限按裸字节算);
@@ -29,13 +29,8 @@
29
29
  * real) session id.
30
30
  */
31
31
  import { isVirtual } from '../../virtual-id.js';
32
+ import { MAX_UPLOAD_BYTES } from '../../upload-limits.js';
32
33
  import { SIDEBAR_METHODS, sidebarMethodNameOf } from './methods.js';
33
- /** The relay's upload budget (`MAX_UPLOAD_BYTES` in lib/relay-server.js).
34
- * Inlined rather than imported — an adapter may only lean on pure utility
35
- * modules — and pinned to the original by a test in
36
- * test/adapter-better-sidebar.test.cjs, which fails the pair apart when
37
- * either side moves. */
38
- const RELAY_UPLOAD_CAP_BYTES = 500 * 1024 * 1024;
39
34
  /** better-sidebar's own JSON body bound (lib/index.js:863
40
35
  * `MAX_BODY_BYTES = 1 << 20`) — the adapter forwards at most what the
41
36
  * backend would accept anyway. */
@@ -68,6 +63,24 @@ export const dshBetterSidebarAdapter = {
68
63
  // and only then a client `cwd` hint — so the server hook's cwd stripping
69
64
  // below stays correct (if anything, the client hint matters less).
70
65
  versions: ['0.25.0', '0.26.0', '0.26.1', '0.28.0-alpha.0', '0.28.0', '0.28.1', '0.28.2'],
66
+ // PB-19 (ADR-0003): the wire-surface whitelist. Keys are digest16 over the
67
+ // PB-18 extractor's reduction (`surface.ts` beside this file) of the npm
68
+ // builds' lib/index.js, computed off /tmp/bs-versions/{version}/lib/index.js:
69
+ // d383e5af5b95e456 — the 0.25.0 generation
70
+ // 96465143373a31a7 — the 0.26.0 generation (0.26.1 hashes identically)
71
+ // 6ed29a896101e681 — the 0.28.0 generation (0.28.0-alpha.0 identically)
72
+ // 65a0acaeb56f87d9 — the 0.28.1 generation (0.28.2 identically)
73
+ // An installed version outside `versions` still registers when its build
74
+ // hashes into one of these (a wire-identical patch release bridges without
75
+ // a zen-remote release); anything else fails closed to the unlisted-version
76
+ // refusal — including an upstream edit that touches only a path-mentioning
77
+ // comment, which the extractor counts. NOT part of the route-table digest.
78
+ surfaces: {
79
+ d383e5af5b95e456: 'the 0.25.0 generation',
80
+ '96465143373a31a7': 'the 0.26.0 generation (0.26.1 identical)',
81
+ '6ed29a896101e681': 'the 0.28.0 generation (0.28.0-alpha.0 identical)',
82
+ '65a0acaeb56f87d9': 'the 0.28.1 generation (0.28.2 identical)',
83
+ },
71
84
  routes: [
72
85
  {
73
86
  // The JSON API: the method name rides the path tail, the session
@@ -105,13 +118,13 @@ export const dshBetterSidebarAdapter = {
105
118
  { path: '/sidebar/archive', match: 'exact', methods: ['GET'], locator: { in: 'query', name: 'sessionId' } },
106
119
  // The raw upload (lib/index.js:6905-6948): binary bytes, capped by the
107
120
  // relay's own upload budget.
108
- { path: '/sidebar/upload', match: 'exact', methods: ['POST'], maxBody: RELAY_UPLOAD_CAP_BYTES, locator: { in: 'query', name: 'sessionId' } },
121
+ { path: '/sidebar/upload', match: 'exact', methods: ['POST'], maxBody: MAX_UPLOAD_BYTES, locator: { in: 'query', name: 'sessionId' } },
109
122
  // The raw text write (0.26.0, lib/index.js:7875-7919): the document IS
110
123
  // the POST body (text/plain — no JSON escaping layer), the session id,
111
124
  // path and a client cwd ride the QUERY. The plugin caps the body at
112
125
  // WRITE_BODY_GUARD_BYTES = 8 MiB (lib/index.js:6854); the adapter takes
113
126
  // 10 MiB — headroom over the plugin's own guard, under the relay's
114
- // 100 MiB upload budget.
127
+ // 500 MiB upload budget.
115
128
  { path: '/sidebar/write', match: 'exact', methods: ['POST'], maxBody: 10 * 1024 * 1024, locator: { in: 'query', name: 'sessionId' } },
116
129
  // The HTML preview (lib/index.js:7026+): the session id is the FIRST
117
130
  // path segment after the prefix; the rest of the segments are the
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/adapters/dsh-better-sidebar/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/adapters/dsh-better-sidebar/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The better-sidebar wire-surface extractor (PB-18, ADR-0003 local draft).
3
+ *
4
+ * The adapter's `versions` list pins EXACT upstream versions, so a wire-identical
5
+ * patch release (0.26.1, 0.28.2 — both shipped with the routes/methods/WebSockets
6
+ * byte-identical to their predecessor) still refuses to bridge until zen-remote
7
+ * itself releases. The `surfaces` table (PB-19) closes that gap: the wiring reads
8
+ * the INSTALLED plugin's `lib/index.js`, this pure function reduces it to the
9
+ * set of `/sidebar/*` routes and `ns.method` dispatch keys it serves, and a
10
+ * digest over that reduction whitelists every version whose WIRE SURFACE matches
11
+ * a blessed one — regardless of the version string.
12
+ *
13
+ * What the extraction is NOT: a general static analyzer, nor a pure wire
14
+ * oracle. It knows better-sidebar's two shapes — the `"x.y": async (…)`
15
+ * dispatch-table entries inside its bundled lib/index.js (the same recognition
16
+ * test/adapter-better-sidebar.test.cjs layer 1 has pinned since PB-12), and
17
+ * every `/sidebar/…` string literal the bundle carries. The npm build KEEPS
18
+ * comments (0.28.2 ships ~470 `/**` blocks, some quoting path strings), and the
19
+ * routes set below therefore collects registered routes, prefix checks and
20
+ * comment mentions alike — the digest is stable exactly as long as ALL those
21
+ * literals are (PB-18 review, verified with comment add/remove probes). That
22
+ * is STRICTER than "wire unchanged": an upstream release that only edits a
23
+ * path-mentioning comment changes the digest and fails closed to the
24
+ * unlisted-version behavior of today — never an unsafe forward, just a smaller
25
+ * auto-bridge hit rate than a pure wire comparison would give. A bundler that
26
+ * stops emitting the literals at all fails the same closed direction.
27
+ *
28
+ * Pure by the adapters rule: node:crypto only, no fs, no DSH imports.
29
+ */
30
+ import { createHash } from 'node:crypto';
31
+ /** The dispatcher namespaces the plugin has used across 0.25.0–0.28.2. The
32
+ * same object-key regex also matches unrelated tables in the bundle, so only
33
+ * these prefixes count (the PB-12 test's filter, verbatim). */
34
+ const NAMESPACE_PATTERN = /^(session|fs|git|changes|subagents|workflows|teams|settings|open|archive|sidechat)\./u;
35
+ /** A healthy build of any supported generation yields at least this many
36
+ * dispatch keys (0.25.0 already serves 45+). Fewer means the extractor lost
37
+ * the table — the wiring logs that distinctly from "surface changed". */
38
+ export const MIN_SURFACE_METHODS = 45;
39
+ /** Every `/sidebar/…` literal in the bundle — registered routes (the upload
40
+ * prefix, the deeper media route) plus prefix checks and comment mentions.
41
+ * Lowercase only on purpose: the routes themselves are lowercase, and a
42
+ * case-insensitive scan would swallow uppercase prose noise for no gain
43
+ * (PB-18 review nit — the `i` flag is deliberately absent). */
44
+ const ROUTE_PATTERN = /\/sidebar\/[a-z0-9./:-]+/gu;
45
+ /** `"ns.method":` dispatch-table entries (`async` optional after the colon). */
46
+ const METHOD_PATTERN = /"([a-zA-Z]+\.[a-zA-Z-]+)":\s*(?:async\s*)?\(/gu;
47
+ /** Reduce one build's lib/index.js source to its surface fingerprint.
48
+ * Deterministic: same source in, same sorted arrays out — the digest below is
49
+ * a pure function of the surface. Empirically, wire-identical patch releases
50
+ * also hash identically (0.26.1's perf work, 0.28.2's cwd fix — both kept
51
+ * every extracted literal); see the header for why that is a fact about those
52
+ * releases, not a structural guarantee. */
53
+ export function extractBetterSidebarSurface(source) {
54
+ const routes = new Set();
55
+ for (const match of source.matchAll(ROUTE_PATTERN)) {
56
+ // A literal matched through a sentence like "/sidebar/api/foo." would drag
57
+ // a sentence-period into the set — trim one trailing dot so prose mentions
58
+ // normalize to the same literal the code registers.
59
+ routes.add(match[0].replace(/\.$/u, ''));
60
+ }
61
+ const methods = new Set();
62
+ for (const match of source.matchAll(METHOD_PATTERN)) {
63
+ if (NAMESPACE_PATTERN.test(match[1]))
64
+ methods.add(match[1]);
65
+ }
66
+ return { routes: [...routes].sort(), methods: [...methods].sort() };
67
+ }
68
+ /** Whether an extraction looks like a real better-sidebar build at all (the
69
+ * "extractor found nothing" tell — logged apart from digest mismatches). */
70
+ export function surfaceLooksHealthy(surface) {
71
+ return surface.methods.length >= MIN_SURFACE_METHODS && surface.routes.length > 0;
72
+ }
73
+ /** The 16-hex digest a `surfaces` table key carries: sha256 over the canonical
74
+ * JSON of the surface. Same surface across versions → same digest → bridged. */
75
+ export function betterSidebarSurfaceDigest(surface) {
76
+ const canonical = JSON.stringify({ routes: surface.routes, methods: surface.methods });
77
+ return createHash('sha256').update(canonical, 'utf8').digest('hex').slice(0, 16);
78
+ }
79
+ //# sourceMappingURL=surface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"surface.js","sourceRoot":"","sources":["../../../src/adapters/dsh-better-sidebar/surface.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAQxC;;+DAE+D;AAC/D,MAAM,iBAAiB,GAAG,uFAAuF,CAAA;AAEjH;;yEAEyE;AACzE,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAA;AAErC;;;;+DAI+D;AAC/D,MAAM,aAAa,GAAG,4BAA4B,CAAA;AAElD,gFAAgF;AAChF,MAAM,cAAc,GAAG,gDAAgD,CAAA;AAEvE;;;;;2CAK2C;AAC3C,MAAM,UAAU,2BAA2B,CAAC,MAAc;IACxD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAA;IAChC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;QACnD,2EAA2E;QAC3E,2EAA2E;QAC3E,oDAAoD;QACpD,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAA;IAC1C,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAA;IACjC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;QACpD,IAAI,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;IAC7D,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAA;AACrE,CAAC;AAED;4EAC4E;AAC5E,MAAM,UAAU,mBAAmB,CAAC,OAA6B;IAC/D,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,IAAI,mBAAmB,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAA;AACnF,CAAC;AAED;gFACgF;AAChF,MAAM,UAAU,0BAA0B,CAAC,OAA6B;IACtE,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;IACtF,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;AAClF,CAAC"}
package/lib/client.js CHANGED
@@ -18196,7 +18196,7 @@ function nginxSnippetText(input) {
18196
18196
  ' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;',
18197
18197
  ' proxy_set_header X-Forwarded-Proto $scheme;',
18198
18198
  ' proxy_buffering off;',
18199
- ' client_max_body_size 128m;',
18199
+ ' client_max_body_size 512m;',
18200
18200
  ' proxy_read_timeout 3600s;',
18201
18201
  ' proxy_send_timeout 3600s;',
18202
18202
  ' }',