# DSH File Explorer — Specification (v2, implemented) Plugin for the DeepSeek Harness (DSH) that adds to the web GUI a **file explorer + code editor** in VS Code style: session workspace tree with full CRUD, Monaco editor with line numbers, multiple tabs, syntax highlighting with real VS Code TextMate grammars (Seti icon theme + Dark+/Light+ themes), agent integration (quick action) and locale following the GUI. --- ## 1. Overview | Item | Decision | |---|---| | Type | npm package installable in the DSH profile via `dsh plugin` (bundle + client) | | Server half | Cordis plugin (patch `cordis.patch.yml`) with the RPC file service | | Client half | Bundle **hand-written** in the `window.__ModuleLoader__.load({id, factory})` format — **no build step** (zero toolchain dependencies) | | File access | Direct server↔client RPC (channel `/explorer`), **not** routed through the LLM | | Tree root | Current session workspace (session cwd); no session → open/create workspace flow | | Permissions | Honors the session sandbox: every operation confined to the workspace root | | Editor | Monaco Editor (AMD build served by the plugin itself) | | Highlighting | Real VS Code TextMate grammars (via `vscode-textmate` + `vscode-oniguruma` WASM) + merged Dark+/Light+ themes | | Icons | VS Code **codicon** font (UI/folders) + **Seti** icon theme (files, VS Code's default) | | UI placement | Panel **docked as a real column of the app grid** (resizes the chat), collapsible, resizable and **movable** (left/right) | | Language | Follows the active GUI locale (dictionaries `pt`, `en`, `zh`) | | Author | dgadelha1 | | Repository | https://github.com/dgadelha1/dsh-explorer-plugin | | License | MIT | ## 2. Package structure ``` dsh-explorer-plugin/ ├── package.json # dsh.bundle.patch + dsh.client + exports ├── cordis.patch.yml # inserts the server plugin line ├── LICENSE # MIT ├── SPEC.md # this document ├── lib/ │ ├── index.js # server plugin (ESM): RPC, static routes, SSE/watcher │ └── client.js # client bundle (CJS factory of __ModuleLoader__) — single source, no build ├── src/ # source copies (exports ./src/*) kept in sync ├── scripts/ │ ├── vendor.mjs # downloads assets into vendor/ (idempotent; pinned versions) │ ├── merge-themes.mjs # JSONC -> strict JSON + merges the themes' include chain │ ├── sync.mjs # copies src/ -> lib/ (--check fails if they diverge; runs at prepack) │ ├── server-test.mjs # server regression test (sandbox/allowlist, caps, crash-free watcher) │ ├── smoke-client.cjs # bundle smoke test (loader stub in Node) │ └── syntax-test-driver.cjs # headless TextMate pipeline test (puppeteer + Firefox) └── vendor/ # assets served at runtime (committed to the repo) ├── monaco/ # monaco-editor (min AMD build; source maps removed) ├── onig/ # vscode-oniguruma (onig.wasm + UMD loader) ├── textmate/ # vscode-textmate (CJS/UMD release) ├── grammars/ # official .tmLanguage.json + manifest.json (scope → file) ├── themes/ # dark_plus.json / light_plus.json (strict JSON, merged) ├── codicon/ # VS Code codicon font (UI + folders) └── seti/ # seti font + vs-seti-icon-theme.json (file icons) ``` ### 2.1 package.json metadata ```jsonc { "name": "dsh-explorer-plugin", "type": "module", "main": "lib/index.js", "exports": { ".": "./lib/index.js", "./client": "./lib/client.js", "./src/*": "./src/*", "./cordis.patch.yml": "./cordis.patch.yml", "./package.json": "./package.json" }, "dsh": { "bundle": { "patch": "./cordis.patch.yml" }, "client": { "platform": "web", "inject": [ "@deepseek-ai/dsh-client-runtime", "@deepseek-ai/dsh-client-connection", "@deepseek-ai/dsh-client-ui-layout", "@deepseek-ai/dsh-client-ui-theme", "@deepseek-ai/dsh-client-locale" ] } } } ``` The server plugin exports `{ name: 'explorer', inject: ['webServer', 'connection'], apply(ctx) }`. ### 2.2 cordis.patch.yml ```yaml - insert: - id: explorer name: 'dsh-explorer-plugin' ``` ## 3. Server half (`lib/index.js`) ### 3.1 RPC channel `/explorer` Registered with `ctx.connection.rpc.handle('/explorer', handler, { authority: 'loopback' })`. > The channel **cannot contain an inner `/`** (`CHANNEL_PATTERN = /^\/[A-Za-z0-9._~-]+$/`) — hence `/explorer` and not `/rpc/explorer`. Handler `(endpoint, payload, signal) → RpcResult`. The client calls `ctx.connection.rpc.call('/explorer', endpoint, payload)` → `POST /explorer/`. Endpoints (all with `{root, …}`; paths always relative to the root): | endpoint | payload | return | |---|---|---| | `fs/stat` | `{root, path}` | `{exists, path, name, isDir, size, mtimeMs, hidden}` (missing → `{exists:false}`) | | `fs/list` | `{root, path, includeHidden}` | `{path, entries:[{name,path,isDir,size,mtimeMs,hidden}]}` (folders first, name-sorted; dotfiles filtered by `includeHidden`) | | `fs/read` | `{root, path}` | `{content, size, mtimeMs}` — binary → `{binary:true}`; > 2 MB → `{tooLarge:true, size}` | | `fs/readLarge` | `{root, path}` | content up to **50 MB** (above → `{tooLarge:true, size}`; used to open read-only) | | `fs/write` | `{root, path, content}` | `{written, mtimeMs, size}` (atomic temp+rename write with `O_EXCL` and temp cleanup; mkdir -p of parent; **payload capped at 50 MB**) | | `fs/create` | `{root, path, kind:'file'\|'dir'}` | `{path}` (fails with `directory-exists` if it already exists) | | `fs/rename` | `{root, path, newName}` | `{path}` (same directory) | | `fs/move` | `{root, path, targetDir}` | `{path}` (another directory; collision → `directory-exists`) | | `fs/delete` | `{root, path}` | `{deleted:true}` (file or recursive folder; root is locked) | Rules: - **Confinement/sandbox**: `path.resolve(root, …)` + prefix check; existing paths go through `realpath` of the deepest ancestor (blocks symlinks that escape the root). Escaping → `bad-request`. Reads re-confirm the file's `realpath` immediately before I/O (reduced TOCTOU window). - **Root validated server-side (not trusted from the client)**: the `root` sent by the client must be the canonical cwd of a live session or a workspace registry path — otherwise `bad-request`/`403`. This prevents reading/writing arbitrary directories (`/`, `/etc`, `~`) through the loopback API. The RPC channel is already CSRF-protected by the platform (`isTrustedApiRequest`: loopback Host + Origin/same-site). - `root` validated as an existing directory on every call. - Error codes only from the shared RPC schema (`bad-request`, `directory-exists`, `directory-unreadable`, `internal`) — the client schema rejects unknown codes. - Binary detected by a NUL byte within the first 8 KB. ### 3.2 Web routes (webServer) | route | type | function | |---|---|---| | `/explorer-assets` | prefix | serves `vendor/` with correct MIME and `Cache-Control: no-cache` | | `/explorer/events` | exact | watcher **SSE**: `data: {"type":"fs","root":...,"events":[...]}` (25 s heartbeat; 503 without watcher) | ### 3.3 Watcher - `fs.watch(root, {recursive:true})` (Node ≥ 20, inotify) with ~120 ms debounce; non-recursive fallback if recursive fails. - **Watcher `error` handled**: an `FSWatcher` without an `error` listener crashes the whole Node process (happened in production). The handler now closes the watcher, wakes the SSE clients once (refresh), and schedules **a single recreation** after 2 s — the server never crashes. - One instance per active root, shared among SSE connections (refcount per client). - Events grouped → broadcast to that root's clients; the client refreshes the tree with debounce. ## 4. Client half (`lib/client.js`) ### 4.1 Registration and architecture - Bundle in the `window.__ModuleLoader__.load({id:'dsh-explorer-plugin', factory})` format, exporting `apply` + `inject`. - `inject` (services): `['slots','layout','connection','sessions','workspaces','locale','theme']`. - `apply(ctx)`: registers `explorer` dictionaries (pt/en/zh) and the `ExplorerPanel` component in the `shell.overlay` (list, root) slot of `ui-layout`. - Runtime dependencies of the bundle: only `react` (via `require`); everything else via `ctx` services. CSS injected via `