@gavinc-cn/dsh-path-rules 0.1.0 → 0.1.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/README.md +80 -4
- package/lib/auto-whitelist.mjs +260 -0
- package/lib/index.mjs +181 -13
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,7 +11,9 @@ DSH 第五权限模式「路径规则」的逐调用闸门:挂在 `tools/pre-e
|
|
|
11
11
|
- **白名单**内的目录可读可写(直通,不弹框);
|
|
12
12
|
- **黑名单**内的目录读写**都要审批**(优先级最高,压过白名单与项目内);
|
|
13
13
|
- `bash` / `pwsh`:只读命令直接放行;写入目标可静态解析时按上述规则判定;
|
|
14
|
-
|
|
14
|
+
解析不出的命令默认拒绝并提示模型改写成可判定形式;
|
|
15
|
+
- **自动白名单**(可选,`autoWhitelist: true`):用户在审批框里批准的沙箱升权写入目标,
|
|
16
|
+
其目录会自动进入白名单(持久化),下次同目录的写入不再弹框。见下文。
|
|
15
17
|
|
|
16
18
|
## 激活条件
|
|
17
19
|
|
|
@@ -51,10 +53,78 @@ shell 判不出 → unanalyzable 策略(默认 deny + 改写指引)
|
|
|
51
53
|
failMode: closed # closed | open:插件内部异常时的兜底
|
|
52
54
|
dryRun: false # true = 只记日志不拦(灰度用)
|
|
53
55
|
modelFallback: false # 预留字段:置 true 仅记一条 warn 日志,判定行为不变
|
|
56
|
+
autoWhitelist: false # true = 用户批准的沙箱升权写入目标自动进白名单(默认关闭)
|
|
57
|
+
autoWhitelistFile: <dsh home>/path-rules/auto-whitelist.json # 自动白名单状态文件(可选)
|
|
54
58
|
logPath: ~/.kimi-code/hooks/logs/path_rules.dsh.log
|
|
55
59
|
```
|
|
56
60
|
|
|
57
|
-
日志事件:`plugin-loaded / allow / ask / deny / pass / dry-run / error
|
|
61
|
+
日志事件:`plugin-loaded / allow / ask / deny / pass / dry-run / error`,
|
|
62
|
+
以及自动白名单的 `auto-whitelist / auto-whitelist-skip / auto-whitelist-load /
|
|
63
|
+
auto-whitelist-save / auto-whitelist-error`。
|
|
64
|
+
|
|
65
|
+
## 自动白名单(`autoWhitelist`,2026-10-18 新增)
|
|
66
|
+
|
|
67
|
+
**用途**:路径规则档下,项目外写入的审批通路是"沙箱拒绝 → 模型带 `sandbox_permissions` 重试 →
|
|
68
|
+
框架弹审批框"。同一个目录反复写就会反复弹框。打开 `autoWhitelist` 后,用户**批准过一次**的写入目标,
|
|
69
|
+
其目录会被自动记进白名单;之后同一目录的升权请求由本插件直接答 `allowed-once`(不弹框)。
|
|
70
|
+
|
|
71
|
+
**触发条件**(缺一不可):
|
|
72
|
+
|
|
73
|
+
1. `autoWhitelist: true` 且 `enabled: true`、`dryRun: false`;
|
|
74
|
+
2. 会话当前处于「路径规则」档(`activationPreset` 命中,子代理沿父链上溯;其它四档不学习);
|
|
75
|
+
3. 审批类型是**沙箱升权**(`reason` 以 `escalate sandbox to ` 开头);
|
|
76
|
+
4. 用户在审批框点了同意(`approval/request` 瀑布下游返回 `allowed-once`;拒绝/取消/不可用都不学)。
|
|
77
|
+
|
|
78
|
+
**学什么**:从该次调用的参数静态解析写入目标 —— `write`/`edit` 的 `file_path`、`bash`/`pwsh` 的
|
|
79
|
+
写目标(复用 `analyze.mjs`);目标是已存在的目录就取它自己,否则取父目录。**解析不出就什么都不学**
|
|
80
|
+
(不猜):MCP/未知工具、判不出的 shell 命令、参数没被记住的调用都只记一条 skip 日志。
|
|
81
|
+
|
|
82
|
+
**生效方式**:条目并入内存白名单(下一条消息起系统提示里可见),此后:
|
|
83
|
+
|
|
84
|
+
- 该目录的写入由本插件直接 `allow`(但**沙箱仍会先拒绝首次写入** —— 跨层限制见下);
|
|
85
|
+
- 模型按提示带 `sandbox_permissions` 重试时,`approval/request` 命中白名单 ⇒ 插件答 `allowed-once`,
|
|
86
|
+
用户**不再看到弹框**。
|
|
87
|
+
|
|
88
|
+
命令类(`bash`/`pwsh`)升权请求的命中判定 2026-10-18 起改为**按静态写目标**:全部写目标都落在
|
|
89
|
+
白名单/项目内/临时区、且至少一个命中白名单才自动放行;任一目标命中黑名单或项目外就交给用户。
|
|
90
|
+
只有命令判不出写目标时才退回旧的「命令文本包含白名单路径」粗粒度口径。
|
|
91
|
+
(旧口径会把 `cp /white/f.txt /elsewhere/f.txt` 这类命令静默放行;自动白名单让白名单动态变化后必须收紧。)
|
|
92
|
+
|
|
93
|
+
**持久化**:默认状态文件 `<dsh home>/path-rules/auto-whitelist.json`(可用 `autoWhitelistFile` 覆盖,
|
|
94
|
+
支持 `~`),格式:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"version": 1,
|
|
99
|
+
"entries": [
|
|
100
|
+
{ "path": "/srv/data", "platform": "linux", "addedAt": "2026-10-18T06:00:00.000Z", "tool": "write", "callId": "call_..." }
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- 按 `process.platform` **分区存放**:容器(linux)与宿主(win32)共享同一份 `~/.dsh` 时互不串味,
|
|
106
|
+
写回时保留另一平台的条目;跨平台写法的路径在加载时直接忽略。
|
|
107
|
+
- 原子写(临时文件 + rename)、坏文件容错(只记 error 日志,不影响插件加载)、上限 200 条
|
|
108
|
+
(超出淘汰最旧)。
|
|
109
|
+
- 用户可直接编辑/删除该文件;**生效时机 = 插件重新加载**(改本行 config 是 patch 层热加载约 2 秒;
|
|
110
|
+
改代码需重启 dsh)。`loader` 日志里 `auto-whitelist loaded=N` 可确认读到了多少条。
|
|
111
|
+
|
|
112
|
+
**安全护栏**(自动加入前逐条过滤,丢弃原因写进日志):
|
|
113
|
+
|
|
114
|
+
| 跳过原因 | 含义 |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `blacklist` | 目标/目录命中黑名单 ⇒ 不学(黑名单优先级最高,加了也无效) |
|
|
117
|
+
| `no-need` | 目标在项目内或临时区,本来就直通,无需白名单 |
|
|
118
|
+
| `already-whitelisted` | 目录已被现有白名单覆盖 |
|
|
119
|
+
| `protected` | 与插件状态/日志文件或 DSH home 相关(防"自动白名单自己改写自己") |
|
|
120
|
+
| `sensitive` | 目录是系统根(`/`、`/etc`、`/usr`、`/var`、`/opt` …)或用户 home **自身或其祖先** |
|
|
121
|
+
|
|
122
|
+
即:批准一个文件**不会**被放大成整个 home/系统目录的长期信任;`/root/data` 这类更深层目录可以自动加入,
|
|
123
|
+
但 `/root` 本身不行(要用就手工写进 config 的 `whitelist`)。
|
|
124
|
+
|
|
125
|
+
**跨层限制(重要)**:自动白名单**不突破沙箱**。工作区外第一次写入仍会被 `workspace-write` 拒绝一次,
|
|
126
|
+
模型需按错误提示带 `sandbox_permissions` + `justification` 重试;区别只在于重试的审批请求
|
|
127
|
+
由插件自动放行、不再弹框。详见下文「已知盲区 8」。
|
|
58
128
|
|
|
59
129
|
## 已知盲区(未覆盖,别当安全)
|
|
60
130
|
|
|
@@ -71,6 +141,12 @@ shell 判不出 → unanalyzable 策略(默认 deny + 改写指引)
|
|
|
71
141
|
项目外/白名单(工作区外)的写会被沙箱拦下,模型需带 `sandbox_permissions` + `justification`
|
|
72
142
|
重试同一调用以触发框架审批;白名单目标的升权请求由本插件自动答 `allowed-once`(无弹框)。
|
|
73
143
|
详见 spec §4.4。
|
|
144
|
+
9. **自动白名单只学"能静态解析写目标"的调用**:MCP/未知工具、`unanalyzable` 的 shell、参数未被记忆的
|
|
145
|
+
调用都不学习(只记 skip 日志);`bash` 的间接产物(脚本内部、后台进程)同既有盲区。
|
|
146
|
+
10. **自动白名单是父目录粒度**:批准 `/a/b/f.txt` 会把整个 `/a/b` 加入白名单(用户语义如此);
|
|
147
|
+
敏感根保护只挡系统根与 home 自身/祖先,不挡更深的目录。
|
|
148
|
+
11. **命令判不出写目标时升权放行退回子串口径**:`bash`/`pwsh` 命令若静态分析判不出(`unanalyzable`、
|
|
149
|
+
参数未记忆),仍按"命令文本包含白名单路径"粗粒度放行 —— 这是旧插件沿用口径,无法在不分析的前提下收紧。
|
|
74
150
|
|
|
75
151
|
## 测试
|
|
76
152
|
|
|
@@ -78,10 +154,10 @@ shell 判不出 → unanalyzable 策略(默认 deny + 改写指引)
|
|
|
78
154
|
cd packages/runtime/dsh-path-rules
|
|
79
155
|
node --test test/rules.test.mjs test/classify.test.mjs test/analyze-lex.test.mjs \
|
|
80
156
|
test/analyze-bash.test.mjs test/analyze-pwsh.test.mjs test/decide.test.mjs \
|
|
81
|
-
test/plugin.test.mjs test/analyze-security.test.mjs
|
|
157
|
+
test/plugin.test.mjs test/analyze-security.test.mjs test/auto-whitelist.test.mjs
|
|
82
158
|
```
|
|
83
159
|
|
|
84
|
-
(Node v24 下 `node --test test/`
|
|
160
|
+
(Node v24 下 `node --test test/` 目录形式会失败,必须显式给出文件;`npm test` 已包含全部 9 个文件。)
|
|
85
161
|
|
|
86
162
|
设计文档:`docs/superpowers/specs/2026-10-16-dsh-path-rules-permission-mode-design.md`;
|
|
87
163
|
实现计划:`docs/superpowers/plans/2026-10-17-dsh-path-rules-permission-mode.md`。
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// dsh-path-rules 的自动白名单(2026-10-18 新增)。
|
|
2
|
+
//
|
|
3
|
+
// 目的:用户批准过一次「沙箱升权写入」后,把该次写入目标折叠成的**目录**记进白名单,
|
|
4
|
+
// 之后同一目录的升权请求由插件自动答 allowed-once、不再反复弹框。
|
|
5
|
+
//
|
|
6
|
+
// 三层职责(全部可独立测试,index.mjs 只做接线):
|
|
7
|
+
// ① writableDirsOf :从一次工具调用(工具名 + 参数)推导写入目标 → 父目录;
|
|
8
|
+
// ② filterAutoWhitelistDirs:安全护栏(黑名单/项目内/临时区/已覆盖/保护路径/敏感根);
|
|
9
|
+
// ③ loadAutoWhitelist / saveAutoWhitelist:按平台分区的 JSON 状态文件读写(原子写)。
|
|
10
|
+
//
|
|
11
|
+
// 重要边界:本模块只负责「学什么、记到哪」,**不改变**判定链本身的优先级
|
|
12
|
+
// (黑名单仍压过白名单;自动条目只是追加进 config.whitelist)。
|
|
13
|
+
|
|
14
|
+
import { mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs'
|
|
15
|
+
import { homedir } from 'node:os'
|
|
16
|
+
import { dirname, isAbsolute, join } from 'node:path'
|
|
17
|
+
import { analyzeCommand } from './analyze.mjs'
|
|
18
|
+
import { classifyTool } from './classify.mjs'
|
|
19
|
+
import { classifyPath, normalizePath, underRoot } from './rules.mjs'
|
|
20
|
+
|
|
21
|
+
/** 自动白名单条数上限(每个平台),超出淘汰最旧。 */
|
|
22
|
+
export const MAX_AUTO_ENTRIES = 200
|
|
23
|
+
|
|
24
|
+
/** 状态文件格式版本。 */
|
|
25
|
+
const STATE_VERSION = 1
|
|
26
|
+
|
|
27
|
+
/** POSIX 系统目录:既不允许自身也不允许它们的祖先目录被自动加入(更深目录可以)。 */
|
|
28
|
+
const POSIX_SENSITIVE_ROOTS = ['/', '/etc', '/usr', '/bin', '/sbin', '/boot', '/var', '/lib', '/lib64', '/opt', '/proc', '/sys', '/dev']
|
|
29
|
+
|
|
30
|
+
/** 展开 `~` / `~/` / `~\` 前缀;其它写法原样返回。 */
|
|
31
|
+
export function expandHome(path, home = homedir()) {
|
|
32
|
+
if (typeof path !== 'string') return path
|
|
33
|
+
if (path === '~') return home
|
|
34
|
+
if (path.startsWith('~/') || path.startsWith('~\\')) return join(home, path.slice(2))
|
|
35
|
+
return path
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 目标路径 → 该自管目录:目标本身是已存在的目录就取它自己,否则取父目录。
|
|
40
|
+
* (批准后写入目标往往还没创建,所以只能靠"是否已是目录"这一条证据;新目录取父目录是保守估计。)
|
|
41
|
+
*/
|
|
42
|
+
function directoryOf(target) {
|
|
43
|
+
try {
|
|
44
|
+
if (statSync(target).isDirectory()) return target
|
|
45
|
+
} catch {
|
|
46
|
+
/* 不存在/不可读 ⇒ 按"待创建的文件"处理 */
|
|
47
|
+
}
|
|
48
|
+
return dirname(target)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 把若干原始写目标归一化并折叠成 {target, dir} 对(去重、丢弃空路径)。 */
|
|
52
|
+
function toPairs(rawTargets, cwd) {
|
|
53
|
+
const pairs = []
|
|
54
|
+
const seen = new Set()
|
|
55
|
+
for (const raw of Array.isArray(rawTargets) ? rawTargets : []) {
|
|
56
|
+
const target = normalizePath(raw, cwd)
|
|
57
|
+
if (target.length === 0) continue
|
|
58
|
+
const dir = directoryOf(target)
|
|
59
|
+
if (dir.length === 0) continue
|
|
60
|
+
const key = `${target}\n${dir}`
|
|
61
|
+
if (seen.has(key)) continue
|
|
62
|
+
seen.add(key)
|
|
63
|
+
pairs.push({ target, dir })
|
|
64
|
+
}
|
|
65
|
+
return pairs
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 从一次工具调用推导"用户批准后要学的目录"。
|
|
70
|
+
*
|
|
71
|
+
* 只认写目标:read 不触发升权;shell 用静态分析结果,判不出就**不猜**(返回空)。
|
|
72
|
+
* 工具/参数不可读时不抛(审批监听器里必须容忍任何异常)。
|
|
73
|
+
*
|
|
74
|
+
* @param toolName - 工具名(审批请求里的 request.toolName)。
|
|
75
|
+
* @param args - 该次调用的参数(pre-execute 按 callId 记忆下来的那份)。
|
|
76
|
+
* @param cwd - 相对路径解析基准(会话 cwd)。
|
|
77
|
+
* @returns { pairs: Array<{target: string, dir: string}>, reason?: string }
|
|
78
|
+
*/
|
|
79
|
+
export function writableDirsOf(toolName, args, cwd) {
|
|
80
|
+
let effect
|
|
81
|
+
try {
|
|
82
|
+
effect = classifyTool(toolName, args)
|
|
83
|
+
} catch {
|
|
84
|
+
return { pairs: [], reason: 'unreadable-args' }
|
|
85
|
+
}
|
|
86
|
+
if (effect.kind === 'write') return { pairs: toPairs(effect.targets, cwd) }
|
|
87
|
+
if (effect.kind !== 'shell') return { pairs: [], reason: 'not-a-write' }
|
|
88
|
+
const base = effect.workdir.length > 0 ? effect.workdir : cwd
|
|
89
|
+
const analysis = analyzeCommand(effect.command, { shell: effect.shell, cwd: base })
|
|
90
|
+
if (analysis.unanalyzable !== null) return { pairs: [], reason: 'unanalyzable' }
|
|
91
|
+
return { pairs: toPairs(analysis.writes, base) }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 目录是否与受保护路径相关(自身、祖先或后代都算)——状态/日志文件与 DSH home 用它。 */
|
|
95
|
+
function isProtected(dir, protectedPaths) {
|
|
96
|
+
return protectedPaths.some((path) => underRoot(path, dir) || underRoot(dir, path))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 目录是否等于某个敏感根、或是敏感根的祖先(更深层目录允许)。 */
|
|
100
|
+
function isSensitive(dir, sensitiveRoots) {
|
|
101
|
+
return sensitiveRoots.some((root) => underRoot(root, dir))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 过滤候选目录:只保留"值得且可以自动加入白名单"的那些。
|
|
106
|
+
*
|
|
107
|
+
* 跳过原因(写进审计日志用):
|
|
108
|
+
* blacklist 目标或目录命中黑名单(黑名单优先级最高,加了也无效)
|
|
109
|
+
* no-need 目标在项目内 / 临时区(本来就直通,无需白名单)
|
|
110
|
+
* already-whitelisted 目录已被现有白名单覆盖
|
|
111
|
+
* protected 与插件状态/日志文件或 DSH home 相关(防"自动白名单自己改写自己")
|
|
112
|
+
* sensitive 目录是系统根/用户 home 自身或其祖先(防止一次批准放大成整个系统/home)
|
|
113
|
+
*
|
|
114
|
+
* @param pairs - writableDirsOf 的输出。
|
|
115
|
+
* @param options - { roots, protectedPaths, sensitiveRoots }。
|
|
116
|
+
* @returns { accepted: string[], skipped: Array<{dir, target, reason}> }
|
|
117
|
+
*/
|
|
118
|
+
export function filterAutoWhitelistDirs(pairs, options = {}) {
|
|
119
|
+
const roots = options.roots ?? {}
|
|
120
|
+
const whitelist = Array.isArray(roots.whitelist) ? roots.whitelist : []
|
|
121
|
+
const protectedPaths = (Array.isArray(options.protectedPaths) ? options.protectedPaths : [])
|
|
122
|
+
.filter((path) => typeof path === 'string' && path.length > 0)
|
|
123
|
+
const sensitive = (Array.isArray(options.sensitiveRoots) ? options.sensitiveRoots : [])
|
|
124
|
+
.filter((path) => typeof path === 'string' && path.length > 0)
|
|
125
|
+
const accepted = []
|
|
126
|
+
const skipped = []
|
|
127
|
+
for (const pair of Array.isArray(pairs) ? pairs : []) {
|
|
128
|
+
const target = pair?.target
|
|
129
|
+
const dir = pair?.dir
|
|
130
|
+
if (typeof target !== 'string' || typeof dir !== 'string' || target.length === 0 || dir.length === 0) continue
|
|
131
|
+
const classification = classifyPath(target, roots)
|
|
132
|
+
if (classification === 'blacklist') {
|
|
133
|
+
skipped.push({ dir, target, reason: 'blacklist' })
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
if (classification === 'project' || classification === 'temp') {
|
|
137
|
+
skipped.push({ dir, target, reason: 'no-need' })
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
if (classifyPath(dir, roots) === 'blacklist') {
|
|
141
|
+
skipped.push({ dir, target, reason: 'blacklist' })
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
if (whitelist.some((root) => underRoot(dir, root))) {
|
|
145
|
+
skipped.push({ dir, target, reason: 'already-whitelisted' })
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
if (isProtected(dir, protectedPaths)) {
|
|
149
|
+
skipped.push({ dir, target, reason: 'protected' })
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
if (isSensitive(dir, sensitive)) {
|
|
153
|
+
skipped.push({ dir, target, reason: 'sensitive' })
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
if (!accepted.includes(dir)) accepted.push(dir)
|
|
157
|
+
}
|
|
158
|
+
return { accepted, skipped }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** 读状态文件里的全部原始条目(其它平台的要原样保留;文件坏了就当没有)。 */
|
|
162
|
+
function readRawEntries(file) {
|
|
163
|
+
try {
|
|
164
|
+
const data = JSON.parse(readFileSync(file, 'utf8'))
|
|
165
|
+
return Array.isArray(data?.entries) ? data.entries : []
|
|
166
|
+
} catch {
|
|
167
|
+
return []
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 读取当前平台的自动白名单条目。
|
|
173
|
+
*
|
|
174
|
+
* 容错是硬要求:状态文件由用户手改/跨平台同步产生,坏了只记 error 日志、不能影响插件加载。
|
|
175
|
+
*
|
|
176
|
+
* @param file - 状态文件绝对路径。
|
|
177
|
+
* @param platform - 当前平台(默认 process.platform)。
|
|
178
|
+
* @returns { entries: Array<object>, error?: string }
|
|
179
|
+
*/
|
|
180
|
+
export function loadAutoWhitelist(file, platform = process.platform) {
|
|
181
|
+
let raw
|
|
182
|
+
try {
|
|
183
|
+
raw = readFileSync(file, 'utf8')
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (error?.code === 'ENOENT') return { entries: [], error: undefined }
|
|
186
|
+
return { entries: [], error: `读取失败:${String(error)}` }
|
|
187
|
+
}
|
|
188
|
+
let data
|
|
189
|
+
try {
|
|
190
|
+
data = JSON.parse(raw)
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return { entries: [], error: `JSON 解析失败:${String(error)}` }
|
|
193
|
+
}
|
|
194
|
+
const entries = []
|
|
195
|
+
const seen = new Set()
|
|
196
|
+
for (const item of Array.isArray(data?.entries) ? data.entries : []) {
|
|
197
|
+
if (item === null || typeof item !== 'object') continue
|
|
198
|
+
const itemPlatform = item.platform === undefined ? platform : item.platform
|
|
199
|
+
if (itemPlatform !== platform) continue
|
|
200
|
+
const path = typeof item.path === 'string' ? item.path.trim() : ''
|
|
201
|
+
// 相对路径(含跨平台同步过来的另一平台写法)一律忽略,且不写回原文件。
|
|
202
|
+
if (path.length === 0 || !isAbsolute(path) || seen.has(path)) continue
|
|
203
|
+
seen.add(path)
|
|
204
|
+
entries.push({ ...item, path, platform })
|
|
205
|
+
}
|
|
206
|
+
return { entries, error: undefined }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 原子写当前平台的自动白名单(保留其它平台条目,防容器/宿主双写互删)。
|
|
211
|
+
*
|
|
212
|
+
* @param file - 状态文件绝对路径。
|
|
213
|
+
* @param platform - 当前平台。
|
|
214
|
+
* @param entries - 当前平台的完整条目列表(调用方已按上限裁剪)。
|
|
215
|
+
* @returns { ok: true } | { ok: false, error: string }
|
|
216
|
+
*/
|
|
217
|
+
export function saveAutoWhitelist(file, platform, entries) {
|
|
218
|
+
const current = Array.isArray(entries) ? entries : []
|
|
219
|
+
const others = readRawEntries(file).filter((item) => item?.platform !== undefined && item.platform !== platform)
|
|
220
|
+
const payload = { version: STATE_VERSION, entries: [...others, ...current] }
|
|
221
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`
|
|
222
|
+
try {
|
|
223
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
224
|
+
writeFileSync(tmp, `${JSON.stringify(payload, null, 2)}\n`, 'utf8')
|
|
225
|
+
renameSync(tmp, file)
|
|
226
|
+
return { ok: true }
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return { ok: false, error: String(error) }
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Windows 敏感根:优先取环境变量(盘符可能不是 C:)。 */
|
|
233
|
+
function windowsSensitiveRoots() {
|
|
234
|
+
const roots = []
|
|
235
|
+
for (const key of ['SystemRoot', 'ProgramFiles', 'ProgramFiles(x86)', 'ProgramData']) {
|
|
236
|
+
const value = process.env[key]
|
|
237
|
+
if (typeof value === 'string' && value.trim().length > 0) roots.push(value.trim())
|
|
238
|
+
}
|
|
239
|
+
roots.push('C:\\')
|
|
240
|
+
return roots
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* 敏感根列表:这些目录自身与其祖先不会被自动加入白名单,更深层目录可以。
|
|
245
|
+
* 用途是防止"批准了一个文件"被放大成"整个系统目录/home"的长期信任。
|
|
246
|
+
*
|
|
247
|
+
* @param options - { platform, home }(测试可注入)。
|
|
248
|
+
* @returns 归一化后的绝对路径列表。
|
|
249
|
+
*/
|
|
250
|
+
export function sensitiveRoots(options = {}) {
|
|
251
|
+
const platform = options.platform ?? process.platform
|
|
252
|
+
const home = typeof options.home === 'string' && options.home.length > 0 ? options.home : homedir()
|
|
253
|
+
const raw = platform === 'win32' ? [...windowsSensitiveRoots(), home] : [...POSIX_SENSITIVE_ROOTS, home]
|
|
254
|
+
const roots = []
|
|
255
|
+
for (const path of raw) {
|
|
256
|
+
const normalized = normalizePath(path, home)
|
|
257
|
+
if (normalized.length > 0 && !roots.includes(normalized)) roots.push(normalized)
|
|
258
|
+
}
|
|
259
|
+
return roots
|
|
260
|
+
}
|
package/lib/index.mjs
CHANGED
|
@@ -18,6 +18,15 @@ import { appendFileSync, mkdirSync } from 'node:fs'
|
|
|
18
18
|
import { homedir, tmpdir } from 'node:os'
|
|
19
19
|
import { dirname, isAbsolute, join } from 'node:path'
|
|
20
20
|
import { analyzeCommand } from './analyze.mjs'
|
|
21
|
+
import {
|
|
22
|
+
MAX_AUTO_ENTRIES,
|
|
23
|
+
expandHome,
|
|
24
|
+
filterAutoWhitelistDirs,
|
|
25
|
+
loadAutoWhitelist,
|
|
26
|
+
saveAutoWhitelist,
|
|
27
|
+
sensitiveRoots,
|
|
28
|
+
writableDirsOf,
|
|
29
|
+
} from './auto-whitelist.mjs'
|
|
21
30
|
import { classifyTool } from './classify.mjs'
|
|
22
31
|
import { combine, classifyPath, decideTarget, normalizePath, normalizeRoots, underRoot } from './rules.mjs'
|
|
23
32
|
|
|
@@ -61,6 +70,9 @@ export function normalizeConfig(raw) {
|
|
|
61
70
|
if (c.failMode !== undefined && !FAIL_MODES.has(c.failMode)) {
|
|
62
71
|
throw new Error(`dsh-path-rules: failMode must be "closed" or "open", got ${JSON.stringify(c.failMode)}`)
|
|
63
72
|
}
|
|
73
|
+
if (c.autoWhitelistFile !== undefined && (typeof c.autoWhitelistFile !== 'string' || c.autoWhitelistFile.trim().length === 0)) {
|
|
74
|
+
throw new Error('dsh-path-rules: autoWhitelistFile must be a non-empty string')
|
|
75
|
+
}
|
|
64
76
|
const activationPreset = c.activationPreset === undefined ? 'path-rules' : c.activationPreset
|
|
65
77
|
if (typeof activationPreset !== 'string' || activationPreset.trim().length === 0) {
|
|
66
78
|
throw new Error('dsh-path-rules: activationPreset must be a non-empty string')
|
|
@@ -75,6 +87,8 @@ export function normalizeConfig(raw) {
|
|
|
75
87
|
failMode: c.failMode ?? 'closed',
|
|
76
88
|
dryRun: c.dryRun === true,
|
|
77
89
|
modelFallback: c.modelFallback === true,
|
|
90
|
+
autoWhitelist: c.autoWhitelist === true,
|
|
91
|
+
autoWhitelistFile: typeof c.autoWhitelistFile === 'string' ? c.autoWhitelistFile.trim() : '',
|
|
78
92
|
logPath: typeof c.logPath === 'string' && c.logPath.trim().length > 0 ? c.logPath.trim() : DEFAULT_LOG,
|
|
79
93
|
}
|
|
80
94
|
}
|
|
@@ -197,13 +211,43 @@ function escalationTargetFromArgs(args) {
|
|
|
197
211
|
return undefined
|
|
198
212
|
}
|
|
199
213
|
|
|
200
|
-
/**
|
|
201
|
-
|
|
214
|
+
/**
|
|
215
|
+
* 命令类升权请求是否允许自动放行(2026-10-18 严格化)。
|
|
216
|
+
*
|
|
217
|
+
* 旧口径只看"命令文本里是否出现白名单路径",`cp /white/f.txt /elsewhere/f.txt` 也会被静默批准
|
|
218
|
+
* (自动白名单让白名单动态变化后风险更大)。这里改为按静态分析出的**写目标**判定:
|
|
219
|
+
*
|
|
220
|
+
* @returns true = 全部写目标都在白名单/项目内/临时区,且至少一个命中白名单 ⇒ 可自动放行;
|
|
221
|
+
* false = 存在黑名单或项目外写目标 ⇒ 必须交给用户;
|
|
222
|
+
* undefined = 判不出(参数缺失/不可解析/没有写目标)⇒ 退回旧的子串口径兜底。
|
|
223
|
+
*/
|
|
224
|
+
function commandEscalationIsWhitelisted(config, session, services, toolName, args) {
|
|
225
|
+
if (args === undefined) return undefined
|
|
226
|
+
const cwd = sessionCwdOf(session, services)
|
|
227
|
+
const derived = writableDirsOf(toolName, args, cwd)
|
|
228
|
+
if (derived.pairs.length === 0) return undefined
|
|
229
|
+
const roots = buildRoots(session, config, services)
|
|
230
|
+
let sawWhitelist = false
|
|
231
|
+
for (const pair of derived.pairs) {
|
|
232
|
+
const kind = classifyPath(pair.target, roots)
|
|
233
|
+
if (kind === 'blacklist' || kind === 'outside') return false
|
|
234
|
+
if (kind === 'whitelist') sawWhitelist = true
|
|
235
|
+
}
|
|
236
|
+
return sawWhitelist ? true : undefined
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 升权请求的目标是否命中白名单(文件按 realpath 后前缀;命令优先按静态写目标严格判定,
|
|
241
|
+
* 判不出时再用粗粒度子串兜底)。
|
|
242
|
+
*/
|
|
243
|
+
function escalationIsWhitelisted(target, config, session, commandCheck) {
|
|
202
244
|
if (config.whitelist.length === 0) return false
|
|
203
245
|
if (target.kind === 'file') {
|
|
204
246
|
const resolved = normalizePath(target.value, session?.header?.cwd ?? process.cwd())
|
|
205
247
|
return config.whitelist.some((root) => underRoot(resolved, root))
|
|
206
248
|
}
|
|
249
|
+
const strict = typeof commandCheck === 'function' ? commandCheck() : undefined
|
|
250
|
+
if (strict !== undefined) return strict
|
|
207
251
|
return config.whitelist.some((root) => target.value.includes(root))
|
|
208
252
|
}
|
|
209
253
|
|
|
@@ -306,8 +350,37 @@ function logLine(logPath, event, summary, detail) {
|
|
|
306
350
|
}
|
|
307
351
|
}
|
|
308
352
|
|
|
353
|
+
/**
|
|
354
|
+
* 解析 DSH home:优先用宿主提供的 `dshHomePath` 服务(尊重 `DSH_HOME`),
|
|
355
|
+
* 缺失时按 `$DSH_HOME` → `~/.dsh` 回退。用于定位自动白名单状态文件与保护路径。
|
|
356
|
+
*/
|
|
357
|
+
function resolveDshHome(ctx) {
|
|
358
|
+
const homePath = ctx.get?.('dshHomePath')
|
|
359
|
+
if (typeof homePath === 'function') {
|
|
360
|
+
try {
|
|
361
|
+
const home = homePath()
|
|
362
|
+
if (typeof home === 'string' && home.length > 0) return home
|
|
363
|
+
} catch {
|
|
364
|
+
/* 服务不可用时走环境变量回退 */
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const fromEnv = typeof process.env.DSH_HOME === 'string' ? process.env.DSH_HOME.trim() : ''
|
|
368
|
+
return fromEnv.length > 0 ? fromEnv : join(homedir(), '.dsh')
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* 自动白名单状态文件路径:配置优先(支持 `~`),否则放在 `<dsh home>/path-rules/auto-whitelist.json`。
|
|
373
|
+
* 按平台分区写在文件内部(见 auto-whitelist.mjs),所以容器与宿主同步同一份文件也不会串味。
|
|
374
|
+
*/
|
|
375
|
+
function resolveAutoWhitelistFile(config, ctx) {
|
|
376
|
+
if (config.autoWhitelistFile.length > 0) {
|
|
377
|
+
return normalizePath(expandHome(config.autoWhitelistFile, homedir()), process.cwd())
|
|
378
|
+
}
|
|
379
|
+
return normalizePath(join(resolveDshHome(ctx), 'path-rules', 'auto-whitelist.json'), process.cwd())
|
|
380
|
+
}
|
|
381
|
+
|
|
309
382
|
/** 给模型看的规则说明;**只有本会话真的受该模式管辖时才注入**(否则模型会误判自己的权限档)。 */
|
|
310
|
-
function renderRules(config, session, services) {
|
|
383
|
+
function renderRules(config, session, services, extras = {}) {
|
|
311
384
|
if (session === undefined || session === null) return ''
|
|
312
385
|
if (typeof services.presetOf !== 'function') return ''
|
|
313
386
|
try {
|
|
@@ -316,15 +389,20 @@ function renderRules(config, session, services) {
|
|
|
316
389
|
return ''
|
|
317
390
|
}
|
|
318
391
|
const cwd = typeof session?.header?.cwd === 'string' ? session.header.cwd : ''
|
|
392
|
+
const declaredWhitelist = Array.isArray(extras.declaredWhitelist) ? extras.declaredWhitelist : config.whitelist
|
|
393
|
+
const autoEntries = Array.isArray(extras.autoEntries) ? extras.autoEntries : []
|
|
319
394
|
const lines = [
|
|
320
395
|
'[path-rules] 当前权限模式:路径规则。',
|
|
321
396
|
`会话工作区(可自由读写):${cwd.length > 0 ? cwd : '(未知)'}`,
|
|
322
397
|
'工作区外:读取允许;写入需要用户审批(会弹工具审批框)。',
|
|
323
398
|
]
|
|
324
|
-
if (
|
|
399
|
+
if (declaredWhitelist.length > 0) lines.push(`白名单(可自由读写):${declaredWhitelist.join(', ')}`)
|
|
400
|
+
if (autoEntries.length > 0) {
|
|
401
|
+
lines.push(`自动白名单(用户批准后自动学习,升权请求会被自动放行、不弹框):${autoEntries.map((entry) => entry.path).join(', ')}`)
|
|
402
|
+
}
|
|
325
403
|
if (config.blacklist.length > 0) lines.push(`黑名单(读写都需要审批):${config.blacklist.join(', ')}`)
|
|
326
404
|
lines.push('bash/pwsh:只读命令直接放行;写命令请写成可静态判定的形式(显式路径),判不出的命令会被拒绝。')
|
|
327
|
-
lines.push('工作区外写入若先被沙箱拒绝:用 sandbox_permissions(能覆盖该目标的最窄更宽模式)+ justification
|
|
405
|
+
lines.push('工作区外写入若先被沙箱拒绝:用 sandbox_permissions(能覆盖该目标的最窄更宽模式)+ justification 重试同一调用,用户审批后即可写入;白名单与自动白名单内的目标会自动放行。')
|
|
328
406
|
return lines.join('\n')
|
|
329
407
|
}
|
|
330
408
|
|
|
@@ -374,17 +452,36 @@ function compose(downstream, decision, config, log, exec) {
|
|
|
374
452
|
*/
|
|
375
453
|
export function apply(ctx, rawConfig) {
|
|
376
454
|
const base = normalizeConfig(rawConfig)
|
|
455
|
+
const declaredWhitelist = normalizeRoots(base.whitelist, process.cwd())
|
|
377
456
|
const config = {
|
|
378
457
|
...base,
|
|
379
|
-
whitelist:
|
|
458
|
+
whitelist: [...declaredWhitelist],
|
|
380
459
|
blacklist: normalizeRoots(base.blacklist, process.cwd()),
|
|
381
460
|
}
|
|
382
461
|
const log = (event, summary, detail) => logLine(config.logPath, event, summary, detail)
|
|
383
|
-
log('plugin-loaded', `enabled=${config.enabled}`, `activationPreset=${config.activationPreset} dryRun=${config.dryRun}`)
|
|
462
|
+
log('plugin-loaded', `enabled=${config.enabled} autoWhitelist=${config.autoWhitelist}`, `activationPreset=${config.activationPreset} dryRun=${config.dryRun}`)
|
|
384
463
|
if (config.modelFallback) {
|
|
385
464
|
log('error', 'warn:model-fallback-ignored', 'modelFallback 本期不改变判定行为(模型裁决路径需另立 spec)')
|
|
386
465
|
}
|
|
387
466
|
|
|
467
|
+
// 自动白名单的状态文件与保护路径(只在开关打开时才读盘;关闭时零 IO、零行为变化)。
|
|
468
|
+
const autoWhitelistFile = resolveAutoWhitelistFile(config, ctx)
|
|
469
|
+
const dshHome = resolveDshHome(ctx)
|
|
470
|
+
const protectedPaths = [...new Set([autoWhitelistFile, config.logPath, dshHome])]
|
|
471
|
+
/** 当前进程内的自动白名单条目(与 config.whitelist 同步追加;持久化只写本平台分区)。 */
|
|
472
|
+
const autoEntries = []
|
|
473
|
+
if (config.autoWhitelist) {
|
|
474
|
+
const loaded = loadAutoWhitelist(autoWhitelistFile, process.platform)
|
|
475
|
+
if (loaded.error !== undefined) log('error', 'auto-whitelist-load', loaded.error)
|
|
476
|
+
for (const entry of loaded.entries) {
|
|
477
|
+
const normalized = normalizePath(entry.path, process.cwd())
|
|
478
|
+
if (normalized.length === 0 || autoEntries.some((item) => item.path === normalized)) continue
|
|
479
|
+
autoEntries.push({ ...entry, path: normalized })
|
|
480
|
+
if (!config.whitelist.includes(normalized)) config.whitelist.push(normalized)
|
|
481
|
+
}
|
|
482
|
+
if (autoEntries.length > 0) log('auto-whitelist', `loaded=${autoEntries.length}`, autoWhitelistFile)
|
|
483
|
+
}
|
|
484
|
+
|
|
388
485
|
const services = {
|
|
389
486
|
presetOf: (session) => ctx.get('permissionPresets')?.current?.(session),
|
|
390
487
|
workspaceRootOf: (session) => ctx.get('sandboxPolicy')?.resolve?.({ session })?.workspaceRoot,
|
|
@@ -412,29 +509,100 @@ export function apply(ctx, rawConfig) {
|
|
|
412
509
|
scope.systemPrompt.context({
|
|
413
510
|
name: 'path-rules',
|
|
414
511
|
order: 111,
|
|
415
|
-
text: (context) => renderRules(config, context?.agent?.session, services),
|
|
512
|
+
text: (context) => renderRules(config, context?.agent?.session, services, { declaredWhitelist, autoEntries }),
|
|
416
513
|
})
|
|
417
514
|
})
|
|
418
515
|
|
|
516
|
+
/**
|
|
517
|
+
* 把一次"用户批准的沙箱升权调用"学进自动白名单。
|
|
518
|
+
*
|
|
519
|
+
* 只在 path-rules 档激活的会话里学(其它档不改变任何状态);任何异常都吞掉并记日志 ——
|
|
520
|
+
* 它只是顺手降低重复弹框,绝不能影响审批结果本身。
|
|
521
|
+
*/
|
|
522
|
+
const learnFromApproval = (request) => {
|
|
523
|
+
const session = request.agent?.session
|
|
524
|
+
if (session === undefined || session === null) return
|
|
525
|
+
if (!isActive(session, config, services)) return
|
|
526
|
+
const args = recentCallArgs.get(request.callId)
|
|
527
|
+
if (args === undefined) {
|
|
528
|
+
log('auto-whitelist-skip', 'no-remembered-args', request.callId)
|
|
529
|
+
return
|
|
530
|
+
}
|
|
531
|
+
const cwd = sessionCwdOf(session, services)
|
|
532
|
+
const derived = writableDirsOf(request.toolName, args, cwd)
|
|
533
|
+
if (derived.pairs.length === 0) {
|
|
534
|
+
log('auto-whitelist-skip', derived.reason ?? 'no-write-target', request.callId)
|
|
535
|
+
return
|
|
536
|
+
}
|
|
537
|
+
const roots = buildRoots(session, config, services)
|
|
538
|
+
const { accepted, skipped } = filterAutoWhitelistDirs(derived.pairs, {
|
|
539
|
+
roots,
|
|
540
|
+
protectedPaths,
|
|
541
|
+
sensitiveRoots: sensitiveRoots(),
|
|
542
|
+
})
|
|
543
|
+
for (const item of skipped) log('auto-whitelist-skip', `${item.reason} ${item.dir}`, describeCall({ name: request.toolName, arguments: args }))
|
|
544
|
+
if (accepted.length === 0) return
|
|
545
|
+
|
|
546
|
+
const added = []
|
|
547
|
+
for (const dir of accepted) {
|
|
548
|
+
if (autoEntries.some((entry) => entry.path === dir)) continue
|
|
549
|
+
autoEntries.push({
|
|
550
|
+
path: dir,
|
|
551
|
+
platform: process.platform,
|
|
552
|
+
addedAt: new Date().toISOString(),
|
|
553
|
+
tool: String(request.toolName ?? ''),
|
|
554
|
+
callId: request.callId,
|
|
555
|
+
})
|
|
556
|
+
if (!config.whitelist.includes(dir)) config.whitelist.push(dir)
|
|
557
|
+
added.push(dir)
|
|
558
|
+
}
|
|
559
|
+
// 上限按"最旧淘汰":自动白名单只是便利,不能无限膨胀。
|
|
560
|
+
while (autoEntries.length > MAX_AUTO_ENTRIES) {
|
|
561
|
+
const dropped = autoEntries.shift()
|
|
562
|
+
const index = config.whitelist.indexOf(dropped.path)
|
|
563
|
+
if (index >= 0) config.whitelist.splice(index, 1)
|
|
564
|
+
}
|
|
565
|
+
if (added.length === 0) return
|
|
566
|
+
const saved = saveAutoWhitelist(autoWhitelistFile, process.platform, autoEntries)
|
|
567
|
+
if (!saved.ok) log('error', 'auto-whitelist-save', saved.error)
|
|
568
|
+
log('auto-whitelist', `added=${added.length}`, added.join(', '))
|
|
569
|
+
}
|
|
570
|
+
|
|
419
571
|
// 白名单目标的沙箱升权请求自动放行(承接已退役的 sandbox-path-allowlist 能力):
|
|
420
572
|
// 只作答 "escalate sandbox to " 开头的请求,且目标必须命中白名单;其余一律交给下游弹框。
|
|
573
|
+
// autoWhitelist 打开时再包一层:等用户的结论,只有 allowed-once(用户点了同意)才学习。
|
|
421
574
|
ctx.on('approval/request', (request, next) => {
|
|
422
575
|
if (typeof request?.callId !== 'string') return next()
|
|
423
576
|
if (typeof request.reason !== 'string' || !request.reason.startsWith(SANDBOX_ESCALATION_PREFIX)) return next()
|
|
577
|
+
|
|
578
|
+
let target
|
|
424
579
|
try {
|
|
425
580
|
const remembered = recentCallArgs.get(request.callId)
|
|
426
581
|
const session = request.agent?.session
|
|
427
582
|
// remembered 优先:真实运行态里审批请求附带的 session 不暴露 events,无法按 callId 回查
|
|
428
583
|
// 会话日志(实测 hasEvents=false),只有 pre-execute 时记下的参数可靠。
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
584
|
+
target = escalationTargetFromArgs(remembered) ?? escalationTargetOf(session, request.callId)
|
|
585
|
+
const commandCheck = () => commandEscalationIsWhitelisted(config, session, services, request.toolName, remembered)
|
|
586
|
+
if (target !== undefined && escalationIsWhitelisted(target, config, session, commandCheck)) {
|
|
587
|
+
// remembered 只进审计日志,便于区分"走记忆命中"与"走会话日志兜底命中"
|
|
588
|
+
log('allow', 'whitelist-escalation', `${request.callId} remembered=${remembered !== undefined} ${target.value}`)
|
|
589
|
+
return 'allowed-once'
|
|
590
|
+
}
|
|
434
591
|
} catch (error) {
|
|
435
592
|
log('error', 'approval-error', String(error))
|
|
436
593
|
return next()
|
|
437
594
|
}
|
|
595
|
+
if (target === undefined || !config.autoWhitelist || !config.enabled || config.dryRun) return next()
|
|
596
|
+
return Promise.resolve(next()).then((outcome) => {
|
|
597
|
+
if (outcome === 'allowed-once') {
|
|
598
|
+
try {
|
|
599
|
+
learnFromApproval(request)
|
|
600
|
+
} catch (error) {
|
|
601
|
+
log('error', 'auto-whitelist-error', String(error))
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return outcome
|
|
605
|
+
})
|
|
438
606
|
}, { prepend: true })
|
|
439
607
|
|
|
440
608
|
ctx.on('tools/pre-execute', async (exec, next) => {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@gavinc-cn/dsh-path-rules",
|
|
3
3
|
"title": "路径规则守卫",
|
|
4
4
|
"description": "第五权限模式「路径规则」的逐调用闸门:项目内可读写 / 项目外读允许·写审批 / 白名单直通 / 黑名单读写都审批 / bash·pwsh 只读放行;经 tools/pre-execute 返回 allow|ask|deny。",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.1",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./lib/index.mjs",
|
|
8
8
|
"files": [
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"./locale/*.json": "./locale/*.json"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
|
-
"test": "node --test test/rules.test.mjs test/classify.test.mjs test/analyze-lex.test.mjs test/analyze-bash.test.mjs test/analyze-pwsh.test.mjs test/decide.test.mjs test/plugin.test.mjs test/analyze-security.test.mjs"
|
|
18
|
+
"test": "node --test test/rules.test.mjs test/classify.test.mjs test/analyze-lex.test.mjs test/analyze-bash.test.mjs test/analyze-pwsh.test.mjs test/decide.test.mjs test/plugin.test.mjs test/analyze-security.test.mjs test/auto-whitelist.test.mjs"
|
|
19
19
|
},
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"repository": {
|