dsh-perm-gate 4.1.1 → 4.2.0-beta.1

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/lib/config.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import z from '@deepseek-ai/schemastery';
2
+ import type { Volatile, VolatileSnapshot } from '@deepseek-ai/cosmokit';
2
3
  import type { RuleAction } from './rule.js';
3
4
  import { DEFAULT_GATE_PRESETS, resolveGatePresets } from './preset.js';
4
5
  /**
@@ -293,7 +294,125 @@ export declare function isRulesConfigured(value: unknown): boolean;
293
294
  export declare function readRulesFromSettings(scope: {
294
295
  get(): unknown;
295
296
  } | undefined): RulesConfig | undefined;
296
- export declare const Config: z<PermGateConfig>;
297
+ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
298
+ rulesFile: z<string, string, "plain">;
299
+ dshHome: z<string, string, "plain">;
300
+ defaultAction: z<"allow" | "ask" | "deny", "allow" | "ask" | "deny", "volatile-defined">;
301
+ caseInsensitivePaths: z<boolean, boolean, "volatile-defined">;
302
+ classifierEnabled: z<boolean, boolean, "volatile-defined">;
303
+ classifierEndpoint: z<string, string, "volatile">;
304
+ classifierModel: z<string, string, "volatile-defined">;
305
+ classifierApiKey: z<string, string, "volatile">;
306
+ riskTimeoutMs: z<number, number, "volatile-defined">;
307
+ riskLearning: z<boolean, boolean, "volatile-defined">;
308
+ riskSediment: z<boolean, boolean, "volatile-defined">;
309
+ classifierSource: z<"host" | "custom", "host" | "custom", "volatile-defined">;
310
+ classifierProvider: z<string, string, "volatile">;
311
+ riskThreshold: z<number, number, "volatile-defined">;
312
+ learningFile: z<string, string, "plain">;
313
+ eventsFile: z<string, string, "plain">;
314
+ grantTtlMs: z<number, number, "volatile-defined">;
315
+ grantMaxUses: z<number, number, "volatile-defined">;
316
+ permissive: z<boolean, boolean, "volatile-defined">;
317
+ gatePresets: z<string[], string[], "plain">;
318
+ permissiveStrategies: z<NoInfer<Schemastery.ObjectS<NoInfer<{
319
+ trustAutoAllow: z<boolean, boolean, "defined">;
320
+ alwaysConfirm: z<boolean, boolean, "defined">;
321
+ llmAssist: z<boolean, boolean, "defined">;
322
+ trustEscalation: z<boolean, boolean, "defined">;
323
+ }>>>, NoInfer<Schemastery.ObjectT<NoInfer<{
324
+ trustAutoAllow: z<boolean, boolean, "defined">;
325
+ alwaysConfirm: z<boolean, boolean, "defined">;
326
+ llmAssist: z<boolean, boolean, "defined">;
327
+ trustEscalation: z<boolean, boolean, "defined">;
328
+ }>>>, "volatile">;
329
+ allowlist: z<NoInfer<string[]>, NoInfer<string[]>, "volatile">;
330
+ denyKeywords: z<NoInfer<string[]>, NoInfer<string[]>, "volatile">;
331
+ autoAllowTools: z<NoInfer<string[]>, NoInfer<string[]>, "volatile">;
332
+ sessionSweep: z<boolean, boolean, "volatile-defined">;
333
+ workspaceStoreFile: z<string, string, "plain">;
334
+ searchUp: z<boolean, boolean, "volatile-defined">;
335
+ fallbackPath: z<string, string, "plain">;
336
+ badFilePolicy: z<"fail" | "warn", "fail" | "warn", "volatile-defined">;
337
+ maxChainLength: z<number, number, "volatile-defined">;
338
+ networkEnabled: z<boolean, boolean, "volatile-defined">;
339
+ networkMode: z<"deny-all" | "whitelist" | "allow-all", "deny-all" | "whitelist" | "allow-all", "volatile-defined">;
340
+ networkUnlisted: z<"ask" | "deny", "ask" | "deny", "volatile-defined">;
341
+ networkUnattributed: z<"allow" | "deny", "allow" | "deny", "volatile-defined">;
342
+ networkLoopback: z<"allow" | "policy", "allow" | "policy", "volatile-defined">;
343
+ networkBind: z<string, string, "volatile-defined">;
344
+ networkPort: z<number, number, "volatile-defined">;
345
+ networkNoProxy: z<"clear" | "preserve", "clear" | "preserve", "volatile-defined">;
346
+ networkInjectEnv: z<boolean, boolean, "volatile-defined">;
347
+ networkAskTimeoutMs: z<number, number, "volatile-defined">;
348
+ networkGrantTtlMs: z<number, number, "volatile-defined">;
349
+ rules: z<NoInfer<RulesConfig>, NoInfer<RulesConfig>, "volatile">;
350
+ watch: z<boolean, boolean, "defined">;
351
+ watchDebounceMs: z<number, number, "defined">;
352
+ }>>, Schemastery.ObjectT<NoInfer<{
353
+ rulesFile: z<string, string, "plain">;
354
+ dshHome: z<string, string, "plain">;
355
+ defaultAction: z<"allow" | "ask" | "deny", "allow" | "ask" | "deny", "volatile-defined">;
356
+ caseInsensitivePaths: z<boolean, boolean, "volatile-defined">;
357
+ classifierEnabled: z<boolean, boolean, "volatile-defined">;
358
+ classifierEndpoint: z<string, string, "volatile">;
359
+ classifierModel: z<string, string, "volatile-defined">;
360
+ classifierApiKey: z<string, string, "volatile">;
361
+ riskTimeoutMs: z<number, number, "volatile-defined">;
362
+ riskLearning: z<boolean, boolean, "volatile-defined">;
363
+ riskSediment: z<boolean, boolean, "volatile-defined">;
364
+ classifierSource: z<"host" | "custom", "host" | "custom", "volatile-defined">;
365
+ classifierProvider: z<string, string, "volatile">;
366
+ riskThreshold: z<number, number, "volatile-defined">;
367
+ learningFile: z<string, string, "plain">;
368
+ eventsFile: z<string, string, "plain">;
369
+ grantTtlMs: z<number, number, "volatile-defined">;
370
+ grantMaxUses: z<number, number, "volatile-defined">;
371
+ permissive: z<boolean, boolean, "volatile-defined">;
372
+ gatePresets: z<string[], string[], "plain">;
373
+ permissiveStrategies: z<NoInfer<Schemastery.ObjectS<NoInfer<{
374
+ trustAutoAllow: z<boolean, boolean, "defined">;
375
+ alwaysConfirm: z<boolean, boolean, "defined">;
376
+ llmAssist: z<boolean, boolean, "defined">;
377
+ trustEscalation: z<boolean, boolean, "defined">;
378
+ }>>>, NoInfer<Schemastery.ObjectT<NoInfer<{
379
+ trustAutoAllow: z<boolean, boolean, "defined">;
380
+ alwaysConfirm: z<boolean, boolean, "defined">;
381
+ llmAssist: z<boolean, boolean, "defined">;
382
+ trustEscalation: z<boolean, boolean, "defined">;
383
+ }>>>, "volatile">;
384
+ allowlist: z<NoInfer<string[]>, NoInfer<string[]>, "volatile">;
385
+ denyKeywords: z<NoInfer<string[]>, NoInfer<string[]>, "volatile">;
386
+ autoAllowTools: z<NoInfer<string[]>, NoInfer<string[]>, "volatile">;
387
+ sessionSweep: z<boolean, boolean, "volatile-defined">;
388
+ workspaceStoreFile: z<string, string, "plain">;
389
+ searchUp: z<boolean, boolean, "volatile-defined">;
390
+ fallbackPath: z<string, string, "plain">;
391
+ badFilePolicy: z<"fail" | "warn", "fail" | "warn", "volatile-defined">;
392
+ maxChainLength: z<number, number, "volatile-defined">;
393
+ networkEnabled: z<boolean, boolean, "volatile-defined">;
394
+ networkMode: z<"deny-all" | "whitelist" | "allow-all", "deny-all" | "whitelist" | "allow-all", "volatile-defined">;
395
+ networkUnlisted: z<"ask" | "deny", "ask" | "deny", "volatile-defined">;
396
+ networkUnattributed: z<"allow" | "deny", "allow" | "deny", "volatile-defined">;
397
+ networkLoopback: z<"allow" | "policy", "allow" | "policy", "volatile-defined">;
398
+ networkBind: z<string, string, "volatile-defined">;
399
+ networkPort: z<number, number, "volatile-defined">;
400
+ networkNoProxy: z<"clear" | "preserve", "clear" | "preserve", "volatile-defined">;
401
+ networkInjectEnv: z<boolean, boolean, "volatile-defined">;
402
+ networkAskTimeoutMs: z<number, number, "volatile-defined">;
403
+ networkGrantTtlMs: z<number, number, "volatile-defined">;
404
+ rules: z<NoInfer<RulesConfig>, NoInfer<RulesConfig>, "volatile">;
405
+ watch: z<boolean, boolean, "defined">;
406
+ watchDebounceMs: z<number, number, "defined">;
407
+ }>>, "plain">;
408
+ /** Live reference the 0.1.7 loader hands `apply` for `.volatile()` config fields. */
409
+ export type VolatileRef<T> = Volatile<T>;
410
+ /** The config fields marked `.volatile()` — live refs inside `apply`'s config. */
411
+ 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"];
412
+ /** Resolve one possibly-volatile field: a live ref on 0.1.7+, a plain value otherwise. */
413
+ export declare function readVolatileValue<T>(value: T | Volatile<T> | undefined): VolatileSnapshot<T> | undefined;
414
+ /** Shallow-resolve every volatile field into a plain snapshot (one per read). */
415
+ export declare function resolveVolatileConfig(config: object): Record<string, unknown>;
297
416
  export type ResolvedPermGateConfig = Required<Pick<PermGateConfig, 'caseInsensitivePaths' | 'grantTtlMs' | 'grantMaxUses' | 'permissive' | 'riskTimeoutMs' | 'riskLearning' | 'riskThreshold'>> & Pick<PermGateConfig, 'rulesFile' | 'dshHome' | 'defaultAction' | 'classifierEnabled' | 'classifierEndpoint' | 'classifierModel' | 'classifierApiKey' | 'learningFile' | 'eventsFile'> & {
298
417
  readonly permissiveStrategies: PermissiveStrategies;
299
418
  readonly gatePresets: readonly string[];
package/lib/config.js CHANGED
@@ -146,58 +146,94 @@ export function readRulesFromSettings(scope) {
146
146
  export const Config = z.object({
147
147
  rulesFile: z.string(),
148
148
  dshHome: z.string(),
149
- defaultAction: z.union(['allow', 'ask', 'deny']).default('ask'),
150
- caseInsensitivePaths: z.boolean().default(true),
151
- classifierEnabled: z.boolean().default(false),
152
- classifierEndpoint: z.string(),
153
- classifierModel: z.string().default('deepseek-chat'),
154
- classifierApiKey: z.string(),
155
- riskTimeoutMs: z.number().min(1000).default(20_000),
156
- riskLearning: z.boolean().default(true),
157
- riskSediment: z.boolean().default(true),
158
- classifierSource: z.union(['custom', 'host']).default('custom'),
159
- classifierProvider: z.string(),
160
- riskThreshold: z.number().min(1).max(10).default(1),
149
+ defaultAction: z.union(['allow', 'ask', 'deny']).default('ask').volatile(),
150
+ caseInsensitivePaths: z.boolean().default(true).volatile(),
151
+ classifierEnabled: z.boolean().default(false).volatile(),
152
+ classifierEndpoint: z.string().volatile(),
153
+ classifierModel: z.string().default('deepseek-chat').volatile(),
154
+ classifierApiKey: z.string().volatile(),
155
+ riskTimeoutMs: z.number().min(1000).default(20_000).volatile(),
156
+ riskLearning: z.boolean().default(true).volatile(),
157
+ riskSediment: z.boolean().default(true).volatile(),
158
+ classifierSource: z.union(['custom', 'host']).default('custom').volatile(),
159
+ classifierProvider: z.string().volatile(),
160
+ riskThreshold: z.number().min(1).max(10).default(1).volatile(),
161
161
  learningFile: z.string(),
162
162
  eventsFile: z.string(),
163
- grantTtlMs: z.number().min(1).default(5 * 60_000),
164
- grantMaxUses: z.number().min(1).default(1),
165
- permissive: z.boolean().default(false),
163
+ grantTtlMs: z.number().min(1).default(5 * 60_000).volatile(),
164
+ grantMaxUses: z.number().min(1).default(1).volatile(),
165
+ permissive: z.boolean().default(false).volatile(),
166
166
  gatePresets: z.array(z.string()),
167
167
  permissiveStrategies: z.object({
168
168
  trustAutoAllow: z.boolean().default(true),
169
169
  alwaysConfirm: z.boolean().default(false),
170
170
  llmAssist: z.boolean().default(false),
171
171
  trustEscalation: z.boolean().default(true),
172
- }),
173
- allowlist: z.array(z.string()),
174
- denyKeywords: z.array(z.string()),
175
- autoAllowTools: z.array(z.string()),
176
- sessionSweep: z.boolean().default(true),
172
+ }).volatile(),
173
+ allowlist: z.array(z.string()).volatile(),
174
+ denyKeywords: z.array(z.string()).volatile(),
175
+ autoAllowTools: z.array(z.string()).volatile(),
176
+ sessionSweep: z.boolean().default(true).volatile(),
177
177
  workspaceStoreFile: z.string(),
178
178
  // Rule chain (T1.9)
179
- searchUp: z.boolean().default(false),
179
+ searchUp: z.boolean().default(false).volatile(),
180
180
  fallbackPath: z.string(),
181
- badFilePolicy: z.union(['fail', 'warn']).default('fail'),
182
- maxChainLength: z.number().min(1).max(50).default(10),
181
+ badFilePolicy: z.union(['fail', 'warn']).default('fail').volatile(),
182
+ maxChainLength: z.number().min(1).max(50).default(10).volatile(),
183
183
  // Network (Phase 2)
184
- networkEnabled: z.boolean().default(false),
185
- networkMode: z.union(['deny-all', 'whitelist', 'allow-all']).default('whitelist'),
186
- networkUnlisted: z.union(['ask', 'deny']).default('ask'),
187
- networkUnattributed: z.union(['allow', 'deny']).default('allow'),
188
- networkLoopback: z.union(['allow', 'policy']).default('allow'),
189
- networkBind: z.string().default('127.0.0.1'),
190
- networkPort: z.number().min(0).max(65535).default(0),
191
- networkNoProxy: z.union(['clear', 'preserve']).default('clear'),
192
- networkInjectEnv: z.boolean().default(true),
193
- networkAskTimeoutMs: z.number().min(1000).max(600_000).default(120_000),
194
- networkGrantTtlMs: z.number().min(0).max(24 * 60 * 60_000).default(30 * 60_000),
184
+ networkEnabled: z.boolean().default(false).volatile(),
185
+ networkMode: z.union(['deny-all', 'whitelist', 'allow-all']).default('whitelist').volatile(),
186
+ networkUnlisted: z.union(['ask', 'deny']).default('ask').volatile(),
187
+ networkUnattributed: z.union(['allow', 'deny']).default('allow').volatile(),
188
+ networkLoopback: z.union(['allow', 'policy']).default('allow').volatile(),
189
+ networkBind: z.string().default('127.0.0.1').volatile(),
190
+ networkPort: z.number().min(0).max(65535).default(0).volatile(),
191
+ networkNoProxy: z.union(['clear', 'preserve']).default('clear').volatile(),
192
+ networkInjectEnv: z.boolean().default(true).volatile(),
193
+ networkAskTimeoutMs: z.number().min(1000).max(600_000).default(120_000).volatile(),
194
+ networkGrantTtlMs: z.number().min(0).max(24 * 60 * 60_000).default(30 * 60_000).volatile(),
195
+ // 0.1.7: the rules document lives on this entry as a volatile whole-object
196
+ // field (a second settings namespace is no longer projectable). Unconfigured
197
+ // (bare defaults) -> the rules file remains the source.
198
+ rules: RulesSchema.volatile(),
195
199
  // Hot reload (Phase 3)
196
200
  watch: z.boolean().default(false),
197
201
  watchDebounceMs: z.number().min(50).max(5000).default(300),
198
202
  });
203
+ /** The config fields marked `.volatile()` — live refs inside `apply`'s config. */
204
+ export const VOLATILE_CONFIG_KEYS = [
205
+ 'defaultAction', 'caseInsensitivePaths', 'classifierEnabled', 'classifierEndpoint',
206
+ 'classifierModel', 'classifierApiKey', 'riskTimeoutMs', 'riskLearning', 'riskSediment',
207
+ 'classifierSource', 'classifierProvider', 'riskThreshold', 'grantTtlMs', 'grantMaxUses',
208
+ 'permissive', 'permissiveStrategies', 'allowlist', 'denyKeywords', 'autoAllowTools',
209
+ 'sessionSweep', 'searchUp', 'badFilePolicy', 'maxChainLength',
210
+ 'networkEnabled', 'networkMode', 'networkUnlisted', 'networkUnattributed', 'networkLoopback',
211
+ 'networkBind', 'networkPort', 'networkNoProxy', 'networkInjectEnv', 'networkAskTimeoutMs',
212
+ 'networkGrantTtlMs', 'rules',
213
+ ];
214
+ /** Resolve one possibly-volatile field: a live ref on 0.1.7+, a plain value otherwise. */
215
+ export function readVolatileValue(value) {
216
+ if (value !== null && typeof value === 'object' && typeof value.get === 'function') {
217
+ return value.get();
218
+ }
219
+ return value;
220
+ }
221
+ /** Shallow-resolve every volatile field into a plain snapshot (one per read). */
222
+ export function resolveVolatileConfig(config) {
223
+ const out = { ...config };
224
+ for (const key of VOLATILE_CONFIG_KEYS) {
225
+ const v = out[key];
226
+ if (v !== null && typeof v === 'object' && typeof v.get === 'function') {
227
+ out[key] = v.get();
228
+ }
229
+ }
230
+ return out;
231
+ }
199
232
  export function resolveConfig(config = {}) {
200
- const parsed = Config(config);
233
+ // 0.1.7: the apply-time config carries live refs for `.volatile()` fields —
234
+ // resolve them before validation; schemastery's own schema call also wraps
235
+ // volatile fields into fresh refs, so resolve the output as well.
236
+ const parsed = resolveVolatileConfig(Config(resolveVolatileConfig(config)));
201
237
  return {
202
238
  rulesFile: parsed.rulesFile,
203
239
  dshHome: parsed.dshHome,
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { Config, RULES_NAMESPACE, RulesSchema, readRulesFromSettings, resolveDshHome, resolveDataDir, ensureDataDir, resolveGatePresets, resolvePermissiveStrategies, resolveRulesFile } from './config.js';
3
+ import { Config, ensureDataDir, readRulesFromSettings, readVolatileValue, resolveVolatileConfig, resolveDshHome, resolveDataDir, resolveGatePresets, resolvePermissiveStrategies, resolveRulesFile } from './config.js';
4
4
  import { runDryRun } from './dry-run.js';
5
5
  import { registerDryRunRoute, registerEventsRoute, registerHealthRoute, registerLearningRoute, registerNetworkRoute, registerReceiverRoute, registerReviewRoutes, registerRulesRoute } from './events.js';
6
6
  import { buildReceiverInfo } from './receiver-info.js';
@@ -36,27 +36,6 @@ export { Config };
36
36
  const APPROVAL_OUTCOMES = new Set(['allowed-once', 'rejected', 'cancelled', 'unavailable']);
37
37
  /** Runtime settings namespace: `permissive` + `permissiveStrategies` (editable in the UI). */
38
38
  export const PERMISSIVE_NAMESPACE = 'dsh-perm-gate';
39
- /**
40
- * Inline equivalent of the official `installSettingsSection` helper: register
41
- * the namespace through the `settings` service, layer the composition entry as
42
- * `base`, and keep the runtime source live so the UI card applies immediately.
43
- * `onScope` receives the live scope after registration (for seeding/syncing
44
- * host-owned fields like the allowlist).
45
- */
46
- function installSettingsSection(ctx, ns, schema, entry, hooks) {
47
- ;
48
- ctx.inject(['settings'], (sctx) => {
49
- const scope = sctx.settings.register(ns, schema, { base: entry });
50
- hooks.setSource(() => scope.get());
51
- hooks.onChange();
52
- hooks.onScope?.(scope);
53
- sctx.effect(() => () => {
54
- hooks.setSource(() => entry);
55
- hooks.onChange();
56
- });
57
- scope.watch(() => hooks.onChange());
58
- });
59
- }
60
39
  /** Column a settings-scope value into the typed live face. */
61
40
  function asSurface(value) {
62
41
  return typeof value === 'object' && value !== null && !Array.isArray(value)
@@ -189,14 +168,14 @@ export function makePreExecuteListener(runtime) {
189
168
  };
190
169
  }
191
170
  export function apply(ctx, config = {}) {
192
- // Runtime-adjustable config: the composition entry is the base; the settings
193
- // namespace layers on top and `current()` always reads the active section.
194
- let current = () => config;
195
- // ─── Rules settings scope (dsh-perm-gate-rules namespace) ────────────────
196
- // Holds the live settings-sourced rules document. When non-undefined, the
197
- // runtime prefers it over the rules file on disk.
198
- let rulesScope;
199
- const readRulesDocument = () => readRulesFromSettings(rulesScope);
171
+ // Runtime-adjustable config: 0.1.7 hands `.volatile()` fields to apply() as
172
+ // live refs; `current()` shallow-resolves one snapshot per read.
173
+ const current = () => resolveVolatileConfig(config);
174
+ // ─── Rules (volatile `rules` field on this entry) ─────────────────────────
175
+ // The rules document is a `.volatile()` whole-object field of the plugin's
176
+ // own entry (0.1.7 has no second projectable namespace). Unconfigured
177
+ // (bare defaults) -> undefined -> the rules file stays the source.
178
+ const readRulesDocument = () => readRulesFromSettings({ get: () => readVolatileValue(config.rules) });
200
179
  // Plugin-owned data files live under $DSH_HOME (node_modules may be
201
180
  // read-only). The default resolves to `$DSH_HOME` / `~/.dsh`, so a profile
202
181
  // entry that omits `config` still records events, snapshots and learning.
@@ -429,54 +408,40 @@ export function apply(ctx, config = {}) {
429
408
  // the settings watch only ever runs on a later user action. Held in an
430
409
  // object so the mutable slots stay lint-clean and the intent is explicit.
431
410
  const networkRefs = {};
432
- installSettingsSection(ctx, PERMISSIVE_NAMESPACE, Config, config, {
433
- setSource: (source) => {
434
- current = source;
435
- },
436
- onChange: () => { },
437
- onScope: (scope) => {
438
- // Seed the editable whitelist from the rules file only when the namespace
439
- // carries no override yet, so the card shows the current allow patterns.
440
- const surface = asSurface(scope.get());
441
- if (surface !== undefined && !Array.isArray(surface.allowlist)) {
442
- const patterns = runtime.allowlist();
443
- if (patterns.length > 0)
444
- void scope.update({ allowlist: patterns.slice() });
411
+ // ─── 0.1.7 declarative settings ───────────────────────────────────────────
412
+ // The volatile fields render the settings form and arrive as live refs;
413
+ // each committed edit raises ONE `loader/volatile-update` (no remount).
414
+ // The host no longer seeds settings (there is no write path): an empty
415
+ // `allowlist` keeps the rules-file default via `runtime.allowlist()`.
416
+ let lastRulesDocKey;
417
+ ctx.on('loader/volatile-update', () => {
418
+ const next = asSurface(current());
419
+ if (next !== undefined) {
420
+ if (Array.isArray(next.allowlist)) {
421
+ runtime.setAllowlist(next.allowlist.filter((x) => typeof x === 'string'));
445
422
  }
446
- // Card edits -> namespace -> rulesFile (mirror + reload).
447
- scope.watch(() => {
448
- const next = asSurface(scope.get());
449
- if (next !== undefined && Array.isArray(next.allowlist)) {
450
- runtime.setAllowlist(next.allowlist.filter((x) => typeof x === 'string'));
451
- }
452
- // A network knob changed: re-mount the proxy so the card's switches
453
- // take effect without a plugin reload. Only a rebind-relevant edit
454
- // triggers it.
455
- const key = networkKeyOf(next);
456
- if (networkRefs.appliedKey !== undefined && networkRefs.appliedKey !== key) {
457
- networkRefs.appliedKey = key;
458
- networkRefs.rebind?.();
459
- }
460
- else if (networkRefs.appliedKey === undefined) {
461
- networkRefs.appliedKey = key;
462
- }
463
- });
464
- },
465
- });
466
- // ─── Rules settings namespace (dsh-perm-gate-rules) ─────────────────────
467
- // Registers the rules document in its own DSH settings namespace so the gate
468
- // loads rules from settings (when configured) instead of the rules file.
469
- installSettingsSection(ctx, RULES_NAMESPACE, RulesSchema, {}, {
470
- setSource: (source) => {
471
- rulesScope = { get: source };
472
- },
473
- onChange: () => { },
474
- onScope: (scope) => {
475
- rulesScope = scope;
476
- // When rules change in settings, reload the runtime so the new ruleset
477
- // takes effect on the next tool call.
478
- scope.watch(() => { runtime.reload(); });
479
- },
423
+ // A network knob changed: re-mount the proxy so the card's switches
424
+ // take effect without a plugin reload. Only a rebind-relevant edit
425
+ // triggers it.
426
+ const key = networkKeyOf(next);
427
+ if (networkRefs.appliedKey !== undefined && networkRefs.appliedKey !== key) {
428
+ networkRefs.appliedKey = key;
429
+ networkRefs.rebind?.();
430
+ }
431
+ else if (networkRefs.appliedKey === undefined) {
432
+ networkRefs.appliedKey = key;
433
+ }
434
+ }
435
+ // A rules edit reloads the runtime so the new ruleset takes effect on the
436
+ // next tool call (allowlist/network edits must not pay for a reload).
437
+ const rulesDoc = readRulesDocument();
438
+ const rulesKey = rulesDoc === undefined ? '' : JSON.stringify(rulesDoc);
439
+ if (rulesKey !== lastRulesDocKey) {
440
+ const changed = lastRulesDocKey !== undefined;
441
+ lastRulesDocKey = rulesKey;
442
+ if (changed)
443
+ runtime.reload();
444
+ }
480
445
  });
481
446
  // Event feed HTTP API (best effort): the dsh webServer service exposes the
482
447
  // JSONL decision events to the browser half; without it events stay on disk.
package/package.json CHANGED
@@ -1,10 +1,8 @@
1
1
  {
2
2
  "name": "dsh-perm-gate",
3
- "version": "4.1.1",
3
+ "version": "4.2.0-beta.1",
4
4
  "publishConfig": {
5
- "registry": "https://registry.npmjs.org",
6
- "access": "public",
7
- "tag": "dsh-0.1.5"
5
+ "tag": "dsh-0.1.7"
8
6
  },
9
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.",
10
8
  "type": "module",
@@ -71,28 +69,21 @@
71
69
  },
72
70
  "peerDependencies": {
73
71
  "@deepseek-ai/cordis": "^4.0.1",
74
- "@deepseek-ai/dsh-client-locale": ">=0.1.5-rc.1 <0.2.0-0",
75
- "@deepseek-ai/dsh-client-ui-settings": ">=0.1.5-rc.1 <0.2.0-0",
76
- "@deepseek-ai/dsh-client-ui-renderer": ">=0.1.5-rc.1 <0.2.0-0",
77
- "@deepseek-ai/dsh-client-ui-conversation": ">=0.1.5-rc.1 <0.2.0-0"
78
- },
79
- "peerDependenciesMeta": {
80
- "@deepseek-ai/dsh-client-locale": { "optional": true },
81
- "@deepseek-ai/dsh-client-ui-settings": { "optional": true },
82
- "@deepseek-ai/dsh-client-ui-renderer": { "optional": true },
83
- "@deepseek-ai/dsh-client-ui-conversation": { "optional": true }
72
+ "@deepseek-ai/dsh-client-locale": ">=0.1.7-rc.1 <0.2.0-0",
73
+ "@deepseek-ai/dsh-client-ui-renderer": ">=0.1.7-rc.1 <0.2.0-0",
74
+ "@deepseek-ai/dsh-client-ui-settings": ">=0.1.7-rc.1 <0.2.0-0"
84
75
  },
85
76
  "dependencies": {
86
- "@deepseek-ai/schemastery": "^3.18.0",
77
+ "@deepseek-ai/schemastery": "^3.18.4",
87
78
  "chokidar": "^5.0.0",
88
79
  "yaml": "^2.5.0"
89
80
  },
90
81
  "devDependencies": {
91
82
  "@deepseek-ai/cordis": "^4.0.1",
92
- "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2",
93
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.2",
94
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.2",
95
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
83
+ "@deepseek-ai/dsh-client-locale": "^0.1.7-rc.1",
84
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.7-rc.1",
85
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.7-rc.1",
86
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.7-rc.1",
96
87
  "@eslint/js": "^10.0.1",
97
88
  "@types/node": "^22.0.0",
98
89
  "@types/react": "~18.3.1",
@@ -151,5 +142,16 @@
151
142
  ],
152
143
  "platform": "web"
153
144
  }
145
+ },
146
+ "peerDependenciesMeta": {
147
+ "@deepseek-ai/dsh-client-locale": {
148
+ "optional": true
149
+ },
150
+ "@deepseek-ai/dsh-client-ui-renderer": {
151
+ "optional": true
152
+ },
153
+ "@deepseek-ai/dsh-client-ui-settings": {
154
+ "optional": true
155
+ }
154
156
  }
155
157
  }