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/dsh.plugin.json +1 -1
- package/lib/allowlist.d.ts +9 -0
- package/lib/allowlist.js +24 -0
- package/lib/client.js +4 -4
- package/lib/client.js.map +1 -1
- package/lib/config.d.ts +6 -3
- package/lib/config.js +10 -3
- package/lib/index.js +50 -4
- package/lib/rules-view.d.ts +1 -1
- package/lib/rules-view.js +1 -1
- package/package.json +1 -1
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
|
|
286
|
-
*
|
|
287
|
-
*
|
|
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
|
|
122
|
-
*
|
|
123
|
-
*
|
|
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:
|
|
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
|
|
509
|
-
//
|
|
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();
|
package/lib/rules-view.d.ts
CHANGED
|
@@ -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 = "
|
|
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 = '
|
|
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}.
|