@dsh-cc/permission-rules 0.5.0

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.
Files changed (65) hide show
  1. package/LICENSE +201 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +86 -0
  4. package/README.zh.md +86 -0
  5. package/lib/approval-listener.d.ts +50 -0
  6. package/lib/approval-listener.d.ts.map +1 -0
  7. package/lib/approval-listener.js +58 -0
  8. package/lib/approval-listener.js.map +1 -0
  9. package/lib/auto-stage.d.ts +138 -0
  10. package/lib/auto-stage.d.ts.map +1 -0
  11. package/lib/auto-stage.js +284 -0
  12. package/lib/auto-stage.js.map +1 -0
  13. package/lib/classifier.d.ts +57 -0
  14. package/lib/classifier.d.ts.map +1 -0
  15. package/lib/classifier.js +129 -0
  16. package/lib/classifier.js.map +1 -0
  17. package/lib/decide.d.ts +80 -0
  18. package/lib/decide.d.ts.map +1 -0
  19. package/lib/decide.js +127 -0
  20. package/lib/decide.js.map +1 -0
  21. package/lib/domain.d.ts +46 -0
  22. package/lib/domain.d.ts.map +1 -0
  23. package/lib/domain.js +103 -0
  24. package/lib/domain.js.map +1 -0
  25. package/lib/evaluate.d.ts +32 -0
  26. package/lib/evaluate.d.ts.map +1 -0
  27. package/lib/evaluate.js +176 -0
  28. package/lib/evaluate.js.map +1 -0
  29. package/lib/index.d.ts +123 -0
  30. package/lib/index.d.ts.map +1 -0
  31. package/lib/index.js +380 -0
  32. package/lib/index.js.map +1 -0
  33. package/lib/invariant.d.ts +28 -0
  34. package/lib/invariant.d.ts.map +1 -0
  35. package/lib/invariant.js +54 -0
  36. package/lib/invariant.js.map +1 -0
  37. package/lib/llm-classifier.d.ts +107 -0
  38. package/lib/llm-classifier.d.ts.map +1 -0
  39. package/lib/llm-classifier.js +231 -0
  40. package/lib/llm-classifier.js.map +1 -0
  41. package/lib/matchers.d.ts +18 -0
  42. package/lib/matchers.d.ts.map +1 -0
  43. package/lib/matchers.js +43 -0
  44. package/lib/matchers.js.map +1 -0
  45. package/lib/mode.d.ts +91 -0
  46. package/lib/mode.d.ts.map +1 -0
  47. package/lib/mode.js +133 -0
  48. package/lib/mode.js.map +1 -0
  49. package/lib/parser.d.ts +91 -0
  50. package/lib/parser.d.ts.map +1 -0
  51. package/lib/parser.js +282 -0
  52. package/lib/parser.js.map +1 -0
  53. package/lib/session-allowlist.d.ts +76 -0
  54. package/lib/session-allowlist.d.ts.map +1 -0
  55. package/lib/session-allowlist.js +122 -0
  56. package/lib/session-allowlist.js.map +1 -0
  57. package/lib/settings-schema.d.ts +99 -0
  58. package/lib/settings-schema.d.ts.map +1 -0
  59. package/lib/settings-schema.js +64 -0
  60. package/lib/settings-schema.js.map +1 -0
  61. package/lib/types.d.ts +150 -0
  62. package/lib/types.d.ts.map +1 -0
  63. package/lib/types.js +33 -0
  64. package/lib/types.js.map +1 -0
  65. package/package.json +71 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-stage.d.ts","sourceRoot":"","sources":["../src/auto-stage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,284 @@
1
+ /**
2
+ * The async LLM-classifier stage for `auto` mode: owns arming (per call, from
3
+ * the live `permissions.autoMode` settings slice), the memoized
4
+ * `createLlmClassifier` instance, and the `permission/classifier` session
5
+ * audit event. The plugin's `tools/pre-execute` listener (index.ts) only
6
+ * wires this stage — the escalate-only decision flow lives here.
7
+ *
8
+ * Arming predicate (§4.4): `autoMode.classifier.enabled === true` AND an llm
9
+ * stream capability is wired AND the alias route resolves. Enabled but
10
+ * unarmable ⇒ disarm with ONE warning per process (plus an `unarmed` audit
11
+ * event when a session is available) and the legacy decision path runs.
12
+ *
13
+ * @module @dsh-cc/permission-rules/auto-stage
14
+ */
15
+ import { KNOWN_SESSION_EVENT_TYPES } from '@deepseek-ai/dsh-session';
16
+ import { createLlmClassifier, expandSoftDeny } from "./llm-classifier.js";
17
+ /** The session event type carrying one classifier verdict audit record. */
18
+ export const CLASSIFIER_EVENT = 'permission/classifier';
19
+ /** Consecutive per-route classifier failures before that route's breaker opens (module constant — no settings knob by design). */
20
+ export const CLASSIFIER_BREAKER_THRESHOLD = 3;
21
+ KNOWN_SESSION_EVENT_TYPES.add(CLASSIFIER_EVENT);
22
+ /**
23
+ * Append one `permission/classifier` audit record through the widened session
24
+ * append face (same cross-pin strategy as `./mode.ts` and
25
+ * `./session-allowlist.ts`).
26
+ */
27
+ export function appendSessionClassifier(session, data) {
28
+ session.append(CLASSIFIER_EVENT, data);
29
+ }
30
+ /**
31
+ * Fold a session log into the classifier verdict records it carries, in log
32
+ * order. Foreign event types are skipped; resume/replay reconstructs why a
33
+ * call did or did not prompt.
34
+ */
35
+ export function foldClassifiers(events) {
36
+ const out = [];
37
+ for (const event of events) {
38
+ const wire = event;
39
+ if (wire.type !== CLASSIFIER_EVENT || typeof wire.data !== 'object' || wire.data === null)
40
+ continue;
41
+ out.push(wire.data);
42
+ }
43
+ return out;
44
+ }
45
+ function readSlice(settings) {
46
+ const autoMode = settings.autoMode;
47
+ const classifier = autoMode?.classifier;
48
+ const softDeny = expandSoftDeny(autoMode?.soft_deny ?? ['$defaults']);
49
+ return {
50
+ softDeny,
51
+ route: classifier?.route ?? 'haiku',
52
+ timeoutMs: classifier?.timeoutMs ?? 8000,
53
+ cacheMaxEntries: classifier?.cacheMaxEntries ?? 256,
54
+ enabled: classifier?.enabled === true,
55
+ raw: JSON.stringify([autoMode?.soft_deny, classifier]),
56
+ };
57
+ }
58
+ /** The failure tags the breaker counts; `cancelled`/`unarmed` are host noise and never count. */
59
+ const BREAKER_FAILURE_TAGS = ['malformed', 'error', 'timeout'];
60
+ /**
61
+ * Trailing consecutive per-route failure streak over attributed classifier
62
+ * audit records (R3, pure fold — unit-testable). Only events carrying
63
+ * `provider`/`model` attribution matching `routeKey` count (unattributed
64
+ * legacy events predate route keying — skipped entirely); a parsed verdict or
65
+ * cache hit resets the streak; malformed/error/timeout increment it; other
66
+ * tags (`cancelled`, `breaker`, `unarmed`) are neutral. Capped at `threshold`.
67
+ */
68
+ export function trailingRouteFailureStreak(events, routeKey, threshold) {
69
+ let streak = 0;
70
+ for (const event of events) {
71
+ if (event.provider === undefined || event.model === undefined)
72
+ continue;
73
+ if (`${event.provider}/${event.model}` !== routeKey)
74
+ continue;
75
+ if (event.failure === undefined)
76
+ streak = 0;
77
+ else if (BREAKER_FAILURE_TAGS.includes(event.failure))
78
+ streak = Math.min(streak + 1, threshold);
79
+ }
80
+ return streak;
81
+ }
82
+ /**
83
+ * Build the stage. The classifier instance is memoized per autoMode slice:
84
+ * `rebuild()` (wired to the plugin's settings onChange/reload) drops it, and
85
+ * the next armed call rebuilds from the fresh slice — never per call.
86
+ */
87
+ export function createAutoStage(deps) {
88
+ let slice = readSlice(deps.settingsRead());
89
+ /** The raw autoMode slice the memoized classifier was built from. */
90
+ let builtRaw = slice.raw;
91
+ let classifier;
92
+ /** Warned-once flag for enabled-but-unarmable (per process). */
93
+ let warnedUnarmed = false;
94
+ /** Consecutive classifier failures per route (`${provider}/${model}`); any success resets the route to 0. */
95
+ const routeFailures = new Map();
96
+ /** Routes whose breaker is open: maybeEscalate returns undefined without touching the stream. */
97
+ const breakerOpen = new Set();
98
+ /** Session ids that already recorded one `breaker` audit event (one per session). */
99
+ const breakerAudited = new Set();
100
+ /** Warned-once flag for an opened breaker (per process). */
101
+ let warnedBreaker = false;
102
+ /** Session ids whose durable log already seeded this process's breaker state (R3). */
103
+ const seededSessions = new Set();
104
+ const ensureClassifier = () => {
105
+ if (classifier !== undefined && builtRaw === slice.raw)
106
+ return classifier;
107
+ classifier = createLlmClassifier({
108
+ // The route for each call is passed as data to classify() (per-call
109
+ // argument, never ambient state) and the caller audits from the returned
110
+ // classification — no session/route fields live on this stage, so
111
+ // concurrent calls cannot cross-contaminate audit attribution.
112
+ stream: (opts) => {
113
+ const stream = deps.stream;
114
+ if (stream === undefined)
115
+ throw new Error('llm service unmounted');
116
+ return stream(opts);
117
+ },
118
+ softDeny: slice.softDeny,
119
+ timeoutMs: slice.timeoutMs,
120
+ cacheMaxEntries: slice.cacheMaxEntries,
121
+ ...(deps.debug === undefined ? {} : { debug: deps.debug }),
122
+ });
123
+ builtRaw = slice.raw;
124
+ return classifier;
125
+ };
126
+ const disarmUnarmed = (exec) => {
127
+ if (!warnedUnarmed) {
128
+ warnedUnarmed = true;
129
+ deps.warn('permission classifier: enabled but unarmable (llm service or model route unavailable); stage disarmed, auto mode uses the legacy path');
130
+ }
131
+ const session = exec.agent?.session;
132
+ if (session !== undefined) {
133
+ deps.audit(session, {
134
+ tool: exec.name,
135
+ verdict: 'ask',
136
+ failure: 'unarmed',
137
+ latencyMs: 0,
138
+ cacheHit: false,
139
+ });
140
+ }
141
+ };
142
+ /**
143
+ * Seed the per-route breaker state from the session's durable log (R3) —
144
+ * lazily, once per session, on its first breaker-eligible call. Synchronous
145
+ * guard: the session id enters `seededSessions` BEFORE any suspension and
146
+ * the fold is over in-memory events, so concurrent first-calls cannot
147
+ * double-seed. A restored streak ≥ threshold opens the route at seed time
148
+ * (its first real call then audits/warns/notices exactly once); a log that
149
+ * already holds a `breaker` event pre-joins `breakerAudited` so replay never
150
+ * re-audits the same open. Never overwrites a live counter: seed only when
151
+ * the session is unseen AND the route counter is 0/absent — in-process
152
+ * accrual is fresher (fail-open undercounting is the accepted direction).
153
+ */
154
+ function seedBreakerFromLog(exec, routeKey, route) {
155
+ const session = exec.agent?.session;
156
+ if (session === undefined)
157
+ return;
158
+ const sessionId = String(session.header.id);
159
+ if (seededSessions.has(sessionId))
160
+ return;
161
+ seededSessions.add(sessionId);
162
+ if ((routeFailures.get(routeKey) ?? 0) > 0)
163
+ return;
164
+ const events = foldClassifiers(session.events);
165
+ if (events.some(event => event.failure === 'breaker'))
166
+ breakerAudited.add(sessionId);
167
+ const streak = trailingRouteFailureStreak(events, routeKey, CLASSIFIER_BREAKER_THRESHOLD);
168
+ if (streak <= 0)
169
+ return;
170
+ routeFailures.set(routeKey, streak);
171
+ if (streak < CLASSIFIER_BREAKER_THRESHOLD)
172
+ return;
173
+ breakerOpen.add(routeKey);
174
+ if (!warnedBreaker) {
175
+ warnedBreaker = true;
176
+ deps.warn(`permission classifier: route ${routeKey} restored with ${streak} consecutive failures from the session log; breaker open for this route, auto mode uses the legacy path`);
177
+ }
178
+ auditBreakerOnce(exec, routeKey, route);
179
+ }
180
+ return {
181
+ rebuild() {
182
+ // Drop the memoized classifier only when the autoMode slice actually
183
+ // changed — an onChange for unrelated keys keeps the instance.
184
+ const current = readSlice(deps.settingsRead());
185
+ if (current.raw !== builtRaw) {
186
+ slice = current;
187
+ classifier = undefined;
188
+ }
189
+ // A settings change is the operator's "I fixed the lane": reset ALL
190
+ // breaker state — route counters, open routes, the per-session audit
191
+ // de-dup set, and the per-process warn-once flag.
192
+ routeFailures.clear();
193
+ breakerOpen.clear();
194
+ breakerAudited.clear();
195
+ warnedBreaker = false;
196
+ },
197
+ async maybeEscalate(decided, exec) {
198
+ slice = readSlice(deps.settingsRead());
199
+ if (!slice.enabled)
200
+ return undefined;
201
+ if (deps.stream === undefined) {
202
+ disarmUnarmed(exec);
203
+ return undefined;
204
+ }
205
+ // The route for this call is passed to classify as data; the audit event
206
+ // is appended from this call's own exec session — no ambient fields.
207
+ const route = deps.resolveRoute(exec);
208
+ if (route === undefined) {
209
+ disarmUnarmed(exec);
210
+ return undefined;
211
+ }
212
+ // Eligibility (§4.1): only auto + LOW + ask/passthrough reaches the LLM.
213
+ // These gates precede the read-only exemption and the breaker gate.
214
+ if (decided.mode !== 'auto' || decided.risk.level !== 'LOW')
215
+ return undefined;
216
+ if (decided.decision.kind !== 'ask' && decided.decision.kind !== 'passthrough')
217
+ return undefined;
218
+ // F2 read-only exemption: read-only calls cannot mutate, so the LLM
219
+ // round-trip adds latency with zero safety — the legacy path applies.
220
+ if (decided.isReadOnly)
221
+ return undefined;
222
+ const routeKey = `${route.provider}/${route.model}`;
223
+ seedBreakerFromLog(exec, routeKey, route);
224
+ if (breakerOpen.has(routeKey)) {
225
+ auditBreakerOnce(exec, routeKey, route);
226
+ return undefined;
227
+ }
228
+ const verdict = await ensureClassifier().classify(exec, { route });
229
+ // F4 per-route breaker bookkeeping, attributed to THIS call's route:
230
+ // any success (parsed verdict, cache hit included) resets the streak;
231
+ // malformed/error/timeout increment it; `cancelled` is caller noise and
232
+ // `unarmed` is a disarm outcome — neither is counted (nor resets).
233
+ if (verdict.failure === undefined) {
234
+ routeFailures.set(routeKey, 0);
235
+ }
236
+ else if (BREAKER_FAILURE_TAGS.includes(verdict.failure)) {
237
+ const count = (routeFailures.get(routeKey) ?? 0) + 1;
238
+ routeFailures.set(routeKey, count);
239
+ if (count >= CLASSIFIER_BREAKER_THRESHOLD) {
240
+ breakerOpen.add(routeKey);
241
+ if (!warnedBreaker) {
242
+ warnedBreaker = true;
243
+ deps.warn(`permission classifier: route ${routeKey} failed ${CLASSIFIER_BREAKER_THRESHOLD} consecutive classifications; breaker open for this route, auto mode uses the legacy path`);
244
+ }
245
+ auditBreakerOnce(exec, routeKey, route);
246
+ }
247
+ }
248
+ const session = exec.agent?.session;
249
+ if (session !== undefined) {
250
+ deps.audit(session, {
251
+ tool: verdict.tool,
252
+ digest: verdict.digest,
253
+ verdict: verdict.verdict,
254
+ ...(verdict.failure === undefined ? {} : { failure: verdict.failure }),
255
+ ...(verdict.routeAlias === undefined ? {} : { route: verdict.routeAlias, provider: verdict.provider, model: verdict.model }),
256
+ latencyMs: verdict.latencyMs,
257
+ cacheHit: verdict.cacheHit,
258
+ });
259
+ }
260
+ return verdict.verdict === 'allow' ? 'allow' : { kind: 'ask', reason: verdict.reason };
261
+ },
262
+ };
263
+ /** One `breaker` audit event per session (de-dup by session id), no stream involved. */
264
+ function auditBreakerOnce(exec, routeKey, route) {
265
+ const session = exec.agent?.session;
266
+ if (session === undefined)
267
+ return;
268
+ const sessionId = String(session.header.id);
269
+ if (breakerAudited.has(sessionId))
270
+ return;
271
+ breakerAudited.add(sessionId);
272
+ deps.audit(session, {
273
+ tool: exec.name,
274
+ verdict: 'ask',
275
+ failure: 'breaker',
276
+ route: routeKey,
277
+ provider: route.provider,
278
+ model: route.model,
279
+ latencyMs: 0,
280
+ cacheHit: false,
281
+ });
282
+ }
283
+ }
284
+ //# sourceMappingURL=auto-stage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-stage.js","sourceRoot":"","sources":["../src/auto-stage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Pure risk classifier for tool calls, a conservative port of Claude Code's
3
+ * `dangerousPatterns` / `pathValidation` heuristics. High-risk shell commands
4
+ * and protected/externally-scoped file writes escalate before the normal rule
5
+ * waterfall. Pure functions — no Cordis/session coupling — mirroring the
6
+ * {@link evaluatePermission} style, so hosts can classify a call directly.
7
+ * @module @dsh-cc/permission-rules/classifier
8
+ */
9
+ /** The severity a classifier assigns to one call. */
10
+ export type RiskLevel = 'LOW' | 'MEDIUM' | 'HIGH';
11
+ /** The outcome of classifying one call, with the reasons that raised it. */
12
+ export interface RiskAssessment {
13
+ /** The raised risk level for the call. */
14
+ level: RiskLevel;
15
+ /** Human-readable reasons for a non-`LOW` level; empty for `LOW`. */
16
+ reasons: string[];
17
+ }
18
+ /** One curated dangerous-command pattern and the reason it is high risk. */
19
+ export interface DangerousPattern {
20
+ readonly regex: RegExp;
21
+ readonly reason: string;
22
+ }
23
+ /**
24
+ * The curated conservative set of catastrophic shell command patterns. Matched
25
+ * against a command string; any match raises the command to `HIGH`. Ports the
26
+ * spirit of Claude Code's table, deliberately conservative (catastrophic only).
27
+ */
28
+ export declare const DEFAULT_DANGEROUS_PATTERNS: readonly DangerousPattern[];
29
+ /**
30
+ * The curated set of protected file paths — dotfiles, credential stores, and
31
+ * sensitive config. Simple wildcard matching: `**` matches any depth, `*`
32
+ * matches a single path segment. A match raises a file write to `HIGH`.
33
+ */
34
+ export declare const DEFAULT_PROTECTED_FILES: readonly string[];
35
+ /**
36
+ * Classify a shell command by its riskiness. HIGH when any dangerous pattern
37
+ * matches, otherwise LOW. When `patterns` is given (raw regex sources, e.g.
38
+ * from `permissions.dangerousPatterns`), they replace the curated defaults.
39
+ * @param command - the shell command string to classify.
40
+ * @param patterns - optional raw regex sources to use instead of the defaults.
41
+ * @returns the assessment — HIGH with the matching reasons, else LOW.
42
+ */
43
+ export declare function assessBashCommand(command: string, patterns?: string[]): RiskAssessment;
44
+ /**
45
+ * Classify the target path of a file write. HIGH when it matches a protected
46
+ * file; MEDIUM when it resolves outside the working directory and its
47
+ * additional directories (an escape from the permission scope); else LOW.
48
+ * @param filePath - the target file path (absolute or relative).
49
+ * @param opts - the working directory scope and classification overrides.
50
+ * @returns the assessment with the matching reasons, or LOW.
51
+ */
52
+ export declare function assessFilePath(filePath: string, opts: {
53
+ cwd: string;
54
+ additionalDirectories?: string[];
55
+ protectedFiles?: string[];
56
+ }): RiskAssessment;
57
+ //# sourceMappingURL=classifier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classifier.d.ts","sourceRoot":"","sources":["../src/classifier.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,qDAAqD;AACrD,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEjD,4EAA4E;AAC5E,MAAM,WAAW,cAAc;IAC7B,0CAA0C;IAC1C,KAAK,EAAE,SAAS,CAAA;IAChB,qEAAqE;IACrE,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,4EAA4E;AAC5E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,EAAE,SAAS,gBAAgB,EAUjE,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,SAAS,MAAM,EAYpD,CAAA;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,cAAc,CAWtF;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GACjF,cAAc,CAYhB"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Pure risk classifier for tool calls, a conservative port of Claude Code's
3
+ * `dangerousPatterns` / `pathValidation` heuristics. High-risk shell commands
4
+ * and protected/externally-scoped file writes escalate before the normal rule
5
+ * waterfall. Pure functions — no Cordis/session coupling — mirroring the
6
+ * {@link evaluatePermission} style, so hosts can classify a call directly.
7
+ * @module @dsh-cc/permission-rules/classifier
8
+ */
9
+ import { isAbsolute, relative, resolve } from 'node:path';
10
+ /**
11
+ * The curated conservative set of catastrophic shell command patterns. Matched
12
+ * against a command string; any match raises the command to `HIGH`. Ports the
13
+ * spirit of Claude Code's table, deliberately conservative (catastrophic only).
14
+ */
15
+ export const DEFAULT_DANGEROUS_PATTERNS = [
16
+ { regex: /\brm\s+-[a-z]*[rf][a-z]*\s+(?:\/(?:\s|$)|~(?:\s|$|\/))/, reason: 'force/recursive remove of root or home' },
17
+ { regex: /\bsudo\s+/i, reason: 'privilege escalation' },
18
+ { regex: /\bchmod\s+[^\n]*\b777\b/, reason: 'world-writable permission change' },
19
+ { regex: /\bdd\b[^\n]*\bof=\/dev\//, reason: 'write directly to a block device' },
20
+ { regex: /\bmkfs(?:\.|$|\s)/, reason: 'create a file system (destructive)' },
21
+ { regex: /\bshutdown\b|\breboot\b|\bhalt\b/, reason: 'system shutdown or reboot' },
22
+ { regex: /\bkill\s+-9\s+1\b/, reason: 'kill PID 1 (system init)' },
23
+ { regex: /\b(?:curl|wget)\b[^\n]*\|\s*(?:sh|bash)\b/i, reason: 'pipe a remote script into a shell' },
24
+ { regex: /(?:^|[;&\n|\s])\s*\b(?:echo|printf|cat|tee)\b[^\n]*>\s*(?:\/etc\/|\/usr\/|\/bin\/)/, reason: 'redirect output into a system path' },
25
+ ];
26
+ /**
27
+ * The curated set of protected file paths — dotfiles, credential stores, and
28
+ * sensitive config. Simple wildcard matching: `**` matches any depth, `*`
29
+ * matches a single path segment. A match raises a file write to `HIGH`.
30
+ */
31
+ export const DEFAULT_PROTECTED_FILES = [
32
+ '.gitconfig',
33
+ '.bashrc',
34
+ '.zshrc',
35
+ '.profile',
36
+ '.bash_profile',
37
+ '.zprofile',
38
+ '.ssh/**',
39
+ '.aws/**',
40
+ '.netrc',
41
+ '.git-credentials',
42
+ '.mcp.json',
43
+ ];
44
+ /**
45
+ * Classify a shell command by its riskiness. HIGH when any dangerous pattern
46
+ * matches, otherwise LOW. When `patterns` is given (raw regex sources, e.g.
47
+ * from `permissions.dangerousPatterns`), they replace the curated defaults.
48
+ * @param command - the shell command string to classify.
49
+ * @param patterns - optional raw regex sources to use instead of the defaults.
50
+ * @returns the assessment — HIGH with the matching reasons, else LOW.
51
+ */
52
+ export function assessBashCommand(command, patterns) {
53
+ const source = patterns ?? [];
54
+ const list = source.length > 0
55
+ ? source.map(pattern => ({ regex: compileSafe(pattern), reason: `command matches configured pattern ${JSON.stringify(pattern)}` }))
56
+ : DEFAULT_DANGEROUS_PATTERNS;
57
+ const reasons = [];
58
+ for (const { regex, reason } of list) {
59
+ regex.lastIndex = 0;
60
+ if (regex.test(command))
61
+ reasons.push(reason);
62
+ }
63
+ return reasons.length > 0 ? { level: 'HIGH', reasons } : { level: 'LOW', reasons: [] };
64
+ }
65
+ /**
66
+ * Classify the target path of a file write. HIGH when it matches a protected
67
+ * file; MEDIUM when it resolves outside the working directory and its
68
+ * additional directories (an escape from the permission scope); else LOW.
69
+ * @param filePath - the target file path (absolute or relative).
70
+ * @param opts - the working directory scope and classification overrides.
71
+ * @returns the assessment with the matching reasons, or LOW.
72
+ */
73
+ export function assessFilePath(filePath, opts) {
74
+ const protectedFiles = opts.protectedFiles ?? DEFAULT_PROTECTED_FILES;
75
+ if (protectedMatch(filePath, protectedFiles)) {
76
+ return { level: 'HIGH', reasons: [`path matches a protected file pattern`] };
77
+ }
78
+ // A non-empty cwd enables the escape check; without one we cannot scope the
79
+ // path and conservatively leave the escape determination at LOW.
80
+ const scope = [opts.cwd, ...(opts.additionalDirectories ?? [])];
81
+ if (opts.cwd !== '' && !inScope(resolve(opts.cwd, filePath), scope)) {
82
+ return { level: 'MEDIUM', reasons: ['path resolves outside the permission scope'] };
83
+ }
84
+ return { level: 'LOW', reasons: [] };
85
+ }
86
+ /** Whether a path (or its basename) matches any protected wildcard pattern. */
87
+ function protectedMatch(filePath, protectedFiles) {
88
+ const base = filePath.split('/').at(-1) ?? filePath;
89
+ const candidates = [
90
+ filePath,
91
+ filePath.replace(/^~\/?/, ''),
92
+ base,
93
+ ];
94
+ for (const pattern of protectedFiles) {
95
+ const regex = wildcardToRegExp(pattern);
96
+ for (const candidate of candidates) {
97
+ regex.lastIndex = 0;
98
+ if (regex.test(candidate))
99
+ return true;
100
+ }
101
+ }
102
+ return false;
103
+ }
104
+ /** Translate a `**`/`*` wildcard pattern into a whole-string matcher. */
105
+ function wildcardToRegExp(pattern) {
106
+ const body = pattern
107
+ .split('/')
108
+ .map(segment => (segment === '**' ? '.*' : segment === '*' ? '[^/]*' : segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
109
+ .join('/');
110
+ return new RegExp(`^${body}$`);
111
+ }
112
+ /** Whether `target` is at or under any of `dirs` (all pre-resolved absolute). */
113
+ function inScope(target, dirs) {
114
+ return dirs.some(dir => {
115
+ const rel = relative(dir, target);
116
+ return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
117
+ });
118
+ }
119
+ /** Compile a raw pattern source into a RegExp, ignoring an invalid one. */
120
+ function compileSafe(source) {
121
+ try {
122
+ return new RegExp(source);
123
+ }
124
+ catch {
125
+ // An invalid pattern must not crash classification; the defaults still run.
126
+ return /(?!)/; // never matches
127
+ }
128
+ }
129
+ //# sourceMappingURL=classifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classifier.js","sourceRoot":"","sources":["../src/classifier.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The decision waterfall for one tool call, extracted from the service so the
3
+ * engine core stays modular. Pure functions over a structural dependency face
4
+ * (`DecideDeps`) that `PermissionRulesService` supplies in its constructor.
5
+ *
6
+ * Stage order: the risk-classifier escalation runs first (a hard-deny HIGH in
7
+ * every mode; an ask MEDIUM outside bypassPermissions, with session-scoped
8
+ * grants overriding the ask), then the normal mode-aware waterfall proceeds.
9
+ * Under `auto`, a classifier-LOW call whose waterfall decision is `ask` is
10
+ * auto-allowed (the classifier proxies the prompt); MEDIUM/HIGH already
11
+ * returned above.
12
+ *
13
+ * @module @dsh-cc/permission-rules/decide
14
+ */
15
+ import type { ToolExecution } from '@dsh-cc/tools';
16
+ import type { SandboxMode } from '@deepseek-ai/dsh-sandbox';
17
+ import { type RiskAssessment } from './classifier.ts';
18
+ import type { PermissionDecision, PermissionMode, PermissionRuleSet } from './types.ts';
19
+ /**
20
+ * Structural dependency face the service supplies to the decision waterfall.
21
+ * `settings()` returns the classifier-relevant slice of the settings-resolved
22
+ * section; `defaultMode()`/`rules()` read the live merged state so a settings
23
+ * reload is observed on the next call.
24
+ */
25
+ export type DecideDeps = {
26
+ /** Whether the risk-classifier escalation stage runs. */
27
+ classifierEnabled: boolean;
28
+ /** Whether sandboxed bash skips a whole-tool `ask`. */
29
+ exemptSandboxedBashFromToolAsk: boolean;
30
+ /** The shell-command tool name for content extraction. */
31
+ bashToolName: string;
32
+ /** File-edit tool names auto-allowed under `acceptEdits` mode. */
33
+ fileEditTools: ReadonlySet<string>;
34
+ /** Read-only tool names auto-allowed under `plan` mode. */
35
+ readOnlyTools: ReadonlySet<string>;
36
+ /** The classifier-relevant slice of the current settings section. */
37
+ settings(): {
38
+ dangerousPatterns?: string[];
39
+ additionalDirectories?: string[];
40
+ protectedFiles?: string[];
41
+ };
42
+ /** The fallback (deployment-default) permission mode. */
43
+ defaultMode(): PermissionMode;
44
+ /** The live merged rule set. */
45
+ rules(): PermissionRuleSet;
46
+ /** Whether switching to `bypassPermissions` is disabled. */
47
+ bypassDisabled(): boolean;
48
+ /** Session-scoped allowlist match (seed-once handled inside the service). */
49
+ sessionAllowMatches(exec: ToolExecution): boolean;
50
+ /** The host shell service's sandbox mode, when mounted. */
51
+ shellMode(): SandboxMode | undefined;
52
+ };
53
+ /**
54
+ * The verbose result of the decision waterfall: the raw waterfall decision
55
+ * (BEFORE any auto-mode proxying) plus the computed risk and effective mode.
56
+ * The async classifier stage (§4.1 of the LLM risk-classifier design) needs
57
+ * all three to decide whether to consult the LLM and how to escalate.
58
+ */
59
+ export type DecidedCall = {
60
+ decision: PermissionDecision;
61
+ risk: RiskAssessment;
62
+ mode: PermissionMode;
63
+ isReadOnly: boolean;
64
+ };
65
+ /**
66
+ * The sync, pure waterfall WITHOUT the auto-proxy conversion. Under `auto`, a
67
+ * classifier-LOW call whose waterfall decision is `ask` is returned as `ask`
68
+ * here — `decideCall` applies the proxy on top.
69
+ */
70
+ export declare function decideCallVerbose(deps: DecideDeps, exec: ToolExecution): DecidedCall;
71
+ /**
72
+ * Fold the engine decision for one call. Bypass-immune matches fall to the
73
+ * guard layer, not here. The risk-classifier escalation runs first (a
74
+ * hard-deny HIGH in every mode; an ask MEDIUM outside bypassPermissions),
75
+ * then the normal waterfall proceeds unchanged. Under `auto`, a classifier-LOW
76
+ * call whose waterfall decision is `ask` is auto-allowed (the classifier
77
+ * proxies the prompt); MEDIUM/HIGH already returned above.
78
+ */
79
+ export declare function decideCall(deps: DecideDeps, exec: ToolExecution): PermissionDecision;
80
+ //# sourceMappingURL=decide.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decide.d.ts","sourceRoot":"","sources":["../src/decide.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAE3D,OAAO,EAAqC,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAIxF,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAEvF;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,yDAAyD;IACzD,iBAAiB,EAAE,OAAO,CAAA;IAC1B,uDAAuD;IACvD,8BAA8B,EAAE,OAAO,CAAA;IACvC,0DAA0D;IAC1D,YAAY,EAAE,MAAM,CAAA;IACpB,kEAAkE;IAClE,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IAClC,2DAA2D;IAC3D,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IAClC,qEAAqE;IACrE,QAAQ,IAAI;QAAE,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAA;IACzG,yDAAyD;IACzD,WAAW,IAAI,cAAc,CAAA;IAC7B,gCAAgC;IAChC,KAAK,IAAI,iBAAiB,CAAA;IAC1B,4DAA4D;IAC5D,cAAc,IAAI,OAAO,CAAA;IACzB,6EAA6E;IAC7E,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAA;IACjD,2DAA2D;IAC3D,SAAS,IAAI,WAAW,GAAG,SAAS,CAAA;CACrC,CAAA;AA2CD;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG;IAAE,QAAQ,EAAE,kBAAkB,CAAC;IAAC,IAAI,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,cAAc,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,CAAA;AAE3H;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,GAAG,WAAW,CAwCpF;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,GAAG,kBAAkB,CAQpF"}