dsh-perm-gate 5.0.4 → 5.1.2-compat.7

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.
Files changed (52) hide show
  1. package/CHANGELOG.ja.md +0 -2
  2. package/CHANGELOG.ko.md +0 -2
  3. package/CHANGELOG.md +4 -2
  4. package/INSTALL.ja.md +0 -15
  5. package/INSTALL.ko.md +0 -15
  6. package/INSTALL.md +0 -15
  7. package/INSTALL.zh.md +0 -15
  8. package/README.ja.md +6 -18
  9. package/README.ko.md +6 -18
  10. package/README.md +40 -19
  11. package/README.zh.md +34 -18
  12. package/assets/appcontainer-launcher.cs +703 -0
  13. package/dsh.plugin.json +2 -2
  14. package/lib/appcontainer/compile.d.ts +17 -0
  15. package/lib/appcontainer/compile.js +74 -0
  16. package/lib/appcontainer/exempt.d.ts +18 -0
  17. package/lib/appcontainer/exempt.js +71 -0
  18. package/lib/appcontainer/index.d.ts +85 -0
  19. package/lib/appcontainer/index.js +286 -0
  20. package/lib/appcontainer/probe.d.ts +44 -0
  21. package/lib/appcontainer/probe.js +66 -0
  22. package/lib/appcontainer/sid.d.ts +23 -0
  23. package/lib/appcontainer/sid.js +33 -0
  24. package/lib/appcontainer/wrap.d.ts +22 -0
  25. package/lib/appcontainer/wrap.js +40 -0
  26. package/lib/bridge.d.ts +34 -0
  27. package/lib/bridge.js +125 -0
  28. package/lib/client.js +1663 -1107
  29. package/lib/client.js.map +1 -1
  30. package/lib/config.d.ts +41 -1
  31. package/lib/config.js +6 -0
  32. package/lib/debug-generation.d.ts +21 -0
  33. package/lib/debug-generation.js +44 -0
  34. package/lib/index.d.ts +1 -1
  35. package/lib/index.js +157 -7
  36. package/lib/runtime.d.ts +22 -0
  37. package/lib/runtime.js +24 -0
  38. package/locale/de.json +1 -0
  39. package/locale/en.json +1 -0
  40. package/locale/es.json +1 -0
  41. package/locale/fr.json +1 -0
  42. package/locale/it.json +1 -0
  43. package/locale/ja.json +1 -0
  44. package/locale/ko.json +1 -0
  45. package/locale/ru.json +1 -0
  46. package/locale/zh.json +1 -0
  47. package/package.json +25 -14
  48. package/README.de.md +0 -487
  49. package/README.es.md +0 -489
  50. package/README.fr.md +0 -498
  51. package/README.it.md +0 -490
  52. package/README.ru.md +0 -478
package/lib/config.d.ts CHANGED
@@ -191,6 +191,38 @@ export interface PermGateConfig {
191
191
  * Default 1800000 (30 min).
192
192
  */
193
193
  readonly networkGrantTtlMs?: number;
194
+ /**
195
+ * Run gate-allowed shell commands inside a Windows AppContainer. The
196
+ * container token carries NO network capability, so non-loopback egress is
197
+ * denied by Windows Firewall (WFP) itself; the container SID is derived
198
+ * deterministically so the loopback exemption is a one-time step. Default
199
+ * false — enabling changes how allowed commands execute.
200
+ */
201
+ readonly sandboxEnabled?: boolean;
202
+ /**
203
+ * Filesystem posture inside the container: 'workspace-write' grants the
204
+ * workspace Modify + temp FullControl; 'read-only' grants RX only.
205
+ * System directories stay readable through the stock ALL APPLICATION
206
+ * PACKAGES ACEs. Default 'workspace-write'.
207
+ */
208
+ readonly sandboxMode?: 'read-only' | 'workspace-write';
209
+ /**
210
+ * THE proxy parameter: the URL injected as HTTP(S)_PROXY / ALL_PROXY into
211
+ * sandboxed processes. Empty (default) follows the builtin filtered proxy
212
+ * (its actual bound 127.0.0.1 port) when networkEnabled; an explicit value
213
+ * must be loopback-bound (the container cannot reach anything else) and the
214
+ * destination filtering is that proxy's job. Neither set → no proxy env;
215
+ * direct egress stays WFP-blocked either way.
216
+ */
217
+ readonly sandboxProxy?: string;
218
+ /**
219
+ * Attempt the one-time `CheckNetIsolation LoopbackExempt -a -p=<sid>` for
220
+ * the container SID so sandboxed processes can reach 127.0.0.1 (the proxy
221
+ * and the host webServer). Denied without elevation → warning with the
222
+ * exact command; the sandbox still runs, only loopback stays closed.
223
+ * Default 'exempt'.
224
+ */
225
+ readonly sandboxLoopback?: 'exempt' | 'off';
194
226
  /** @deprecated Rules now live in the `dsh-perm-gate-rules` settings namespace; file watching was removed. Accepted (ignored) for composition compatibility. */
195
227
  readonly watch?: boolean;
196
228
  /** @deprecated See {@link watch}. */
@@ -349,6 +381,10 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
349
381
  networkInjectEnv: z<boolean, boolean, "volatile-defined">;
350
382
  networkAskTimeoutMs: z<number, number, "volatile-defined">;
351
383
  networkGrantTtlMs: z<number, number, "volatile-defined">;
384
+ sandboxEnabled: z<boolean, boolean, "volatile-defined">;
385
+ sandboxMode: z<"read-only" | "workspace-write", "read-only" | "workspace-write", "volatile-defined">;
386
+ sandboxProxy: z<string, string, "volatile-defined">;
387
+ sandboxLoopback: z<"exempt" | "off", "exempt" | "off", "volatile-defined">;
352
388
  rules: z<NoInfer<RulesConfig>, NoInfer<RulesConfig>, "volatile">;
353
389
  watch: z<boolean, boolean, "defined">;
354
390
  watchDebounceMs: z<number, number, "defined">;
@@ -404,6 +440,10 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
404
440
  networkInjectEnv: z<boolean, boolean, "volatile-defined">;
405
441
  networkAskTimeoutMs: z<number, number, "volatile-defined">;
406
442
  networkGrantTtlMs: z<number, number, "volatile-defined">;
443
+ sandboxEnabled: z<boolean, boolean, "volatile-defined">;
444
+ sandboxMode: z<"read-only" | "workspace-write", "read-only" | "workspace-write", "volatile-defined">;
445
+ sandboxProxy: z<string, string, "volatile-defined">;
446
+ sandboxLoopback: z<"exempt" | "off", "exempt" | "off", "volatile-defined">;
407
447
  rules: z<NoInfer<RulesConfig>, NoInfer<RulesConfig>, "volatile">;
408
448
  watch: z<boolean, boolean, "defined">;
409
449
  watchDebounceMs: z<number, number, "defined">;
@@ -411,7 +451,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
411
451
  /** Live reference the 0.1.7 loader hands `apply` for `.volatile()` config fields. */
412
452
  export type VolatileRef<T> = Volatile<T>;
413
453
  /** The config fields marked `.volatile()` — live refs inside `apply`'s config. */
414
- export declare const VOLATILE_CONFIG_KEYS: readonly ["defaultAction", "caseInsensitivePaths", "classifierEnabled", "classifierEndpoint", "classifierModel", "classifierApiKey", "riskTimeoutMs", "riskLearning", "riskSediment", "classifierSource", "classifierProvider", "riskThreshold", "grantTtlMs", "grantMaxUses", "permissive", "permissiveStrategies", "allowlist", "denyKeywords", "autoAllowTools", "sessionSweep", "searchUp", "badFilePolicy", "maxChainLength", "networkEnabled", "networkMode", "networkUnlisted", "networkUnattributed", "networkLoopback", "networkBind", "networkPort", "networkNoProxy", "networkInjectEnv", "networkAskTimeoutMs", "networkGrantTtlMs", "rules"];
454
+ export declare const VOLATILE_CONFIG_KEYS: readonly ["defaultAction", "caseInsensitivePaths", "classifierEnabled", "classifierEndpoint", "classifierModel", "classifierApiKey", "riskTimeoutMs", "riskLearning", "riskSediment", "classifierSource", "classifierProvider", "riskThreshold", "grantTtlMs", "grantMaxUses", "permissive", "permissiveStrategies", "allowlist", "denyKeywords", "autoAllowTools", "sessionSweep", "searchUp", "badFilePolicy", "maxChainLength", "networkEnabled", "networkMode", "networkUnlisted", "networkUnattributed", "networkLoopback", "networkBind", "networkPort", "networkNoProxy", "networkInjectEnv", "networkAskTimeoutMs", "networkGrantTtlMs", "rules", "sandboxEnabled", "sandboxMode", "sandboxProxy", "sandboxLoopback"];
415
455
  /** Resolve one possibly-volatile field: a live ref on 0.1.7+, a plain value otherwise. */
416
456
  export declare function readVolatileValue<T>(value: T | Volatile<T> | undefined): VolatileSnapshot<T> | undefined;
417
457
  /** Shallow-resolve every volatile field into a plain snapshot (one per read). */
package/lib/config.js CHANGED
@@ -199,6 +199,11 @@ export const Config = z.object({
199
199
  networkInjectEnv: z.boolean().default(true).volatile(),
200
200
  networkAskTimeoutMs: z.number().min(1000).max(600_000).default(120_000).volatile(),
201
201
  networkGrantTtlMs: z.number().min(0).max(24 * 60 * 60_000).default(30 * 60_000).volatile(),
202
+ // AppContainer sandbox (Windows; goal 2)
203
+ sandboxEnabled: z.boolean().default(false).volatile(),
204
+ sandboxMode: z.union(['read-only', 'workspace-write']).default('workspace-write').volatile(),
205
+ sandboxProxy: z.string().default('').volatile(),
206
+ sandboxLoopback: z.union(['exempt', 'off']).default('exempt').volatile(),
202
207
  // 0.1.7: the rules document lives on this entry as a volatile whole-object
203
208
  // field (a second settings namespace is no longer projectable). Unconfigured
204
209
  // (bare defaults) -> the rules file remains the source.
@@ -217,6 +222,7 @@ export const VOLATILE_CONFIG_KEYS = [
217
222
  'networkEnabled', 'networkMode', 'networkUnlisted', 'networkUnattributed', 'networkLoopback',
218
223
  'networkBind', 'networkPort', 'networkNoProxy', 'networkInjectEnv', 'networkAskTimeoutMs',
219
224
  'networkGrantTtlMs', 'rules',
225
+ 'sandboxEnabled', 'sandboxMode', 'sandboxProxy', 'sandboxLoopback',
220
226
  ];
221
227
  /** Resolve one possibly-volatile field: a live ref on 0.1.7+, a plain value otherwise. */
222
228
  export function readVolatileValue(value) {
@@ -0,0 +1,21 @@
1
+ /**
2
+ * debug-generation — T10b served 集合诊断端点(临时,上线前删,spec served 七步第 2 步)。
3
+ *
4
+ * GET /api/dsh-perm-gate/debug-generation
5
+ * 返回插件持有的 settings 服务实例自检:全量 describe 的 ns 列表 + 方法面。
6
+ * 判读:ns 列表若同时含 dsh-perm-gate / dsh-session-guard / dsh-free-search-settings
7
+ * 等其他插件命名空间 → 持有的是宿主共享单例(identitySame=TRUE,H3 孤儿假设死);
8
+ * 若只有自家 ns → 孤儿实例坐实(H3 成立)。
9
+ */
10
+ export interface SettingsServiceLike {
11
+ update(ns: string, patch: object): Promise<void>;
12
+ register?(ns: string, schema: unknown, options?: {
13
+ base?: unknown;
14
+ }): unknown;
15
+ installSection?(owner: unknown, ns: string, schema: unknown, entry: unknown, hooks: unknown): unknown;
16
+ describe?(): Array<{
17
+ ns: unknown;
18
+ }>;
19
+ }
20
+ export declare const DEBUG_GENERATION_ROUTE = "/api/dsh-perm-gate/debug-generation";
21
+ export declare function registerDebugGenerationRoute(server: unknown, getSettingsSvc: () => SettingsServiceLike | undefined): (() => void) | undefined;
@@ -0,0 +1,44 @@
1
+ export const DEBUG_GENERATION_ROUTE = '/api/dsh-perm-gate/debug-generation';
2
+ export function registerDebugGenerationRoute(server, getSettingsSvc) {
3
+ if (typeof server !== 'object' || server === null)
4
+ return undefined;
5
+ const ws = typeof server.register === 'function' ? server : undefined;
6
+ if (ws === undefined || typeof ws.register !== 'function')
7
+ return undefined;
8
+ return ws.register({
9
+ kind: 'exact',
10
+ path: DEBUG_GENERATION_ROUTE,
11
+ handler: (rawReq, rawRes) => {
12
+ const req = rawReq;
13
+ const res = rawRes;
14
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
15
+ res.writeHead(405);
16
+ res.end();
17
+ return;
18
+ }
19
+ const svc = getSettingsSvc();
20
+ let namespaces = [];
21
+ let describeError;
22
+ if (svc !== undefined && typeof svc.describe === 'function') {
23
+ try {
24
+ const descriptors = svc.describe();
25
+ namespaces = descriptors.map((d) => String(d.ns));
26
+ }
27
+ catch (e) {
28
+ describeError = e instanceof Error ? e.message : String(e);
29
+ }
30
+ }
31
+ res.writeHead(200, { 'content-type': 'application/json' });
32
+ res.end(JSON.stringify({
33
+ hasSvc: svc !== undefined,
34
+ describeError,
35
+ namespaces,
36
+ methods: svc === undefined ? null : {
37
+ register: typeof svc.register === 'function',
38
+ installSection: typeof svc.installSection === 'function',
39
+ update: typeof svc.update === 'function',
40
+ },
41
+ }, null, 2));
42
+ },
43
+ });
44
+ }
package/lib/index.d.ts CHANGED
@@ -53,5 +53,5 @@ export declare function makeApprovalAnswerer(runtime: Pick<PermGateRuntime, 'set
53
53
  * `riskTimeoutMs` (default 20 s), and a grader failure keeps the original ask
54
54
  * (fail-closed).
55
55
  */
56
- export declare function makePreExecuteListener(runtime: Pick<PermGateRuntime, 'decideExecution' | 'refineAsk' | 'beginShellExecution'>): (exec: ToolExecutionLike, next: () => Promise<unknown>) => Promise<unknown>;
56
+ export declare function makePreExecuteListener(runtime: Pick<PermGateRuntime, 'decideExecution' | 'refineAsk' | 'beginShellExecution'> & Partial<Pick<PermGateRuntime, 'sandboxRewrite'>>): (exec: ToolExecutionLike, next: () => Promise<unknown>) => Promise<unknown>;
57
57
  export declare function apply(ctx: Context, config?: Record<string, unknown>): PermGateRuntime;
package/lib/index.js CHANGED
@@ -1,9 +1,12 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { Config, ensureDataDir, readRulesFromSettings, readVolatileValue, resolveVolatileConfig, resolveDshHome, resolveDataDir, resolveGatePresets, resolvePermissiveStrategies, resolveRulesFile } from './config.js';
3
+ import { SandboxController } from './appcontainer/index.js';
4
+ import { Config, ensureDataDir, readRulesFromSettings, readVolatileValue, resolveVolatileConfig, resolveConfig, resolveDshHome, resolveDataDir, resolveGatePresets, resolvePermissiveStrategies, resolveRulesFile } from './config.js';
4
5
  import { appendAllowToSettings, readRulesFileDoc, replaceAllowInSettings } from './allowlist.js';
5
6
  import { runDryRun } from './dry-run.js';
6
7
  import { registerDryRunRoute, registerEventsRoute, registerHealthRoute, registerLearningRoute, registerNetworkRoute, registerReceiverRoute, registerReviewRoutes, registerRulesRoute } from './events.js';
8
+ // T12 设置数据桥(describe/mutate;client BridgeDocHandle 消费)
9
+ import { registerSettingsBridgeRoutes } from './bridge.js';
7
10
  import { buildReceiverInfo } from './receiver-info.js';
8
11
  import { readRulesView, readRulesViewFromSettings } from './rules-view.js';
9
12
  import { PermGateRuntime } from './runtime.js';
@@ -142,6 +145,22 @@ export function makePreExecuteListener(runtime) {
142
145
  // The call proceeds: register it so a network connection made by its
143
146
  // child process can be attributed back to this session.
144
147
  runtime.beginShellExecution(exec);
148
+ // AppContainer sandbox (goal 2): rewrite the allowed shell call into
149
+ // the launcher invocation, or fail closed. The host executes THIS exec
150
+ // object after the waterfall resolves, so the in-place arguments swap
151
+ // is the sanctioned seam; the base64 wrapper keeps every pipe/&&
152
+ // inside the container (outer shell sees a plain exe call).
153
+ if (typeof runtime.sandboxRewrite === 'function') {
154
+ const rewrite = await runtime.sandboxRewrite(exec);
155
+ if (rewrite !== undefined) {
156
+ if (rewrite.kind === 'deny')
157
+ return { kind: 'deny', reason: rewrite.reason };
158
+ exec.arguments = {
159
+ ...exec.arguments,
160
+ command: rewrite.command,
161
+ };
162
+ }
163
+ }
145
164
  return next();
146
165
  }
147
166
  if (decision.kind !== 'ask')
@@ -171,7 +190,7 @@ export function makePreExecuteListener(runtime) {
171
190
  export function apply(ctx, config = {}) {
172
191
  // Runtime-adjustable config: 0.1.7 hands `.volatile()` fields to apply() as
173
192
  // live refs; `current()` shallow-resolves one snapshot per read.
174
- const current = () => resolveVolatileConfig(config);
193
+ let current = () => resolveVolatileConfig(config);
175
194
  // ─── Rules (volatile `rules` field on this entry) ─────────────────────────
176
195
  // The rules document is a `.volatile()` whole-object field of the plugin's
177
196
  // own entry (0.1.7 has no second projectable namespace). Unconfigured
@@ -190,17 +209,104 @@ export function apply(ctx, config = {}) {
190
209
  ctx.inject(['settings'], (child) => {
191
210
  const svc = child.settings;
192
211
  settingsRef.svc = svc;
212
+ if (svc === undefined)
213
+ return;
214
+ // setSource 换绑运行时读取源:source 可能是 () => values(≤0.1.5 scope.get)
215
+ // 或普通对象;base 与运行时读分用两个 resolve(手册 §P1-3)。
216
+ const rebindFrom = (source) => {
217
+ current = () => resolveVolatileConfig((typeof source === 'function' ? source() : (source ?? config)));
218
+ };
219
+ const seedDoc = () => ({ ...readRulesFileDoc(rulesFilePath), initialized: true });
220
+ const seedUpdate = () => svc.update(ENTRY_ID, { rules: seedDoc() });
221
+ // F4 加固(spec served 段判「无论如何无条件做」):宿主 installSection 在
222
+ // register 之后同步调 setSource(dsh-settings 0.1.5 lib/index.js:332),换绑
223
+ // 或 onChange 抛错会从 installSection 一路抛穿插件加载、连带注册失败——
224
+ // 这里兜住异常保住注册(卡派发依赖 ns∈served,注册比换绑成败更金贵)。
225
+ const guardedSetSource = (source) => {
226
+ try {
227
+ rebindFrom(source);
228
+ hooks.onChange();
229
+ }
230
+ catch (e) {
231
+ console.warn('[dsh-perm-gate] setSource rebind failed (registration kept):', e);
232
+ }
233
+ };
234
+ const hooks = {
235
+ setSource: guardedSetSource,
236
+ onChange: () => { applyVolatileUpdate(); },
237
+ };
193
238
  // Activation-time seed: a never-configured `rules` field is populated once
194
239
  // — from the rules file when one exists (implicit migration), otherwise a
195
240
  // bare starter document — and marked `initialized`, so the panel shows a
196
241
  // live rules document from the first boot instead of a missing-file
197
242
  // fallback. The field watcher below picks the seed up via reload.
198
- if (svc !== undefined && readRulesDocument() === undefined) {
199
- const seed = { ...readRulesFileDoc(rulesFilePath), initialized: true };
200
- void svc.update(ENTRY_ID, { rules: seed }).catch((e) => {
201
- console.warn('[dsh-perm-gate] rules seed write failed:', e);
243
+ // ── 三代 waist:三分支执行体互斥(P1-1/P1-2),register-retry 仅存活于
244
+ // 兜底分支且在 rc.1+ 永不命中;rc.1+ 不引入 configure(P1-5)。
245
+ if (typeof svc.installSection === 'function') {
246
+ console.warn('[dsh-perm-gate] settings generation = service.installSection (0.1.2/0.1.5 line)');
247
+ svc.installSection(ctx, ENTRY_ID, Config, resolveConfig(config), {
248
+ setSource: guardedSetSource,
249
+ onChange: () => { applyVolatileUpdate(); },
202
250
  });
251
+ if (readRulesDocument() === undefined) {
252
+ void seedUpdate().catch((e) => {
253
+ console.warn('[dsh-perm-gate] legacy seed update failed:', e);
254
+ });
255
+ }
256
+ return;
203
257
  }
258
+ void (async () => {
259
+ try {
260
+ // 变量说明符:dsh-settings 现为 optional peer(T35,2026-10-07——dependencies
261
+ // 硬钉 0.1.1-rc.2 已删:六线运行时它本来就无效——0.1.5 分支互斥不加载、老线
262
+ // nested 链接悬空后动态 import 经 node 向上解析落宿主自身副本,见
263
+ // cross-version-adaptation-all/findings.md T10b 附录)。变量形式让 TS 按 any
264
+ // 解析不做模块查证;另注意 ctx.get 不跨插件隔离上下文(index.ts:44),
265
+ // legacy 分支只能动态 import,不能 ctx.get 直取宿主单例。
266
+ const legacySpecifier = '@deepseek-ai/dsh-settings';
267
+ const legacy = (await import(legacySpecifier));
268
+ if (typeof legacy.installSettingsSection === 'function' && typeof legacy.settingsNamespace === 'function') {
269
+ console.warn('[dsh-perm-gate] settings generation = module.installSettingsSection (0.1.0/0.1.1 line)');
270
+ legacy.installSettingsSection(ctx, legacy.settingsNamespace(ENTRY_ID), Config, resolveConfig(config), {
271
+ setSource: (source) => {
272
+ rebindFrom(source);
273
+ hooks.onChange();
274
+ },
275
+ onChange: () => { applyVolatileUpdate(); },
276
+ });
277
+ if (readRulesDocument() === undefined) {
278
+ await seedUpdate().catch((e) => {
279
+ console.warn('[dsh-perm-gate] legacy seed update failed:', e);
280
+ });
281
+ }
282
+ return;
283
+ }
284
+ }
285
+ catch {
286
+ // 模块级 legacy API 缺失/导入失败 → 落到 rc.1 兜底
287
+ }
288
+ console.warn('[dsh-perm-gate] settings generation = rc1.loader (rc.1+: loader 自动注册 ns,update 直接成功;register-retry 兜底永不命中)');
289
+ if (readRulesDocument() === undefined) {
290
+ void (async () => {
291
+ try {
292
+ await seedUpdate();
293
+ }
294
+ catch (e) {
295
+ const message = e instanceof Error ? e.message : String(e);
296
+ // register-retry 兜底:rc.1+ 上 ns 已被 loader 占用,永不命中
297
+ if (!/not registered/i.test(message) || typeof svc.register !== 'function') {
298
+ throw e;
299
+ }
300
+ console.warn(`[dsh-perm-gate] legacy fallback: registering settings namespace ${ENTRY_ID}`);
301
+ svc.register(ENTRY_ID, Config, { base: { rules: seedDoc() } });
302
+ await seedUpdate();
303
+ console.warn('[dsh-perm-gate] legacy fallback registered + seeded');
304
+ }
305
+ })().catch((e) => {
306
+ console.warn('[dsh-perm-gate] rules seed write failed:', e);
307
+ });
308
+ }
309
+ })();
204
310
  });
205
311
  const settingsWrite = (write) => {
206
312
  const svc = settingsRef.svc;
@@ -461,7 +567,7 @@ export function apply(ctx, config = {}) {
461
567
  // The host no longer seeds settings (there is no write path): an empty
462
568
  // `allowlist` keeps the rules-file default via `runtime.allowlist()`.
463
569
  let lastRulesDocKey;
464
- ctx.on('loader/volatile-update', () => {
570
+ function applyVolatileUpdate() {
465
571
  const next = asSurface(current());
466
572
  if (next !== undefined) {
467
573
  if (Array.isArray(next.allowlist)) {
@@ -489,6 +595,10 @@ export function apply(ctx, config = {}) {
489
595
  if (changed)
490
596
  runtime.reload();
491
597
  }
598
+ }
599
+ ;
600
+ ctx.on('loader/volatile-update', () => {
601
+ applyVolatileUpdate();
492
602
  });
493
603
  // Event feed HTTP API (best effort): the dsh webServer service exposes the
494
604
  // JSONL decision events to the browser half; without it events stay on disk.
@@ -529,6 +639,10 @@ export function apply(ctx, config = {}) {
529
639
  const offHealth = registerHealthRoute(webServer, { check: () => runtime.healthCheck() });
530
640
  if (offHealth !== undefined)
531
641
  ctx.effect(() => () => { offHealth(); }, 'dsh-perm-gate: health route');
642
+ // T12: 设置数据桥(client 卡在 settingsScope 不解析的线上由此取数/写回)
643
+ for (const offBridge of registerSettingsBridgeRoutes(webServer, () => settingsRef.svc, ENTRY_ID)) {
644
+ ctx.effect(() => () => { offBridge(); }, 'dsh-perm-gate: settings bridge route');
645
+ }
532
646
  // Network diagnostics: mode, bind, port, proxy liveness, env injection,
533
647
  // block counters and recent blocks. Read-only.
534
648
  const offNetwork = registerNetworkRoute(webServer, {
@@ -709,6 +823,42 @@ export function apply(ctx, config = {}) {
709
823
  networkRefs.snapshot = () => networkLifecycle.snapshot();
710
824
  if (networkRefs.appliedKey === undefined)
711
825
  networkRefs.appliedKey = networkKeyOf(current());
826
+ // ─── AppContainer sandbox (goal 2, Windows) ─────────────────────────
827
+ // The gate-aware rewrite hook: an ALLOWED shell call is rewritten into the
828
+ // launcher invocation (or denied, fail-closed). Settings are read per call
829
+ // so the card's switches apply without a reload; the controller memoizes
830
+ // the launcher compile + loopback exemption.
831
+ const readSandboxSettings = () => {
832
+ const live = current();
833
+ const str = (key, fallback) => typeof live[key] === 'string' && live[key] !== '' ? live[key] : fallback;
834
+ const bool = (key, fallback) => typeof live[key] === 'boolean' ? live[key] : fallback;
835
+ return {
836
+ enabled: bool('sandboxEnabled', false),
837
+ mode: str('sandboxMode', 'workspace-write'),
838
+ proxy: str('sandboxProxy', ''),
839
+ loopback: str('sandboxLoopback', 'exempt'),
840
+ };
841
+ };
842
+ const sandboxController = new SandboxController({
843
+ cwdRoot: () => process.cwd(),
844
+ builtinProxyPort: () => {
845
+ try {
846
+ const snap = networkLifecycle.snapshot();
847
+ return snap.proxyActive === true && typeof snap.port === 'number' ? snap.port : undefined;
848
+ }
849
+ catch {
850
+ return undefined;
851
+ }
852
+ },
853
+ log: (message) => loggerWarn(message),
854
+ });
855
+ runtime.setSandboxHook((exec) => sandboxController.wrap(readSandboxSettings(), exec));
856
+ // The sandbox is Windows-only: on any other platform an enabled switch is a
857
+ // misconfiguration — warn once and treat it as off (wrap() also guards).
858
+ if (readSandboxSettings().enabled && process.platform !== 'win32') {
859
+ loggerWarn('[dsh-perm-gate] sandbox: sandboxEnabled is set but this host is not Windows — the AppContainer sandbox stays OFF');
860
+ }
861
+ ctx.effect(() => () => { void sandboxController.dispose(); }, 'dsh-perm-gate: sandbox scratch cleanup');
712
862
  // ─── Hot reload watcher (Phase 3, T3.6) ────────────────────────────
713
863
  const watchEnabled = typeof config.watch === 'boolean' ? config.watch : true;
714
864
  if (watchEnabled) {
package/lib/runtime.d.ts CHANGED
@@ -12,6 +12,14 @@ export interface PreToolDecisionLike {
12
12
  kind: 'deny' | 'ask';
13
13
  reason: string;
14
14
  }
15
+ /** The sandbox hook's verdict for one allowed shell call (appcontainer seam). */
16
+ export type SandboxRewriteOutcome = {
17
+ readonly kind: 'wrap';
18
+ readonly command: string;
19
+ } | {
20
+ readonly kind: 'deny';
21
+ readonly reason: string;
22
+ };
15
23
  /**
16
24
  * The P2 rule chain's own verdict for one call, as reported by
17
25
  * {@link PermGateRuntime.explainRules}. Distinct from the gate's effective
@@ -349,6 +357,20 @@ export declare class PermGateRuntime {
349
357
  private auditNetworkAsk;
350
358
  /** The compiled ruleset (read-only view for network module). */
351
359
  get compiledRuleset(): CompiledRuleset;
360
+ /**
361
+ * The sandbox hook installed by the plugin: gate-aware, fail-closed rewrite
362
+ * of an allowed shell call. Undefined → the feature is off entirely.
363
+ */
364
+ private sandboxHook;
365
+ /** Install/clear the sandbox rewrite hook (plugin wiring). */
366
+ setSandboxHook(hook: ((exec: ToolExecutionLike) => Promise<SandboxRewriteOutcome | undefined>) | undefined): void;
367
+ /**
368
+ * Gate-aware sandbox rewrite for the pre-execute waterfall's allow path.
369
+ * Mirrors {@link decideExecution}'s stand-down: outside the gate's presets
370
+ * the sandbox must not silently confine either — same scope, same fence.
371
+ * Returns undefined when not applicable.
372
+ */
373
+ sandboxRewrite(exec: ToolExecutionLike): Promise<SandboxRewriteOutcome | undefined>;
352
374
  private liveRiskLearning;
353
375
  private liveRiskSediment;
354
376
  private liveClassifySource;
package/lib/runtime.js CHANGED
@@ -581,6 +581,30 @@ export class PermGateRuntime {
581
581
  get compiledRuleset() {
582
582
  return this.ruleset;
583
583
  }
584
+ // ─── AppContainer sandbox seam (goal 2) ─────────────────────────────
585
+ /**
586
+ * The sandbox hook installed by the plugin: gate-aware, fail-closed rewrite
587
+ * of an allowed shell call. Undefined → the feature is off entirely.
588
+ */
589
+ sandboxHook;
590
+ /** Install/clear the sandbox rewrite hook (plugin wiring). */
591
+ setSandboxHook(hook) {
592
+ this.sandboxHook = hook;
593
+ }
594
+ /**
595
+ * Gate-aware sandbox rewrite for the pre-execute waterfall's allow path.
596
+ * Mirrors {@link decideExecution}'s stand-down: outside the gate's presets
597
+ * the sandbox must not silently confine either — same scope, same fence.
598
+ * Returns undefined when not applicable.
599
+ */
600
+ async sandboxRewrite(exec) {
601
+ const hook = this.sandboxHook;
602
+ if (hook === undefined)
603
+ return undefined;
604
+ if (!this.gateActive(this.presetOf(exec)))
605
+ return undefined;
606
+ return hook(exec);
607
+ }
584
608
  liveRiskLearning() {
585
609
  const read = this.options.readRiskLearning;
586
610
  if (read !== undefined)
package/locale/de.json ADDED
@@ -0,0 +1 @@
1
+ { "meta": { "title": "Berechtigungsschleuse", "description": "Deterministisch-zuerst, fehlgeschlossene Berechtigungsschleuse für DeepSeek Harness: P0 harte Ablehnung → P1 Sitzungsfreigabe → P2 statische Regelkette → P3 optionaler LLM-Semantik-Klassifikator → P4 manuelle Bestätigung, mit Befehls-Whitelist und -Blacklist." } }
package/locale/en.json ADDED
@@ -0,0 +1 @@
1
+ { "meta": { "title": "Permission Gate", "description": "Deterministic-first, fail-closed permission gate for DeepSeek Harness: P0 hard-deny → P1 session grant → P2 static rules → P3 LLM classifier → P4 ask, with command whitelist and blacklist." } }
package/locale/es.json ADDED
@@ -0,0 +1 @@
1
+ { "meta": { "title": "Puerta de permisos", "description": "Puerta de permisos determinista y de cierre seguro para DeepSeek Harness: P0 denegación estricta → P1 concesión de sesión → P2 cadena de reglas estáticas → P3 clasificador semántico LLM opcional → P4 confirmación manual, con lista blanca y lista negra de comandos." } }
package/locale/fr.json ADDED
@@ -0,0 +1 @@
1
+ { "meta": { "title": "Passerelle de permissions", "description": "Passerelle de permissions déterministe et à échec fermé pour DeepSeek Harness : P0 refus strict → P1 autorisation de session → P2 chaîne de règles statiques → P3 classifieur sémantique LLM optionnel → P4 confirmation manuelle, avec liste blanche et liste noire de commandes." } }
package/locale/it.json ADDED
@@ -0,0 +1 @@
1
+ { "meta": { "title": "Gateway dei permessi", "description": "Gateway dei permessi deterministico e fail-closed per DeepSeek Harness: P0 rifiuto rigido → P1 concessione di sessione → P2 catena di regole statiche → P3 classificatore semantico LLM opzionale → P4 conferma manuale, con whitelist e blacklist dei comandi." } }
package/locale/ja.json ADDED
@@ -0,0 +1 @@
1
+ { "meta": { "title": "権限ゲート", "description": "DeepSeek Harness 用の決定論優先・フェイルクローズド権限ゲート:P0 ハード拒否 → P1 セッション許可 → P2 静的ルールチェーン → P3 オプション LLM セマンティック分類器 → P4 人手確認、コマンドホワイトリスト/ブラックリスト付き。" } }
package/locale/ko.json ADDED
@@ -0,0 +1 @@
1
+ { "meta": { "title": "권한 게이트", "description": "DeepSeek Harness용 결정론 우선·실패 시 차단 권한 게이트: P0 하드 거부 → P1 세션 승인 → P2 정적 규칙 체인 → P3 선택적 LLM 시맨틱 분류기 → P4 수동 확인, 명령 화이트리스트/블랙리스트 포함." } }
package/locale/ru.json ADDED
@@ -0,0 +1 @@
1
+ { "meta": { "title": "Шлюз разрешений", "description": "Детерминированный шлюз разрешений с закрытием при сбое для DeepSeek Harness: P0 жёсткий запрет → P1 сессионное разрешение → P2 цепочка статических правил → P3 необязательный LLM-семантический классификатор → P4 ручной запрос, с белым и чёрным списком команд." } }
package/locale/zh.json ADDED
@@ -0,0 +1 @@
1
+ { "meta": { "title": "权限门禁", "description": "面向 DeepSeek Harness 的确定性优先、fail-closed 权限门禁:P0 硬拒绝 → P1 会话授权 → P2 静态规则链 → P3 可选 LLM 语义分类器 → P4 人工确认,含命令白名单与黑名单。" } }
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "dsh-perm-gate",
3
- "version": "5.0.4",
3
+ "version": "5.1.2-compat.7",
4
4
  "publishConfig": {
5
- "tag": "dsh-0.2.0"
5
+ "tag": "compat"
6
6
  },
7
7
  "description": "DSH permission-gate for the DeepSeek Harness 0.1.5 line: a single self-sufficient, deterministic-first, fail-closed gate covering P0 hard-deny -> P1 session grant -> P2 static rule (allow/deny) chain -> P3 optional LLM semantic classifier -> P4 ask, with command whitelist/blacklist.",
8
8
  "type": "module",
9
9
  "files": [
10
10
  "lib",
11
+ "locale",
11
12
  "scripts/patch-permission-glyph.mjs",
12
13
  "cordis.patch.yml",
13
14
  "dsh.plugin.json",
@@ -57,6 +58,7 @@
57
58
  "./client": {
58
59
  "default": "./lib/client.js"
59
60
  },
61
+ "./locale/*.json": "./locale/*.json",
60
62
  "./package.json": "./package.json"
61
63
  },
62
64
  "bin": {
@@ -65,13 +67,15 @@
65
67
  },
66
68
  "engines": {
67
69
  "node": ">=24",
68
- "dsh": ">=0.2.0-rc.1 <0.2.1-0"
70
+ "dsh": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2"
69
71
  },
70
72
  "peerDependencies": {
71
73
  "@deepseek-ai/cordis": "^4.0.1",
72
- "@deepseek-ai/dsh-client-locale": ">=0.2.0-rc.1 <0.2.1-0",
73
- "@deepseek-ai/dsh-client-ui-renderer": ">=0.2.0-rc.1 <0.2.1-0",
74
- "@deepseek-ai/dsh-client-ui-settings": ">=0.2.0-rc.1 <0.2.1-0"
74
+ "@deepseek-ai/dsh-client-locale": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2",
75
+ "@deepseek-ai/dsh-client-ui-renderer": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2",
76
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2",
77
+ "@deepseek-ai/dsh-settings": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2",
78
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2"
75
79
  },
76
80
  "dependencies": {
77
81
  "@deepseek-ai/schemastery": "^3.18.4",
@@ -80,10 +84,10 @@
80
84
  },
81
85
  "devDependencies": {
82
86
  "@deepseek-ai/cordis": "^4.0.1",
83
- "@deepseek-ai/dsh-client-locale": "0.2.0-rc.1",
84
- "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.1",
85
- "@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.1",
86
- "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.1",
87
+ "@deepseek-ai/dsh-client-locale": "0.2.0-rc.2",
88
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2",
89
+ "@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.2",
90
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2",
87
91
  "@eslint/js": "^10.0.1",
88
92
  "@types/node": "^22.0.0",
89
93
  "@types/react": "~18.3.1",
@@ -92,24 +96,25 @@
92
96
  "tsdown": "^0.22.14",
93
97
  "typescript": "^5.6.0",
94
98
  "typescript-eslint": "^8.69.0",
95
- "vitest": "^3.0.0"
99
+ "vitest": "^3.0.0",
100
+ "@deepseek-ai/dsh-settings": "0.1.1-rc.2"
96
101
  },
97
102
  "scripts": {
98
103
  "build": "tsc -p tsconfig.build.json && tsdown",
104
+ "prepack": "npm run build",
99
105
  "prepublishOnly": "npm run build",
100
106
  "typecheck": "tsc --noEmit && tsc -p tsconfig.client.json --noEmit",
101
107
  "lint": "eslint src test scripts tsdown.config.ts eslint.config.mjs",
102
108
  "test": "vitest run",
103
109
  "patch:glyph": "node scripts/patch-permission-glyph.mjs",
104
110
  "patch:glyph:check": "node scripts/patch-permission-glyph.mjs --check",
105
- "verify:line": "node scripts/verify-line.mjs next",
111
+ "verify:line": "node scripts/verify-line.mjs new",
106
112
  "verify:lines": "node scripts/verify-line.mjs both",
107
113
  "check:line": "node scripts/sync-0.1.5-line.mjs check",
108
114
  "release:latest": "npm publish --tag latest",
109
115
  "release:2x": "npm publish --tag 2.x",
110
116
  "release:legacy": "npm publish --tag legacy",
111
- "release:3x": "npm publish --tag dsh-0.1.5",
112
- "release:5x": "npm publish --tag dsh-0.2.0"
117
+ "release:3x": "npm publish --tag dsh-0.1.5"
113
118
  },
114
119
  "license": "MIT",
115
120
  "repository": {
@@ -153,6 +158,12 @@
153
158
  },
154
159
  "@deepseek-ai/dsh-client-ui-settings": {
155
160
  "optional": true
161
+ },
162
+ "@deepseek-ai/dsh-client-ui-conversation": {
163
+ "optional": true
164
+ },
165
+ "@deepseek-ai/dsh-settings": {
166
+ "optional": true
156
167
  }
157
168
  }
158
169
  }