dsh-better-sandbox
Verified@rkfg/dsh-better-sandbox · v0.4.1 · MIT
Sandbox rules for DeepSeek Harness: extra writable roots, write-protected path names with a narrow escalation level for them, read-protected paths.
Install
dsh plugin add @rkfg/dsh-better-sandbox Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
@rkfg/dsh-better-sandbox
Бандл DSH: расширяет песочницу дополнительными корнями записи, запрещает изменять
.git и закрывает от чтения каталоги и файлы с секретами. Работает в трёх
контурах: файловые инструменты (ctx.fs), команды через bash (bwrap), поиск
через упакованный ripgrep. Русская копия этого файла; первичен README.md.
Состав
| Модуль | Роль |
|---|---|
lib/rules.js |
сервис betterSandbox: эффективные списки правил, отбраковка неприменимых (rejected), маска mode 000 |
lib/escalation.js |
вписывает именованный уровень эскалации в словарь харнесса: таблицу «строже шире» и enum sandbox_permissions инструментов |
lib/fs.js |
провайдер fs: забор на запись + запрет чтения |
lib/runner.js |
провайдер sandbox: аргументы bwrap для команд |
lib/subprocess.js |
провайдер subprocess: оборачивает в bwrap только ripgrep (иначе grep/glob читают закрытое) |
lib/masks.js |
общие аргументы bwrap: их собирают и раннер, и обёртка ripgrep; расхождение означало бы дыру в защите |
lib/policy.js |
канонизация путей, поиск защищённых каталогов с ограниченным обходом и кэшем |
lib/guard-api.js |
Remote-сервис sandboxGuard: state и validate |
lib/bundle.js |
группа записей бандла (cordis.group) |
cordis.patch.yml — слой бандла: гасит базовые fs-sandbox, sandbox,
subprocess и вставляет свои строки. Значения списков он не задаёт.
Раздел настроек
Правила правятся в веб-UI: диалог настроек → раздел Sandbox (четыре поля со
списками, по строке на значение, и одно поле с именем уровня эскалации) → «Save». Раздел приносит отдельный бандл
@rkfg/dsh-better-sandbox-ui: пакет с клиентской половиной обязан иметь ровно одну
запись загрузчика, поэтому он не может жить внутри @rkfg/dsh-better-sandbox.
Подписи раздела существуют в en и zh; по-русски они появятся вместе с
русской локализацией самого харнесса.
Клиентский рукав регистрирует слот settings.section и работает через штатный
слой настроек: ctx.remote.settings.describe() (без аргументов, ответ —
{ namespaces: [{ ns, value, revision, schema }] }) и
ctx.remote.settings.update('better-sandbox', patch, revision). Хостовая
запись пакета — пустой сервис: вся логика в клиенте и в слое настроек.
Собственного ctx.remote-пространства имён у клиента сознательно нет: согласно
cookbook/adding-a-remote-api.md клиент обязан объявить в inject имя сервиса
remote.<namespace>, а это пространство появляется из генерируемых артефактов
./typert и ./remote, которых у рукописного JS-пакета нет. Поэтому правка идёт
через ctx.remote.settings — тот же путь, которым пользуется сам диалог настроек.
Установка
Из корня репозитория:
./scripts/install.sh # профиль web
./scripts/install.sh --home DIR --profile ИМЯ # другой DSH_HOME и профиль
./scripts/install.sh --drop СТАРОЕ_ИМЯ # вычистить прежнее имя пакета
Скрипт кладёт оба пакета в node_modules профиля, дописывает их в
dsh.profile.bundles (с копией package.json), проверяет синтаксис и пересоздаёт
заглушки маски.
Штатный путь установки плагина — dsh plugin --profile web add <package>.
Скрипт существует потому, что пакеты лежат рядом с исходниками в workspace,
требуют фиксации версий харнесса и пересоздания заглушек маски сразу после
rm -rf. После него нужен рестарт профиля: новую строку загрузчик в запущенный
процесс не добавляет.
В профильном cordis.patch.yml остаются только значения:
- id: better-sandbox
name: '@rkfg/dsh-better-sandbox/rules'
config:
writableDirs: ["/opt/go", "/home/you/.cache"]
readDenyDirs: ["/home/you/.ssh", "/home/you/.aws"]
readDenyFiles: ["/home/you/.npmrc", "/home/you/.netrc"]
Правила
| Поле | Значение | Правка без рестарта |
|---|---|---|
writableDirs |
каталоги вне рабочей папки, куда разрешена запись | страница «Sandbox» |
denySegments |
имена путей, запрещённые к изменению (.git) |
страница «Sandbox» |
readDenyDirs, readDenyFiles |
закрыто от чтения (маска mode 000) | страница «Sandbox» |
denyDepth, denySkipDirs, denyScanTtlMs |
обход при поиске .git для bwrap |
нет |
maskFallback, readDenialSignature |
поведение раннера | нет |
Все четыре списка помечены .volatile(): харнесс отдаёт их как ссылки { get() }
(cosmokit), обновляет без перемонтирования записи, а значение сохраняется в
profile patch. Единственный источник правил — config строки better-sandbox:
списки приходят из слоя настроек и лежат в profile patch.
Программная правка — settings.update('better-sandbox', patch, revision)
(так делает страница настроек). Вызывать её нужно вне стека активации записи:
редактор конфигурации берёт лок на package.json профиля, который стартовый
проход удерживает, и вызов из apply() упирается в таймаут блокировки.
Что закрыто и где границы
.git: чтение свободно (status,log,diff), запись → EROFS. Имя сегмента проверяется на любой глубине вctx.fs; в bwrap — доdenyDepth, глубже нужна правкаdenyDepth.- Репозиторий, созданный внутри самой команды, закрыт начиная со следующей команды.
rm -rfзащищённого каталога требует эскалации.danger-full-accessснимает защиту — это и есть штатный путь, когда файл нужен.- Процессы вне песочницы не покрыты: MCP-серверы и git-интеграция харнесса через
ctx.subprocessидут pass-through. - git по SSH внутри песочницы не работает:
sshне читает~/.ssh. Варианты — эскалация, ssh-agent, вынесенныйknown_hosts. - Маска — bind пустого каталога или файла с режимом 000 → EACCES.
--tmpfsне подходит: запись в него «удаётся» и исчезает. - Удаление каталога с маской: сначала
chmod 0755на каталог, корень и заглушку. - Заглушки маски лежат в папке плагина. Если их стерли во время работы (так делает
переустановка пакета),
confineне падает: наличие заглушек проверяется перед каждой командой, маска восстанавливается сама, а до восстановления закрытые пути прячутся запасным вариантом изmaskFallback.
Проверка
Всё внутри workspace, без эскалаций и без риска для рабочего профиля:
./scripts/check.sh # базовый контур: basetest и allowtest
DSH_BUNDLE=1 DSH_PROBE=driver ./scripts/webfull.sh # полный веб-профиль с бандлом
Профили живут в .dsh-test/ (свой DSH_HOME, свой порт). Копирование идёт из
исходников в тестовые профили, поэтому правки, сделанные внутри .dsh-test/,
пропадают при следующем прогоне. Для проверки чтения заведите canary-файл вида
~/.ssh/secret.key; реальные секретные файлы для проверки не читаются — отказ
должна выдавать сама песочница.
Model Experience
Контекст запроса: анонс правил песочницы
What the model sees
Секция sandbox:rules добавляется к контексту рантайма сразу после секции
политики песочницы (SANDBOX_POLICY) и существует, только если хотя бы один список
не пуст и announceInPrompt включён. Текст строится по четырём шаблонам; третий
появляется только при непустом списке защищённых имён и включённом уровне.
Verbatim text for this field
Additional write roots beyond the session workspace (workspace-write mode only): <список путей в кавычках, через запятую>.
Protected path names inside every writable root (workspace-write mode): <список имён в кавычках> — writing there is refused the same way as writing outside the workspace.
Escalation level "<токен>": when a write or a command is refused only because of one of those names, retry that exact call once with sandbox_permissions set to "<токен>" and a one-sentence justification. It opens every name above for that single call, with the user's approval; every other rule stays exactly as it is. Prefer it over a wider mode.
Protected from reading (secrets and personal data): <каталоги и файлы в кавычках, через запятую> — reading their contents is refused for tools and denied inside sandboxed commands. No escalation target of this plugin opens a read, so ask the user to change the rule when the file is genuinely needed.
Sandbox guards are NOT applied here: <причины>. The rules above still bind the file tools, but commands run without that part of the protection.
Последняя строка появляется только тогда, когда какой-то из швов confinement сообщил, что настроенная защита не действует (нераспознанный раннер, раннер, который не умеет правило, отсутствующий стаб маски), и исчезает, как только защита снова применяется.
Кроме того, lib/escalation.js дописывает одно предложение к описанию параметра
sandbox_permissions у bash, pwsh, write, edit: оттуда модель узнаёт, что
существует цель уже, чем danger-full-access. Другого канала нет — отказ write и
edit инструмент перезаписывает общим маркером с подсказкой, и текст плагина туда не
попадает.
Token effect
Conditional. Строки присутствуют только при непустых списках: одна строка на каждое непустое семейство правил, длина пропорциональна числу путей. Пустые списки дают ноль токенов.
KV Cache effect
Append-only относительно политики песочницы: секция добавляется после уже существующих строк контекста и не меняет их. Правка списков через слой настроек заменяет содержимое секции на ходу, поэтому переиспользование префикса сбрасывается от места этой секции и дальше; порядок строк внутри секции фиксирован (корни → имена путей → чтение), поэтому неизменные списки дают идентичный текст.
Отказ как текст модели
What the model sees
Косвенно, через dsh-tool-fs и dsh-tool-bash: отказ fence'а и refusal-сигнатура
раннера попадают в результат инструмента. Пакет поставляет текст причины отказа
чтения ("<путь>" is protected from reading by the better-sandbox policy) и
дополняет диалект bwrap сигнатурой permission denied, чтобы bash показал
политический отказ, а не системную ошибку. Подсказку об эскалации в отказе чтения
плагин намеренно не пишет: ни один инструмент не объявляет sandbox_permissions для
чтения, поэтому такая подсказка отправила бы модель в ошибку схемы.
Token effect
Zero-direct: пакет не добавляет текста в контекст, кроме уже учтённого выше. Появление текста отказа зависит от действия модели и оплачивается как результат инструмента.
KV Cache effect
Independent: сообщения об отказах принадлежат истории разговора, а не системному префиксу, и не влияют на переиспользование последних.
Known Limitations and Deferred Work
- Чтение эскалации не имеет —
read,globиgrepне объявляютsandbox_permissions, а методы чтения файлового провайдера вообще не получают политику. Уровень покрывает две мутации и confined-оболочки; открыть закрытый от чтения путь можно только сменой правила. - Имя уровня — словарь модели — одно строчное слово, не совпадающее с
read-only,workspace-write,danger-full-access; иначе плагин пишет предупреждение и выключает уровень, вместо того чтобы предлагать цель, которую некому выдать. - Постоянные оболочки помнят свой confinement — терминал и pwsh-сессия сохраняют правила, полученные при запуске; уровень действует на один вызов.
- Уровень предлагается там, где он действительно расширяет права — он означает
«те же права вызова, но защищённые имена открыты», поэтому добавляется в список
целей только сессии
workspace-write. Сессииread-onlyнуженworkspace-write, и она просит именно его: более узкая цель была бы карточкой, обещающей меньше, чем она даёт. - Глубина защищённых имён в bwrap ограничена
denyDepth— сегмент имени проверяется на любой глубине вctx.fs, но в confinement только доdenyDepth, и обход не спускается вdenySkipDirs(node_modules,dist, кэши). Репозиторий глубже для файловых инструментов закрыт, а внутри команды — нет: место обрыва видно в отчёте скана, а нужное дерево добавляется вdenyScanRoots. - Свежесозданное защищённое имя защищено для команд не мгновенно —
denyScanTtlMsзадаёт срок годности скана;0означает пересканировать корни перед каждой командой, платя синхронным обходом за каждую. - Неприменимое правило отвергается, а не приближается — корень файловой системы,
правило поверх файлов самого харнесса или внутри них, а также правило на чтение,
накрывающее рабочую папку, попадают в
rejected: их пишет лог хоста и возвращаетsandboxGuard.state(). - Стабы маски проверяются, но не защищены ядром — стаб является источником привязки, а привязка следует за симлинком-источником: подменённый стаб смонтировал бы то, на что он указывает, поверх защищённого пути. Каждая команда проверяет тип и режим стаба и пересобирает его — режим 000 сам по себе не защита, ведь владелец всегда вернёт себе запись. Если плагин установлен внутри рабочей папки, её обитатели могут писать в папку стабов, и страница настроек об этом сообщает.
- Грант на запись не может лежать внутри защищённого от чтения пути — маска
родителя спрятала бы грант, а грант, привязанный первым, спрятал бы родителя от
маски. Побеждает правило на чтение: грант отвергается по имени
(
inside-read-deniedвrejected), потому что отсутствующий грант заметен и поправим, а молча читаемый секретный путь — нет. - Правило на чтение закрывает чтение, но не запись — закрытый от чтения путь,
который к тому же записываем (
.envв рабочей папке), можно перезаписать, не читая. - Диалект отказа текстовый — отказ маски опознаётся как политический, когда вывод
команды называет защищённый путь. Команда, назвавшая тот же путь иначе (
../file, относительный путь), по-прежнему получает отказ маски, но доходит до модели как обычная ошибка;readDenialSignature: anyматчит вместо этого формулировку ядра — ценой того, что посторонние ошибки прав тоже станут «политическими отказами». - Landlock не выражает ни вычитание записи, ни запрет чтения подпути — при
выборе этого раннера
denySegmentsиreadDeny*не применяются; причина идёт в лог, на страницу настроек и в контекст модели вместо имитации защиты. - Процессы вне песочницы не покрыты: MCP-серверы и git-интеграция самого
харнесса через
ctx.subprocessидут pass-through и не должны ломаться. - git по SSH внутри песочницы неработоспособен, пока
~/.sshзакрыт от чтения: нужен эскалированный запуск, ssh-agent или вынесенныйknown_hosts. - Имена закрытых каталогов остаются видны в списке родителя: маска подменяет точку mode 000, а не фильтрует каталог-родитель.
- Установщик не пользуется
dsh plugin add: нужны фиксация версий и пересоздание заглушек маски до рестарта.