dsh-code-server-app 0.2.14 → 0.3.7

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.
package/README.en.md CHANGED
@@ -189,6 +189,74 @@ this plugin's threat model.
189
189
  browser page could complete a handshake against `ws://127.0.0.1:<port>/stable-<commit>` and drive the IDE.
190
190
 
191
191
 
192
+ ## Working with DSH: the editor bridge (since 0.3.0, on by default)
193
+
194
+ Having the IDE next to DSH and having the agent **know what is going on in the editor** are two different
195
+ things. The editor bridge covers the second half: it is a **read-only** channel that hands the agent what
196
+ only the editor knows, and lets editor gestures drive the current session.
197
+
198
+ | Direction | Capability | Mechanism |
199
+ |---|---|---|
200
+ | editor → agent | **unsaved buffers** (disk ≠ what the user sees), active file and selection, **language-server diagnostics** with `file:line`, source and code | agent tools `editor_context` / `editor_diagnostics`; plus a notice attached before writing a dirty file |
201
+ | editor → DSH | select code → context menu **"DSH: ask about selection"** → the message lands in the current session (with `file:line` and a fenced block) | extension command `dsh-code-server.askAboutSelection` |
202
+ | agent → editor | the agent changed a file → a **native diff** opens; if that buffer has unsaved changes you get a warning and **no overwrite** | host watches `tools/result`, the extension polls and opens the diff |
203
+
204
+ - The tools are only registered while the bridge is live (so the model never sees an unusable tool), and the
205
+ system-prompt section renders only then too.
206
+ - **Everything is read-only**: the bridge never writes files, applies edits, or runs commands. The agent's writes
207
+ still go through its own `fs` tools; the bridge only *knows about* them.
208
+ - Status bar shows `$(plug) DSH` while connected (click it for the log in the "DSH Editor Bridge" output channel).
209
+
210
+ ### The three channels
211
+
212
+ ```
213
+ extension → host POST /api/code-server/bridge/sync one round trip: push editor state + take pending events
214
+ extension → host POST /api/code-server/bridge/ask push an editor question into the current session
215
+ extension → host GET /api/code-server/bridge/health unauthenticated liveness probe
216
+ host → extension POST /api/code-server/bridge/event extension reports open/close etc. (host log tail)
217
+ host → extension <extensionsDir>/.dshcs-bridge/bridge.json port + token, re-read by the extension every 5s
218
+ ```
219
+
220
+ **Why state is pushed, not pulled**: the extension host is a child process of the VS Code server and **listens on
221
+ no port** — the host cannot call into it. Editor state therefore rides the extension's own polling request, and
222
+ the host caches it for the tools (at most one 600 ms cycle behind; older than 10 s and the tool says so instead
223
+ of passing stale data off as fresh).
224
+
225
+ **Why no SSE/WebSocket**: `ctx.connection.fetch.register` only allows `GET | HEAD | POST` (streaming would need
226
+ the WS mux already owned by `dsh-api-gateway`). Polling also buys two useful properties: it is idempotent (a
227
+ dropped event only costs one notification — the data always lives in the editor) and the cached state is
228
+ inherently fresh.
229
+
230
+ ### Security model (four invariants; read before touching `lib/bridge.mjs`)
231
+
232
+ The token lives in `<extensionsDir>/.dshcs-bridge/bridge.json`, **readable by any process of the same local
233
+ user**, so:
234
+
235
+ 1. **`/api/code-server/bridge/*` is permanently read-only.** No route writes files, edits documents, or runs
236
+ commands. A leaked token is therefore bounded to "sees information that is in the editor" and **can never**
237
+ become arbitrary file writes or command execution. A whitelist assertion in `scripts/test-bridge-routes.mjs`
238
+ guards this.
239
+ 2. **Any request carrying `Origin` gets 403.** Browsers always send one (including a sandboxed iframe's literal
240
+ `Origin: null`); the Node extension host never does. Origin is checked **before** the token — otherwise the
241
+ bridge would be a "did you guess the token right" oracle for a web page.
242
+ 3. **Paths are confined to the editor's current workspace folders.**
243
+ 4. **Everything is bounded**: 200 diagnostics, 500-char messages, 256 KB request bodies, a 64-entry event ring.
244
+
245
+ This layer stops "another local app or a browser page that got hold of the file". A malicious program running as
246
+ the same user could read your files and the token anyway — that is outside this plugin's threat model, exactly
247
+ as stated for the loopback port.
248
+
249
+ ### Turning it off / diagnostics
250
+
251
+ | How | Effect |
252
+ |---|---|
253
+ | `config.editorBridge: false` in `cordis.patch.yml` | next start writes no `bridge.json` and registers no tools |
254
+ | `code-server.editorBridge: false` in the settings document | **immediate**: config removed, tools unregistered, the extension goes dormant |
255
+ | disable the `dshcs-editor-bridge` extension inside the IDE | the bridge simply becomes unavailable |
256
+
257
+ Diagnostics: `GET /api/code-server/status` exposes
258
+ `bridge: { enabled, live, toolsRegistered, supported, url, file }` — **never the token** (that only exists in the file).
259
+
192
260
  ## Legacy DSH (unsupported since 0.2.3)
193
261
 
194
262
  **Behaviour**: when `sidebarRightTabs` / `sidebarRight` cannot be found, the plugin registers a single settings card:
@@ -479,6 +547,7 @@ reports the tree version / `productPath` / server entry, VS Code inner dependenc
479
547
  | `userDataDir` | `$DSH_HOME/code-server/user-data` | User-data isolation directory |
480
548
  | `extensionsDir` | `$DSH_HOME/code-server/extensions` | Extensions directory |
481
549
  | `readyTimeoutMs` | `60000` | `/healthz` readiness probe timeout |
550
+ | `editorBridge` | `true` | **Editor bridge** (since 0.3.0): the read-only channel between the in-tree `dshcs-editor-bridge` extension and the host (see "Working with DSH"). Off = no `bridge.json`, no `editor_context`/`editor_diagnostics`, the extension stays dormant. `code-server.editorBridge` in the settings document toggles it **live** |
482
551
 
483
552
  User-level override example (write in `$DSH_HOME/profiles/web/cordis.patch.yml`, using the `- id: code-server` row):
484
553
 
@@ -505,6 +574,13 @@ Host/Origin fence and browser auth); in the desktop profile `apps/desktop-host`
505
574
  | POST | `/api/code-server/stop` | Stop and recycle the process tree |
506
575
  | POST | `/api/code-server/setup` | **Compatibility no-op**: since 0.1.36 dependencies are installed by the package manager, so this only re-runs the env self-check and returns |
507
576
  | POST | `/api/code-server/open-file` | body `{ file }` — writes the signal consumed by the built-in `dshcs-open-file` extension to open the file in code-server |
577
+ | GET | `/api/code-server/bridge/health` | **unauthenticated**: `{ ok, bridge, pid, url }` — liveness only, no editor data |
578
+ | POST | `/api/code-server/bridge/sync` | editor bridge: the extension pushes state (`{context, diagnostics, workspace, at}`) and takes back events; `?since=<seq>` is the event cursor. Requires `x-dshcs-bridge-token` |
579
+ | POST | `/api/code-server/bridge/ask` | editor bridge: push an editor question into the current session (`{text, file?, lineStart?, lineEnd?, selection?, languageId?}`); **409** when no session can receive it |
580
+ | POST | `/api/code-server/bridge/event` | editor bridge: extension reports open/close and similar (host log tail). Requires the token |
581
+
582
+ > All four bridge routes carry their own token check — they **cannot** rely on DSH's cookie fence, because the
583
+ > extension host has no browser cookie — and they are read-only by construction. See "Working with DSH" above.
508
584
 
509
585
  > The plugin no longer registers `/code-server/*` webServer-only routes, and the code-server icon is inlined as a data URI
510
586
  > in the client bundle — the client requests no plugin-owned HTTP resource at all.
@@ -577,6 +653,15 @@ What remains on the plugin side:
577
653
 
578
654
  ## Known limitations
579
655
 
656
+ - **The editor bridge only works under `serve: loopback`.** `serve: dsh` is a named-pipe mode with no dedicated
657
+ port, so the bridge's "loopback host + own token" model does not apply: it stays disabled there
658
+ (`bridge.supported=false` in `status`). Use the default `loopback` when you want the integration.
659
+ - **Bridged state can lag by up to 600 ms**, and the tools say "stale" rather than serving data older than 10 s.
660
+ - **Unsaved buffers are reported, not taken over.** The agent still edits via its own `fs` tools, i.e. against
661
+ disk. What the bridge adds is a notice *before* writing a dirty file, a diff *after*, and a warning instead of
662
+ an overwrite. It does not decide whether the user saves — that would mean changing the agent's read path,
663
+ which is out of scope for this version.
664
+
580
665
  - ~~No sub-path~~ **no longer true (corrected with measurements in 0.2.0)**: the workbench HTML VS Code renders references
581
666
  **only relative URLs** (9 references measured, 0 absolute; `serverBasePath="."`, `rootEndpoint="."`), and the client
582
667
  builds its WebSocket path from `location.pathname + join(serverBasePath ?? '/', <quality>-<commit>)`. The IDE can
package/README.md CHANGED
@@ -184,6 +184,70 @@ DSH 用**资源地址**命名文件,`openFile` 只负责把地址交给右侧栏
184
184
  (含 `Forwarded: host=` / `X-Forwarded-Host` 的反代语义),否则回 `403`;缺 `Origin` 的非浏览器请求放行。
185
185
  没有这道检查时,本机任意浏览器页面都能对 `ws://127.0.0.1:<port>/stable-<commit>` 完成握手并驱动 IDE。
186
186
 
187
+ ## 与 DSH 的协同:编辑器桥(0.3.0 起,默认开)
188
+
189
+ "IDE 就在旁边"和"agent 真的知道编辑器里发生了什么"是两件事。编辑器桥补的是后一半:**只读**地把
190
+ 只有编辑器才知道的信息交给 agent,并让用户在编辑器里的动作能反过来驱动当前会话。
191
+
192
+ ### 双向能力
193
+
194
+ | 方向 | 能力 | 落地方式 |
195
+ |---|---|---|
196
+ | 编辑器 → agent | **未保存缓冲区**(磁盘内容 ≠ 用户所见)、活动文件与选区、**语言服务器诊断**(含 file:line、来源、code) | agent 工具 `editor_context` / `editor_diagnostics`;写脏文件前额外附一条提醒 |
197
+ | 编辑器 → DSH | 选中代码 → 右键「DSH: 针对选中内容提问」→ 消息进入当前会话(带 `文件:行` 与代码块) | 扩展命令 `dsh-code-server.askAboutSelection`(编辑器右键菜单 + 命令面板) |
198
+ | agent → 编辑器 | agent 改了哪个文件 → 开**原生 diff** 审阅;缓冲区有未保存改动时**告警而不覆盖** | host 观察 `tools/result`,扩展轮询后开 diff + 非模态告警 |
199
+
200
+ - 工具只在桥就绪时注册(IDE 没起来时模型看不到"有个用不了的工具");提示词段落也只在桥存活时渲染。
201
+ - **全部只读**:桥不写文件、不改文档、不执行命令。agent 的写操作仍然全部走它自己的 `fs` 工具,
202
+ 桥只是"知道它写了什么"。
203
+ - 编辑器侧的入口还有状态栏的 `$(plug) DSH`(连通时显示,点击打开日志),日志在输出面板
204
+ 「DSH Editor Bridge」里 —— 出问题时先看它。
205
+
206
+ ### 三条通道
207
+
208
+ ```
209
+ 扩展 → host POST /api/code-server/bridge/sync 一趟来回:上报编辑器状态 + 取回待处理事件
210
+ 扩展 → host POST /api/code-server/bridge/ask 把编辑器里的提问投进当前会话
211
+ 扩展 → host GET /api/code-server/bridge/health 无鉴权探活(便于重启后一眼确认)
212
+ host → 扩展 POST /api/code-server/bridge/event 扩展上报打开/关闭文件等(进 host 日志尾)
213
+ host → 扩展 <extensionsDir>/.dshcs-bridge/bridge.json 端口 + 令牌(扩展每 5s 重读)
214
+ ```
215
+
216
+ **为什么状态是"推"而不是"拉"**:扩展宿主是 VS Code server 的一个子进程,**不监听任何端口** ——
217
+ host 反向请求不到它。所以编辑器状态只能在扩展主动发起的那趟轮询里带上来,host 缓存后给工具读
218
+ (缓存滞后最多一个轮询周期 600ms,超过 10s 没更新就判为过期,工具会明说"状态已过期");
219
+
220
+ **为什么不用 SSE/WebSocket**:`ctx.connection.fetch.register` 的 methods 只允许 `GET | HEAD | POST`
221
+ (流式要另走已被 `dsh-api-gateway` 占用的 WS mux)。轮询反而给了两条好性质:幂等(丢一次事件只是
222
+ 少一次提示,数据本身永远在编辑器里),以及状态天然最新(每趟都刷新)。
223
+
224
+ ### 安全模型(四条不变量,改 `lib/bridge.mjs` 之前先读)
225
+
226
+ 桥的令牌写在 `<extensionsDir>/.dshcs-bridge/bridge.json`(**对本机同用户进程可读**),所以:
227
+
228
+ 1. **`/api/code-server/bridge/*` 永久只读。** 没有写文件、改文档、执行命令的路由。
229
+ 令牌泄露的爆炸半径被封在"看到编辑器里的信息",**不会**变成任意文件写/任意命令执行。
230
+ `scripts/test-bridge-routes.mjs` 里有一条白名单断言盯着这件事。
231
+ 2. **带 `Origin` 的请求一律 403。** 浏览器发起必带 Origin(含沙箱 iframe 的 `Origin: null`),
232
+ 扩展宿主是 Node 进程、不带。判定顺序上 Origin **先于令牌** —— 否则等于给浏览器一个
233
+ "令牌猜对没有"的 oracle。
234
+ 3. **路径收敛在编辑器当前工作区**(`workspaceFolder` 之外的诊断直接丢弃)。
235
+ 4. **有界**:诊断默认 200 条 / 单条截断 500 字符 / 上报体上限 256KB / 事件环形缓冲 64 条。
236
+
237
+ 这一层挡的是"本机其它应用或浏览器页面拿到那个文件后乱调桥";**同用户的本地恶意程序**
238
+ 本来就能直接读你的文件与令牌文件 —— 那不在本插件的威胁模型内(与「回环端口的安全模型」同一句话)。
239
+
240
+ ### 开关与诊断
241
+
242
+ | 怎么关 | 效果 |
243
+ |---|---|
244
+ | `cordis.patch.yml` 的 `config.editorBridge: false` | 下次启动不写 bridge.json、不注册工具 |
245
+ | 设置文档里的 `code-server.editorBridge: false` | **即时生效**:删配置 + 注销工具,扩展随即休眠 |
246
+ | 在 IDE 里禁用扩展 `dshcs-editor-bridge` | 桥自然不可用(工具会注册但立刻报"状态未上报";IDE 侧无任何动作) |
247
+
248
+ 诊断:`GET /api/code-server/status` 的 `bridge` 字段返回
249
+ `{ enabled, live, toolsRegistered, supported, url, file }` —— **不含令牌**(令牌只在那个文件里)。
250
+
187
251
  ## 旧版 DSH(0.2.3 起不再支持)
188
252
 
189
253
  **行为**:探测不到 `sidebarRightTabs` / `sidebarRight` 时,插件只注册一张设置卡片,内容是:
@@ -284,6 +348,21 @@ pnpm run promote -- <version>
284
348
  > **秒级 no-op**,所以日常只改插件代码的话直接 `pnpm pack` 即可(不会偷偷升级 VS Code)。
285
349
  > 升级树必须显式 `pnpm run vendor:latest`(或 `--force`/`--version`),并重发子包。
286
350
 
351
+ ### 回归脚本(改完跑一遍)
352
+
353
+ ```powershell
354
+ pnpm test:apply # 桩 ctx 下跑通 apply(回归:apply 期的 ReferenceError)
355
+ pnpm test:claim-types # 认领类型语法与默认值
356
+ pnpm test:bridge-routes # 编辑器桥:路由表只读白名单 / Origin 与令牌的判定顺序 / 令牌头三处一致
357
+ pnpm test:bridge-extension # 编辑器桥扩展侧纯逻辑:未保存缓冲区上报、诊断排序截断、diff 判据、投递降级
358
+ pnpm test:launcher-routes # launcher 的 HTTP 面(起真进程,较慢)
359
+ pnpm test:workspace-switch # 切工作区不重启进程
360
+ pnpm test:fullscreen # 打开标签即全屏
361
+ ```
362
+
363
+ > `test:bridge-routes` 会把 `DSH_HOME` 指向临时目录(否则它会 adopt 开发机上正在跑的那个实例,
364
+ > 并改写真实的 `bridge.json`);脚本最后有一条"隔离自检"断言真实配置一字未动。
365
+
287
366
  ## 安装插件(一条命令;依赖全部由包管理器装好)
288
367
 
289
368
  ```powershell
@@ -470,6 +549,7 @@ host 探测顺序:`@jinsiyu/dshcs-vscode-server/vscode`(**0.2.0+ 正式布局**)
470
549
  | `extensionsDir` | `$DSH_HOME/code-server/extensions` | 扩展目录 |
471
550
  | `locale` | `''` | 界面语言(空 = 跟随浏览器),如 `zh-cn` |
472
551
  | `readyTimeoutMs` | `60000` | `/healthz` 就绪探测超时(TCP 或命名管道) |
552
+ | `editorBridge` | `true` | **编辑器桥**(0.3.0 起):树内扩展 `dshcs-editor-bridge` 与 host 之间的只读通道(见「与 DSH 的协同」)。关掉 = 不写 `bridge.json`、不注册 `editor_context`/`editor_diagnostics`、扩展休眠。设置文档里的 `code-server.editorBridge` 可**即时**开关 |
473
553
 
474
554
  用户级覆盖示例(写在 `$DSH_HOME/profiles/web/cordis.patch.yml`,应使用 `- id: code-server` 行覆盖):
475
555
 
@@ -494,6 +574,13 @@ desktop profile 由 `apps/desktop-host` 把 `/api/*` 交给同一个 `createShar
494
574
  | POST | `/api/code-server/stop` | 停止并回收进程树 |
495
575
  | POST | `/api/code-server/setup` | **兼容空操作**:0.1.36 起依赖由包管理器安装,调用只重新自检 `env` 并返回 |
496
576
  | POST | `/api/code-server/open-file` | body `{ file }` — 写信号文件,由内置扩展 `dshcs-open-file` 在 code-server 中打开 |
577
+ | GET | `/api/code-server/bridge/health` | **无鉴权**:`{ ok, bridge, pid, url }`。只回答"桥活着吗",不含任何编辑器数据 |
578
+ | POST | `/api/code-server/bridge/sync` | 编辑器桥:扩展上报状态(`{context, diagnostics, workspace, at}`)并取回事件;`?since=<seq>` 是事件游标。需 `x-dshcs-bridge-token` |
579
+ | POST | `/api/code-server/bridge/ask` | 编辑器桥:把编辑器里的提问投进当前会话(`{text, file?, lineStart?, lineEnd?, selection?, languageId?}`);没有可投递的会话时回 **409** |
580
+ | POST | `/api/code-server/bridge/event` | 编辑器桥:扩展上报打开/关闭文件等(进 host 日志尾)。需令牌 |
581
+
582
+ > 桥的四条路由都自带令牌鉴权(它们**不依赖** DSH 的 cookie fence —— 扩展宿主拿不到浏览器 cookie),
583
+ > 且永远只读。前面的守护规则与取舍见「与 DSH 的协同」。
497
584
 
498
585
  > 插件不再注册 `/code-server/*` 这类 webServer 专有路由;code-server 图标已内联为 data URI(client bundle 内),
499
586
  > 因此客户端不请求任何插件自有 HTTP 资源。
@@ -545,6 +632,15 @@ desktop profile 由 `apps/desktop-host` 把 `/api/*` 交给同一个 `createShar
545
632
 
546
633
  ## 已知限制
547
634
 
635
+ - **编辑器桥只在 `serve: loopback` 下工作**:`serve: dsh` 是管道模式、没有独立端口,桥的
636
+ "回环 Host + 独立令牌"模型不适用 → 该模式下自动不启用(status 的 `bridge.supported=false`)。
637
+ 需要协同能力就用默认的 `loopback`(0.2.0 起也是默认)。
638
+ - **桥的状态有最多 600ms 滞后**:扩展每 600ms 推一次;超过 10s 没更新时工具会明说"状态已过期"
639
+ 而不是拿旧数据当新数据(例如用户在 IDE 里关掉面板之后)。
640
+ - **未保存缓冲区是"上报"而不是"接管"**:agent 仍然通过它自己的 `fs` 工具按磁盘内容编辑。
641
+ 桥能做的是**在写之前提醒**、**写之后给 diff**、**冲突时告警而不覆盖** ——
642
+ 它不能替用户决定保存与否(那需要改动 agent 的读路径,不在本版本范围内)。
643
+
548
644
  - ~~子路径不支持~~ **已不成立(0.2.0 实测更正)**:VS Code 渲染出的 workbench HTML 里
549
645
  **资源引用全是相对路径**(实测 9 条引用中绝对路径 0 条,`serverBasePath="."`、`rootEndpoint="."`),
550
646
  客户端 WebSocket 路径由 `location.pathname + join(serverBasePath ?? '/', <quality>-<commit>)` 拼成,