dsh-perm-gate 4.1.3 → 4.1.4
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 +8 -0
- package/lib/allowlist.js +23 -0
- package/lib/config.d.ts +6 -3
- package/lib/config.js +10 -3
- package/lib/index.js +38 -3
- package/package.json +1 -1
package/dsh.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-perm-gate",
|
|
3
3
|
"name": "dsh-perm-gate",
|
|
4
|
-
"version": "4.1.
|
|
4
|
+
"version": "4.1.4",
|
|
5
5
|
"description": {
|
|
6
6
|
"en": "Deterministic-first, fail-closed permission gate for DeepSeek Harness 0.1.2+: a P0 hard-deny -> P1 precise session grant -> P2 static allow/deny rule chain -> P3 optional LLM classifier -> P4 ask pipeline over the tools/pre-execute waterfall, with a standalone CLI dry-run evaluator and an auditable decision log. It only vetoes with deny/ask and always delegates otherwise, and unknown or malformed rules fail loud instead of silently standing the gate down.",
|
|
7
7
|
"zh": "面向 DeepSeek Harness 0.1.2+ 的确定性优先、失败即拒的权限闸门:在 tools/pre-execute 瀑布上串起 P0 硬拒绝 → P1 精确会话授权 → P2 静态 allow/deny 规则链 → P3 可选 LLM 语义分类 → P4 ask,并附带独立 CLI 干跑评估器与可审计的决策日志。只以 deny/ask 否决、其余一律放行,规则未知或非法时直接报错,绝不静默失效。"
|
package/lib/allowlist.d.ts
CHANGED
|
@@ -44,3 +44,11 @@ export declare function listAllowCommands(rulesFile: string): string[];
|
|
|
44
44
|
* @param reason - reason recorded on each generated allow entry.
|
|
45
45
|
*/
|
|
46
46
|
export declare function replaceAllowCommands(rulesFile: string, patterns: readonly string[], reason?: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Parse the rules file into a plain document — the seed for the first
|
|
49
|
+
* settings-backed allowlist write, so migrating a file-defined document into
|
|
50
|
+
* the rules namespace cannot drop its deny/ask sections. The file's optional
|
|
51
|
+
* `permissions:` wrapper is unwrapped (the settings document stores the bare
|
|
52
|
+
* form). Returns `{}` when the file is absent or unreadable.
|
|
53
|
+
*/
|
|
54
|
+
export declare function readRulesFileDoc(rulesFile: string): unknown;
|
package/lib/allowlist.js
CHANGED
|
@@ -197,3 +197,26 @@ function readFileSafe(p) {
|
|
|
197
197
|
return '';
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Parse the rules file into a plain document — the seed for the first
|
|
202
|
+
* settings-backed allowlist write, so migrating a file-defined document into
|
|
203
|
+
* the rules namespace cannot drop its deny/ask sections. The file's optional
|
|
204
|
+
* `permissions:` wrapper is unwrapped (the settings document stores the bare
|
|
205
|
+
* form). Returns `{}` when the file is absent or unreadable.
|
|
206
|
+
*/
|
|
207
|
+
export function readRulesFileDoc(rulesFile) {
|
|
208
|
+
const text = readFileSafe(rulesFile);
|
|
209
|
+
if (text === '')
|
|
210
|
+
return {};
|
|
211
|
+
try {
|
|
212
|
+
const root = parse(text);
|
|
213
|
+
if (root !== null && typeof root === 'object' && !Array.isArray(root)
|
|
214
|
+
&& root['permissions'] !== null && typeof root['permissions'] === 'object') {
|
|
215
|
+
return root['permissions'];
|
|
216
|
+
}
|
|
217
|
+
return root ?? {};
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return {};
|
|
221
|
+
}
|
|
222
|
+
}
|
package/lib/config.d.ts
CHANGED
|
@@ -276,14 +276,17 @@ export interface RulesConfig {
|
|
|
276
276
|
readonly deny?: unknown[];
|
|
277
277
|
readonly allow?: unknown[];
|
|
278
278
|
readonly ask?: unknown[];
|
|
279
|
+
/** Activation-time seed marker (see RulesSchema). */
|
|
280
|
+
readonly initialized?: boolean;
|
|
279
281
|
}
|
|
280
282
|
/** Settings namespace for rules (separate from the main perm-gate namespace). */
|
|
281
283
|
export declare const RULES_NAMESPACE = "dsh-perm-gate-rules";
|
|
282
284
|
/**
|
|
283
285
|
* Whether a settings-sourced rules document carries a REAL configuration —
|
|
284
|
-
* entries
|
|
285
|
-
*
|
|
286
|
-
*
|
|
286
|
+
* entries, a non-default `defaultAction`, or the `initialized` marker the
|
|
287
|
+
* activation-time seed writes after populating an empty document. A namespace
|
|
288
|
+
* still holding bare schema defaults is "not configured" and must not shadow
|
|
289
|
+
* the rules file (the dual-source contract: settings first, file fallback).
|
|
287
290
|
*/
|
|
288
291
|
export declare function isRulesConfigured(value: unknown): boolean;
|
|
289
292
|
/**
|
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
|
-
import { Config, RULES_NAMESPACE, RulesSchema, readRulesFromSettings, resolveDshHome, resolveDataDir, ensureDataDir, resolveGatePresets, resolvePermissiveStrategies, resolveRulesFile } from './config.js';
|
|
3
|
+
import { Config, RULES_NAMESPACE, RulesSchema, readRulesFromSettings, resolveDshHome, resolveDataDir, ensureDataDir, resolveGatePresets, resolvePermissiveStrategies, resolveRulesFile, isRulesConfigured } 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';
|
|
@@ -194,13 +195,29 @@ export function apply(ctx, config = {}) {
|
|
|
194
195
|
let current = () => config;
|
|
195
196
|
// ─── Rules settings scope (dsh-perm-gate-rules namespace) ────────────────
|
|
196
197
|
// Holds the live settings-sourced rules document. When non-undefined, the
|
|
197
|
-
// runtime prefers it over the rules file on disk.
|
|
198
|
+
// runtime prefers it over the rules file on disk. The write-capable view is
|
|
199
|
+
// captured separately so allowlist writes can go through the namespace.
|
|
198
200
|
let rulesScope;
|
|
201
|
+
let rulesWriteScope;
|
|
199
202
|
const readRulesDocument = () => readRulesFromSettings(rulesScope);
|
|
200
203
|
// Plugin-owned data files live under $DSH_HOME (node_modules may be
|
|
201
204
|
// read-only). The default resolves to `$DSH_HOME` / `~/.dsh`, so a profile
|
|
202
205
|
// entry that omits `config` still records events, snapshots and learning.
|
|
203
206
|
const dataDir = resolveDataDir(typeof config.dshHome === 'string' ? config.dshHome : undefined);
|
|
207
|
+
const rulesFilePath = resolveRulesFile(typeof config.rulesFile === 'string' ? config.rulesFile : undefined, dataDir);
|
|
208
|
+
// Allowlist write-back through the rules namespace: "allow always" and the
|
|
209
|
+
// panel's allowlist editor land in the live settings document (seeded from
|
|
210
|
+
// the rules file on first write) instead of the file the document shadows.
|
|
211
|
+
// A falsy verdict (namespace not wired yet) falls back to the rules file.
|
|
212
|
+
const settingsWrite = (write) => {
|
|
213
|
+
const scope = rulesWriteScope;
|
|
214
|
+
if (scope === undefined)
|
|
215
|
+
return false;
|
|
216
|
+
void write(scope, readRulesFileDoc(rulesFilePath)).catch((e) => {
|
|
217
|
+
console.warn('[dsh-perm-gate] settings rules write failed:', e);
|
|
218
|
+
});
|
|
219
|
+
return true;
|
|
220
|
+
};
|
|
204
221
|
// Host model-group services (typed minimally; supplied by the dsh runtime
|
|
205
222
|
// through the loader `inject` — dsh-approval-gate demonstrates the same
|
|
206
223
|
// contract). The llm service backs the `host` receiver; agentDefaultModel
|
|
@@ -342,9 +359,15 @@ export function apply(ctx, config = {}) {
|
|
|
342
359
|
// The rules document lives in the plugin's data dir by default, so a
|
|
343
360
|
// `$DSH_HOME/perm-gate/rules.yml` the user writes is actually loaded without
|
|
344
361
|
// also having to declare `rulesFile` in the composition entry.
|
|
345
|
-
rulesFile:
|
|
362
|
+
rulesFile: rulesFilePath,
|
|
346
363
|
// Settings-sourced rules take precedence over the file when present.
|
|
347
364
|
readRulesDocument,
|
|
365
|
+
// Allowlist write-back (see settingsWrite above); falsy verdict falls back
|
|
366
|
+
// to the rules file paths.
|
|
367
|
+
allowlistWriter: {
|
|
368
|
+
append: (pattern, reason) => settingsWrite((scope, seed) => appendAllowToSettings(scope, pattern, reason, seed)),
|
|
369
|
+
replace: (patterns, reason) => settingsWrite((scope, seed) => replaceAllowInSettings(scope, patterns, reason, seed)),
|
|
370
|
+
},
|
|
348
371
|
// The whole gate is scoped to the presets that opt into it (default: the
|
|
349
372
|
// `permissive` tier this plugin adds); elsewhere it stands down entirely.
|
|
350
373
|
gatePresets: resolveGatePresets(Array.isArray(config.gatePresets) ? config.gatePresets : undefined),
|
|
@@ -473,9 +496,21 @@ export function apply(ctx, config = {}) {
|
|
|
473
496
|
onChange: () => { },
|
|
474
497
|
onScope: (scope) => {
|
|
475
498
|
rulesScope = scope;
|
|
499
|
+
rulesWriteScope = scope;
|
|
476
500
|
// When rules change in settings, reload the runtime so the new ruleset
|
|
477
501
|
// takes effect on the next tool call.
|
|
478
502
|
scope.watch(() => { runtime.reload(); });
|
|
503
|
+
// Activation-time seed: an empty (never-configured) namespace is
|
|
504
|
+
// populated once — from the rules file when one exists (implicit
|
|
505
|
+
// migration), otherwise a bare starter document — and marked
|
|
506
|
+
// `initialized`, so the panel shows a live rules document from the
|
|
507
|
+
// first boot instead of a missing-file fallback.
|
|
508
|
+
if (!isRulesConfigured(scope.get())) {
|
|
509
|
+
const seed = { ...readRulesFileDoc(rulesFilePath), initialized: true };
|
|
510
|
+
void Promise.resolve(scope.update(seed)).catch((e) => {
|
|
511
|
+
console.warn('[dsh-perm-gate] rules seed write failed:', e);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
479
514
|
},
|
|
480
515
|
});
|
|
481
516
|
// Event feed HTTP API (best effort): the dsh webServer service exposes the
|