dsh-permission-rules 0.2.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 (49) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/LICENSE +201 -0
  3. package/README.es.md +182 -0
  4. package/README.hi.md +182 -0
  5. package/README.md +182 -0
  6. package/README.pt.md +182 -0
  7. package/README.zh.md +182 -0
  8. package/cordis.patch.yml +16 -0
  9. package/docs/rules-format.en.md +105 -0
  10. package/docs/rules-format.md +103 -0
  11. package/docs/rules-format.schema.json +117 -0
  12. package/lib/index.js +1606 -0
  13. package/lib/types/config.d.ts +83 -0
  14. package/lib/types/config.d.ts.map +1 -0
  15. package/lib/types/config.js +72 -0
  16. package/lib/types/config.js.map +1 -0
  17. package/lib/types/events.d.ts +68 -0
  18. package/lib/types/events.d.ts.map +1 -0
  19. package/lib/types/events.js +21 -0
  20. package/lib/types/events.js.map +1 -0
  21. package/lib/types/glob.d.ts +55 -0
  22. package/lib/types/glob.d.ts.map +1 -0
  23. package/lib/types/glob.js +307 -0
  24. package/lib/types/glob.js.map +1 -0
  25. package/lib/types/index.d.ts +25 -0
  26. package/lib/types/index.d.ts.map +1 -0
  27. package/lib/types/index.js +25 -0
  28. package/lib/types/index.js.map +1 -0
  29. package/lib/types/prose.d.ts +66 -0
  30. package/lib/types/prose.d.ts.map +1 -0
  31. package/lib/types/prose.js +143 -0
  32. package/lib/types/prose.js.map +1 -0
  33. package/lib/types/rules.d.ts +226 -0
  34. package/lib/types/rules.d.ts.map +1 -0
  35. package/lib/types/rules.js +581 -0
  36. package/lib/types/rules.js.map +1 -0
  37. package/lib/types/runtime.d.ts +175 -0
  38. package/lib/types/runtime.d.ts.map +1 -0
  39. package/lib/types/runtime.js +497 -0
  40. package/lib/types/runtime.js.map +1 -0
  41. package/package.json +103 -0
  42. package/scripts/repair-session-logs.mjs +327 -0
  43. package/src/config.ts +141 -0
  44. package/src/events.ts +75 -0
  45. package/src/glob.ts +303 -0
  46. package/src/index.ts +27 -0
  47. package/src/prose.ts +206 -0
  48. package/src/rules.ts +695 -0
  49. package/src/runtime.ts +504 -0
@@ -0,0 +1,105 @@
1
+ # dsh-permission-rules rule file format
2
+
3
+ The rule file is a YAML document, discovered per session working directory: `<cwd>/.dsh/rules.yaml` by default. The `rulesFile` config can rename it or pin an absolute path; `searchUp: true` additionally merges every matching file from the session cwd up to the filesystem root (nearest first, so a child can override a parent); when discovery finds nothing, `fallbackPath` is used; with neither, an empty rule set applies (everything passes through).
4
+
5
+ A JSON Schema for this format ships at `rules-format.schema.json`; wire it into editors with the first line:
6
+
7
+ ```yaml
8
+ # yaml-language-server: $schema=https://raw.githubusercontent.com/PerryLink/dsh-permission-rules/main/docs/rules-format.schema.json
9
+ ```
10
+
11
+ ## Top-level structure
12
+
13
+ ```yaml
14
+ rules: # rule list, evaluated in written order; may be omitted or empty
15
+ - match: ...
16
+ action: ...
17
+ reason: ...
18
+ ```
19
+
20
+ Only `rules` is allowed at the top level; each rule allows only `match`, `action`, `reason`, `enabled`, `description`, `tags`; each `match` allows only `tools`, `params`, `paths`, `absent`, `when`. Any unknown field fails the load loudly — never silently ignored.
21
+
22
+ ## match dimensions (AND)
23
+
24
+ | Dimension | Type | Semantics |
25
+ |---|---|---|
26
+ | `tools` | `string[]` | Tool-name globs (always globs, regardless of `patternMode`). Empty/absent = unrestricted. Supports prefixes like `mcp__*`. |
27
+ | `params` | `Record<string, string \| number \| boolean \| (…)[]>` | Argument key → pattern (or pattern list). **Every listed key must be present AND match** (AND); absent = unrestricted. Scalars match as strings (numbers/booleans stringified); array elements match any-of; nested objects contribute their scalar leaves (depth-capped at 8). A `!`-prefixed pattern negates: the value must NOT match it; a key with only negations matches when the key is present and no negation hits (quote `!` patterns in YAML: `"!git*"`). `/` is an ordinary character in params patterns — `*` crosses it. |
28
+ | `paths` | `string[]` | Workspace-relative path patterns; any candidate matching any pattern satisfies the dimension. Candidates come from argument values under these keys at ANY nesting depth (depth-capped): `path` `paths` `file` `files` `file_path` `dir` `directory` `directories` `cwd` `workspace` `root` `target` `targets` `output`. Absolute candidates outside the workspace are dropped; relative `../` forms are kept so explicit out-of-root globs still work. In path patterns `*` does not cross `/`; `**` crosses any depth (including zero). With `caseInsensitivePaths` on (Windows default) the root comparison and the patterns ignore ASCII case. |
29
+ | `absent` | `string[]` | Argument keys that must be ABSENT; **every listed key must be missing** (non-object arguments satisfy this trivially). |
30
+ | `when` | `{ env, platform }` | Host conditions: `env` is env-var name → pattern(s), **every listed var must be present AND match**; `platform` is any-of a closed list (`aix` `android` `darwin` `freebsd` `linux` `openbsd` `sunos` `win32`). |
31
+
32
+ All dimensions are ANDed: a rule matches only when **every non-empty dimension matches**. Rules evaluate in order — **the first match wins**; no match = passthrough.
33
+
34
+ ## action, reason, and metadata
35
+
36
+ - `action: allow | deny | ask` (required).
37
+ - `reason: string` (required, non-empty). A `deny` reason becomes the model-visible error in the denied tool result; an `ask` reason becomes the approval reason on the official approval seam.
38
+ - `enabled: false` keeps the rule visible but inert (displayed as disabled); `description` and `tags` are free-form annotations shown by `/rules`.
39
+
40
+ ## Pattern interpretation (patternMode)
41
+
42
+ `params`, `paths`, and `when.env` patterns follow the plugin config `patternMode` (default `glob`):
43
+
44
+ - `glob`: `*`, `**`, `?`, `[abc]`/`[!abc]` character classes, `\x` escapes. In params, `*` crosses `/`; in paths, `*` stays in one segment and `**` crosses segments. Invalid globs (unclosed `[`, empty class, dangling escape) fail the load, and a pattern expanding to more than `maxGlobStars` (default 2) unbounded star quantifiers is rejected — the backtracking degree of a compiled glob equals its star count. Split wider patterns into several.
45
+ - `regex`: unanchored JavaScript regex (`RegExp.test` semantics); invalid regexes fail the load. Nested unbounded quantifiers (`(a+)+`, `(ab*)+`) and quantified groups with overlapping literal alternation branches (`(a|aa)+`) are rejected as catastrophic-backtracking risks; chains of independent quantifiers (`\d+\.\d+\.\d+`) stay allowed. In YAML double-quoted strings, `\` needs escaping — single quotes are recommended.
46
+
47
+ ## Rule count cap
48
+
49
+ A total rule count above `maxRules` (default 256) fails the load — across the whole merged chain under `searchUp`. This is the simple bound for the `tools/pre-execute` hot path: tool-name globs, param keys, and path candidates are all precompiled regexes; matching is O(rules × patterns per dimension).
50
+
51
+ ## Security baseline example (5 rules)
52
+
53
+ ```yaml
54
+ # .dsh/rules.yaml — recommended security baseline
55
+ rules:
56
+ # 1. Dangerous commands: destructive/forceful shell commands go to the
57
+ # second model (or a human) first
58
+ - match:
59
+ tools: [bash, pwsh]
60
+ params:
61
+ command: ["rm -rf*", "git push*--force*", "git push* -f*", "*:(){ :|:& };:*"]
62
+ action: ask
63
+ reason: "Dangerous command needs a second-model verdict"
64
+
65
+ # 2. Protected paths: deny any tool operation on secrets/credentials/config
66
+ - match:
67
+ paths: ["**/secrets/**", "**/.env", "**/*.pem", "**/*.key", "**/.ssh/**"]
68
+ action: deny
69
+ reason: "Protected secrets and credential paths are off-limits"
70
+
71
+ # 3. Releases: package publishing needs confirmation
72
+ - match:
73
+ tools: [bash, pwsh]
74
+ params:
75
+ command: ["npm publish*", "pnpm publish*", "cargo publish*"]
76
+ action: ask
77
+ reason: "Publishing needs confirmation"
78
+
79
+ # 4. Remote side effects: pushing to main/master needs confirmation
80
+ # (two-star globs — maxGlobStars caps unbounded star expansions at 2)
81
+ - match:
82
+ tools: [bash, pwsh]
83
+ params:
84
+ command: ["git push*origin main*", "git push*origin master*"]
85
+ action: ask
86
+ reason: "Pushing to the main branch needs confirmation"
87
+
88
+ # 5. Write confirmation: editor-class tools (and MCP tools) ask before writing
89
+ # (with dsh-auto-review mounted, these asks get an automatic second-model verdict)
90
+ - match:
91
+ tools: [edit, write, bash, pwsh, mcp__*]
92
+ action: ask
93
+ reason: "Write operations need confirmation"
94
+ ```
95
+
96
+ ## Division of labor with dsh-auto-review
97
+
98
+ This plugin only produces `ask` decisions; the `ask` goes to the official `ctx.approval` approval seam. With `dsh-auto-review` mounted, its answerer claims matching requests and delivers a second-model verdict; otherwise a human answers; with neither, the official seam fails closed (`unavailable` → denied). This plugin **never** starts a reviewer subagent.
99
+
100
+ ## Loading and hot reload
101
+
102
+ - Lazily discovered per session cwd and cached on the first `tools/pre-execute` (least-recently-used eviction beyond `maxCachedWorkspaces`).
103
+ - Invalid YAML, unknown fields/actions, bad globs/regexes, backtracking-prone patterns, and counts over `maxRules` are load-time errors: `badFilePolicy: fail` (default) makes the first tool call in that workspace fail loudly; `ignore-with-warning` warns and degrades to an empty rule set.
104
+ - An absolute `rulesFile` or a configured `fallbackPath` is validated at plugin mount — missing or invalid fails the mount.
105
+ - File changes reload through Chokidar with a debounce (`watch`, `watchStabilityThresholdMs`); a failed reload keeps the previous rules and only warns — never crashes. `/rules reload` triggers the same path manually; `/rules decisions` and `/rules test` inspect the trail and dry-run the rules.
@@ -0,0 +1,103 @@
1
+ # dsh-permission-rules 规则文件格式
2
+
3
+ 规则文件是 YAML 文档,默认按会话工作目录发现:`<cwd>/.dsh/rules.yaml`。插件配置 `rulesFile` 可以改文件名或固定为绝对路径;`searchUp: true` 会额外从会话 cwd 向上合并每一份同名文件(最近的优先,子目录可覆盖父目录);发现不到时使用 `fallbackPath`,两者都没有则为空规则集(全部透传)。
4
+
5
+ 本格式的 JSON Schema 随包发布在 `rules-format.schema.json`,可在编辑器首行接线启用补全:
6
+
7
+ ```yaml
8
+ # yaml-language-server: $schema=https://raw.githubusercontent.com/PerryLink/dsh-permission-rules/main/docs/rules-format.schema.json
9
+ ```
10
+
11
+ ## 顶层结构
12
+
13
+ ```yaml
14
+ rules: # 规则列表,按书写顺序求值;可以省略或为空
15
+ - match: ...
16
+ action: ...
17
+ reason: ...
18
+ ```
19
+
20
+ 顶层只允许 `rules` 一个键;每条规则只允许 `match`、`action`、`reason`、`enabled`、`description`、`tags` 六个键;`match` 只允许 `tools`、`params`、`paths`、`absent`、`when` 五个键。出现任何未知键都在加载期报错(响亮失败),绝不静默忽略。
21
+
22
+ ## match 维度(全部 AND)
23
+
24
+ | 维度 | 类型 | 语义 |
25
+ |---|---|---|
26
+ | `tools` | `string[]` | 工具名 glob(始终 glob 解释,与 `patternMode` 无关)。空/缺省 = 不限制。支持 `mcp__*` 等前缀匹配。 |
27
+ | `params` | `Record<string, string \| number \| boolean \| (…)[]>` | 参数键 → 模式(或模式列表)。**列出的每个键都必须存在且命中**(AND);缺省 = 不限制。标量值按字符串匹配(数字/布尔字符串化);数组任一元素命中即可;嵌套对象贡献其标量叶子(深度上限 8)。`!` 前缀为否定:值必须**不**命中该模式;只含否定的键在键存在且全部否定未命中时命中(YAML 里请给 `!` 模式加引号:`"!git*"`)。params 模式中 `/` 是普通字符,`*` 可跨 `/`。 |
28
+ | `paths` | `string[]` | 工作区相对路径模式,任一候选命中任一模式即可。候选来自参数中下列键的值(**任意嵌套深度**、深度上限):`path` `paths` `file` `files` `file_path` `dir` `directory` `directories` `cwd` `workspace` `root` `target` `targets` `output`。工作区外的绝对候选被丢弃;相对 `../` 形式保留,可显式匹配工作区外。路径模式中 `*` 不跨 `/`,`**` 跨任意深度(含零层)。`caseInsensitivePaths` 开启时(Windows 默认)根比较与模式匹配忽略 ASCII 大小写。 |
29
+ | `absent` | `string[]` | 必须**缺席**的参数键;**每个列出的键都必须缺失**(非对象参数天然满足)。 |
30
+ | `when` | `{ env, platform }` | 宿主条件:`env` 为环境变量名 → 模式(**每个列出的变量都必须存在且命中**);`platform` 为任一命中即可的封闭列表(`aix` `android` `darwin` `freebsd` `linux` `openbsd` `sunos` `win32`)。 |
31
+
32
+ 所有维度为 AND:一条规则只有在它的**每个非空维度都命中**时才命中。规则按顺序求值,**首条命中生效**;无命中 = 透传。
33
+
34
+ ## action、reason 与元数据
35
+
36
+ - `action: allow | deny | ask`(必填)。
37
+ - `reason: string`(必填,非空)。`deny` 的 reason 会进入被拒绝工具的结果、对模型可见;`ask` 的 reason 会作为审批理由进入官方审批 seam。
38
+ - `enabled: false` 保留规则但使其失效(显示为已禁用);`description`、`tags` 为 `/rules` 展示的自由注解。
39
+
40
+ ## 模式解释(patternMode)
41
+
42
+ `params`、`paths` 与 `when.env` 的模式按插件配置 `patternMode` 解释(默认 `glob`):
43
+
44
+ - `glob`:`*`、`**`、`?`、`[abc]`/`[!abc]` 字符类、`\x` 转义。params 中 `*` 跨 `/`;paths 中 `*` 不跨 `/`、`**` 跨。非法 glob(未闭合 `[`、空字符类、悬空转义)加载期报错;展开出超过 `maxGlobStars`(默认 2)个无界星号量词的模式会被拒绝——编译产物的回溯度恰好等于星号数。更宽的模式请拆成多条。
45
+ - `regex`:不锚定的 JavaScript 正则(`RegExp.test` 语义),非法正则在加载期报错。嵌套无界量词(`(a+)+`、`(ab*)+`)与被量化且字面量交替分支重叠的分组(`(a|aa)+`)会被判定为灾难回溯风险而拒绝;独立量词链(`\d+\.\d+\.\d+`)保持放行。注意 YAML 双引号字符串中 `\` 需要转义,建议用单引号。
46
+
47
+ ## 规则数上界
48
+
49
+ 规则总数(`searchUp` 下为整个合并链的总数)超过配置 `maxRules`(默认 256)时加载失败。这是 `tools/pre-execute` 热路径的简单上界:每条规则的工具名 glob、参数键、路径候选都是预编译正则,匹配成本 O(规则数 × 各维度模式数)。
50
+
51
+ ## 安全基线示例(5 条)
52
+
53
+ ```yaml
54
+ # .dsh/rules.yaml — 推荐的安全基线
55
+ rules:
56
+ # 1. 危险命令:任何 shell 中的删除/强推/格式化都先要第二模型(或人类)裁决
57
+ - match:
58
+ tools: [bash, pwsh]
59
+ params:
60
+ command: ["rm -rf*", "git push*--force*", "git push* -f*", "*:(){ :|:& };:*"]
61
+ action: ask
62
+ reason: "危险命令需要第二模型裁决"
63
+
64
+ # 2. 保护路径:对密钥/凭据/配置目录的任何工具操作直接拒绝
65
+ - match:
66
+ paths: ["**/secrets/**", "**/.env", "**/*.pem", "**/*.key", "**/.ssh/**"]
67
+ action: deny
68
+ reason: "禁止操作受保护的密钥与凭据路径"
69
+
70
+ # 3. 发布动作:npm/pnpm 发布需要确认
71
+ - match:
72
+ tools: [bash, pwsh]
73
+ params:
74
+ command: ["npm publish*", "pnpm publish*", "cargo publish*"]
75
+ action: ask
76
+ reason: "发布动作需要确认"
77
+
78
+ # 4. 远程副作用:git 推送到主分支需要确认
79
+ # (二星模式——maxGlobStars 将无界星号量词数封顶在 2)
80
+ - match:
81
+ tools: [bash, pwsh]
82
+ params:
83
+ command: ["git push*origin main*", "git push*origin master*"]
84
+ action: ask
85
+ reason: "推送到主分支需要确认"
86
+
87
+ # 5. 写文件确认:编辑器类工具改写文件前统一确认(配合 dsh-auto-review 可全自动裁决)
88
+ - match:
89
+ tools: [edit, write, bash, pwsh, mcp__*]
90
+ action: ask
91
+ reason: "写操作需要确认"
92
+ ```
93
+
94
+ ## 与 dsh-auto-review 的分工
95
+
96
+ 本插件只产出 `ask` 决定;`ask` 交给官方 `ctx.approval` 审批 seam。挂载 `dsh-auto-review` 后,其 answerer 会认领符合策略的请求并给出第二模型裁决,否则交给人类审批;两者都没有时官方 fail-closed(`unavailable` → 拒绝)。本插件**不**自己启动 reviewer 子代理。
97
+
98
+ ## 加载与热更新
99
+
100
+ - 按会话 cwd 惰性发现,首次 `tools/pre-execute` 时加载并缓存(超出 `maxCachedWorkspaces` 逐出最久未用项)。
101
+ - 非法 YAML / 未知字段或 action / 坏 glob 与正则 / 易回溯灾难的模式 / 超 `maxRules` 都是加载期错误:`badFilePolicy: fail`(默认)时首个使用该工作区的工具调用响亮失败;`ignore-with-warning` 时告警并降级为空规则集。
102
+ - 绝对 `rulesFile` 或配置的 `fallbackPath` 在插件挂载时即校验,缺失/非法直接挂载失败。
103
+ - 文件变更后经 Chokidar 去抖重读(`watch`、`watchStabilityThresholdMs`);重读失败保留旧规则、只告警,绝不崩溃。`/rules reload` 手动触发同样路径;`/rules decisions` 回放审计轨迹,`/rules test` 对假设调用干跑规则。
@@ -0,0 +1,117 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/PerryLink/dsh-permission-rules/main/docs/rules-format.schema.json",
4
+ "title": "dsh-permission-rules rule file",
5
+ "description": "Declarative allow/deny/ask permission rules for DeepSeek Harness, evaluated in file order with first-match-wins semantics.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "rules": {
10
+ "description": "The ordered rule list; omitted or empty means an empty rule set (everything passes through).",
11
+ "type": "array",
12
+ "items": { "$ref": "#/$defs/rule" }
13
+ }
14
+ },
15
+ "$defs": {
16
+ "rule": {
17
+ "type": "object",
18
+ "additionalProperties": false,
19
+ "required": ["action", "reason"],
20
+ "properties": {
21
+ "match": { "$ref": "#/$defs/match" },
22
+ "action": {
23
+ "description": "What the rule does to a matching tool call.",
24
+ "enum": ["allow", "deny", "ask"]
25
+ },
26
+ "reason": {
27
+ "description": "Non-empty explanation; deny/ask reasons become model-visible tool results or approval reasons.",
28
+ "type": "string",
29
+ "minLength": 1
30
+ },
31
+ "enabled": {
32
+ "description": "false keeps the rule visible but inert (displayed as disabled).",
33
+ "type": "boolean"
34
+ },
35
+ "description": {
36
+ "description": "Optional one-line explanation shown by /rules.",
37
+ "type": "string",
38
+ "minLength": 1
39
+ },
40
+ "tags": {
41
+ "description": "Free-form labels shown by /rules.",
42
+ "type": "array",
43
+ "items": { "type": "string", "minLength": 1 }
44
+ }
45
+ }
46
+ },
47
+ "match": {
48
+ "description": "Match dimensions combined with AND; absent dimensions impose no restriction.",
49
+ "type": "object",
50
+ "additionalProperties": false,
51
+ "properties": {
52
+ "tools": {
53
+ "description": "Tool-name globs (always glob semantics, regardless of patternMode).",
54
+ "type": "array",
55
+ "items": { "type": "string", "minLength": 1 }
56
+ },
57
+ "params": {
58
+ "description": "Param key to pattern(s). EVERY key must be present and match; a !-prefixed pattern negates. Scalars are stringified; arrays and nested objects contribute their scalar leaves.",
59
+ "type": "object",
60
+ "additionalProperties": {
61
+ "oneOf": [
62
+ { "$ref": "#/$defs/pattern" },
63
+ {
64
+ "type": "array",
65
+ "items": { "$ref": "#/$defs/pattern" },
66
+ "minItems": 1
67
+ }
68
+ ]
69
+ }
70
+ },
71
+ "paths": {
72
+ "description": "Workspace-relative path patterns; ANY candidate path matching ANY pattern satisfies the dimension. Candidates come from documented argument keys at any nesting depth.",
73
+ "type": "array",
74
+ "items": { "type": "string", "minLength": 1 }
75
+ },
76
+ "absent": {
77
+ "description": "Argument keys that must be ABSENT; every listed key must be missing.",
78
+ "type": "array",
79
+ "items": { "type": "string", "minLength": 1 }
80
+ },
81
+ "when": {
82
+ "description": "Host conditions: every listed env var must be present and match; any listed platform matches.",
83
+ "type": "object",
84
+ "additionalProperties": false,
85
+ "properties": {
86
+ "env": {
87
+ "type": "object",
88
+ "additionalProperties": {
89
+ "oneOf": [
90
+ { "$ref": "#/$defs/pattern" },
91
+ {
92
+ "type": "array",
93
+ "items": { "$ref": "#/$defs/pattern" },
94
+ "minItems": 1
95
+ }
96
+ ]
97
+ }
98
+ },
99
+ "platform": {
100
+ "type": "array",
101
+ "items": {
102
+ "enum": ["aix", "android", "darwin", "freebsd", "linux", "openbsd", "sunos", "win32"]
103
+ }
104
+ }
105
+ }
106
+ }
107
+ }
108
+ },
109
+ "pattern": {
110
+ "oneOf": [
111
+ { "type": "string", "minLength": 1 },
112
+ { "type": "number" },
113
+ { "type": "boolean" }
114
+ ]
115
+ }
116
+ }
117
+ }