dsh-perm-gate 4.2.0-beta.4 → 4.2.0-beta.6

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
@@ -277,14 +277,17 @@ export interface RulesConfig {
277
277
  readonly deny?: unknown[];
278
278
  readonly allow?: unknown[];
279
279
  readonly ask?: unknown[];
280
+ /** Activation-time seed marker (see RulesSchema). */
281
+ readonly initialized?: boolean;
280
282
  }
281
283
  /** Settings namespace for rules (separate from the main perm-gate namespace). */
282
284
  export declare const RULES_NAMESPACE = "dsh-perm-gate-rules";
283
285
  /**
284
286
  * Whether a settings-sourced rules document carries a REAL configuration —
285
- * entries or a non-default `defaultAction`. A namespace still holding bare
286
- * schema defaults is "not configured" and must not shadow the rules file
287
- * (the dual-source contract: settings first, file fallback).
287
+ * entries, a non-default `defaultAction`, or the `initialized` marker the
288
+ * activation-time seed writes after populating an empty document. A namespace
289
+ * still holding bare schema defaults is "not configured" and must not shadow
290
+ * the rules file (the dual-source contract: settings first, file fallback).
288
291
  */
289
292
  export declare function isRulesConfigured(value: unknown): boolean;
290
293
  /**
package/lib/config.js CHANGED
@@ -113,19 +113,26 @@ export const RulesSchema = z.object({
113
113
  deny: z.array(RuleEntrySchema).default([]),
114
114
  allow: z.array(RuleEntrySchema).default([]),
115
115
  ask: z.array(RuleEntrySchema).default([]),
116
+ // Activation-time seed marker: the gate writes `true` the first time it
117
+ // populates an empty document (file migration or bare starter), so the
118
+ // seeded namespace becomes the rules source instead of shadowing nothing.
119
+ initialized: z.boolean().default(false),
116
120
  });
117
121
  /** Settings namespace for rules (separate from the main perm-gate namespace). */
118
122
  export const RULES_NAMESPACE = 'dsh-perm-gate-rules';
119
123
  /**
120
124
  * Whether a settings-sourced rules document carries a REAL configuration —
121
- * entries or a non-default `defaultAction`. A namespace still holding bare
122
- * schema defaults is "not configured" and must not shadow the rules file
123
- * (the dual-source contract: settings first, file fallback).
125
+ * entries, a non-default `defaultAction`, or the `initialized` marker the
126
+ * activation-time seed writes after populating an empty document. A namespace
127
+ * still holding bare schema defaults is "not configured" and must not shadow
128
+ * the rules file (the dual-source contract: settings first, file fallback).
124
129
  */
125
130
  export function isRulesConfigured(value) {
126
131
  if (typeof value !== 'object' || value === null || Array.isArray(value))
127
132
  return false;
128
133
  const doc = value;
134
+ if (doc.initialized === true)
135
+ return true;
129
136
  if (doc.defaultAction !== undefined && doc.defaultAction !== 'ask')
130
137
  return true;
131
138
  for (const key of ['deny', 'allow', 'ask']) {
package/lib/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { Config, ensureDataDir, readRulesFromSettings, readVolatileValue, resolveVolatileConfig, resolveDshHome, resolveDataDir, resolveGatePresets, resolvePermissiveStrategies, resolveRulesFile } from './config.js';
4
+ import { appendAllowToSettings, readRulesFileDoc, replaceAllowInSettings } from './allowlist.js';
4
5
  import { runDryRun } from './dry-run.js';
5
6
  import { registerDryRunRoute, registerEventsRoute, registerHealthRoute, registerLearningRoute, registerNetworkRoute, registerReceiverRoute, registerReviewRoutes, registerRulesRoute } from './events.js';
6
7
  import { buildReceiverInfo } from './receiver-info.js';
@@ -176,10 +177,49 @@ export function apply(ctx, config = {}) {
176
177
  // own entry (0.1.7 has no second projectable namespace). Unconfigured
177
178
  // (bare defaults) -> undefined -> the rules file stays the source.
178
179
  const readRulesDocument = () => readRulesFromSettings({ get: () => readVolatileValue(config.rules) });
180
+ // ─── Host settings seam (allowlist write-back) ────────────────────────────
181
+ // The `settings` service mutates this entry's own config in the active
182
+ // profile patch (agent-default-model injects the same service). Its writes
183
+ // land in the `rules` volatile field, whose change watcher below reloads the
184
+ // runtime — so an "allow always" / allowlist edit takes effect on the next
185
+ // tool call even when the rules document (not the file) is the live source.
186
+ // The inject is a child effect: a host without the settings service leaves
187
+ // the ref unset and the runtime falls back to its rules-file write paths.
188
+ const ENTRY_ID = 'dsh-perm-gate';
189
+ const settingsRef = {};
190
+ ctx.inject(['settings'], (child) => {
191
+ const svc = child.settings;
192
+ settingsRef.svc = svc;
193
+ // Activation-time seed: a never-configured `rules` field is populated once
194
+ // — from the rules file when one exists (implicit migration), otherwise a
195
+ // bare starter document — and marked `initialized`, so the panel shows a
196
+ // live rules document from the first boot instead of a missing-file
197
+ // 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);
202
+ });
203
+ }
204
+ });
205
+ const settingsWrite = (write) => {
206
+ const svc = settingsRef.svc;
207
+ if (svc === undefined)
208
+ return false;
209
+ const scope = {
210
+ get: () => readVolatileValue(config.rules),
211
+ update: async (section) => { await svc.update(ENTRY_ID, { rules: section }); },
212
+ };
213
+ void write(scope, readRulesFileDoc(rulesFilePath)).catch((e) => {
214
+ console.warn('[dsh-perm-gate] settings rules write failed:', e);
215
+ });
216
+ return true;
217
+ };
179
218
  // Plugin-owned data files live under $DSH_HOME (node_modules may be
180
219
  // read-only). The default resolves to `$DSH_HOME` / `~/.dsh`, so a profile
181
220
  // entry that omits `config` still records events, snapshots and learning.
182
221
  const dataDir = resolveDataDir(typeof config.dshHome === 'string' ? config.dshHome : undefined);
222
+ const rulesFilePath = resolveRulesFile(typeof config.rulesFile === 'string' ? config.rulesFile : undefined, dataDir);
183
223
  // Host model-group services (typed minimally; supplied by the dsh runtime
184
224
  // through the loader `inject` — dsh-approval-gate demonstrates the same
185
225
  // contract). The llm service backs the `host` receiver; agentDefaultModel
@@ -321,9 +361,16 @@ export function apply(ctx, config = {}) {
321
361
  // The rules document lives in the plugin's data dir by default, so a
322
362
  // `$DSH_HOME/perm-gate/rules.yml` the user writes is actually loaded without
323
363
  // also having to declare `rulesFile` in the composition entry.
324
- rulesFile: resolveRulesFile(typeof config.rulesFile === 'string' ? config.rulesFile : undefined, dataDir),
364
+ rulesFile: rulesFilePath,
325
365
  // Settings-sourced rules take precedence over the file when present.
326
366
  readRulesDocument,
367
+ // Allowlist write-back through the settings service (this entry's own
368
+ // `rules` config field, seeded from the file document on first write);
369
+ // a falsy verdict (service not wired) falls back to the rules file.
370
+ allowlistWriter: {
371
+ append: (pattern, reason) => settingsWrite((scope, seed) => appendAllowToSettings(scope, pattern, reason, seed)),
372
+ replace: (patterns, reason) => settingsWrite((scope, seed) => replaceAllowInSettings(scope, patterns, reason, seed)),
373
+ },
327
374
  // The whole gate is scoped to the presets that opt into it (default: the
328
375
  // `permissive` tier this plugin adds); elsewhere it stands down entirely.
329
376
  gatePresets: resolveGatePresets(Array.isArray(config.gatePresets) ? config.gatePresets : undefined),
@@ -505,9 +552,8 @@ export function apply(ctx, config = {}) {
505
552
  });
506
553
  if (offDryRun !== undefined)
507
554
  ctx.effect(() => () => { offDryRun(); }, 'dsh-perm-gate: dry-run route');
508
- // Permissions view: prefer the settings namespace (dsh-perm-gate-rules)
509
- // when it carries a real configuration; fall back to the rules file.
510
- const rulesFilePath = resolveRulesFile(typeof config.rulesFile === 'string' ? config.rulesFile : undefined, dataDir);
555
+ // Permissions view: prefer the entry's `rules` config field when it
556
+ // carries a real configuration; fall back to the rules file.
511
557
  const offRules = registerRulesRoute(webServer, {
512
558
  view: () => {
513
559
  const settingsDoc = readRulesDocument();
@@ -37,7 +37,7 @@ export interface RulesView {
37
37
  */
38
38
  export declare function readRulesView(rulesFile: string): RulesView;
39
39
  /** The pseudo-path a settings view reports (there is no file to name). */
40
- export declare const SETTINGS_RULES_VIEW_PATH = "settings:dsh-perm-gate-rules";
40
+ export declare const SETTINGS_RULES_VIEW_PATH = "config:dsh-perm-gate#rules";
41
41
  /**
42
42
  * Render the settings-sourced rules for display — the read-only face of the
43
43
  * `dsh-perm-gate-rules` namespace, the twin of {@link readRulesView}.
package/lib/rules-view.js CHANGED
@@ -79,7 +79,7 @@ export function readRulesView(rulesFile) {
79
79
  }
80
80
  }
81
81
  /** The pseudo-path a settings view reports (there is no file to name). */
82
- export const SETTINGS_RULES_VIEW_PATH = 'settings:dsh-perm-gate-rules';
82
+ export const SETTINGS_RULES_VIEW_PATH = 'config:dsh-perm-gate#rules';
83
83
  /**
84
84
  * Render the settings-sourced rules for display — the read-only face of the
85
85
  * `dsh-perm-gate-rules` namespace, the twin of {@link readRulesView}.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-perm-gate",
3
- "version": "4.2.0-beta.4",
3
+ "version": "4.2.0-beta.6",
4
4
  "publishConfig": {
5
5
  "tag": "dsh-0.1.7"
6
6
  },