dsh-code-server-app 0.2.13 → 0.3.6
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 +122 -4
- package/README.md +130 -8
- package/assets/extensions/dshcs-editor-bridge/extension.js +539 -0
- package/assets/extensions/dshcs-editor-bridge/lib/bridge-client.js +241 -0
- package/assets/extensions/dshcs-editor-bridge/lib/context-model.js +204 -0
- package/assets/extensions/dshcs-editor-bridge/lib/diff-model.js +123 -0
- package/assets/extensions/dshcs-editor-bridge/package.json +58 -0
- package/cordis.patch.yml +16 -2
- package/lib/bridge-observe.mjs +184 -0
- package/lib/bridge-session.mjs +124 -0
- package/lib/bridge-tools.mjs +330 -0
- package/lib/bridge.mjs +325 -0
- package/lib/client.js +1 -1
- package/lib/dsh-resolve.mjs +91 -0
- package/lib/index.js +472 -64
- package/lib/launcher.mjs +120 -4
- package/package.json +11 -3
- package/vendor/VENDOR.json +1 -1
package/README.en.md
CHANGED
|
@@ -47,7 +47,9 @@ A static profile plugin (npm package with host + client bundle) that ships the *
|
|
|
47
47
|
Open the IDE from the **Code Server box** on the sidebar's guide page, or by clicking DSH's own produced-file chips / delivered-file previews / inline file names;
|
|
48
48
|
diagnostics stay out of the UI — the `[code-server]` lines in the DSH host log are the place to look (`/api/code-server/status` still returns `env` for scripts).
|
|
49
49
|
The old `windowedOpen` (open in a window) and `reserveComposer` were **removed in 0.2.6**: leftover keys in an old settings document neither fail nor apply (they are no longer part of the schema).
|
|
50
|
-
To use the IDE in a browser tab,
|
|
50
|
+
To use the IDE in a browser tab, **copy the full address** from the settings card / empty-state hint (it contains the
|
|
51
|
+
path token: `http://127.0.0.1:<port>/<token>/`; under `serve: dsh` it is DSH's `/code-server/`) — dropping the token
|
|
52
|
+
segment yields a 404.
|
|
51
53
|
|
|
52
54
|
## Opening files (official entry points since 0.2.5)
|
|
53
55
|
|
|
@@ -143,9 +145,36 @@ state preserved (no full reload). Full evidence and probe scripts: `docs/analysi
|
|
|
143
145
|
|
|
144
146
|
| Mode | What it does | Requires |
|
|
145
147
|
|---|---|---|
|
|
146
|
-
| **`loopback` (default)** | the plugin listens on its own loopback port (`
|
|
148
|
+
| **`loopback` (default)** | the plugin listens on its own loopback port (**`port: 0` by default = a random port assigned per start**), the sidebar iframe connects cross-origin, and the URL carries a **random path token** (`http://127.0.0.1:<port>/<token>/`, see "Security model of the loopback port" below); the process can be adopted after a DSH host restart | nothing |
|
|
147
149
|
| **`dsh`** | the IDE is mounted on **DSH's own HTTP port** at `/code-server/*` (HTTP prefix route) plus `/code-server/<quality>-<commit>` (exact WebSocket route), forwarded to the launcher's **named pipe**; **no extra port**; every request (including the WS handshake) first passes `ctx.connection.requestRejection()` — the same Host/Origin fence and browser-cookie authentication as `/api` | DSH providing `webServer` (web profile); desktop falls back to loopback automatically |
|
|
148
150
|
|
|
151
|
+
### Security model of the loopback port (since 0.2.14)
|
|
152
|
+
|
|
153
|
+
`loopback` is the only transport desktop has (no webServer, no same-origin mount), so it is hardened on its own:
|
|
154
|
+
|
|
155
|
+
- **Random port**: `port` defaults to `0` → the OS assigns a free port and the launcher writes the **actual** one to
|
|
156
|
+
`$DSH_HOME/code-server/endpoint.json`, which the host reads back. The port therefore changes on every start and the old
|
|
157
|
+
"8090 is busy" class of conflicts is gone. Pin `port` explicitly if you need a fixed address.
|
|
158
|
+
- **Path token**: a fresh 32-character token (`[0-9A-Za-z_-]`, 24 random bytes) is generated on every **new start**, stored in
|
|
159
|
+
`$DSH_HOME/code-server/path-token` (inside the user profile, readable only by the owner under the default ACL), and becomes
|
|
160
|
+
the URL path prefix. Requests without that prefix get a plain **404** (nothing reveals that an IDE lives there); a prefix
|
|
161
|
+
without the trailing slash is answered with a 302.
|
|
162
|
+
- **Why not VS Code's own `connection-token`**: it works through `?tkn=` → 302 + `Set-Cookie: vscode-tkn; SameSite=Lax`.
|
|
163
|
+
The desktop iframe is **cross-origin** (`dsh-app://` → `127.0.0.1`), and a Lax cookie is not sent from a cross-site
|
|
164
|
+
subframe — the IDE would simply fail to load. A path prefix needs no cookie at all: the workbench derives every asset and
|
|
165
|
+
WebSocket URL from `location.pathname` (the same mechanism already proven by mounting under `/code-server/` in `serve: dsh`),
|
|
166
|
+
so the prefix rides along on every subrequest and on the WS handshake. (Verified with a real Edge + CDP run: with a random
|
|
167
|
+
port and a token, the workbench renders **inside a cross-origin iframe** and establishes its WebSocket.)
|
|
168
|
+
- **Host allowlist**: in loopback mode only `127.0.0.1 | localhost | [::1] : <actual port>` is accepted. This is what stops
|
|
169
|
+
DNS rebinding, whose requests can arrive without an `Origin` header and therefore slip past the `Origin == Host` check.
|
|
170
|
+
- **`Referrer-Policy: no-referrer`**: the token lives in the path, so it must not leak through `Referer` when external resources load.
|
|
171
|
+
- **The token never reaches argv or the logs**: command lines are readable by any local process, so it travels through a file;
|
|
172
|
+
the log only says "enabled".
|
|
173
|
+
|
|
174
|
+
Boundary, stated plainly: this layer stops other local applications, port scanners and browser pages from casually reaching
|
|
175
|
+
your IDE. A **malicious program running as the same user** can already read your files and that token file — that is outside
|
|
176
|
+
this plugin's threat model.
|
|
177
|
+
|
|
149
178
|
- Switch it in `config.serve` in `cordis.patch.yml` or in Settings → Plugins → Code Server (takes effect on the next start).
|
|
150
179
|
- Benefits of `dsh`: a single URL/port (remote access to DSH gives you the IDE), no extra loopback listener, authentication on par with DSH.
|
|
151
180
|
- Two **known trade-offs** of `dsh`: the iframe shares DSH's origin, so `sandbox` is dropped there (same-origin plus
|
|
@@ -160,6 +189,74 @@ state preserved (no full reload). Full evidence and probe scripts: `docs/analysi
|
|
|
160
189
|
browser page could complete a handshake against `ws://127.0.0.1:<port>/stable-<commit>` and drive the IDE.
|
|
161
190
|
|
|
162
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
|
+
|
|
163
260
|
## Legacy DSH (unsupported since 0.2.3)
|
|
164
261
|
|
|
165
262
|
**Behaviour**: when `sidebarRightTabs` / `sidebarRight` cannot be found, the plugin registers a single settings card:
|
|
@@ -444,12 +541,13 @@ reports the tree version / `productPath` / server entry, VS Code inner dependenc
|
|
|
444
541
|
|---|---|---|
|
|
445
542
|
| `bin` | `code-server` (placeholder) | Launch priority: explicit `bin` in config > the tree package `@jinsiyu/dshcs-code-server/code-server/out/node/entry.js` > the old platform sub-packages `@jinsiyu/dshcs-code-server-<platform>-<arch>` > the in-package `vendor/code-server` > the old install root `.code-server-app` > plugin-internal `node_modules` > `code-server` on PATH. None present → startup error with troubleshooting hints |
|
|
446
543
|
| `host` | `127.0.0.1` | Bind address; `auth: none` only allows loopback (localhost/127.0.0.1/::1) |
|
|
447
|
-
| `port` | `
|
|
544
|
+
| `port` | `0` | Port for loopback mode; **`0` = a random free port assigned per start** (the actual one is written to `endpoint.json` and read back by the host). Give an explicit port to pin it; when that port is taken and no valid `pid.json` exists, startup fails with diagnostics instead of killing a stranger |
|
|
448
545
|
| `auth` | `none` | `none` \| `password`; non-loopback host automatically requires password |
|
|
449
546
|
| `passwordToken` | `''` | Token for password mode (passed to code-server via the `PASSWORD` env var) |
|
|
450
547
|
| `userDataDir` | `$DSH_HOME/code-server/user-data` | User-data isolation directory |
|
|
451
548
|
| `extensionsDir` | `$DSH_HOME/code-server/extensions` | Extensions directory |
|
|
452
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** |
|
|
453
551
|
|
|
454
552
|
User-level override example (write in `$DSH_HOME/profiles/web/cordis.patch.yml`, using the `- id: code-server` row):
|
|
455
553
|
|
|
@@ -476,6 +574,13 @@ Host/Origin fence and browser auth); in the desktop profile `apps/desktop-host`
|
|
|
476
574
|
| POST | `/api/code-server/stop` | Stop and recycle the process tree |
|
|
477
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 |
|
|
478
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.
|
|
479
584
|
|
|
480
585
|
> The plugin no longer registers `/code-server/*` webServer-only routes, and the code-server icon is inlined as a data URI
|
|
481
586
|
> in the client bundle — the client requests no plugin-owned HTTP resource at all.
|
|
@@ -548,6 +653,15 @@ What remains on the plugin side:
|
|
|
548
653
|
|
|
549
654
|
## Known limitations
|
|
550
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
|
+
|
|
551
665
|
- ~~No sub-path~~ **no longer true (corrected with measurements in 0.2.0)**: the workbench HTML VS Code renders references
|
|
552
666
|
**only relative URLs** (9 references measured, 0 absolute; `serverBasePath="."`, `rootEndpoint="."`), and the client
|
|
553
667
|
builds its WebSocket path from `location.pathname + join(serverBasePath ?? '/', <quality>-<commit>)`. The IDE can
|
|
@@ -568,4 +682,8 @@ What remains on the plugin side:
|
|
|
568
682
|
switching tabs or collapsing the sidebar and back **does not reload** it. Browsers without `moveBefore` fall back to
|
|
569
683
|
the old behaviour (`appendChild` → full reload), reported as `degraded`; see "Why switching tabs no longer reloads".
|
|
570
684
|
- **Remote access**: with `serve: dsh` the browser only needs to reach DSH itself (one port, protected exactly like `/api`);
|
|
571
|
-
`serve: loopback`
|
|
685
|
+
`serve: loopback` binds to loopback only (random port + path token + Host allowlist — see "Security model of the loopback
|
|
686
|
+
port"), so use `serve: dsh` for cross-machine access (0.2.0 no longer supports `auth: password`).
|
|
687
|
+
- **The loopback token rotates per instance**: port and token change on every new start; adoption after a host restart
|
|
688
|
+
matches the live instance through the `endpoint.json` and `path-token` files, so **do not delete those two files**
|
|
689
|
+
(without them the host cannot recognise the old instance and treats the port as foreign).
|
package/README.md
CHANGED
|
@@ -61,7 +61,8 @@
|
|
|
61
61
|
打开 IDE 用右侧栏「开始」页的 **Code Server 入口框**,或直接点官方的产物 chip / 交付卡片 / 正文文件名;
|
|
62
62
|
诊断看 DSH host 日志里的 `[code-server]` 输出(`/api/code-server/status` 仍返回 `env` 供脚本排查)。
|
|
63
63
|
旧版的 `windowedOpen`(窗口化打开)、`reserveComposer` 已在 0.2.6 移除:旧设置文档里残留的这两个键不会报错,只是被忽略(不再出现在 schema 里)。
|
|
64
|
-
需要在新标签页用 IDE
|
|
64
|
+
需要在新标签页用 IDE 时,从设置卡/空态提示里**复制完整地址**(含路径令牌,`http://127.0.0.1:<port>/<token>/`;
|
|
65
|
+
`serve: dsh` 时为 DSH 的 `/code-server/`)—— 少了令牌那一段会 404。
|
|
65
66
|
|
|
66
67
|
## 文件打开(0.2.5 起走官方入口)
|
|
67
68
|
|
|
@@ -146,9 +147,31 @@ DSH 用**资源地址**命名文件,`openFile` 只负责把地址交给右侧栏
|
|
|
146
147
|
|
|
147
148
|
| 方式 | 说明 | 需要 |
|
|
148
149
|
|---|---|---|
|
|
149
|
-
| **`loopback`(默认)** | 插件自己起一个回环端口(`
|
|
150
|
+
| **`loopback`(默认)** | 插件自己起一个回环端口(**默认 `port: 0` = 每次启动由系统分配随机端口**),右侧栏 iframe 跨源直连;**URL 带随机路径令牌**(`http://127.0.0.1:<port>/<token>/`,见下「回环端口的安全模型」);进程可被 adopt(DSH host 重启后接管) | 无 |
|
|
150
151
|
| **`dsh`** | IDE 挂到 **DSH 自己的 HTTP 端口**上的 `/code-server/*`(HTTP prefix 路由)+ `/code-server/<quality>-<commit>`(WS 精确路由),转发到 launcher 的**命名管道**;**没有额外端口**;每条请求(含 WS 握手)先过 `ctx.connection.requestRejection()` —— 与 `/api` 同一套 Host/Origin fence + 浏览器 cookie 认证 | DSH 提供 `webServer` 服务(web profile);desktop 无此服务 → 自动回退 loopback |
|
|
151
152
|
|
|
153
|
+
### 回环端口的安全模型(0.2.14 起)
|
|
154
|
+
|
|
155
|
+
`loopback` 是 desktop 端唯一的通路(无 webServer、无同源挂载),所以它单独加固了一层:
|
|
156
|
+
|
|
157
|
+
- **随机端口**:`port` 默认 `0` → 由系统分配空闲端口,launcher 把**实际**端口写进 `$DSH_HOME/code-server/endpoint.json`,
|
|
158
|
+
host 读回(因此端口每次都变、也不存在"8090 被占用"这类冲突)。要固定地址就显式配 `port`。
|
|
159
|
+
- **路径令牌**:每次**新启动**生成 32 位随机令牌(`[0-9A-Za-z_-]`,24 字节随机),写在
|
|
160
|
+
`$DSH_HOME/code-server/path-token`(用户 profile 下,默认 ACL 仅本人可读),成为 URL 的路径前缀。
|
|
161
|
+
没有这个前缀的请求一律 **404**(不泄露"这里跑着 IDE"),前缀不带结尾斜杠会 302 补上。
|
|
162
|
+
- **为什么不用 VS Code 自带的 `connection-token`**:它靠 `?tkn=` → 302 + `Set-Cookie: vscode-tkn; SameSite=Lax`;
|
|
163
|
+
而 desktop 的 iframe 是**跨源**的(`dsh-app://` → `127.0.0.1`),Lax cookie 在跨站子框架里不会被带上
|
|
164
|
+
→ 会让 IDE 直接打不开。路径前缀不需要 cookie:workbench 的资源与 WS 全部由 `location.pathname` 派生
|
|
165
|
+
(`serve: dsh` 挂在 `/code-server/` 下已验证同一机制),前缀天然跟随每个子请求与 WS 握手。
|
|
166
|
+
(已用真实 Edge + CDP 在**跨源 iframe** 里验证:随机端口 + 令牌下 workbench 正常渲染并建立 WS。)
|
|
167
|
+
- **Host 白名单**:回环模式只接受 `127.0.0.1 | localhost | [::1] : <实际端口>`。挡的是 DNS rebinding ——
|
|
168
|
+
这类攻击构造的请求可以不带 `Origin`,只靠 `Origin == Host` 那条检查拦不住。
|
|
169
|
+
- **`Referrer-Policy: no-referrer`**:令牌在路径里,不能让它在加载站外资源时经 `Referer` 漏出去。
|
|
170
|
+
- **令牌不落 argv、不进日志**:命令行对本机任意进程可见,所以走文件传递;日志里只打印"已启用"。
|
|
171
|
+
|
|
172
|
+
边界(说清楚,不夸大):这一层挡的是"本机其它应用/端口扫描器/浏览器页面"顺手访问你的 IDE;
|
|
173
|
+
**同用户的本地恶意程序**本来就能直接读你的文件、也能读那个令牌文件 —— 那不在本插件的威胁模型内。
|
|
174
|
+
|
|
152
175
|
- 在 `cordis.patch.yml` 的 `config.serve`(或设置文档里的 `code-server.serve`)切换,下次启动生效 —— **设置卡片不提供这一行**(卡片只有认领类型/打开即全屏/后台常驻三个设置)。
|
|
153
176
|
- `dsh` 模式的实际收益:单一 URL/单一端口(远程访问 DSH 即可用 IDE)、不再暴露额外回环端口、认证与 DSH 同级。
|
|
154
177
|
- `dsh` 模式的两点**已知取舍**:
|
|
@@ -161,6 +184,70 @@ DSH 用**资源地址**命名文件,`openFile` 只负责把地址交给右侧栏
|
|
|
161
184
|
(含 `Forwarded: host=` / `X-Forwarded-Host` 的反代语义),否则回 `403`;缺 `Origin` 的非浏览器请求放行。
|
|
162
185
|
没有这道检查时,本机任意浏览器页面都能对 `ws://127.0.0.1:<port>/stable-<commit>` 完成握手并驱动 IDE。
|
|
163
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
|
+
|
|
164
251
|
## 旧版 DSH(0.2.3 起不再支持)
|
|
165
252
|
|
|
166
253
|
**行为**:探测不到 `sidebarRightTabs` / `sidebarRight` 时,插件只注册一张设置卡片,内容是:
|
|
@@ -261,6 +348,21 @@ pnpm run promote -- <version>
|
|
|
261
348
|
> **秒级 no-op**,所以日常只改插件代码的话直接 `pnpm pack` 即可(不会偷偷升级 VS Code)。
|
|
262
349
|
> 升级树必须显式 `pnpm run vendor:latest`(或 `--force`/`--version`),并重发子包。
|
|
263
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
|
+
|
|
264
366
|
## 安装插件(一条命令;依赖全部由包管理器装好)
|
|
265
367
|
|
|
266
368
|
```powershell
|
|
@@ -441,12 +543,13 @@ host 探测顺序:`@jinsiyu/dshcs-vscode-server/vscode`(**0.2.0+ 正式布局**)
|
|
|
441
543
|
| `serve` | `loopback` | 服务方式:`loopback`(独立回环端口,iframe 跨源)→ `dsh`(挂到 DSH 自身端口的 `/code-server/*`,转发到命名管道,复用 DSH 的 Host/Origin + cookie 防护)。需 DSH 提供 `webServer`,缺失时自动回退 loopback |
|
|
442
544
|
| `bin` | `''`(空 = 用自带 launcher) | 逃生舱:显式指定外部 code-server 可执行文件 / `out/node/entry.js` 时退回旧模型(不经 `lib/launcher.mjs`) |
|
|
443
545
|
| `host` | `127.0.0.1` | loopback 模式的绑定地址(仅允许回环) |
|
|
444
|
-
| `port` | `
|
|
445
|
-
| `auth` | `none` | 固定 `none`(0.2.0 起 argon2
|
|
546
|
+
| `port` | `0` | loopback 模式的端口;**`0` = 每次启动由系统分配随机端口**(实际端口写在 `endpoint.json`,host 读回)。显式给端口则固定使用;该端口被占用且无有效 `pid.json` 时报错并给诊断(拒绝误杀) |
|
|
547
|
+
| `auth` | `none` | 固定 `none`(0.2.0 起 argon2 已移除);回环模式的访问控制由**随机端口 + 路径令牌 + Host 白名单**承担(见「回环端口的安全模型」),对外访问请用 `serve: dsh` |
|
|
446
548
|
| `userDataDir` | `$DSH_HOME/code-server/user-data` | 用户数据隔离目录 |
|
|
447
549
|
| `extensionsDir` | `$DSH_HOME/code-server/extensions` | 扩展目录 |
|
|
448
550
|
| `locale` | `''` | 界面语言(空 = 跟随浏览器),如 `zh-cn` |
|
|
449
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` 可**即时**开关 |
|
|
450
553
|
|
|
451
554
|
用户级覆盖示例(写在 `$DSH_HOME/profiles/web/cordis.patch.yml`,应使用 `- id: code-server` 行覆盖):
|
|
452
555
|
|
|
@@ -466,11 +569,18 @@ desktop profile 由 `apps/desktop-host` 把 `/api/*` 交给同一个 `createShar
|
|
|
466
569
|
|
|
467
570
|
| 方法 | 路径 | 说明 |
|
|
468
571
|
|---|---|---|
|
|
469
|
-
| GET | `/api/code-server/status` | `{ ok, running, status, host, port, pid, cwd, launchCwd, url, version, error, logTail, adopted }`(另含 `env` 环境检测与 `setup` 兼容字段;`cwd` = 当前 workbench 目录,`launchCwd` =
|
|
470
|
-
| POST | `/api/code-server/start` | body `{ cwd? }`(省略 cwd 不切换工作目录);幂等;运行中切 cwd = **只换目录不重启进程**(0.2.12) |
|
|
572
|
+
| GET | `/api/code-server/status` | `{ ok, running, status, host, port, pid, cwd, launchCwd, url, version, error, logTail, adopted }`(另含 `env` 环境检测与 `setup` 兼容字段;`cwd` = 当前 workbench 目录,`launchCwd` = 进程启动目录,loopback 下 `port` = **实际**端口、`url` = 含路径令牌的完整地址) |
|
|
573
|
+
| POST | `/api/code-server/start` | body `{ cwd? }`(省略 cwd 不切换工作目录);幂等;运行中切 cwd = **只换目录不重启进程**(0.2.12);loopback 新启动会**轮换端口与路径令牌**(0.2.14) |
|
|
471
574
|
| POST | `/api/code-server/stop` | 停止并回收进程树 |
|
|
472
575
|
| POST | `/api/code-server/setup` | **兼容空操作**:0.1.36 起依赖由包管理器安装,调用只重新自检 `env` 并返回 |
|
|
473
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 的协同」。
|
|
474
584
|
|
|
475
585
|
> 插件不再注册 `/code-server/*` 这类 webServer 专有路由;code-server 图标已内联为 data URI(client bundle 内),
|
|
476
586
|
> 因此客户端不请求任何插件自有 HTTP 资源。
|
|
@@ -522,6 +632,15 @@ desktop profile 由 `apps/desktop-host` 把 `/api/*` 交给同一个 `createShar
|
|
|
522
632
|
|
|
523
633
|
## 已知限制
|
|
524
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
|
+
|
|
525
644
|
- ~~子路径不支持~~ **已不成立(0.2.0 实测更正)**:VS Code 渲染出的 workbench HTML 里
|
|
526
645
|
**资源引用全是相对路径**(实测 9 条引用中绝对路径 0 条,`serverBasePath="."`、`rootEndpoint="."`),
|
|
527
646
|
客户端 WebSocket 路径由 `location.pathname + join(serverBasePath ?? '/', <quality>-<commit>)` 拼成,
|
|
@@ -539,5 +658,8 @@ desktop profile 由 `apps/desktop-host` 把 `/api/*` 交给同一个 `createShar
|
|
|
539
658
|
切标签/收起侧栏再回来**不重载**。不支持 `moveBefore` 的浏览器退回旧行为(`appendChild` → 整页重载),
|
|
540
659
|
状态里以 `degraded` 明示;详见下方「为什么切标签不再重载」。
|
|
541
660
|
- **远程访问**:`serve: dsh` 下浏览器只需能到达 DSH 本身(单一端口,认证与 `/api` 同级);
|
|
542
|
-
`serve: loopback`
|
|
543
|
-
(0.2.0 起不再支持 `auth: password`)。
|
|
661
|
+
`serve: loopback` 仅回环绑定(随机端口 + 路径令牌 + Host 白名单,见「回环端口的安全模型」),
|
|
662
|
+
跨机访问请改用 `serve: dsh`(0.2.0 起不再支持 `auth: password`)。
|
|
663
|
+
- **回环模式的令牌会随实例轮换**:每次新启动端口与令牌都变;`adopt`(host 重启后接管存活实例)靠
|
|
664
|
+
`endpoint.json` + `path-token` 两个文件对上,所以**别手动删**这两个文件(删了 host 认不出旧实例,
|
|
665
|
+
会当成陌生端口占用处理)。
|