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
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command dispatcher - routes commands to appropriate parsers.
|
|
3
|
+
* Runs parsers in priority order, returns first match.
|
|
4
|
+
*/
|
|
5
|
+
import type { CommandParser, ClassificationResult } from './command-semantics.js';
|
|
6
|
+
/**
|
|
7
|
+
* Register a parser manually (for testing or custom parsers).
|
|
8
|
+
*/
|
|
9
|
+
export declare function registerParser(parser: CommandParser): void;
|
|
10
|
+
/**
|
|
11
|
+
* Clear all registered parsers (for testing).
|
|
12
|
+
*/
|
|
13
|
+
export declare function clearParsers(): void;
|
|
14
|
+
/**
|
|
15
|
+
* Dispatch a command to the appropriate parser.
|
|
16
|
+
* Returns classification result or null if no parser matches.
|
|
17
|
+
*/
|
|
18
|
+
export declare function dispatchCommand(command: string): ClassificationResult | null;
|
|
19
|
+
/**
|
|
20
|
+
* Get all registered parsers (for inspection/testing).
|
|
21
|
+
*/
|
|
22
|
+
export declare function getParsers(): readonly CommandParser[];
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command dispatcher - routes commands to appropriate parsers.
|
|
3
|
+
* Runs parsers in priority order, returns first match.
|
|
4
|
+
*/
|
|
5
|
+
import { GitParser } from './parsers/git.js';
|
|
6
|
+
import { ShellDangerousParser } from './parsers/shell-cmds.js';
|
|
7
|
+
/** Registered parsers (loaded lazily) */
|
|
8
|
+
const parsers = [];
|
|
9
|
+
/** Parser registry flag */
|
|
10
|
+
let initialized = false;
|
|
11
|
+
/**
|
|
12
|
+
* Lazy-load all parsers.
|
|
13
|
+
* Called once on first dispatch.
|
|
14
|
+
*/
|
|
15
|
+
function initParsers() {
|
|
16
|
+
if (initialized)
|
|
17
|
+
return;
|
|
18
|
+
initialized = true;
|
|
19
|
+
// P0: Git parser (highest priority)
|
|
20
|
+
parsers.push(new GitParser());
|
|
21
|
+
// P1: Shell dangerous commands
|
|
22
|
+
parsers.push(new ShellDangerousParser());
|
|
23
|
+
// P2: Package managers (TODO: implement)
|
|
24
|
+
// parsers.push(new PkgManagerParser())
|
|
25
|
+
// P3: Docker (TODO: implement)
|
|
26
|
+
// parsers.push(new DockerParser())
|
|
27
|
+
// P4: SQL (TODO: implement)
|
|
28
|
+
// parsers.push(new SqlParser())
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Register a parser manually (for testing or custom parsers).
|
|
32
|
+
*/
|
|
33
|
+
export function registerParser(parser) {
|
|
34
|
+
// Insert in priority order (lower priority number = higher priority)
|
|
35
|
+
const idx = parsers.findIndex(p => p.priority > parser.priority);
|
|
36
|
+
if (idx === -1) {
|
|
37
|
+
parsers.push(parser);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
parsers.splice(idx, 0, parser);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Clear all registered parsers (for testing).
|
|
45
|
+
*/
|
|
46
|
+
export function clearParsers() {
|
|
47
|
+
parsers.length = 0;
|
|
48
|
+
initialized = false;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Dispatch a command to the appropriate parser.
|
|
52
|
+
* Returns classification result or null if no parser matches.
|
|
53
|
+
*/
|
|
54
|
+
export function dispatchCommand(command) {
|
|
55
|
+
initParsers();
|
|
56
|
+
const startTime = performance.now();
|
|
57
|
+
for (const parser of parsers) {
|
|
58
|
+
if (parser.canParse(command)) {
|
|
59
|
+
const semantics = parser.parse(command);
|
|
60
|
+
if (semantics) {
|
|
61
|
+
const parseTimeMs = performance.now() - startTime;
|
|
62
|
+
return {
|
|
63
|
+
semantics,
|
|
64
|
+
confidence: 1.0, // Static parsers have high confidence
|
|
65
|
+
parser: parser.name,
|
|
66
|
+
parseTimeMs,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// No parser matched - return unknown semantics
|
|
72
|
+
const parseTimeMs = performance.now() - startTime;
|
|
73
|
+
return {
|
|
74
|
+
semantics: {
|
|
75
|
+
family: 'unknown',
|
|
76
|
+
destructiveness: 0,
|
|
77
|
+
destructiveFlags: false,
|
|
78
|
+
targets: [],
|
|
79
|
+
flags: [],
|
|
80
|
+
requiresConfirmation: false,
|
|
81
|
+
compound: false,
|
|
82
|
+
gitOperation: false,
|
|
83
|
+
targetsSharedBranch: false,
|
|
84
|
+
rawCommand: command,
|
|
85
|
+
},
|
|
86
|
+
confidence: 0,
|
|
87
|
+
parser: 'none',
|
|
88
|
+
parseTimeMs,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get all registered parsers (for inspection/testing).
|
|
93
|
+
*/
|
|
94
|
+
export function getParsers() {
|
|
95
|
+
initParsers();
|
|
96
|
+
return parsers;
|
|
97
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified command semantics output from all classifiers.
|
|
3
|
+
* Every parser (git, shell-dangerous, docker, sql, pkg-manager)
|
|
4
|
+
* returns this interface — the evaluator does one matching pass.
|
|
5
|
+
*/
|
|
6
|
+
/** Command families supported by static parsers */
|
|
7
|
+
export type CommandFamily = 'git' | 'shell-dangerous' | 'docker' | 'sql' | 'pkg-manager' | 'unknown';
|
|
8
|
+
/** Destructiveness rating (0=safe, 5=extremely destructive) */
|
|
9
|
+
export type DestructivenessLevel = 0 | 1 | 2 | 3 | 4 | 5;
|
|
10
|
+
/** Unified semantics output */
|
|
11
|
+
export interface CommandSemantics {
|
|
12
|
+
/** Command family (git, shell-dangerous, docker, sql, pkg-manager, unknown) */
|
|
13
|
+
family: CommandFamily;
|
|
14
|
+
/** Subcommand (e.g. 'push', 'rm', 'run') */
|
|
15
|
+
subcommand?: string;
|
|
16
|
+
/** Specific operation (e.g. 'force-push', 'recursive-delete') */
|
|
17
|
+
operation?: string;
|
|
18
|
+
/** Destructiveness level 0-5 */
|
|
19
|
+
destructiveness: DestructivenessLevel;
|
|
20
|
+
/** Whether destructive flags are present (--force, --recursive, etc.) */
|
|
21
|
+
destructiveFlags: boolean;
|
|
22
|
+
/** Target identifiers (branch names, file paths, container IDs, table names, package names) */
|
|
23
|
+
targets: string[];
|
|
24
|
+
/** All parsed flags (normalized) */
|
|
25
|
+
flags: string[];
|
|
26
|
+
/** Whether the command requires explicit confirmation */
|
|
27
|
+
requiresConfirmation: boolean;
|
|
28
|
+
/** Whether the command is compound (contains &&, ||, |) */
|
|
29
|
+
compound: boolean;
|
|
30
|
+
/** Whether the command involves git operations */
|
|
31
|
+
gitOperation: boolean;
|
|
32
|
+
/** Branch name (extracted from git commands) */
|
|
33
|
+
branch?: string;
|
|
34
|
+
/** Remote name (for git push/fetch) */
|
|
35
|
+
remote?: string;
|
|
36
|
+
/** Whether the command targets shared/protected branches */
|
|
37
|
+
targetsSharedBranch: boolean;
|
|
38
|
+
/** Raw command string */
|
|
39
|
+
rawCommand?: string;
|
|
40
|
+
}
|
|
41
|
+
/** Classification result with confidence */
|
|
42
|
+
export interface ClassificationResult {
|
|
43
|
+
/** The semantics object */
|
|
44
|
+
semantics: CommandSemantics;
|
|
45
|
+
/** Parser confidence (0-1) */
|
|
46
|
+
confidence: number;
|
|
47
|
+
/** Parser used */
|
|
48
|
+
parser: string;
|
|
49
|
+
/** Parse time in milliseconds */
|
|
50
|
+
parseTimeMs: number;
|
|
51
|
+
}
|
|
52
|
+
/** Parser interface - all parsers implement this */
|
|
53
|
+
export interface CommandParser {
|
|
54
|
+
/** Parser name */
|
|
55
|
+
readonly name: string;
|
|
56
|
+
/** Priority (lower = runs first) */
|
|
57
|
+
readonly priority: number;
|
|
58
|
+
/** Try to parse the command. Return null if not applicable. */
|
|
59
|
+
parse(command: string): CommandSemantics | null;
|
|
60
|
+
/** Check if this parser can handle the command */
|
|
61
|
+
canParse(command: string): boolean;
|
|
62
|
+
}
|
package/lib/compiler.d.ts
CHANGED
|
@@ -28,3 +28,53 @@ export declare function compileGlob(pattern: string, options?: {
|
|
|
28
28
|
export declare function compileLiteral(pattern: string): CompiledPattern;
|
|
29
29
|
/** Stable SHA-256 content hash (used for rule-file compile caching). */
|
|
30
30
|
export declare function hashText(text: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Compile a CIDR notation string (e.g. `10.0.0.0/8`) into a matcher that
|
|
33
|
+
* tests whether an IPv4 address falls within the range.
|
|
34
|
+
*
|
|
35
|
+
* Returns a pure function `(ip: string) => boolean`. Non-IPv4 inputs always
|
|
36
|
+
* return false. Invalid CIDR throws at compile time (fail-loud).
|
|
37
|
+
*/
|
|
38
|
+
export declare function compileCidr(cidr: string): (ip: string) => boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Compile a port specification into a matcher function.
|
|
41
|
+
*
|
|
42
|
+
* Accepts:
|
|
43
|
+
* - A single port: `"443"` → matches exactly 443
|
|
44
|
+
* - A port range: `"8000-9000"` → matches 8000–9000 inclusive
|
|
45
|
+
*
|
|
46
|
+
* Returns `(port: number) => boolean`. Invalid specs throw at compile time.
|
|
47
|
+
*/
|
|
48
|
+
export declare function compilePortSpec(spec: string): (port: number) => boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Compile a domain pattern into a matcher that supports subdomain inclusion.
|
|
51
|
+
*
|
|
52
|
+
* - `"github.com"` matches `github.com` AND any subdomain (`api.github.com`)
|
|
53
|
+
* - `"*.example.com"` matches only subdomains, not `example.com` itself
|
|
54
|
+
* - Glob characters (`*`, `?`, `[...]`) are handled by {@link compileGlob}.
|
|
55
|
+
*
|
|
56
|
+
* Returns a {@link CompiledPattern} whose `.re` is the anchored RegExp.
|
|
57
|
+
*/
|
|
58
|
+
export declare function compileDomainPattern(pattern: string): CompiledPattern;
|
|
59
|
+
/**
|
|
60
|
+
* Compile a git branch-name pattern into a matcher.
|
|
61
|
+
*
|
|
62
|
+
* Branch names are path-like (`release/1.0`) but a rule author thinks in
|
|
63
|
+
* segments-with-globs terms (`release*`), so this is a **non-segment** glob:
|
|
64
|
+
* `*` crosses `/` on purpose, matching how `args` patterns behave.
|
|
65
|
+
*
|
|
66
|
+
* Rejects a pattern that is nothing but a separator so a typo can not degrade
|
|
67
|
+
* into "matches everything".
|
|
68
|
+
*/
|
|
69
|
+
export declare function compileBranchPattern(pattern: string): CompiledPattern;
|
|
70
|
+
/**
|
|
71
|
+
* Compile a list of patterns (possibly `!`-prefixed) into param matchers.
|
|
72
|
+
* Returns `{ pattern, negated, compiled }` entries for the params dimension.
|
|
73
|
+
*/
|
|
74
|
+
export declare function compileParamPatterns(patterns: readonly string[], opts?: {
|
|
75
|
+
maxStars?: number;
|
|
76
|
+
}): Array<{
|
|
77
|
+
readonly pattern: string;
|
|
78
|
+
readonly negated: boolean;
|
|
79
|
+
readonly compiled: CompiledPattern;
|
|
80
|
+
}>;
|
package/lib/compiler.js
CHANGED
|
@@ -124,3 +124,125 @@ function escapeRegexChar(ch) {
|
|
|
124
124
|
export function hashText(text) {
|
|
125
125
|
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
126
126
|
}
|
|
127
|
+
// ─── Extended matchers for new dimensions ──────────────────────────────────
|
|
128
|
+
/**
|
|
129
|
+
* Compile a CIDR notation string (e.g. `10.0.0.0/8`) into a matcher that
|
|
130
|
+
* tests whether an IPv4 address falls within the range.
|
|
131
|
+
*
|
|
132
|
+
* Returns a pure function `(ip: string) => boolean`. Non-IPv4 inputs always
|
|
133
|
+
* return false. Invalid CIDR throws at compile time (fail-loud).
|
|
134
|
+
*/
|
|
135
|
+
export function compileCidr(cidr) {
|
|
136
|
+
const slash = cidr.indexOf('/');
|
|
137
|
+
if (slash <= 0 || slash >= cidr.length - 1) {
|
|
138
|
+
throw new PatternError(`CIDR "${cidr}" must be in address/prefix format`);
|
|
139
|
+
}
|
|
140
|
+
const addrStr = cidr.slice(0, slash);
|
|
141
|
+
const prefixStr = cidr.slice(slash + 1);
|
|
142
|
+
const prefix = Number.parseInt(prefixStr, 10);
|
|
143
|
+
if (!Number.isFinite(prefix) || prefix < 0 || prefix > 32) {
|
|
144
|
+
throw new PatternError(`CIDR "${cidr}" prefix must be 0–32`);
|
|
145
|
+
}
|
|
146
|
+
const addr = ipv4ToNumber(addrStr);
|
|
147
|
+
if (addr === undefined) {
|
|
148
|
+
throw new PatternError(`CIDR "${cidr}" has invalid IPv4 address`);
|
|
149
|
+
}
|
|
150
|
+
const mask = prefix === 0 ? 0 : (~0 << (32 - prefix)) >>> 0;
|
|
151
|
+
const network = (addr & mask) >>> 0;
|
|
152
|
+
return (ip) => {
|
|
153
|
+
const n = ipv4ToNumber(ip);
|
|
154
|
+
if (n === undefined)
|
|
155
|
+
return false;
|
|
156
|
+
return ((n & mask) >>> 0) === network;
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/** Parse an IPv4 dotted-decimal string to a 32-bit unsigned integer, or undefined. */
|
|
160
|
+
function ipv4ToNumber(addr) {
|
|
161
|
+
const parts = addr.split('.');
|
|
162
|
+
if (parts.length !== 4)
|
|
163
|
+
return undefined;
|
|
164
|
+
let result = 0;
|
|
165
|
+
for (const part of parts) {
|
|
166
|
+
const n = Number.parseInt(part, 10);
|
|
167
|
+
if (!Number.isFinite(n) || n < 0 || n > 255)
|
|
168
|
+
return undefined;
|
|
169
|
+
if (part.length > 1 && part.startsWith('0'))
|
|
170
|
+
return undefined; // no leading zeros
|
|
171
|
+
result = (result * 256 + n) >>> 0;
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Compile a port specification into a matcher function.
|
|
177
|
+
*
|
|
178
|
+
* Accepts:
|
|
179
|
+
* - A single port: `"443"` → matches exactly 443
|
|
180
|
+
* - A port range: `"8000-9000"` → matches 8000–9000 inclusive
|
|
181
|
+
*
|
|
182
|
+
* Returns `(port: number) => boolean`. Invalid specs throw at compile time.
|
|
183
|
+
*/
|
|
184
|
+
export function compilePortSpec(spec) {
|
|
185
|
+
const dash = spec.indexOf('-');
|
|
186
|
+
if (dash === -1) {
|
|
187
|
+
const n = Number.parseInt(spec, 10);
|
|
188
|
+
if (!Number.isFinite(n) || n < 0 || n > 65535) {
|
|
189
|
+
throw new PatternError(`port "${spec}" must be 0–65535`);
|
|
190
|
+
}
|
|
191
|
+
return (port) => port === n;
|
|
192
|
+
}
|
|
193
|
+
const lo = Number.parseInt(spec.slice(0, dash), 10);
|
|
194
|
+
const hi = Number.parseInt(spec.slice(dash + 1), 10);
|
|
195
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi) || lo < 0 || hi > 65535 || lo > hi) {
|
|
196
|
+
throw new PatternError(`port range "${spec}" must be low–high within 0–65535`);
|
|
197
|
+
}
|
|
198
|
+
return (port) => port >= lo && port <= hi;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Compile a domain pattern into a matcher that supports subdomain inclusion.
|
|
202
|
+
*
|
|
203
|
+
* - `"github.com"` matches `github.com` AND any subdomain (`api.github.com`)
|
|
204
|
+
* - `"*.example.com"` matches only subdomains, not `example.com` itself
|
|
205
|
+
* - Glob characters (`*`, `?`, `[...]`) are handled by {@link compileGlob}.
|
|
206
|
+
*
|
|
207
|
+
* Returns a {@link CompiledPattern} whose `.re` is the anchored RegExp.
|
|
208
|
+
*/
|
|
209
|
+
export function compileDomainPattern(pattern) {
|
|
210
|
+
// If the pattern already contains glob chars, use glob compilation.
|
|
211
|
+
if (/[*?[\]]/.test(pattern)) {
|
|
212
|
+
return compileGlob(pattern, { segments: false, maxStars: DEFAULT_MAX_STARS });
|
|
213
|
+
}
|
|
214
|
+
// Literal domain: match exactly OR as a parent of a subdomain.
|
|
215
|
+
// e.g. "github.com" matches "github.com" and "api.github.com"
|
|
216
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
217
|
+
const re = new RegExp(`^(?:.*\\.)?${escaped}$`, 'u');
|
|
218
|
+
return { source: pattern, re };
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Compile a git branch-name pattern into a matcher.
|
|
222
|
+
*
|
|
223
|
+
* Branch names are path-like (`release/1.0`) but a rule author thinks in
|
|
224
|
+
* segments-with-globs terms (`release*`), so this is a **non-segment** glob:
|
|
225
|
+
* `*` crosses `/` on purpose, matching how `args` patterns behave.
|
|
226
|
+
*
|
|
227
|
+
* Rejects a pattern that is nothing but a separator so a typo can not degrade
|
|
228
|
+
* into "matches everything".
|
|
229
|
+
*/
|
|
230
|
+
export function compileBranchPattern(pattern) {
|
|
231
|
+
if (!/[A-Za-z0-9_*?[\]]/.test(pattern)) {
|
|
232
|
+
throw new PatternError(`branch pattern ${JSON.stringify(pattern)} has no matchable character`);
|
|
233
|
+
}
|
|
234
|
+
return compileGlob(pattern, { segments: false, maxStars: DEFAULT_MAX_STARS });
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Compile a list of patterns (possibly `!`-prefixed) into param matchers.
|
|
238
|
+
* Returns `{ pattern, negated, compiled }` entries for the params dimension.
|
|
239
|
+
*/
|
|
240
|
+
export function compileParamPatterns(patterns, opts = {}) {
|
|
241
|
+
const maxStars = opts.maxStars ?? DEFAULT_MAX_STARS;
|
|
242
|
+
return patterns.map((p) => {
|
|
243
|
+
if (p.startsWith('!') && p.length > 1) {
|
|
244
|
+
return { pattern: p, negated: true, compiled: compileGlob(p.slice(1), { segments: false, maxStars }) };
|
|
245
|
+
}
|
|
246
|
+
return { pattern: p, negated: false, compiled: compileGlob(p, { segments: false, maxStars }) };
|
|
247
|
+
});
|
|
248
|
+
}
|
package/lib/config.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import z from '@deepseek-ai/schemastery';
|
|
2
2
|
import type { RuleAction } from './rule.js';
|
|
3
|
+
import { DEFAULT_GATE_PRESETS, resolveGatePresets } from './preset.js';
|
|
3
4
|
/**
|
|
4
5
|
* The DSH home directory: an explicit `dshHome`, else `$DSH_HOME`, else
|
|
5
6
|
* `~/.dsh`. A profile entry that omits `config` must still get a writable data
|
|
@@ -100,6 +101,96 @@ export interface PermGateConfig {
|
|
|
100
101
|
* never widen authority for a destructive or credential-bearing call.
|
|
101
102
|
*/
|
|
102
103
|
readonly autoAllowTools?: string[];
|
|
104
|
+
/**
|
|
105
|
+
* Session-lifecycle sweep: on startup and hourly, drop the authorization
|
|
106
|
+
* chain's decision events and pre-change snapshots of sessions DSH has
|
|
107
|
+
* archived or no longer tracks. Fail-open, best-effort. Default true.
|
|
108
|
+
*/
|
|
109
|
+
readonly sessionSweep?: boolean;
|
|
110
|
+
/** Path to DSH's workspace store; defaults to `<dshHome>/storages/workspace.json`. Read-only to the gate. */
|
|
111
|
+
readonly workspaceStoreFile?: string;
|
|
112
|
+
/**
|
|
113
|
+
* Whether to enable multi-file rule chain resolution. When true, the gate
|
|
114
|
+
* searches up directory ancestors for the rules file. Default false.
|
|
115
|
+
*/
|
|
116
|
+
readonly searchUp?: boolean;
|
|
117
|
+
/** Fallback rules file path when no file is found in the chain search. */
|
|
118
|
+
readonly fallbackPath?: string;
|
|
119
|
+
/** Error policy for malformed files in the chain: `fail` (throw) or `warn` (skip). Default fail. */
|
|
120
|
+
readonly badFilePolicy?: 'fail' | 'warn';
|
|
121
|
+
/** Maximum number of files in the rule chain. Default 10. */
|
|
122
|
+
readonly maxChainLength?: number;
|
|
123
|
+
/**
|
|
124
|
+
* Master network switch. **Default false** — the network proxy is an
|
|
125
|
+
* opt-in capability, not part of the baseline gate.
|
|
126
|
+
*
|
|
127
|
+
* When false: no proxy is started, no environment variables are injected,
|
|
128
|
+
* and no network interception occurs — zero behavior change from the
|
|
129
|
+
* pre-network baseline.
|
|
130
|
+
*
|
|
131
|
+
* Enable it from the settings card only after verifying it in your
|
|
132
|
+
* environment. The proxy binds a loopback port and rewrites proxy env
|
|
133
|
+
* vars for subprocesses, so it must never be enabled implicitly.
|
|
134
|
+
*/
|
|
135
|
+
readonly networkEnabled?: boolean;
|
|
136
|
+
/** Network policy mode when auto-mapping from sandbox is not used. Default 'whitelist'. */
|
|
137
|
+
readonly networkMode?: import('./network.js').NetworkMode;
|
|
138
|
+
/** How unlisted targets are handled in whitelist mode.
|
|
139
|
+
*
|
|
140
|
+
* - `'deny'` — block outright, never prompt.
|
|
141
|
+
* - `'ask'` — raise an interactive approval for the attributed shell command;
|
|
142
|
+
* approve to let THIS target through for this session.
|
|
143
|
+
*
|
|
144
|
+
* A `deny` rule always wins: approval can widen reach for a target no rule
|
|
145
|
+
* allows, but it can never override a rule that says no.
|
|
146
|
+
*/
|
|
147
|
+
readonly networkUnlisted?: import('./network.js').UnlistedAction;
|
|
148
|
+
/**
|
|
149
|
+
* How traffic with **no shell attribution** is handled. Default `'allow'`.
|
|
150
|
+
*
|
|
151
|
+
* A connection that cannot be tied to an in-flight shell execution did not
|
|
152
|
+
* come from a subprocess this gate manages — it is DSH's own client (a
|
|
153
|
+
* built-in network tool, the LLM transport). The proxy is a *subprocess*
|
|
154
|
+
* policy surface, so reviewing the host's own traffic risks the host
|
|
155
|
+
* blocking itself, which is far worse than a missed block. `'deny'` reviews
|
|
156
|
+
* it anyway, and would break DSH if a built-in client ever honors the proxy
|
|
157
|
+
* environment (e.g. Node 24+ with `NODE_USE_ENV_PROXY=1`).
|
|
158
|
+
*/
|
|
159
|
+
readonly networkUnattributed?: import('./network.js').UnattributedAction;
|
|
160
|
+
/** Loopback handling: 'allow' short-circuits before rules; 'policy' evaluates normally. Default 'allow'. */
|
|
161
|
+
readonly networkLoopback?: 'allow' | 'policy';
|
|
162
|
+
/** Proxy bind address. Default '127.0.0.1'. */
|
|
163
|
+
readonly networkBind?: string;
|
|
164
|
+
/** Proxy bind port. Default 0 (ephemeral). */
|
|
165
|
+
readonly networkPort?: number;
|
|
166
|
+
/** NO_PROXY handling: 'clear' empties it so policy cannot be bypassed; 'preserve' keeps ambient values. Default 'clear'. */
|
|
167
|
+
readonly networkNoProxy?: 'clear' | 'preserve';
|
|
168
|
+
/**
|
|
169
|
+
* Rewrite `HTTP(S)_PROXY` / `ALL_PROXY` for subprocesses so their traffic
|
|
170
|
+
* reaches the proxy. Default true.
|
|
171
|
+
*
|
|
172
|
+
* Turn it off to run the proxy WITHOUT touching `process.env` — the
|
|
173
|
+
* listener still adjudicates whatever is explicitly pointed at it, but no
|
|
174
|
+
* ambient state is rewritten. Useful when the environment is managed
|
|
175
|
+
* elsewhere or when verifying the proxy in isolation.
|
|
176
|
+
*/
|
|
177
|
+
readonly networkInjectEnv?: boolean;
|
|
178
|
+
/**
|
|
179
|
+
* How long an unlisted-target approval waits for a human before failing
|
|
180
|
+
* closed to a block (ms). Default 120000 (2 min).
|
|
181
|
+
*/
|
|
182
|
+
readonly networkAskTimeoutMs?: number;
|
|
183
|
+
/**
|
|
184
|
+
* How long one approved network target stays approved for the session (ms).
|
|
185
|
+
* One shell command routinely opens many connections to the same host, so
|
|
186
|
+
* without this the human would be prompted once per connection.
|
|
187
|
+
* Default 1800000 (30 min).
|
|
188
|
+
*/
|
|
189
|
+
readonly networkGrantTtlMs?: number;
|
|
190
|
+
/** @deprecated Rules now live in the `dsh-perm-gate-rules` settings namespace; file watching was removed. Accepted (ignored) for composition compatibility. */
|
|
191
|
+
readonly watch?: boolean;
|
|
192
|
+
/** @deprecated See {@link watch}. */
|
|
193
|
+
readonly watchDebounceMs?: number;
|
|
103
194
|
}
|
|
104
195
|
/** Backend combinable approval strategies for the Permissive tier (all opt-in). */
|
|
105
196
|
export interface PermissiveStrategies {
|
|
@@ -127,15 +218,78 @@ export interface PermissiveStrategies {
|
|
|
127
218
|
readonly trustEscalation: boolean;
|
|
128
219
|
}
|
|
129
220
|
export declare const DEFAULT_PERMISSIVE_STRATEGIES: Readonly<PermissiveStrategies>;
|
|
130
|
-
|
|
131
|
-
* Presets in which the gate is active. It owns the `permissive` tier; `'*'`
|
|
132
|
-
* makes it global (every preset, including the hard-deny layer).
|
|
133
|
-
*/
|
|
134
|
-
export declare const DEFAULT_GATE_PRESETS: readonly string[];
|
|
135
|
-
/** Normalize the gate scope: an unset/empty list means the default. */
|
|
136
|
-
export declare function resolveGatePresets(configured?: readonly string[]): readonly string[];
|
|
221
|
+
export { DEFAULT_GATE_PRESETS, resolveGatePresets };
|
|
137
222
|
/** Normalize a backend strategy bag to fully-specified booleans (backend-part combinable). */
|
|
138
223
|
export declare function resolvePermissiveStrategies(bag?: Partial<PermissiveStrategies>): PermissiveStrategies;
|
|
224
|
+
/**
|
|
225
|
+
* One rule entry as stored in settings. Deliberately permissive (`z.any()`
|
|
226
|
+
* fields): strict validation happens in `parseRuleEntry` at compile time (fail
|
|
227
|
+
* loud), and the stored form must round-trip BOTH the raw YAML shape (string
|
|
228
|
+
* scalars, `params` as a key→patterns mapping) and the parsed document shape
|
|
229
|
+
* (parsed dimensions), so a file migration can be seeded without lossy
|
|
230
|
+
* normalization. Schemastery preserves unknown keys in non-strict object mode.
|
|
231
|
+
*/
|
|
232
|
+
export declare const RuleEntrySchema: z<unknown>;
|
|
233
|
+
/**
|
|
234
|
+
* The rules structure stored in the DSH settings namespace — the JSON form of
|
|
235
|
+
* rules.yml. Unknown extra keys on the document itself are preserved too.
|
|
236
|
+
*/
|
|
237
|
+
export declare const RulesSchema: z<RulesConfig>;
|
|
238
|
+
/** One rule entry in the settings-stored rules document (JSON form; all optional). */
|
|
239
|
+
export interface RulesEntryConfig {
|
|
240
|
+
readonly tools?: readonly string[];
|
|
241
|
+
readonly command?: readonly string[];
|
|
242
|
+
readonly args?: readonly string[];
|
|
243
|
+
readonly paths?: readonly string[];
|
|
244
|
+
readonly params?: unknown;
|
|
245
|
+
readonly absent?: readonly string[];
|
|
246
|
+
readonly agents?: readonly string[];
|
|
247
|
+
readonly when?: unknown;
|
|
248
|
+
readonly argv?: unknown;
|
|
249
|
+
readonly network?: {
|
|
250
|
+
readonly domains?: readonly string[];
|
|
251
|
+
readonly ips?: readonly string[];
|
|
252
|
+
readonly ports?: readonly string[];
|
|
253
|
+
readonly schemes?: readonly string[];
|
|
254
|
+
};
|
|
255
|
+
readonly branch?: {
|
|
256
|
+
readonly target?: readonly string[];
|
|
257
|
+
readonly remote?: readonly string[];
|
|
258
|
+
readonly shared?: boolean;
|
|
259
|
+
};
|
|
260
|
+
readonly reason?: string;
|
|
261
|
+
readonly enabled?: boolean;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* The rules document stored in the settings namespace. The action lists are
|
|
265
|
+
* `unknown[]` on purpose: entries may be the raw YAML shape or the parsed
|
|
266
|
+
* document shape, and strict validation belongs to `parseRuleEntry` at compile
|
|
267
|
+
* time — see {@link RulesEntryConfig} for the documented JSON form. (Mutable
|
|
268
|
+
* arrays: the type doubles as the Schemastery input face of {@link RulesSchema},
|
|
269
|
+
* which validates in place.)
|
|
270
|
+
*/
|
|
271
|
+
export interface RulesConfig {
|
|
272
|
+
readonly defaultAction?: RuleAction;
|
|
273
|
+
readonly deny?: unknown[];
|
|
274
|
+
readonly allow?: unknown[];
|
|
275
|
+
readonly ask?: unknown[];
|
|
276
|
+
}
|
|
277
|
+
/** Settings namespace for rules (separate from the main perm-gate namespace). */
|
|
278
|
+
export declare const RULES_NAMESPACE = "dsh-perm-gate-rules";
|
|
279
|
+
/**
|
|
280
|
+
* Whether a settings-sourced rules document carries a REAL configuration —
|
|
281
|
+
* entries or a non-default `defaultAction`. A namespace still holding bare
|
|
282
|
+
* schema defaults is "not configured" and must not shadow the rules file
|
|
283
|
+
* (the dual-source contract: settings first, file fallback).
|
|
284
|
+
*/
|
|
285
|
+
export declare function isRulesConfigured(value: unknown): boolean;
|
|
286
|
+
/**
|
|
287
|
+
* Read the rules from a settings scope: the document when the namespace is
|
|
288
|
+
* configured, otherwise `undefined` so the caller falls back to the rules file.
|
|
289
|
+
*/
|
|
290
|
+
export declare function readRulesFromSettings(scope: {
|
|
291
|
+
get(): unknown;
|
|
292
|
+
} | undefined): RulesConfig | undefined;
|
|
139
293
|
export declare const Config: z<PermGateConfig>;
|
|
140
294
|
export type ResolvedPermGateConfig = Required<Pick<PermGateConfig, 'caseInsensitivePaths' | 'grantTtlMs' | 'grantMaxUses' | 'permissive' | 'riskTimeoutMs' | 'riskLearning' | 'riskThreshold'>> & Pick<PermGateConfig, 'rulesFile' | 'dshHome' | 'defaultAction' | 'classifierEnabled' | 'classifierEndpoint' | 'classifierModel' | 'classifierApiKey' | 'learningFile' | 'eventsFile'> & {
|
|
141
295
|
readonly permissiveStrategies: PermissiveStrategies;
|
package/lib/config.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import z from '@deepseek-ai/schemastery';
|
|
8
|
+
import { DEFAULT_GATE_PRESETS, resolveGatePresets } from './preset.js';
|
|
8
9
|
/**
|
|
9
10
|
* The DSH home directory: an explicit `dshHome`, else `$DSH_HOME`, else
|
|
10
11
|
* `~/.dsh`. A profile entry that omits `config` must still get a writable data
|
|
@@ -42,17 +43,10 @@ export const DEFAULT_PERMISSIVE_STRATEGIES = {
|
|
|
42
43
|
// bulk) — the escalation-worthy ones — auto-deny rather than allow.
|
|
43
44
|
trustEscalation: true,
|
|
44
45
|
};
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
export const DEFAULT_GATE_PRESETS = ['permissive'];
|
|
50
|
-
/** Normalize the gate scope: an unset/empty list means the default. */
|
|
51
|
-
export function resolveGatePresets(configured) {
|
|
52
|
-
if (configured === undefined || configured.length === 0)
|
|
53
|
-
return DEFAULT_GATE_PRESETS;
|
|
54
|
-
return configured.filter((name) => typeof name === 'string' && name !== '');
|
|
55
|
-
}
|
|
46
|
+
// The scope constants live in the dependency-free `preset` module (the browser
|
|
47
|
+
// half renders them); bound locally AND re-exported so every existing
|
|
48
|
+
// `./config.js` import keeps resolving.
|
|
49
|
+
export { DEFAULT_GATE_PRESETS, resolveGatePresets };
|
|
56
50
|
/** Normalize a backend strategy bag to fully-specified booleans (backend-part combinable). */
|
|
57
51
|
export function resolvePermissiveStrategies(bag = {}) {
|
|
58
52
|
return {
|
|
@@ -62,6 +56,57 @@ export function resolvePermissiveStrategies(bag = {}) {
|
|
|
62
56
|
trustEscalation: bag.trustEscalation ?? DEFAULT_PERMISSIVE_STRATEGIES.trustEscalation,
|
|
63
57
|
};
|
|
64
58
|
}
|
|
59
|
+
// ─── Rules Schema (for settings namespace) ──────────────────────────────────
|
|
60
|
+
// The JSON twin of the rules.yml permissions document, stored in its own DSH
|
|
61
|
+
// settings namespace so the gate loads without any file on disk.
|
|
62
|
+
/**
|
|
63
|
+
* One rule entry as stored in settings. Deliberately permissive (`z.any()`
|
|
64
|
+
* fields): strict validation happens in `parseRuleEntry` at compile time (fail
|
|
65
|
+
* loud), and the stored form must round-trip BOTH the raw YAML shape (string
|
|
66
|
+
* scalars, `params` as a key→patterns mapping) and the parsed document shape
|
|
67
|
+
* (parsed dimensions), so a file migration can be seeded without lossy
|
|
68
|
+
* normalization. Schemastery preserves unknown keys in non-strict object mode.
|
|
69
|
+
*/
|
|
70
|
+
export const RuleEntrySchema = z.any();
|
|
71
|
+
/**
|
|
72
|
+
* The rules structure stored in the DSH settings namespace — the JSON form of
|
|
73
|
+
* rules.yml. Unknown extra keys on the document itself are preserved too.
|
|
74
|
+
*/
|
|
75
|
+
export const RulesSchema = z.object({
|
|
76
|
+
defaultAction: z.union(['allow', 'ask', 'deny']).default('ask'),
|
|
77
|
+
deny: z.array(RuleEntrySchema).default([]),
|
|
78
|
+
allow: z.array(RuleEntrySchema).default([]),
|
|
79
|
+
ask: z.array(RuleEntrySchema).default([]),
|
|
80
|
+
});
|
|
81
|
+
/** Settings namespace for rules (separate from the main perm-gate namespace). */
|
|
82
|
+
export const RULES_NAMESPACE = 'dsh-perm-gate-rules';
|
|
83
|
+
/**
|
|
84
|
+
* Whether a settings-sourced rules document carries a REAL configuration —
|
|
85
|
+
* entries or a non-default `defaultAction`. A namespace still holding bare
|
|
86
|
+
* schema defaults is "not configured" and must not shadow the rules file
|
|
87
|
+
* (the dual-source contract: settings first, file fallback).
|
|
88
|
+
*/
|
|
89
|
+
export function isRulesConfigured(value) {
|
|
90
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
91
|
+
return false;
|
|
92
|
+
const doc = value;
|
|
93
|
+
if (doc.defaultAction !== undefined && doc.defaultAction !== 'ask')
|
|
94
|
+
return true;
|
|
95
|
+
for (const key of ['deny', 'allow', 'ask']) {
|
|
96
|
+
const list = doc[key];
|
|
97
|
+
if (Array.isArray(list) && list.length > 0)
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Read the rules from a settings scope: the document when the namespace is
|
|
104
|
+
* configured, otherwise `undefined` so the caller falls back to the rules file.
|
|
105
|
+
*/
|
|
106
|
+
export function readRulesFromSettings(scope) {
|
|
107
|
+
const value = scope?.get();
|
|
108
|
+
return isRulesConfigured(value) ? value : undefined;
|
|
109
|
+
}
|
|
65
110
|
export const Config = z.object({
|
|
66
111
|
rulesFile: z.string(),
|
|
67
112
|
dshHome: z.string(),
|
|
@@ -92,6 +137,28 @@ export const Config = z.object({
|
|
|
92
137
|
allowlist: z.array(z.string()),
|
|
93
138
|
denyKeywords: z.array(z.string()),
|
|
94
139
|
autoAllowTools: z.array(z.string()),
|
|
140
|
+
sessionSweep: z.boolean().default(true),
|
|
141
|
+
workspaceStoreFile: z.string(),
|
|
142
|
+
// Rule chain (T1.9)
|
|
143
|
+
searchUp: z.boolean().default(false),
|
|
144
|
+
fallbackPath: z.string(),
|
|
145
|
+
badFilePolicy: z.union(['fail', 'warn']).default('fail'),
|
|
146
|
+
maxChainLength: z.number().min(1).max(50).default(10),
|
|
147
|
+
// Network (Phase 2)
|
|
148
|
+
networkEnabled: z.boolean().default(false),
|
|
149
|
+
networkMode: z.union(['deny-all', 'whitelist', 'allow-all']).default('whitelist'),
|
|
150
|
+
networkUnlisted: z.union(['ask', 'deny']).default('ask'),
|
|
151
|
+
networkUnattributed: z.union(['allow', 'deny']).default('allow'),
|
|
152
|
+
networkLoopback: z.union(['allow', 'policy']).default('allow'),
|
|
153
|
+
networkBind: z.string().default('127.0.0.1'),
|
|
154
|
+
networkPort: z.number().min(0).max(65535).default(0),
|
|
155
|
+
networkNoProxy: z.union(['clear', 'preserve']).default('clear'),
|
|
156
|
+
networkInjectEnv: z.boolean().default(true),
|
|
157
|
+
networkAskTimeoutMs: z.number().min(1000).max(600_000).default(120_000),
|
|
158
|
+
networkGrantTtlMs: z.number().min(0).max(24 * 60 * 60_000).default(30 * 60_000),
|
|
159
|
+
// Hot reload (Phase 3)
|
|
160
|
+
watch: z.boolean().default(true),
|
|
161
|
+
watchDebounceMs: z.number().min(50).max(5000).default(300),
|
|
95
162
|
});
|
|
96
163
|
export function resolveConfig(config = {}) {
|
|
97
164
|
const parsed = Config(config);
|