dsh-perm-gate 2.1.0 → 2.6.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/CHANGELOG.ja.md +108 -0
- package/CHANGELOG.ko.md +104 -0
- package/CHANGELOG.md +347 -0
- package/INSTALL.ja.md +84 -8
- package/INSTALL.ko.md +80 -9
- package/INSTALL.md +83 -8
- package/INSTALL.zh.md +70 -7
- package/README.ja.md +130 -16
- package/README.ko.md +128 -17
- package/README.md +179 -25
- package/README.zh.md +121 -12
- package/assets/allow1.png +0 -0
- package/assets/allow2.png +0 -0
- package/assets/gateoff.png +0 -0
- package/assets/llms.png +0 -0
- package/assets/permlist.png +0 -0
- package/assets/setting-head.png +0 -0
- package/assets/setting-rules.png +0 -0
- package/cordis.patch.yml +37 -3
- package/docs/baseline-guard.md +56 -0
- package/docs/plan-rules-to-settings.md +217 -0
- package/docs/rules-format.en.md +294 -0
- package/docs/rules-format.md +279 -0
- package/docs/ui-gap-analysis-and-branch-permissions.md +338 -0
- package/dsh.plugin.json +22 -0
- package/examples/permissions.example.yaml +16 -1
- package/lib/agent-identity.d.ts +43 -0
- package/lib/agent-identity.js +95 -0
- package/lib/allowlist.d.ts +27 -0
- package/lib/allowlist.js +103 -1
- package/lib/cli.js +11 -14
- package/lib/client.js +598 -6
- package/lib/client.js.map +1 -1
- package/lib/command-dispatcher.d.ts +22 -0
- package/lib/command-dispatcher.js +97 -0
- package/lib/command-semantics.d.ts +62 -0
- package/lib/command-semantics.js +6 -0
- package/lib/compiler.d.ts +50 -0
- package/lib/compiler.js +122 -0
- package/lib/config.d.ts +161 -7
- package/lib/config.js +78 -11
- package/lib/dry-run.d.ts +87 -0
- package/lib/dry-run.js +110 -0
- package/lib/engine.js +61 -4
- package/lib/evaluate.d.ts +9 -0
- package/lib/evaluate.js +264 -1
- package/lib/events.d.ts +69 -2
- package/lib/events.js +123 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +320 -11
- package/lib/network-lifecycle.d.ts +107 -0
- package/lib/network-lifecycle.js +180 -0
- package/lib/network.d.ts +109 -0
- package/lib/network.js +196 -0
- package/lib/parsers/git.d.ts +43 -0
- package/lib/parsers/git.js +404 -0
- package/lib/parsers/shell-cmds.d.ts +32 -0
- package/lib/parsers/shell-cmds.js +293 -0
- package/lib/preset.d.ts +13 -0
- package/lib/preset.js +17 -0
- package/lib/proxy.d.ts +160 -0
- package/lib/proxy.js +529 -0
- package/lib/risk.d.ts +17 -3
- package/lib/risk.js +19 -6
- package/lib/rule-chain.d.ts +39 -0
- package/lib/rule-chain.js +213 -0
- package/lib/rule-dims.d.ts +174 -0
- package/lib/rule-dims.js +230 -0
- package/lib/rule.d.ts +60 -0
- package/lib/rule.js +69 -7
- package/lib/rules-view.d.ts +52 -0
- package/lib/rules-view.js +126 -0
- package/lib/runtime.d.ts +208 -7
- package/lib/runtime.js +433 -27
- package/lib/session-sweep.d.ts +45 -0
- package/lib/session-sweep.js +153 -0
- package/lib/shadow.d.ts +34 -0
- package/lib/shadow.js +143 -0
- package/lib/shell.d.ts +14 -0
- package/lib/shell.js +17 -0
- package/lib/watch.d.ts +42 -0
- package/lib/watch.js +170 -0
- package/package.json +26 -3
- package/scripts/patch-permission-glyph.mjs +392 -0
package/lib/rule.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type CompiledPattern } from './compiler.js';
|
|
2
|
+
import { type ParamsDimension, type AbsentDimension, type AgentsDimension, type WhenDimension, type ArgvDimension, type NetworkDimension, type BranchDimension } from './rule-dims.js';
|
|
2
3
|
export type RuleAction = 'allow' | 'ask' | 'deny';
|
|
3
4
|
/** A parsed, shape-validated permissions document (patterns not yet compiled). */
|
|
4
5
|
export interface PermissionsDoc {
|
|
@@ -20,6 +21,20 @@ export interface RuleEntryDoc {
|
|
|
20
21
|
readonly args: string[];
|
|
21
22
|
/** Workspace-relative path globs; empty = no constraint. */
|
|
22
23
|
readonly paths: string[];
|
|
24
|
+
/** Parameter key→value matching (AND over keys, `!` prefix negates). */
|
|
25
|
+
readonly params: ParamsDimension;
|
|
26
|
+
/** Parameter keys that must NOT be present. */
|
|
27
|
+
readonly absent: AbsentDimension;
|
|
28
|
+
/** Agent identity candidates (main / subagent / preset:<name>). */
|
|
29
|
+
readonly agents: AgentsDimension;
|
|
30
|
+
/** Environment / platform conditions. */
|
|
31
|
+
readonly when: WhenDimension | undefined;
|
|
32
|
+
/** Extra argv patterns (pipeline etc.). */
|
|
33
|
+
readonly argv: ArgvDimension | undefined;
|
|
34
|
+
/** Network dimension (domain / IP / port / scheme). */
|
|
35
|
+
readonly network: NetworkDimension | undefined;
|
|
36
|
+
/** Git branch / remote / protected-branch dimension. */
|
|
37
|
+
readonly branch: BranchDimension | undefined;
|
|
23
38
|
readonly action: RuleAction;
|
|
24
39
|
readonly reason: string;
|
|
25
40
|
readonly enabled: boolean;
|
|
@@ -34,8 +49,28 @@ export interface CompiledRuleEntry {
|
|
|
34
49
|
readonly command: readonly CommandSpec[];
|
|
35
50
|
readonly args: readonly CompiledPattern[];
|
|
36
51
|
readonly paths: readonly CompiledPattern[];
|
|
52
|
+
/** Parsed params dimension (raw patterns; compiled on match). */
|
|
53
|
+
readonly params: ParamsDimension;
|
|
54
|
+
/** Parsed absent dimension (raw key names). */
|
|
55
|
+
readonly absent: AbsentDimension;
|
|
56
|
+
/** Parsed agents dimension (raw patterns). */
|
|
57
|
+
readonly agents: AgentsDimension;
|
|
58
|
+
/** Parsed when dimension (raw conditions). */
|
|
59
|
+
readonly when: WhenDimension | undefined;
|
|
60
|
+
/** Parsed argv dimension (raw patterns). */
|
|
61
|
+
readonly argv: ArgvDimension | undefined;
|
|
62
|
+
/** Parsed network dimension (raw patterns). */
|
|
63
|
+
readonly network: NetworkDimension | undefined;
|
|
64
|
+
/** Compiled branch dimension (patterns compiled; absent = no constraint). */
|
|
65
|
+
readonly branch: CompiledBranchDimension | undefined;
|
|
37
66
|
readonly source: RuleEntryDoc;
|
|
38
67
|
}
|
|
68
|
+
/** Compiled `branch` dimension: `target`/`remote` globs, raw `shared` flag. */
|
|
69
|
+
export interface CompiledBranchDimension {
|
|
70
|
+
readonly target: readonly CompiledPattern[];
|
|
71
|
+
readonly remote: readonly CompiledPattern[];
|
|
72
|
+
readonly shared: boolean;
|
|
73
|
+
}
|
|
39
74
|
export interface CommandSpec {
|
|
40
75
|
readonly word: CompiledPattern;
|
|
41
76
|
readonly flag?: 'recursive' | 'force';
|
|
@@ -56,8 +91,33 @@ export declare class RuleError extends Error {
|
|
|
56
91
|
}
|
|
57
92
|
/** Parse a raw YAML permissions document; malformed files fail loud at load. */
|
|
58
93
|
export declare function parsePermissionsDocument(text: string): PermissionsDoc;
|
|
94
|
+
/**
|
|
95
|
+
* Parse a permissions document given as an already-parsed object — the shape
|
|
96
|
+
* the DSH settings namespace stores (the JSON twin of the YAML file). Accepts
|
|
97
|
+
* both the bare `{ defaultAction, deny, allow, ask }` form and the file's
|
|
98
|
+
* `permissions:`-wrapped form. Malformed input fails loud, exactly like the
|
|
99
|
+
* YAML path, so a settings doc that cannot compile is a state the operator
|
|
100
|
+
* sees, never a silent ruleset change.
|
|
101
|
+
*/
|
|
102
|
+
export declare function parsePermissionsObject(raw: unknown): PermissionsDoc;
|
|
103
|
+
/**
|
|
104
|
+
* Compile one parsed entry into a hot-path rule.
|
|
105
|
+
*
|
|
106
|
+
* Exported because the multi-file rule chain (`rule-chain.ts`) merges entries
|
|
107
|
+
* from several documents into one ruleset and must produce the SAME shape here
|
|
108
|
+
* — an entry built by hand would silently drop every compiled dimension, and an
|
|
109
|
+
* empty dimension means "no constraint", i.e. "matches everything".
|
|
110
|
+
*/
|
|
111
|
+
export declare function compileRuleEntry(entry: RuleEntryDoc, action: RuleAction, index: number, opts?: CompileOptions): CompiledRuleEntry;
|
|
59
112
|
/** Compile a validated document into hot-path rules. */
|
|
60
113
|
export declare function compileDocument(doc: PermissionsDoc, opts?: CompileOptions): CompiledRuleset;
|
|
114
|
+
/**
|
|
115
|
+
* Compile rules given as a structured settings object (already parsed — no
|
|
116
|
+
* YAML in the loop). The settings-first half of the dual-source read path:
|
|
117
|
+
* `parsePermissionsObject` validates the JSON form exactly as strictly as the
|
|
118
|
+
* YAML path, then the standard `compileDocument` runs.
|
|
119
|
+
*/
|
|
120
|
+
export declare function compileRulesObject(root: unknown, opts?: CompileOptions): CompiledRuleset;
|
|
61
121
|
/** SHA-256 hash of the raw document (compile-cache key without recompiling). */
|
|
62
122
|
export declare function documentHash(text: string): string;
|
|
63
123
|
export declare function extractPathCandidates(args: Record<string, unknown>): string[];
|
package/lib/rule.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* nothing matches, `defaultAction` applies.
|
|
10
10
|
*/
|
|
11
11
|
import { parse } from 'yaml';
|
|
12
|
-
import { compileGlob, hashText } from './compiler.js';
|
|
12
|
+
import { compileGlob, compileBranchPattern, hashText } from './compiler.js';
|
|
13
|
+
import { parseParamsDimension, parseAbsentDimension, parseAgentsDimension, parseWhenDimension, parseArgvDimension, parseNetworkDimension, parseBranchDimension, } from './rule-dims.js';
|
|
13
14
|
const VALID_ACTIONS = ['allow', 'ask', 'deny'];
|
|
14
15
|
const PATH_CANDIDATE_KEYS = [
|
|
15
16
|
'path', 'paths', 'file', 'files', 'file_path', 'dir', 'directory', 'directories',
|
|
@@ -53,6 +54,17 @@ export function parsePermissionsDocument(text) {
|
|
|
53
54
|
catch (error) {
|
|
54
55
|
throw new RuleError(`invalid YAML: ${String(error)}`);
|
|
55
56
|
}
|
|
57
|
+
return parsePermissionsObject(raw);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Parse a permissions document given as an already-parsed object — the shape
|
|
61
|
+
* the DSH settings namespace stores (the JSON twin of the YAML file). Accepts
|
|
62
|
+
* both the bare `{ defaultAction, deny, allow, ask }` form and the file's
|
|
63
|
+
* `permissions:`-wrapped form. Malformed input fails loud, exactly like the
|
|
64
|
+
* YAML path, so a settings doc that cannot compile is a state the operator
|
|
65
|
+
* sees, never a silent ruleset change.
|
|
66
|
+
*/
|
|
67
|
+
export function parsePermissionsObject(raw) {
|
|
56
68
|
if (raw === null || raw === undefined) {
|
|
57
69
|
return { defaultAction: 'ask', deny: [], allow: [], ask: [] };
|
|
58
70
|
}
|
|
@@ -82,7 +94,8 @@ export function parsePermissionsDocument(text) {
|
|
|
82
94
|
function parseRuleEntry(raw, action, at) {
|
|
83
95
|
if (!isRecord(raw))
|
|
84
96
|
throw new RuleError(`${at} must be a mapping`);
|
|
85
|
-
const
|
|
97
|
+
const VALID_KEYS = ['tools', 'command', 'args', 'paths', 'params', 'absent', 'agents', 'when', 'argv', 'network', 'branch', 'action', 'reason', 'enabled'];
|
|
98
|
+
const unknown = Object.keys(raw).filter((k) => !VALID_KEYS.includes(k));
|
|
86
99
|
if (unknown.length > 0) {
|
|
87
100
|
throw new RuleError(`${at} unknown field${unknown.length > 1 ? 's' : ''} ${unknown.map((k) => JSON.stringify(k)).join(', ')}`);
|
|
88
101
|
}
|
|
@@ -98,6 +111,13 @@ function parseRuleEntry(raw, action, at) {
|
|
|
98
111
|
command: stringList(raw.command, `${at}.command`),
|
|
99
112
|
args: stringList(raw.args, `${at}.args`),
|
|
100
113
|
paths: stringList(raw.paths, `${at}.paths`),
|
|
114
|
+
params: parseParamsDimension(raw.params, `${at}.params`),
|
|
115
|
+
absent: parseAbsentDimension(raw.absent, `${at}.absent`),
|
|
116
|
+
agents: parseAgentsDimension(raw.agents, `${at}.agents`),
|
|
117
|
+
when: parseWhenDimension(raw.when, `${at}.when`),
|
|
118
|
+
argv: parseArgvDimension(raw.argv, `${at}.argv`),
|
|
119
|
+
network: parseNetworkDimension(raw.network, `${at}.network`),
|
|
120
|
+
branch: parseBranchDimension(raw.branch, `${at}.branch`),
|
|
101
121
|
action,
|
|
102
122
|
reason: reason === undefined ? `${action}` : reason,
|
|
103
123
|
enabled: raw.enabled === undefined ? true : typeof raw.enabled === 'boolean' ? raw.enabled : (() => { throw new RuleError(`${at}.enabled must be a boolean`); })(),
|
|
@@ -133,10 +153,32 @@ function compileCommand(entry, opts) {
|
|
|
133
153
|
}
|
|
134
154
|
return { word: compileGlob(word, { segments: false, maxStars: opts.maxGlobStars ?? 2 }), flag };
|
|
135
155
|
}
|
|
136
|
-
/**
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Compile the `branch` dimension (undefined = the rule does not constrain on it).
|
|
158
|
+
*
|
|
159
|
+
* Glob degree is bounded inside `compileBranchPattern` (DEFAULT_MAX_STARS), so
|
|
160
|
+
* this takes no options: branch patterns are never path-segmented.
|
|
161
|
+
*/
|
|
162
|
+
function compileBranchDimension(dim) {
|
|
163
|
+
if (dim === undefined)
|
|
164
|
+
return undefined;
|
|
165
|
+
return {
|
|
166
|
+
target: (dim.target ?? []).map((p) => compileBranchPattern(p)),
|
|
167
|
+
remote: (dim.remote ?? []).map((p) => compileBranchPattern(p)),
|
|
168
|
+
shared: dim.shared === true,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Compile one parsed entry into a hot-path rule.
|
|
173
|
+
*
|
|
174
|
+
* Exported because the multi-file rule chain (`rule-chain.ts`) merges entries
|
|
175
|
+
* from several documents into one ruleset and must produce the SAME shape here
|
|
176
|
+
* — an entry built by hand would silently drop every compiled dimension, and an
|
|
177
|
+
* empty dimension means "no constraint", i.e. "matches everything".
|
|
178
|
+
*/
|
|
179
|
+
export function compileRuleEntry(entry, action, index, opts = {}) {
|
|
180
|
+
return {
|
|
181
|
+
index,
|
|
140
182
|
action,
|
|
141
183
|
reason: entry.reason,
|
|
142
184
|
enabled: entry.enabled,
|
|
@@ -144,8 +186,19 @@ export function compileDocument(doc, opts = {}) {
|
|
|
144
186
|
command: entry.command.map((c) => compileCommand(c, opts)),
|
|
145
187
|
args: compilePatternList(entry.args, 'args', opts),
|
|
146
188
|
paths: compilePatternList(entry.paths, 'paths', opts),
|
|
189
|
+
params: entry.params,
|
|
190
|
+
absent: entry.absent,
|
|
191
|
+
agents: entry.agents,
|
|
192
|
+
when: entry.when,
|
|
193
|
+
argv: entry.argv,
|
|
194
|
+
network: entry.network,
|
|
195
|
+
branch: compileBranchDimension(entry.branch),
|
|
147
196
|
source: entry,
|
|
148
|
-
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/** Compile a validated document into hot-path rules. */
|
|
200
|
+
export function compileDocument(doc, opts = {}) {
|
|
201
|
+
const comp = (list, action, offset) => list.map((entry, i) => compileRuleEntry(entry, action, offset + i, opts));
|
|
149
202
|
return {
|
|
150
203
|
defaultAction: doc.defaultAction,
|
|
151
204
|
deny: comp(doc.deny, 'deny', 0),
|
|
@@ -154,6 +207,15 @@ export function compileDocument(doc, opts = {}) {
|
|
|
154
207
|
caseInsensitivePaths: opts.caseInsensitivePaths ?? false,
|
|
155
208
|
};
|
|
156
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Compile rules given as a structured settings object (already parsed — no
|
|
212
|
+
* YAML in the loop). The settings-first half of the dual-source read path:
|
|
213
|
+
* `parsePermissionsObject` validates the JSON form exactly as strictly as the
|
|
214
|
+
* YAML path, then the standard `compileDocument` runs.
|
|
215
|
+
*/
|
|
216
|
+
export function compileRulesObject(root, opts = {}) {
|
|
217
|
+
return compileDocument(parsePermissionsObject(root), opts);
|
|
218
|
+
}
|
|
157
219
|
/** SHA-256 hash of the raw document (compile-cache key without recompiling). */
|
|
158
220
|
export function documentHash(text) {
|
|
159
221
|
return hashText(text);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type RuleAction } from './rule.js';
|
|
2
|
+
/** Rules per action, so the panel can say "3 deny / 7 allow / 1 ask" at a glance. */
|
|
3
|
+
export interface RulesViewCounts {
|
|
4
|
+
readonly allow: number;
|
|
5
|
+
readonly deny: number;
|
|
6
|
+
readonly ask: number;
|
|
7
|
+
}
|
|
8
|
+
/** The whole read-only face of one permissions document. */
|
|
9
|
+
export interface RulesView {
|
|
10
|
+
/** Absolute path, or `''` when the gate has no rules file configured. */
|
|
11
|
+
readonly path: string;
|
|
12
|
+
readonly exists: boolean;
|
|
13
|
+
/** File size on disk, not the length of `raw` — a truncated view still reports the truth. */
|
|
14
|
+
readonly bytes: number;
|
|
15
|
+
/** Content hash of the text the gate would load, for correlating with a reload. */
|
|
16
|
+
readonly hash: string;
|
|
17
|
+
readonly lines: number;
|
|
18
|
+
/** The document itself. Empty when absent, unreadable, or truncated away. */
|
|
19
|
+
readonly raw: string;
|
|
20
|
+
readonly truncated: boolean;
|
|
21
|
+
/** Parsed only when the document compiled; absent when it did not. */
|
|
22
|
+
readonly defaultAction?: RuleAction;
|
|
23
|
+
readonly counts: RulesViewCounts;
|
|
24
|
+
/** Why the document could not be shown or parsed. Absent when all is well. */
|
|
25
|
+
readonly error?: string;
|
|
26
|
+
/** Where the rules were read from. Absent on views produced before the settings migration. */
|
|
27
|
+
readonly source?: 'file' | 'settings';
|
|
28
|
+
/** The structured rules object the settings namespace stores (settings source only). */
|
|
29
|
+
readonly rules?: unknown;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Read one permissions document for display.
|
|
33
|
+
*
|
|
34
|
+
* Never throws: a missing, unreadable or malformed file is a state the panel has
|
|
35
|
+
* to render, not an error that should blank the section. Every failure mode is
|
|
36
|
+
* reported in the returned value instead.
|
|
37
|
+
*/
|
|
38
|
+
export declare function readRulesView(rulesFile: string): RulesView;
|
|
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";
|
|
41
|
+
/**
|
|
42
|
+
* Render the settings-sourced rules for display — the read-only face of the
|
|
43
|
+
* `dsh-perm-gate-rules` namespace, the twin of {@link readRulesView}.
|
|
44
|
+
*
|
|
45
|
+
* The YAML rendering of the structured object fills `raw`, so the settings card
|
|
46
|
+
* shows the document in the same format the file view used; `rules` carries the
|
|
47
|
+
* structured object itself and `source: 'settings'` states the provenance.
|
|
48
|
+
*
|
|
49
|
+
* Never throws: a doc that fails `compileRulesObject` is a state the panel has
|
|
50
|
+
* to render (the `error` field), not an error that should blank the section.
|
|
51
|
+
*/
|
|
52
|
+
export declare function readRulesViewFromSettings(rules: unknown): RulesView;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading the permissions YAML, for the panel that shows it.
|
|
3
|
+
*
|
|
4
|
+
* The card could already WRITE the rules file — the allowlist section appends
|
|
5
|
+
* and replaces patterns, and `index.ts` mirrors those edits back into the
|
|
6
|
+
* document. Nothing could read it back, so an operator editing patterns through
|
|
7
|
+
* the panel had no way to see the document they were editing: the file the gate
|
|
8
|
+
* actually loads was invisible from the UI.
|
|
9
|
+
*
|
|
10
|
+
* This is the missing read half. It is deliberately **read-only** — a view must
|
|
11
|
+
* never be able to change the ruleset it is describing, or "let me look at the
|
|
12
|
+
* rules" becomes a risky action. Writing stays where it already lives
|
|
13
|
+
* (`allowlist.ts`, the settings namespace).
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
16
|
+
import { stringify } from 'yaml';
|
|
17
|
+
import { RuleError, compileDocument, compileRulesObject, documentHash, parsePermissionsDocument } from './rule.js';
|
|
18
|
+
/** Beyond this the panel would be rendering a log, not a rules file. */
|
|
19
|
+
const MAX_VIEW_BYTES = 512 * 1024;
|
|
20
|
+
const NO_COUNTS = { allow: 0, deny: 0, ask: 0 };
|
|
21
|
+
/**
|
|
22
|
+
* Read one permissions document for display.
|
|
23
|
+
*
|
|
24
|
+
* Never throws: a missing, unreadable or malformed file is a state the panel has
|
|
25
|
+
* to render, not an error that should blank the section. Every failure mode is
|
|
26
|
+
* reported in the returned value instead.
|
|
27
|
+
*/
|
|
28
|
+
export function readRulesView(rulesFile) {
|
|
29
|
+
if (rulesFile === '') {
|
|
30
|
+
return {
|
|
31
|
+
path: '', exists: false, bytes: 0, hash: '', lines: 0, raw: '', truncated: false,
|
|
32
|
+
counts: NO_COUNTS, error: 'no rules file is configured',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
let bytes = 0;
|
|
36
|
+
let exists = true;
|
|
37
|
+
try {
|
|
38
|
+
bytes = statSync(rulesFile).size;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
exists = false;
|
|
42
|
+
}
|
|
43
|
+
let raw = '';
|
|
44
|
+
try {
|
|
45
|
+
raw = readFileSync(rulesFile, 'utf8');
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
return {
|
|
49
|
+
path: rulesFile, exists, bytes, hash: '', lines: 0, raw: '', truncated: false,
|
|
50
|
+
counts: NO_COUNTS,
|
|
51
|
+
error: exists ? `unreadable: ${String(e?.message ?? e)}` : 'file does not exist',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const lines = raw === '' ? 0 : raw.split('\n').length;
|
|
55
|
+
const truncated = raw.length > MAX_VIEW_BYTES;
|
|
56
|
+
const shown = truncated ? raw.slice(0, MAX_VIEW_BYTES) : raw;
|
|
57
|
+
// Parsing is what makes the view worth having: a YAML file that loads as text
|
|
58
|
+
// but not as rules is exactly the failure the panel exists to expose.
|
|
59
|
+
try {
|
|
60
|
+
const doc = parsePermissionsDocument(raw);
|
|
61
|
+
const ruleset = compileDocument(doc);
|
|
62
|
+
return {
|
|
63
|
+
path: rulesFile, exists: true, bytes, hash: documentHash(raw), lines,
|
|
64
|
+
raw: shown, truncated,
|
|
65
|
+
defaultAction: ruleset.defaultAction,
|
|
66
|
+
counts: { allow: ruleset.allow.length, deny: ruleset.deny.length, ask: ruleset.ask.length },
|
|
67
|
+
source: 'file',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
const detail = e instanceof RuleError ? e.message : String(e?.message ?? e);
|
|
72
|
+
return {
|
|
73
|
+
path: rulesFile, exists: true, bytes, hash: documentHash(raw), lines,
|
|
74
|
+
raw: shown, truncated,
|
|
75
|
+
counts: NO_COUNTS,
|
|
76
|
+
error: `does not compile: ${detail}`,
|
|
77
|
+
source: 'file',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
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';
|
|
83
|
+
/**
|
|
84
|
+
* Render the settings-sourced rules for display — the read-only face of the
|
|
85
|
+
* `dsh-perm-gate-rules` namespace, the twin of {@link readRulesView}.
|
|
86
|
+
*
|
|
87
|
+
* The YAML rendering of the structured object fills `raw`, so the settings card
|
|
88
|
+
* shows the document in the same format the file view used; `rules` carries the
|
|
89
|
+
* structured object itself and `source: 'settings'` states the provenance.
|
|
90
|
+
*
|
|
91
|
+
* Never throws: a doc that fails `compileRulesObject` is a state the panel has
|
|
92
|
+
* to render (the `error` field), not an error that should blank the section.
|
|
93
|
+
*/
|
|
94
|
+
export function readRulesViewFromSettings(rules) {
|
|
95
|
+
let raw = '';
|
|
96
|
+
try {
|
|
97
|
+
raw = stringify(rules ?? {});
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
raw = '';
|
|
101
|
+
}
|
|
102
|
+
const lines = raw === '' ? 0 : raw.split('\n').length;
|
|
103
|
+
const hash = documentHash(JSON.stringify(rules ?? {}));
|
|
104
|
+
try {
|
|
105
|
+
const ruleset = compileRulesObject(rules);
|
|
106
|
+
return {
|
|
107
|
+
path: SETTINGS_RULES_VIEW_PATH, exists: true, bytes: raw.length, hash, lines,
|
|
108
|
+
raw, truncated: false,
|
|
109
|
+
defaultAction: ruleset.defaultAction,
|
|
110
|
+
counts: { allow: ruleset.allow.length, deny: ruleset.deny.length, ask: ruleset.ask.length },
|
|
111
|
+
source: 'settings',
|
|
112
|
+
rules,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
const detail = e instanceof RuleError ? e.message : String(e?.message ?? e);
|
|
117
|
+
return {
|
|
118
|
+
path: SETTINGS_RULES_VIEW_PATH, exists: true, bytes: raw.length, hash, lines,
|
|
119
|
+
raw, truncated: false,
|
|
120
|
+
counts: NO_COUNTS,
|
|
121
|
+
error: `does not compile: ${detail}`,
|
|
122
|
+
source: 'settings',
|
|
123
|
+
rules,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|