dsh-allowed-workspace-roots
Verifieddsh-allowed-workspace-roots · v1.0.0 · MIT · Web UI
DSH Desktop plugin: allow selected non-NTFS roots (e.g. an rclone/WinFsp SFTP mount) as workspaces, and repair fs.realpath on such volumes.
Install
dsh plugin add dsh-allowed-workspace-roots Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Readme
dsh-allowed-workspace-roots
Плагин для DSH Desktop: разрешает использовать как рабочие области папки на
томах, которые штатная политика считает неподходящими, и чинит realpath на
таких томах.
Эталонный случай — корпоративный диск, смонтированный rclone через WinFsp
(например SFTP-сервер): Windows показывает его как обычный фиксированный диск,
но имя файловой системы у него не NTFS/ReFS, и нативный realpath на нём не
работает.
Что именно чинится
WinFsp не поддерживает GetFinalPathNameByHandleW при монтировании буквой
диска:
GetFinalPathNameByHandleW('<том>\каталог') → GetLastError = 1005
(ERROR_NO_UNICODE_TRANSLATION)
в Node это код UNKNOWN
GetFinalPathNameByHandleW('C:\Users') → OK
При этом stat, opendir и синхронный fs.realpathSync (JS-реализация, без
libuv) на том же томе работают. Ломается только нативный вариант.
DSH канонизирует пути через realpath в нескольких независимых местах, и без
плагина отказ выглядит как последовательность ошибок:
| Что делает пользователь | Что происходит без плагина |
|---|---|
| добавляет папку на томе как рабочую область | FUSE-RCLONE cannot safely host a DSH Desktop workspace |
| повторяет после обхода проверки тома | workspace/invalid-path: ... UNKNOWN: unknown error, realpath |
| отправляет первый запрос в сессии | UNKNOWN: unknown error, realpath из слоя файлов |
| перезапускает DSH | сессия исчезает из списка |
Как устроено
Три независимых патча. Все три нужны при любой политике файлов — они не про песочницу.
1. Клиентская половина (lib/client.js). Штатная проверка тома живёт в
клиентской половине Desktop-плагина и на каждом вызове читает функцию из
window.__DSH_DESKTOP_VALIDATE_DIRECTORY__. Плагин подменяет это свойство
аксессором: get возвращает обёртку, которая для путей из allow-list отвечает
«разрешено» сразу, а для всех остальных вызывает настоящий валидатор DSH;
set перехватывает настоящий валидатор, когда Desktop-плагин его
устанавливает, поэтому порядок загрузки не важен.
2. Реестр рабочих областей (lib/registry-patch.mjs). Оборачивает
create, resolveByPath, indexHeader и attachSession, подменяя
канонизацию на JS-реализацию realpathSync — только для путей из allow-list.
create дальше идёт ровно как в оригинале, минуя realpath.
attachSession живёт на сущности рабочей области, поэтому патчится её
прототип.
Отдельно: реестр индексирует заголовки сохранённых сессий при своём старте, то есть раньше, чем плагин получит управление. На «сломанном» томе сессии помечаются недействительными и после перезапуска пропадают из списка, хотя во время работы видны. Поэтому сразу после установки патча индексация прогоняется заново.
3. Слой файлов (lib/fs-patch.mjs). dsh-fs-local канонизирует цель через
realpath и пробрасывает UNKNOWN наружу, потому что обрабатывает только
ENOTDIR и ENOENT. Канонизация там вызывается ровно в двух местах, поэтому
хватает двух методов: resolve — единственная точка входа для путей, и
резолв детей при листинге каталога. Все остальные методы (stat, readText,
writeText, editText) принимают уже готовую цель, где targetKey и есть
путь для ввода-вывода.
resolve обязан переживать ещё не существующий файл — иначе создание
файла падает с ENOENT. Для этого канонизируется ближайший существующий
предок, а недостающий хвост приклеивается обратно.
Почему не подмена модуля node:fs/promises
Естественный вопрос: почему бы просто не подменить realpath в
node:fs/promises и не закрыть все вызовы разом? Пробовали, в этом окружении
не работает:
- правка объекта
fs.promisesне помогает —dsh-app-bootимпортируетnode:fs/promisesзадолго до плагинов, а ESM-фасад фиксирует нативную функцию в момент первого импорта; - хук загрузчика (
module.registerHooks) помогает, но обязан встать раньше первого импорта, а плагин загружается позже — проверено счётчиком модулей:dsh-workspace среди них=false; - предзагрузка через
NODE_OPTIONSуспевает (среди них=true), но переменная окружения до процесса хоста не доходит: хост DSH — это utility-процесс внутри Electron-приложения, а не процесс, запускаемый через сгенерированныйdsh.cmd. Правка самогоdsh.cmdне переживает перезапуск: приложение сверяет содержимое генерации и создаёт новую.
Поэтому вместо одного общего шима — три точечных патча, которые работают независимо от порядка загрузки.
Установка
dsh plugin --profile desktop add dsh-allowed-workspace-roots
Или из скачанного архива:
dsh plugin --profile desktop add ./dsh-allowed-workspace-roots-1.0.0.tgz
После установки нужен полный перезапуск DSH Desktop: граф клиентских бандлов собирается при старте хоста.
Настройка
Список разрешённых корней задаётся в cordis.patch.yml пакета:
- insert:
- id: allowed-workspace-roots
name: 'dsh-allowed-workspace-roots'
config:
roots:
- 'T:\'
# - 'T:\Shared' # пример сужения области
Сравнение идёт по границе сегмента пути: корень T:\Shared не пропустит
T:\SharedOther. Регистр, прямые слэши и завершающий разделитель значения не
имеют. После правки конфигурации нужен перезапуск DSH.
Для всего, что не входит в allow-list, поведение DSH не меняется: решение принимает штатный валидатор.
Ограничения
Нужен режим «Полный доступ» (danger-full-access). При
workspace-write команда выполняется отдельным процессом — ACL-runner'ом
песочницы, — а до него внутрипроцессные патчи не достают. При
danger-full-access dsh-sandbox-local не вызывает confine() вовсе, runner
не запускается, и этот слой исчезает.
Учтите цену: песочница ограничивает, куда агент может писать. При полном доступе он пишет всюду, куда может писать пользователь.
Патчи опираются на внутренности DSH. Проверено на DSH Desktop 2.0.9. После обновления DSH стоит пройти сценарий заново: добавить папку, создать сессию, отправить запрос. Отказ безопасен — пользователь просто увидит прежнюю ошибку.
Сам том остаётся сетевым. Плагин чинит канонизацию путей, но не меняет природу монтирования: при обрыве связи с сервером рабочая область виснет, блокировки и атомарные переименования ненадёжны, а VFS-кэш rclone делает обход больших каталогов заметно медленнее локального диска.
Не используйте --network-mode как альтернативу. Он действительно чинит
realpath, но переводит монтирование в общее на машину пространство имён:
имя шары видно через net use из чужого сеанса, и пользователи получают
доступ к файлам друг друга. Изоляция букв диска этого не допускает.
Проверка
node test/verify.mjs
41 проверка: логика allow-list, клиентская половина в обоих порядках загрузки,
патч реестра и патч слоя файлов. Часть проверок идёт по настоящему тому —
подделка не воспроизведёт сломанный realpath. Корень задаётся переменной
DSH_TEST_ROOT (по умолчанию T:\); если тома нет или он обычный NTFS, эти
проверки пропускаются с пояснением.
Если Node нет, тесты запускаются встроенным в DSH Desktop:
$env:ELECTRON_RUN_AS_NODE='1'
& 'C:\Program Files\DSH Desktop\DSH Desktop.exe' test/verify.mjs
Внутри запущенного DSH есть и самопроверка хоста: переменная
DSH_ALLOWED_ROOTS_PROBE=1 заставляет плагин сразу создать рабочую область,
разрешить путь и перечислить каталог, записав результат в лог.
Состав
| Файл | Назначение |
|---|---|
lib/client.js |
патч 1: снятие проверки тома для путей из allow-list |
lib/registry-patch.mjs |
патч 2: реестр рабочих областей |
lib/fs-patch.mjs |
патч 3: слой файлов |
lib/roots.mjs |
общая логика allow-list |
lib/index.js |
host-половина: ставит патчи 2 и 3 |
cordis.patch.yml |
строка плагина в дереве загрузчика + allow-list |
test/verify.mjs |
автотесты |