dsh-desktop-kit 2.7.7 → 2.7.8

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.7.8 (2026-11-12)
4
+
5
+ - **Permissions UI.** Настройки → **Права**: эффективный preset DSH (`current`), sandbox/approval, каталог пресетов, жёсткие правила kit и защищённые пути. Страница read-only, без второго picker и без хранения approvals.
6
+ - **Guardrails RPC.** `/api/dsh-guardrails/permissions`: `catalog/current/resolve/optionOf` + снимок политики; привязка к сессии и workspace.
7
+ - **UI.** 390/768/1440: overflow 0, console 0, markup 0; скриншоты 1440/390.
8
+ - **2.7.7** (staged) содержала MCP prompts; 2.7.8 включает и prompts, и Permissions UI.
9
+
3
10
  ## 2.7.7 (2026-11-12)
4
11
 
5
12
  - **MCP prompts.** Settings → MCP → **Prompts**: список промптов сервера, аргументы, запуск, результат и копирование. Новые RPC `prompts`/`prompt`; клиент поддерживает `prompts/list` и `prompts/get` с пагинацией.
package/README.md CHANGED
@@ -4,17 +4,17 @@
4
4
 
5
5
  Набор из **16 плагинов и 30 навыков** для DeepSeek Harness 0.2.x: русский интерфейс, автопилот навыков, токен-лаборатория, студия навыков, память проектов, обмен памятью между машинами (GitHub и USB), guardrails, карта репозитория, чекпоинты, plan mode, browser QA, GitHub review, Claude Code-подобные hooks/agents/commands, перенос чатов и резервные копии профиля.
6
6
 
7
- ![DSH Desktop Kit 2.7](https://unpkg.com/dsh-desktop-kit@2.7.4/docs/images/hero.png)
7
+ ![DSH Desktop Kit 2.7](https://unpkg.com/dsh-desktop-kit@2.7.8/docs/images/hero.png)
8
8
 
9
9
  ## MCP: подключения без JSON
10
10
 
11
11
  Настройки → **MCP**: пресеты GitHub, PostgreSQL и Filesystem, живые статусы, health, список tools и безопасное хранение токенов в `~/.dsh/mcp.secrets.json`. Промпты сервера открываются кнопкой **Prompts**: аргументы, запуск и копирование результата.
12
12
 
13
- ![MCP Settings](https://unpkg.com/dsh-desktop-kit@2.7.4/plugins/dsh-mcp-manager/screenshots/settings-1440.png)
13
+ ![MCP Settings](https://unpkg.com/dsh-desktop-kit@2.7.8/plugins/dsh-mcp-manager/screenshots/settings-1440.png)
14
14
 
15
- ![MCP на 390px](https://unpkg.com/dsh-desktop-kit@2.7.4/plugins/dsh-mcp-manager/screenshots/settings-390.png)
15
+ ![MCP на 390px](https://unpkg.com/dsh-desktop-kit@2.7.8/plugins/dsh-mcp-manager/screenshots/settings-390.png)
16
16
 
17
- ![Добавление MCP-сервера](https://unpkg.com/dsh-desktop-kit@2.7.4/plugins/dsh-mcp-manager/screenshots/modal-390.png)
17
+ ![Добавление MCP-сервера](https://unpkg.com/dsh-desktop-kit@2.7.8/plugins/dsh-mcp-manager/screenshots/modal-390.png)
18
18
 
19
19
  ## Права и субагенты
20
20
 
@@ -23,9 +23,11 @@
23
23
  - **Approval** — штатная карточка DSH: Allow once / Always allow / Reject.
24
24
  - **`.dsh/`** создаётся автоматически: agents, commands, rules, hooks, verify, mcp, PROJECT.md.
25
25
 
26
- ![Агенты проекта](https://unpkg.com/dsh-desktop-kit@2.7.4/plugins/dsh-agent-extensions/screenshots/agents-1440.png)
26
+ ![Агенты проекта](https://unpkg.com/dsh-desktop-kit@2.7.8/plugins/dsh-agent-extensions/screenshots/agents-1440.png)
27
27
 
28
- **v2.7.7** · MIT · проверено на DSH `0.2.1-rc.5` и `0.2.2-rc.1` (Windows 11) · без внешних зависимостей и телеметрии
28
+ ![Права и ограничения](https://unpkg.com/dsh-desktop-kit@2.7.8/plugins/dsh-guardrails/screenshots/permissions-1440.png)
29
+
30
+ **v2.7.8** · MIT · проверено на DSH `0.2.1-rc.5` и `0.2.2-rc.1` (Windows 11) · без внешних зависимостей и телеметрии
29
31
 
30
32
  **План развития:** [docs/ROADMAP-3.0.md](docs/ROADMAP-3.0.md) — Memory Fabric, семантическое
31
33
  слияние, локальный индекс памяти, шифрованные переносы, Memory Hub и npm trusted publishing.
@@ -113,27 +115,27 @@
113
115
  - перед каждой синхронизацией проверяется, что репозиторий всё ещё приватный;
114
116
  - секреты и PII вычищаются, домашние пути заменяются токеном `%DSH_HOME%`; LLM не вызывается — **0 токенов**.
115
117
 
116
- ![Настройки памяти проектов](https://unpkg.com/dsh-desktop-kit@2.7.4/plugins/dsh-memory-bridge/docs/images/settings-light.png)
118
+ ![Настройки памяти проектов](https://unpkg.com/dsh-desktop-kit@2.7.8/plugins/dsh-memory-bridge/docs/images/settings-light.png)
117
119
 
118
- ![Конфликты досье](https://unpkg.com/dsh-desktop-kit@2.7.4/plugins/dsh-memory-bridge/docs/images/conflicts.png)
120
+ ![Конфликты досье](https://unpkg.com/dsh-desktop-kit@2.7.8/plugins/dsh-memory-bridge/docs/images/conflicts.png)
119
121
 
120
122
  ## Скриншоты
121
123
 
122
124
  ### Skills Studio: каталог, стоимость и редактор
123
125
 
124
- ![Каталог навыков](https://unpkg.com/dsh-desktop-kit@2.7.4/plugins/dsh-gkb-ru-ui/screenshots/02-skills-1440-light.png)
126
+ ![Каталог навыков](https://unpkg.com/dsh-desktop-kit@2.7.8/plugins/dsh-gkb-ru-ui/screenshots/02-skills-1440-light.png)
125
127
 
126
- ![Редактор навыка](https://unpkg.com/dsh-desktop-kit@2.7.4/plugins/dsh-gkb-ru-ui/screenshots/03-skills-editor-1440-light.png)
128
+ ![Редактор навыка](https://unpkg.com/dsh-desktop-kit@2.7.8/plugins/dsh-gkb-ru-ui/screenshots/03-skills-editor-1440-light.png)
127
129
 
128
130
  ### Token Lab: куда уходит контекст
129
131
 
130
- ![Токен-лаборатория](https://unpkg.com/dsh-desktop-kit@2.7.4/plugins/dsh-token-lab/screenshots/01-token-lab-1440-light.png)
132
+ ![Токен-лаборатория](https://unpkg.com/dsh-desktop-kit@2.7.8/plugins/dsh-token-lab/screenshots/01-token-lab-1440-light.png)
131
133
 
132
134
  ### Тёмная тема и мобильный
133
135
 
134
- ![Тёмная тема](https://unpkg.com/dsh-desktop-kit@2.7.4/plugins/dsh-gkb-ru-ui/screenshots/04-skills-1440-dark.png)
136
+ ![Тёмная тема](https://unpkg.com/dsh-desktop-kit@2.7.8/plugins/dsh-gkb-ru-ui/screenshots/04-skills-1440-dark.png)
135
137
 
136
- ![Каталог навыков на 390 px](https://unpkg.com/dsh-desktop-kit@2.7.4/plugins/dsh-gkb-ru-ui/screenshots/05-skills-390-light.png)
138
+ ![Каталог навыков на 390 px](https://unpkg.com/dsh-desktop-kit@2.7.8/plugins/dsh-gkb-ru-ui/screenshots/05-skills-390-light.png)
137
139
 
138
140
  ## Окно-напоминание: пиковый тариф
139
141
 
@@ -143,11 +145,11 @@ DeepSeek включает двойной тариф в пиковые часы (
143
145
  - **в окне** — красная карточка: причина, границы окна в местном времени и UTC, живой отсчёт до конца и рекомендация, когда вернуться;
144
146
  - **после окна** — карточка исчезает; состояние всегда доступно через `window.__DSH_PEAK_GUARD__.state()`.
145
147
 
146
- ![Окно-напоминание: светлая тема](https://unpkg.com/dsh-desktop-kit@2.7.4/plugins/dsh-peak-guard/screenshots/03-peak-card-1440-light.png)
148
+ ![Окно-напоминание: светлая тема](https://unpkg.com/dsh-desktop-kit@2.7.8/plugins/dsh-peak-guard/screenshots/03-peak-card-1440-light.png)
147
149
 
148
- ![Окно-напоминание: тёмная тема](https://unpkg.com/dsh-desktop-kit@2.7.4/plugins/dsh-peak-guard/screenshots/04-peak-card-1440-dark.png)
150
+ ![Окно-напоминание: тёмная тема](https://unpkg.com/dsh-desktop-kit@2.7.8/plugins/dsh-peak-guard/screenshots/04-peak-card-1440-dark.png)
149
151
 
150
- ![Окно-напоминание на 390 px](https://unpkg.com/dsh-desktop-kit@2.7.4/plugins/dsh-peak-guard/screenshots/05-peak-card-390-light.png)
152
+ ![Окно-напоминание на 390 px](https://unpkg.com/dsh-desktop-kit@2.7.8/plugins/dsh-peak-guard/screenshots/05-peak-card-390-light.png)
151
153
 
152
154
  Подробности и настройки — [plugins/dsh-peak-guard/README.md](plugins/dsh-peak-guard/README.md).
153
155
 
package/client.js CHANGED
@@ -7530,7 +7530,220 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
7530
7530
  return module.exports;
7531
7531
  })());
7532
7532
 
7533
- const __partNames = ["dsh-gkb-ru-ui","dsh-token-lab","dsh-peak-guard","dsh-session-move","dsh-profile-transfer","dsh-memory-bridge","dsh-mcp-manager","dsh-agent-extensions"];
7533
+ /* ---- dsh-guardrails ---- */
7534
+ __parts.push((function () {
7535
+ var module = { exports: {} };
7536
+ var exports = module.exports;
7537
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7538
+ const React = require("react");
7539
+ const h = React.createElement;
7540
+ const useEffect = React.useEffect;
7541
+ const useState = React.useState;
7542
+
7543
+ const CSS = `
7544
+ .pr-root{--pr-line:var(--dsw-alias-border-l1,rgba(20,40,36,.13));--pr-soft:var(--dsw-alias-bg-layer-1,#f4f7f5);--pr-panel:var(--dsw-alias-bg-layer-2,#fff);--pr-text:var(--dsw-alias-label-primary,#17231f);--pr-muted:var(--dsw-alias-label-secondary,#61706b);--pr-accent:var(--dsw-alias-brand-primary,#0f766e);display:flex;flex-direction:column;gap:20px;max-width:1040px;min-width:0;color:var(--pr-text)}
7545
+ .pr-head{display:flex;align-items:flex-end;justify-content:space-between;gap:20px;flex-wrap:wrap}
7546
+ .pr-eyebrow{margin:0 0 6px;font-size:11px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--pr-accent)}
7547
+ .pr-title{margin:0;font-size:clamp(24px,2.6vw,32px);line-height:1.1;letter-spacing:-.025em;font-weight:680}
7548
+ .pr-sub{margin:8px 0 0;max-width:68ch;color:var(--pr-muted);font-size:14px;line-height:1.5}
7549
+ .pr-mode{padding:20px;border:1px solid var(--pr-line);border-radius:18px;background:linear-gradient(145deg,var(--pr-panel),var(--pr-soft));min-width:0}
7550
+ .pr-mode-top{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap}
7551
+ .pr-mode h3{margin:0;font-size:24px;line-height:1.1;letter-spacing:-.03em}
7552
+ .pr-chips{display:flex;gap:6px;flex-wrap:wrap}
7553
+ .pr-chip{padding:3px 8px;border:1px solid var(--pr-line);border-radius:999px;background:var(--pr-panel);font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--pr-muted);white-space:nowrap}
7554
+ .pr-chip.sandbox{color:#0f766e;border-color:rgba(15,118,110,.25);background:rgba(15,118,110,.07)}
7555
+ .pr-chip.approval{color:#9a3412;border-color:rgba(154,52,18,.22);background:rgba(154,52,18,.06)}
7556
+ .pr-mode p{margin:8px 0 0;color:var(--pr-muted);font-size:13px;line-height:1.5;max-width:70ch}
7557
+ .pr-note{display:flex;gap:8px;align-items:flex-start;margin-top:14px;padding:10px 12px;border:1px solid rgba(15,118,110,.16);border-radius:10px;background:rgba(15,118,110,.05);color:var(--pr-muted);font-size:12px;line-height:1.45}
7558
+ .pr-note b{color:var(--pr-text);font-weight:650}
7559
+ .pr-stats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}
7560
+ .pr-stat{padding:14px 16px;border:1px solid var(--pr-line);border-radius:14px;background:var(--pr-panel);min-width:0}
7561
+ .pr-stat b{display:block;font-size:24px;line-height:1.1;letter-spacing:-.03em;font-variant-numeric:tabular-nums}
7562
+ .pr-stat span{display:block;margin-top:5px;color:var(--pr-muted);font-size:11px;font-weight:650;letter-spacing:.07em;text-transform:uppercase}
7563
+ .pr-section{display:flex;flex-direction:column;gap:10px;min-width:0}
7564
+ .pr-section h3{margin:0;font-size:15px;letter-spacing:-.01em}
7565
+ .pr-section .pr-hint{margin:0;color:var(--pr-muted);font-size:12px;line-height:1.45}
7566
+ .pr-presets{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}
7567
+ .pr-preset{padding:12px;border:1px solid var(--pr-line);border-radius:12px;background:var(--pr-panel);min-width:0}
7568
+ .pr-preset.active{border-color:rgba(15,118,110,.35);background:rgba(15,118,110,.06)}
7569
+ .pr-preset b{display:flex;justify-content:space-between;gap:8px;font-size:13px;align-items:center}
7570
+ .pr-preset span{display:block;margin-top:4px;color:var(--pr-muted);font-size:11.5px;line-height:1.4}
7571
+ .pr-preset em{font-style:normal;color:var(--pr-accent);font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}
7572
+ .pr-list{display:flex;flex-direction:column;border:1px solid var(--pr-line);border-radius:14px;overflow:hidden;background:var(--pr-panel)}
7573
+ .pr-row{display:flex;gap:10px;align-items:baseline;padding:9px 12px;border-top:1px solid var(--pr-line);font-size:12px;line-height:1.4;min-width:0}
7574
+ .pr-row:first-child{border-top:0}
7575
+ .pr-sev{flex:none;width:46px;font-size:9.5px;font-weight:750;letter-spacing:.08em;text-transform:uppercase}
7576
+ .pr-sev.deny{color:#9f1239}.pr-sev.ask{color:#9a3412}
7577
+ .pr-row code{flex:none;color:var(--pr-text);font:inherit;font-weight:650;white-space:nowrap}
7578
+ .pr-row span{color:var(--pr-muted);overflow:hidden;text-overflow:ellipsis;min-width:0}
7579
+ .pr-path{padding:9px 12px;border-top:1px solid var(--pr-line);font-size:12px;line-height:1.45;min-width:0}
7580
+ .pr-path:first-child{border-top:0}
7581
+ .pr-path b{display:block;font-size:12px}
7582
+ .pr-path code{display:block;margin-top:3px;color:var(--pr-muted);font:11.5px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;overflow-wrap:anywhere}
7583
+ .pr-error{padding:8px 10px;border:1px solid rgba(159,18,57,.16);border-radius:9px;background:rgba(159,18,57,.06);color:#9f1239;font-size:12px;line-height:1.4}
7584
+ .pr-loading{color:var(--pr-muted);font-size:13px;padding:18px 0}
7585
+ .pr-empty{padding:18px;border:1px dashed var(--pr-line);border-radius:14px;color:var(--pr-muted);font-size:12.5px;line-height:1.5;background:linear-gradient(160deg,var(--pr-soft),transparent 75%)}
7586
+ .pr-foot{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;color:var(--pr-muted);font-size:11px;line-height:1.4}
7587
+ @media(max-width:860px){.pr-presets{grid-template-columns:1fr 1fr}.pr-stats{grid-template-columns:1fr 1fr}}
7588
+ @media(max-width:520px){.pr-presets{grid-template-columns:1fr}.pr-stats{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.pr-stat{padding:10px 12px}.pr-stat b{font-size:20px}.pr-stat span{font-size:9px;letter-spacing:.04em}.pr-root{gap:16px}.pr-mode{padding:16px}.pr-note{flex-direction:column;gap:4px}.pr-row{display:flex;flex-wrap:wrap;gap:4px 8px}.pr-row code{white-space:normal;overflow-wrap:anywhere}.pr-row span{flex-basis:100%;padding-left:0}}
7589
+ `;
7590
+
7591
+ const SEVERITY_ORDER = { deny: 0, ask: 1 };
7592
+
7593
+ function currentSessionSelector(state) {
7594
+ try {
7595
+ const sessions = Object.values(state.byId || {});
7596
+ const main = sessions.find((session) => session && session.retainedBy && (session.retainedBy.mainView || 0) > 0);
7597
+ if (main) return main.id;
7598
+ const ids = Array.isArray(state.ids) ? state.ids : [];
7599
+ return ids.length > 0 ? ids[ids.length - 1] : undefined;
7600
+ } catch { return undefined; }
7601
+ }
7602
+
7603
+ async function call(ctx, operation, payload) {
7604
+ let signal;
7605
+ try { signal = AbortSignal.timeout(30000); } catch { signal = undefined; }
7606
+ const result = await ctx.connection.rpc.call("/api", `dsh-guardrails/${operation}`, payload ?? {}, signal);
7607
+ if (!result || result.ok !== true) throw new Error(result?.error?.message || "Ошибка RPC");
7608
+ return result.value;
7609
+ }
7610
+
7611
+ function errorOf(value) { return value instanceof Error ? value.message : String(value); }
7612
+
7613
+ function SandboxChip({ value }) {
7614
+ const labels = {
7615
+ "read-only": "Read only",
7616
+ "workspace-write": "Workspace write",
7617
+ "danger-full-access": "Full access",
7618
+ };
7619
+ return h("span", { className: "pr-chip sandbox" }, labels[value] || value || "sandbox");
7620
+ }
7621
+
7622
+ function PermissionsSection(props) {
7623
+ const [state, setState] = useState({ loading: true, error: null, data: null });
7624
+ const useSessions = props && props.useSessions;
7625
+ const useWorkspaces = props && props.useWorkspaces;
7626
+ const sessionId = useSessions ? useSessions(currentSessionSelector) : undefined;
7627
+ const workspaceSnapshot = useWorkspaces ? useWorkspaces((snapshot) => snapshot) : undefined;
7628
+ const workspaces = Array.isArray(workspaceSnapshot?.items) ? workspaceSnapshot.items : [];
7629
+ const workspace = workspaces.find((item) => Array.isArray(item.sessionIds) && item.sessionIds.includes(sessionId))
7630
+ ?? workspaces.find((item) => Array.isArray(item.sessionIds) && item.sessionIds.length > 0)
7631
+ ?? workspaces[0];
7632
+ const cwd = typeof workspace?.path === "string" ? workspace.path : undefined;
7633
+
7634
+ useEffect(() => {
7635
+ let cancelled = false;
7636
+ (async () => {
7637
+ try {
7638
+ const data = await call(window.__DSH_GUARDRAILS_CTX__, "permissions", { sessionId, cwd });
7639
+ if (!cancelled) setState({ loading: false, error: null, data });
7640
+ } catch (error) {
7641
+ if (!cancelled) setState({ loading: false, error: errorOf(error), data: null });
7642
+ }
7643
+ })();
7644
+ return () => { cancelled = true; };
7645
+ }, [sessionId, cwd]);
7646
+
7647
+ const preset = state.data?.preset;
7648
+ const guard = state.data?.guard;
7649
+ const options = Array.isArray(preset?.catalog?.options) ? preset.catalog.options : [];
7650
+ const rules = Array.isArray(guard?.rules)
7651
+ ? [...guard.rules].sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 9) - (SEVERITY_ORDER[b.severity] ?? 9))
7652
+ : [];
7653
+ const protectedPaths = Array.isArray(guard?.protectedPaths) ? guard.protectedPaths : [];
7654
+ const current = preset?.current || "custom";
7655
+ const spec = preset?.spec;
7656
+ const option = preset?.option;
7657
+ const title = option?.name || current;
7658
+
7659
+ return h("section", { className: "pr-root", "aria-label": "Права и ограничения" },
7660
+ h("div", { className: "pr-head" },
7661
+ h("div", null,
7662
+ h("p", { className: "pr-eyebrow" }, "Guardrails · effective permissions"),
7663
+ h("h2", { className: "pr-title" }, "Права"),
7664
+ h("p", { className: "pr-sub" }, "Что разрешено в этом чате и что kit запрещает в любом режиме. Режим переключается штатным picker DSH в композере — здесь только прозрачный срез."),
7665
+ ),
7666
+ ),
7667
+ state.error ? h("div", { className: "pr-error", role: "alert" }, state.error) : null,
7668
+ state.loading ? h("div", { className: "pr-loading" }, "Читаем эффективные права…") : null,
7669
+ !state.loading && state.data ? h(React.Fragment, null,
7670
+ h("div", { className: "pr-mode" },
7671
+ h("div", { className: "pr-mode-top" },
7672
+ h("div", null,
7673
+ h("h3", null, title),
7674
+ h("p", null, option?.description || spec?.description || "Эффективный режим этого чата."),
7675
+ ),
7676
+ h("div", { className: "pr-chips" },
7677
+ h(SandboxChip, { value: spec?.sandbox }),
7678
+ h("span", { className: "pr-chip approval" }, spec?.approval === "never" ? "approval never" : "approval ask"),
7679
+ ),
7680
+ ),
7681
+ h("div", { className: "pr-note" },
7682
+ h("b", null, "Не второй picker."),
7683
+ h("span", null, "Смена режима — только в DSH. Эта страница не меняет разрешения и не хранит approvals; каждая карточка подтверждения остаётся в чате."),
7684
+ ),
7685
+ ),
7686
+ h("div", { className: "pr-stats" },
7687
+ h("div", { className: "pr-stat" }, h("b", null, rules.length), h("span", null, "жёстких правил")),
7688
+ h("div", { className: "pr-stat" }, h("b", null, protectedPaths.length), h("span", null, "защищённых путей")),
7689
+ h("div", { className: "pr-stat" }, h("b", null, options.length), h("span", null, "пресетов DSH")),
7690
+ ),
7691
+ h("div", { className: "pr-section" },
7692
+ h("h3", null, "Пресеты DSH"),
7693
+ h("p", { className: "pr-hint" }, "Доступны в композере. Текущий выделен; kit не дублирует переключатель."),
7694
+ h("div", { className: "pr-presets" }, options.map((item) => h("div", { key: item.value, className: `pr-preset${item.value === current ? " active" : ""}` },
7695
+ h("b", null, h("span", null, item.name || item.value), item.value === current ? h("em", null, "сейчас") : null),
7696
+ h("span", null, item.description || "Без описания"),
7697
+ ))),
7698
+ ),
7699
+ h("div", { className: "pr-section" },
7700
+ h("h3", null, "Жёсткие правила kit"),
7701
+ h("p", { className: "pr-hint" }, "Действуют даже в Full access. Отключаются только политикой guardrails, не режимом DSH."),
7702
+ rules.length > 0 ? h("div", { className: "pr-list" }, rules.map((rule) => h("div", { className: "pr-row", key: rule.id },
7703
+ h("span", { className: `pr-sev ${rule.severity}` }, rule.severity),
7704
+ h("code", null, rule.id),
7705
+ h("span", { title: rule.description }, rule.description || ""),
7706
+ ))) : h("div", { className: "pr-empty" }, "Правила отключены политикой guardrails."),
7707
+ ),
7708
+ h("div", { className: "pr-section" },
7709
+ h("h3", null, "Защищённые пути"),
7710
+ h("p", { className: "pr-hint" }, "Запись, редактирование и удаление этих путей отклоняются до выполнения."),
7711
+ protectedPaths.length > 0 ? h("div", { className: "pr-list" }, protectedPaths.map((entry) => h("div", { className: "pr-path", key: entry.id },
7712
+ h("b", null, entry.id),
7713
+ h("code", null, `${entry.match} /${entry.pattern}/`),
7714
+ ))) : h("div", { className: "pr-empty" }, "Защищённые пути отключены."),
7715
+ ),
7716
+ ) : null,
7717
+ h("div", { className: "pr-foot" },
7718
+ h("span", null, guard?.policyPath ? `Политика: ${guard.policyPath}` : "Политика guardrails"),
7719
+ h("span", null, guard ? (guard.enabled ? "hooks активны" : "hooks отключены") : ""),
7720
+ ),
7721
+ );
7722
+ }
7723
+
7724
+ function apply(ctx) {
7725
+ window.__DSH_GUARDRAILS_CTX__ = ctx;
7726
+ ctx.effect(() => {
7727
+ const style = document.createElement("style");
7728
+ style.id = "dsh-guardrails";
7729
+ style.textContent = CSS;
7730
+ document.head.appendChild(style);
7731
+ return () => style.remove();
7732
+ }, "guardrails: styles");
7733
+ ctx.effect(() => ctx.slots.inject("settings.section", () => ctx.slots.register({
7734
+ name: "settings.section",
7735
+ id: "guardrails",
7736
+ order: 35,
7737
+ label: () => "Права",
7738
+ }, PermissionsSection)), "guardrails: settings");
7739
+ }
7740
+
7741
+ exports.apply = apply;
7742
+ exports.inject = ["slots", "connection"];
7743
+ return module.exports;
7744
+ })());
7745
+
7746
+ const __partNames = ["dsh-gkb-ru-ui","dsh-token-lab","dsh-peak-guard","dsh-session-move","dsh-profile-transfer","dsh-memory-bridge","dsh-mcp-manager","dsh-agent-extensions","dsh-guardrails"];
7534
7747
  const __kitErrors = [];
7535
7748
  const __pushKitError = (label, error) => {
7536
7749
  const message = String(error && error.message ? error.message : error);
@@ -7538,7 +7751,7 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
7538
7751
  if (__kitErrors.length <= 5) console.error("[dsh-desktop-kit] registration failed:", label, message);
7539
7752
  };
7540
7753
  try {
7541
- window.__DSH_DESKTOP_KIT__ = { version: "2.7.7", parts: __partNames, errors: __kitErrors, ready: false };
7754
+ window.__DSH_DESKTOP_KIT__ = { version: "2.7.8", parts: __partNames, errors: __kitErrors, ready: false };
7542
7755
  } catch {}
7543
7756
  const __inject = [];
7544
7757
  for (const __part of __parts) {
@@ -0,0 +1,29 @@
1
+ # DSH Desktop Kit 2.7.8 — тест
2
+
3
+ Дата: 2026-11-12.
4
+
5
+ ## Permissions UI
6
+
7
+ - Настройки → **Права**.
8
+ - RPC `/api/dsh-guardrails/permissions` — read-only.
9
+ - `permissionPresets.catalog/current/resolve/optionOf` + `loadPolicy`.
10
+ - Каталог пресетов, текущий режим, sandbox/approval, жёсткие правила, защищённые пути.
11
+ - Смена режима не дублируется: только штатный picker DSH.
12
+
13
+ ## MCP prompts (из 2.7.7)
14
+
15
+ - `prompts/list`, `prompts/get`, UI-модалка, `/mcp prompts`, `/mcp prompt`.
16
+
17
+ ## Проверки
18
+
19
+ - `node tools/permissions-ui-tests.mjs` — OK.
20
+ - `node tools/mcp-manager-tests.mjs` — OK.
21
+ - `npm test` — exit 0.
22
+ - `build:client` — 9 частей; `check:client` — OK.
23
+ - browser_qa audit 390/768/1440 (Permissions): overflow 0, console 0, markup 0.
24
+ - Скриншоты: `plugins/dsh-guardrails/screenshots/permissions-1440.png`, `permissions-390.png`.
25
+
26
+ ## Ограничения
27
+
28
+ - История approvals не хранится: DSH показывает карточку в чате, kit её не дублирует.
29
+ - OAuth и sampling MCP не реализованы.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-desktop-kit",
3
- "version": "2.7.7",
3
+ "version": "2.7.8",
4
4
  "description": "DeepSeek Harness desktop kit 2.7: 16 plugins and 30 skills — self-applying skills, token lab, Skills Studio, project memory, GitHub and USB memory transfer, guardrails, repo map, checkpoints, plan mode, browser QA, GitHub review, Claude Code-like hooks/agents/commands, Russian UI, peak guard, chat move and profile transfer.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -74,7 +74,7 @@
74
74
  "build:client": "node tools/build-client.mjs",
75
75
  "build:skills": "node tools/build-skill-manifest.mjs",
76
76
  "check:client": "node tools/check-client.mjs client.js",
77
- "test": "node tools/smoke-tests.mjs && node tools/memory-tests.mjs && node tools/skills-sync-tests.mjs && node tools/kit-upgrade-tests.mjs && node plugins/dsh-profile-transfer/test/transfer.test.mjs && node --test plugins/dsh-memory-bridge/test/bridge.test.mjs plugins/dsh-memory-bridge/test/github.test.mjs plugins/dsh-memory-bridge/test/merge.test.mjs plugins/dsh-memory-bridge/test/user-specific.test.mjs && node tools/repo-map-tests.mjs && node tools/checkpoints-tests.mjs && node tools/plan-mode-tests.mjs && node tools/browser-qa-tests.mjs && node tools/ui-gate-tests.mjs && node tools/agent-extensions-tests.mjs && node tools/mcp-manager-tests.mjs && node tools/github-review-tests.mjs && node plugins/dsh-guardrails/tools/guardrails-tests.mjs",
77
+ "test": "node tools/smoke-tests.mjs && node tools/memory-tests.mjs && node tools/skills-sync-tests.mjs && node tools/kit-upgrade-tests.mjs && node plugins/dsh-profile-transfer/test/transfer.test.mjs && node --test plugins/dsh-memory-bridge/test/bridge.test.mjs plugins/dsh-memory-bridge/test/github.test.mjs plugins/dsh-memory-bridge/test/merge.test.mjs plugins/dsh-memory-bridge/test/user-specific.test.mjs && node tools/repo-map-tests.mjs && node tools/checkpoints-tests.mjs && node tools/plan-mode-tests.mjs && node tools/browser-qa-tests.mjs && node tools/ui-gate-tests.mjs && node tools/agent-extensions-tests.mjs && node tools/mcp-manager-tests.mjs && node tools/github-review-tests.mjs && node tools/permissions-ui-tests.mjs && node plugins/dsh-guardrails/tools/guardrails-tests.mjs",
78
78
  "skills:cost": "node tools/skill-cost.mjs",
79
79
  "skills:check": "node tools/skill-cost.mjs --check",
80
80
  "release": "node tools/release.mjs"
@@ -19,6 +19,19 @@ bundled plugins. The kit passes no services to it beyond `tools`; the
19
19
  `fs/*` hooks are event-only, which is why no `fs` injection is declared
20
20
  (the shipped `dsh-fs-observation-policy` also injects nothing).
21
21
 
22
+ ## Permissions UI
23
+
24
+ Настройки → **Права** — read-only срез эффективных разрешений:
25
+
26
+ - текущий preset DSH (`current(session)`), sandbox и approval;
27
+ - каталог всех пресетов с пометкой текущего;
28
+ - жёсткие правила kit и защищённые пути;
29
+ - явное предупреждение: смена режима — только штатным picker DSH, страница ничего не переключает и не хранит approvals.
30
+
31
+ ![Права](screenshots/permissions-1440.png)
32
+
33
+ ![Права на 390px](screenshots/permissions-390.png)
34
+
22
35
  ## Tool `guardrails`
23
36
 
24
37
  Read-only. The canonical value is lossless JSON; the rendered text is a
@@ -0,0 +1,220 @@
1
+ /**
2
+ * dsh-guardrails — Permissions settings page (read-only).
3
+ *
4
+ * The DSH permission picker remains the only way to change the mode. This page
5
+ * explains the effective preset and shows the kit's independent hard rules.
6
+ */
7
+ window.__ModuleLoader__.load({
8
+ id: "dsh-guardrails",
9
+ factory: (require) => {
10
+ var module = { exports: {} };
11
+ var exports = module.exports;
12
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
13
+ const React = require("react");
14
+ const h = React.createElement;
15
+ const useEffect = React.useEffect;
16
+ const useState = React.useState;
17
+
18
+ const CSS = `
19
+ .pr-root{--pr-line:var(--dsw-alias-border-l1,rgba(20,40,36,.13));--pr-soft:var(--dsw-alias-bg-layer-1,#f4f7f5);--pr-panel:var(--dsw-alias-bg-layer-2,#fff);--pr-text:var(--dsw-alias-label-primary,#17231f);--pr-muted:var(--dsw-alias-label-secondary,#61706b);--pr-accent:var(--dsw-alias-brand-primary,#0f766e);display:flex;flex-direction:column;gap:20px;max-width:1040px;min-width:0;color:var(--pr-text)}
20
+ .pr-head{display:flex;align-items:flex-end;justify-content:space-between;gap:20px;flex-wrap:wrap}
21
+ .pr-eyebrow{margin:0 0 6px;font-size:11px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--pr-accent)}
22
+ .pr-title{margin:0;font-size:clamp(24px,2.6vw,32px);line-height:1.1;letter-spacing:-.025em;font-weight:680}
23
+ .pr-sub{margin:8px 0 0;max-width:68ch;color:var(--pr-muted);font-size:14px;line-height:1.5}
24
+ .pr-mode{padding:20px;border:1px solid var(--pr-line);border-radius:18px;background:linear-gradient(145deg,var(--pr-panel),var(--pr-soft));min-width:0}
25
+ .pr-mode-top{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap}
26
+ .pr-mode h3{margin:0;font-size:24px;line-height:1.1;letter-spacing:-.03em}
27
+ .pr-chips{display:flex;gap:6px;flex-wrap:wrap}
28
+ .pr-chip{padding:3px 8px;border:1px solid var(--pr-line);border-radius:999px;background:var(--pr-panel);font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--pr-muted);white-space:nowrap}
29
+ .pr-chip.sandbox{color:#0f766e;border-color:rgba(15,118,110,.25);background:rgba(15,118,110,.07)}
30
+ .pr-chip.approval{color:#9a3412;border-color:rgba(154,52,18,.22);background:rgba(154,52,18,.06)}
31
+ .pr-mode p{margin:8px 0 0;color:var(--pr-muted);font-size:13px;line-height:1.5;max-width:70ch}
32
+ .pr-note{display:flex;gap:8px;align-items:flex-start;margin-top:14px;padding:10px 12px;border:1px solid rgba(15,118,110,.16);border-radius:10px;background:rgba(15,118,110,.05);color:var(--pr-muted);font-size:12px;line-height:1.45}
33
+ .pr-note b{color:var(--pr-text);font-weight:650}
34
+ .pr-stats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}
35
+ .pr-stat{padding:14px 16px;border:1px solid var(--pr-line);border-radius:14px;background:var(--pr-panel);min-width:0}
36
+ .pr-stat b{display:block;font-size:24px;line-height:1.1;letter-spacing:-.03em;font-variant-numeric:tabular-nums}
37
+ .pr-stat span{display:block;margin-top:5px;color:var(--pr-muted);font-size:11px;font-weight:650;letter-spacing:.07em;text-transform:uppercase}
38
+ .pr-section{display:flex;flex-direction:column;gap:10px;min-width:0}
39
+ .pr-section h3{margin:0;font-size:15px;letter-spacing:-.01em}
40
+ .pr-section .pr-hint{margin:0;color:var(--pr-muted);font-size:12px;line-height:1.45}
41
+ .pr-presets{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}
42
+ .pr-preset{padding:12px;border:1px solid var(--pr-line);border-radius:12px;background:var(--pr-panel);min-width:0}
43
+ .pr-preset.active{border-color:rgba(15,118,110,.35);background:rgba(15,118,110,.06)}
44
+ .pr-preset b{display:flex;justify-content:space-between;gap:8px;font-size:13px;align-items:center}
45
+ .pr-preset span{display:block;margin-top:4px;color:var(--pr-muted);font-size:11.5px;line-height:1.4}
46
+ .pr-preset em{font-style:normal;color:var(--pr-accent);font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}
47
+ .pr-list{display:flex;flex-direction:column;border:1px solid var(--pr-line);border-radius:14px;overflow:hidden;background:var(--pr-panel)}
48
+ .pr-row{display:flex;gap:10px;align-items:baseline;padding:9px 12px;border-top:1px solid var(--pr-line);font-size:12px;line-height:1.4;min-width:0}
49
+ .pr-row:first-child{border-top:0}
50
+ .pr-sev{flex:none;width:46px;font-size:9.5px;font-weight:750;letter-spacing:.08em;text-transform:uppercase}
51
+ .pr-sev.deny{color:#9f1239}.pr-sev.ask{color:#9a3412}
52
+ .pr-row code{flex:none;color:var(--pr-text);font:inherit;font-weight:650;white-space:nowrap}
53
+ .pr-row span{color:var(--pr-muted);overflow:hidden;text-overflow:ellipsis;min-width:0}
54
+ .pr-path{padding:9px 12px;border-top:1px solid var(--pr-line);font-size:12px;line-height:1.45;min-width:0}
55
+ .pr-path:first-child{border-top:0}
56
+ .pr-path b{display:block;font-size:12px}
57
+ .pr-path code{display:block;margin-top:3px;color:var(--pr-muted);font:11.5px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;overflow-wrap:anywhere}
58
+ .pr-error{padding:8px 10px;border:1px solid rgba(159,18,57,.16);border-radius:9px;background:rgba(159,18,57,.06);color:#9f1239;font-size:12px;line-height:1.4}
59
+ .pr-loading{color:var(--pr-muted);font-size:13px;padding:18px 0}
60
+ .pr-empty{padding:18px;border:1px dashed var(--pr-line);border-radius:14px;color:var(--pr-muted);font-size:12.5px;line-height:1.5;background:linear-gradient(160deg,var(--pr-soft),transparent 75%)}
61
+ .pr-foot{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;color:var(--pr-muted);font-size:11px;line-height:1.4}
62
+ @media(max-width:860px){.pr-presets{grid-template-columns:1fr 1fr}.pr-stats{grid-template-columns:1fr 1fr}}
63
+ @media(max-width:520px){.pr-presets{grid-template-columns:1fr}.pr-stats{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.pr-stat{padding:10px 12px}.pr-stat b{font-size:20px}.pr-stat span{font-size:9px;letter-spacing:.04em}.pr-root{gap:16px}.pr-mode{padding:16px}.pr-note{flex-direction:column;gap:4px}.pr-row{display:flex;flex-wrap:wrap;gap:4px 8px}.pr-row code{white-space:normal;overflow-wrap:anywhere}.pr-row span{flex-basis:100%;padding-left:0}}
64
+ `;
65
+
66
+ const SEVERITY_ORDER = { deny: 0, ask: 1 };
67
+
68
+ function currentSessionSelector(state) {
69
+ try {
70
+ const sessions = Object.values(state.byId || {});
71
+ const main = sessions.find((session) => session && session.retainedBy && (session.retainedBy.mainView || 0) > 0);
72
+ if (main) return main.id;
73
+ const ids = Array.isArray(state.ids) ? state.ids : [];
74
+ return ids.length > 0 ? ids[ids.length - 1] : undefined;
75
+ } catch { return undefined; }
76
+ }
77
+
78
+ async function call(ctx, operation, payload) {
79
+ let signal;
80
+ try { signal = AbortSignal.timeout(30000); } catch { signal = undefined; }
81
+ const result = await ctx.connection.rpc.call("/api", `dsh-guardrails/${operation}`, payload ?? {}, signal);
82
+ if (!result || result.ok !== true) throw new Error(result?.error?.message || "Ошибка RPC");
83
+ return result.value;
84
+ }
85
+
86
+ function errorOf(value) { return value instanceof Error ? value.message : String(value); }
87
+
88
+ function SandboxChip({ value }) {
89
+ const labels = {
90
+ "read-only": "Read only",
91
+ "workspace-write": "Workspace write",
92
+ "danger-full-access": "Full access",
93
+ };
94
+ return h("span", { className: "pr-chip sandbox" }, labels[value] || value || "sandbox");
95
+ }
96
+
97
+ function PermissionsSection(props) {
98
+ const [state, setState] = useState({ loading: true, error: null, data: null });
99
+ const useSessions = props && props.useSessions;
100
+ const useWorkspaces = props && props.useWorkspaces;
101
+ const sessionId = useSessions ? useSessions(currentSessionSelector) : undefined;
102
+ const workspaceSnapshot = useWorkspaces ? useWorkspaces((snapshot) => snapshot) : undefined;
103
+ const workspaces = Array.isArray(workspaceSnapshot?.items) ? workspaceSnapshot.items : [];
104
+ const workspace = workspaces.find((item) => Array.isArray(item.sessionIds) && item.sessionIds.includes(sessionId))
105
+ ?? workspaces.find((item) => Array.isArray(item.sessionIds) && item.sessionIds.length > 0)
106
+ ?? workspaces[0];
107
+ const cwd = typeof workspace?.path === "string" ? workspace.path : undefined;
108
+
109
+ useEffect(() => {
110
+ let cancelled = false;
111
+ (async () => {
112
+ try {
113
+ const data = await call(window.__DSH_GUARDRAILS_CTX__, "permissions", { sessionId, cwd });
114
+ if (!cancelled) setState({ loading: false, error: null, data });
115
+ } catch (error) {
116
+ if (!cancelled) setState({ loading: false, error: errorOf(error), data: null });
117
+ }
118
+ })();
119
+ return () => { cancelled = true; };
120
+ }, [sessionId, cwd]);
121
+
122
+ const preset = state.data?.preset;
123
+ const guard = state.data?.guard;
124
+ const options = Array.isArray(preset?.catalog?.options) ? preset.catalog.options : [];
125
+ const rules = Array.isArray(guard?.rules)
126
+ ? [...guard.rules].sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 9) - (SEVERITY_ORDER[b.severity] ?? 9))
127
+ : [];
128
+ const protectedPaths = Array.isArray(guard?.protectedPaths) ? guard.protectedPaths : [];
129
+ const current = preset?.current || "custom";
130
+ const spec = preset?.spec;
131
+ const option = preset?.option;
132
+ const title = option?.name || current;
133
+
134
+ return h("section", { className: "pr-root", "aria-label": "Права и ограничения" },
135
+ h("div", { className: "pr-head" },
136
+ h("div", null,
137
+ h("p", { className: "pr-eyebrow" }, "Guardrails · effective permissions"),
138
+ h("h2", { className: "pr-title" }, "Права"),
139
+ h("p", { className: "pr-sub" }, "Что разрешено в этом чате и что kit запрещает в любом режиме. Режим переключается штатным picker DSH в композере — здесь только прозрачный срез."),
140
+ ),
141
+ ),
142
+ state.error ? h("div", { className: "pr-error", role: "alert" }, state.error) : null,
143
+ state.loading ? h("div", { className: "pr-loading" }, "Читаем эффективные права…") : null,
144
+ !state.loading && state.data ? h(React.Fragment, null,
145
+ h("div", { className: "pr-mode" },
146
+ h("div", { className: "pr-mode-top" },
147
+ h("div", null,
148
+ h("h3", null, title),
149
+ h("p", null, option?.description || spec?.description || "Эффективный режим этого чата."),
150
+ ),
151
+ h("div", { className: "pr-chips" },
152
+ h(SandboxChip, { value: spec?.sandbox }),
153
+ h("span", { className: "pr-chip approval" }, spec?.approval === "never" ? "approval never" : "approval ask"),
154
+ ),
155
+ ),
156
+ h("div", { className: "pr-note" },
157
+ h("b", null, "Не второй picker."),
158
+ h("span", null, "Смена режима — только в DSH. Эта страница не меняет разрешения и не хранит approvals; каждая карточка подтверждения остаётся в чате."),
159
+ ),
160
+ ),
161
+ h("div", { className: "pr-stats" },
162
+ h("div", { className: "pr-stat" }, h("b", null, rules.length), h("span", null, "жёстких правил")),
163
+ h("div", { className: "pr-stat" }, h("b", null, protectedPaths.length), h("span", null, "защищённых путей")),
164
+ h("div", { className: "pr-stat" }, h("b", null, options.length), h("span", null, "пресетов DSH")),
165
+ ),
166
+ h("div", { className: "pr-section" },
167
+ h("h3", null, "Пресеты DSH"),
168
+ h("p", { className: "pr-hint" }, "Доступны в композере. Текущий выделен; kit не дублирует переключатель."),
169
+ h("div", { className: "pr-presets" }, options.map((item) => h("div", { key: item.value, className: `pr-preset${item.value === current ? " active" : ""}` },
170
+ h("b", null, h("span", null, item.name || item.value), item.value === current ? h("em", null, "сейчас") : null),
171
+ h("span", null, item.description || "Без описания"),
172
+ ))),
173
+ ),
174
+ h("div", { className: "pr-section" },
175
+ h("h3", null, "Жёсткие правила kit"),
176
+ h("p", { className: "pr-hint" }, "Действуют даже в Full access. Отключаются только политикой guardrails, не режимом DSH."),
177
+ rules.length > 0 ? h("div", { className: "pr-list" }, rules.map((rule) => h("div", { className: "pr-row", key: rule.id },
178
+ h("span", { className: `pr-sev ${rule.severity}` }, rule.severity),
179
+ h("code", null, rule.id),
180
+ h("span", { title: rule.description }, rule.description || ""),
181
+ ))) : h("div", { className: "pr-empty" }, "Правила отключены политикой guardrails."),
182
+ ),
183
+ h("div", { className: "pr-section" },
184
+ h("h3", null, "Защищённые пути"),
185
+ h("p", { className: "pr-hint" }, "Запись, редактирование и удаление этих путей отклоняются до выполнения."),
186
+ protectedPaths.length > 0 ? h("div", { className: "pr-list" }, protectedPaths.map((entry) => h("div", { className: "pr-path", key: entry.id },
187
+ h("b", null, entry.id),
188
+ h("code", null, `${entry.match} /${entry.pattern}/`),
189
+ ))) : h("div", { className: "pr-empty" }, "Защищённые пути отключены."),
190
+ ),
191
+ ) : null,
192
+ h("div", { className: "pr-foot" },
193
+ h("span", null, guard?.policyPath ? `Политика: ${guard.policyPath}` : "Политика guardrails"),
194
+ h("span", null, guard ? (guard.enabled ? "hooks активны" : "hooks отключены") : ""),
195
+ ),
196
+ );
197
+ }
198
+
199
+ function apply(ctx) {
200
+ window.__DSH_GUARDRAILS_CTX__ = ctx;
201
+ ctx.effect(() => {
202
+ const style = document.createElement("style");
203
+ style.id = "dsh-guardrails";
204
+ style.textContent = CSS;
205
+ document.head.appendChild(style);
206
+ return () => style.remove();
207
+ }, "guardrails: styles");
208
+ ctx.effect(() => ctx.slots.inject("settings.section", () => ctx.slots.register({
209
+ name: "settings.section",
210
+ id: "guardrails",
211
+ order: 35,
212
+ label: () => "Права",
213
+ }, PermissionsSection)), "guardrails: settings");
214
+ }
215
+
216
+ exports.apply = apply;
217
+ exports.inject = ["slots", "connection"];
218
+ return module.exports;
219
+ },
220
+ });
@@ -21,6 +21,7 @@ import fs from "node:fs";
21
21
  import path from "node:path";
22
22
 
23
23
  import { createGuard, createHookState, registerHooks } from "./lib/hooks.js";
24
+ import { registerPermissionsApi } from "./lib/api.js";
24
25
  import { resolveMode } from "./lib/modes.js";
25
26
  import {
26
27
  DEFAULT_COMMAND_RULES,
@@ -385,5 +386,10 @@ export function apply(ctx) {
385
386
  };
386
387
 
387
388
  ctx.tools.register(tool);
389
+ try {
390
+ ctx.inject(["connection"], (child) => registerPermissionsApi(child));
391
+ } catch (error) {
392
+ logger?.warn?.(`[guardrails] permissions UI skipped: ${messageOf(error)}`);
393
+ }
388
394
  logger?.info?.(`[guardrails] tool ready (policy: ${policyPath(env)})`);
389
395
  }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Read-only Permissions RPC.
3
+ *
4
+ * DSH owns the permission picker and the approval card. This endpoint only
5
+ * reports the effective preset plus the kit's independent hard rules, so the
6
+ * Settings page never becomes a second source of truth.
7
+ */
8
+ import { loadPolicy } from "./policy.js";
9
+
10
+ export const RPC_NAMESPACE = "dsh-guardrails";
11
+ const ROUTE_BASE = "/api/dsh-guardrails";
12
+
13
+ function ok(rpcId, value) {
14
+ return Response.json({ type: "server-response", rpcId, result: { ok: true, value } });
15
+ }
16
+
17
+ function fail(rpcId, message) {
18
+ return Response.json({ type: "server-response", rpcId, result: { ok: false, error: { code: "guardrails/bad-request", message, details: {} } } });
19
+ }
20
+
21
+ async function envelope(request) {
22
+ const body = await request.json();
23
+ if (!body || body.type !== "client-request" || typeof body.rpcId !== "string" || typeof body.method !== "string") {
24
+ throw new Error("bad Connection envelope");
25
+ }
26
+ return { rpcId: body.rpcId, method: body.method, payload: body.payload ?? {} };
27
+ }
28
+
29
+ function liveSession(ctx, payload) {
30
+ const sessions = ctx.get?.("sessions");
31
+ if (payload?.sessionId) {
32
+ const direct = sessions?.get?.(payload.sessionId);
33
+ if (direct) return direct;
34
+ }
35
+ const live = typeof sessions?.list === "function" ? sessions.list() : [];
36
+ return live.length > 0 ? live[live.length - 1] : undefined;
37
+ }
38
+
39
+ function presetSnapshot(ctx, session) {
40
+ const service = ctx.get?.("permissionPresets");
41
+ if (!service) return { available: false, current: null, spec: null, option: null, catalog: { options: [], defaultPreset: null } };
42
+ let catalog = { options: [], defaultPreset: null };
43
+ try { catalog = service.catalog(); } catch { /* optional service */ }
44
+ let name = null;
45
+ try { name = session ? service.current(session) : null; } catch { name = null; }
46
+ let spec = null;
47
+ let option = null;
48
+ if (name) {
49
+ try { spec = service.resolve(name); } catch { spec = null; }
50
+ try { option = service.optionOf(name); } catch { option = null; }
51
+ }
52
+ return { available: true, current: name, spec, option, catalog };
53
+ }
54
+
55
+ function guardSnapshot(env) {
56
+ const loaded = loadPolicy({ env });
57
+ const rules = loaded.policy.rules
58
+ .filter((rule) => rule.enabled !== false)
59
+ .map((rule) => ({ id: rule.id, severity: rule.severity, description: rule.description }));
60
+ const protectedPaths = loaded.policy.protectedPaths
61
+ .filter((entry) => entry.enabled !== false)
62
+ .map((entry) => ({ id: entry.id, match: entry.match, pattern: entry.pattern, description: entry.description }));
63
+ return {
64
+ enabled: loaded.enabled,
65
+ killSwitch: loaded.killSwitch,
66
+ policyPath: loaded.path,
67
+ loadedFromFile: loaded.loadedFromFile,
68
+ error: loaded.error,
69
+ warnings: loaded.warnings,
70
+ rules,
71
+ protectedPaths,
72
+ hooks: loaded.policy.hooks,
73
+ };
74
+ }
75
+
76
+ export function registerPermissionsApi(ctx) {
77
+ const route = {
78
+ path: `${ROUTE_BASE}/permissions`,
79
+ methods: ["POST"],
80
+ requestBody: "buffered",
81
+ fetch: async (request) => {
82
+ let rpcId = "invalid-request";
83
+ try {
84
+ const value = await envelope(request);
85
+ rpcId = value.rpcId;
86
+ if (value.method !== `${RPC_NAMESPACE}/permissions`) throw new Error("endpoint/method mismatch");
87
+ const session = liveSession(ctx, value.payload);
88
+ const preset = presetSnapshot(ctx, session);
89
+ return ok(rpcId, {
90
+ project: session?.header?.cwd ?? null,
91
+ preset,
92
+ guard: guardSnapshot(process.env),
93
+ });
94
+ } catch (error) {
95
+ return fail(rpcId, error instanceof Error ? error.message : String(error));
96
+ }
97
+ },
98
+ };
99
+ ctx.effect(() => {
100
+ const dispose = ctx.connection.fetch.register(route);
101
+ return () => { try { void Promise.resolve(dispose()).catch(() => {}); } catch { /* unloading */ } };
102
+ }, "guardrails: permissions route");
103
+ }
@@ -7,11 +7,13 @@
7
7
  "main": "index.js",
8
8
  "exports": {
9
9
  ".": "./index.js",
10
+ "./client": "./client.js",
10
11
  "./lib": "./lib/scan.js",
11
12
  "./package.json": "./package.json"
12
13
  },
13
14
  "files": [
14
15
  "index.js",
16
+ "client.js",
15
17
  "lib/**",
16
18
  "tools/**",
17
19
  "cordis.patch.yml",