@goodandready/dsh-approval-gate 0.1.20 → 0.1.21
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.md +8 -0
- package/lib/bands.js +243 -239
- package/lib/engine.js +27 -45
- package/lib/index.js +41 -84
- package/lib/measure-kinds.js +135 -0
- package/lib/messages.js +6 -0
- package/lib/snapshots.js +268 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.21
|
|
4
|
+
|
|
5
|
+
- Feature: Автоматические легковесные пре-снимки файлов (`Pre-execution Snapshots` в `lib/snapshots.js`) перед выполнением мутирующих инструментов с отслеживанием размера и FIFO-вытеснением по сессиям (Refs: #77).
|
|
6
|
+
- Feature: Однократный откат изменений (`Undo this change` / `rollbackSnapshot`) из сохраненного пре-снимка с восстановлением исходного контента или удалением созданного файла (Refs: #76).
|
|
7
|
+
- Feature: Двухуровневая очистка файловых снимков (`clearSnapshots` для безопасной очистки текущей сессии без влияния на соседние диалоги vs `clearAllSnapshots` для глобального сброса) (Refs: #78).
|
|
8
|
+
- Feature: Многоуровневая категоризация масштаба влияния операций (`Measure Kinds` в `lib/measure-kinds.js`: `local-single`, `workspace-wide`, `system-level`) с обогащением вердикта и фиксацией в аудите (Refs: #97).
|
|
9
|
+
- Security: Завершение перехода #163 — перевод конфигурации по умолчанию в строгий fail-closed режим (`defaultVerdict: 'ask'`) для неизвестных команд оболочки (Refs: #172).
|
|
10
|
+
|
|
3
11
|
## 0.1.20
|
|
4
12
|
|
|
5
13
|
- Feature: Безопасная альтернатива необратимому удалению (`Reversible Trash Move` в `lib/trash.js`) с переносом удаляемых файлов в изолированную сессионную корзину `<workspace>/.dsh/trash/<session_id>/` вместо деструктивного `rm` (Refs: #104).
|
package/lib/bands.js
CHANGED
|
@@ -1,239 +1,243 @@
|
|
|
1
|
-
// lib/bands.js
|
|
2
|
-
// Two-band deterministic filtering engine for dsh-approval-gate.
|
|
3
|
-
// Band 0: Hard Deny (fast regexes for exfiltration, fork-bombs, disk wipes)
|
|
4
|
-
// Band 1: Safe Allow (prefix globs & curated safe read-only inspection commands and tools)
|
|
5
|
-
|
|
6
|
-
export const DEFAULT_DENY_PATTERNS = Object.freeze([
|
|
7
|
-
/\b(?:nc|ncat|netcat)\s+.*-e\s+/i,
|
|
8
|
-
/\/dev\/tcp\/\d+\.\d+\.\d+\.\d+\/\d+/i,
|
|
9
|
-
/\/dev\/udp\/\d+\.\d+\.\d+\.\d+\/\d+/i,
|
|
10
|
-
/\bmkfs(?:\.[a-z0-9]+)?\s+/i,
|
|
11
|
-
/\bdd\s+.*of=\/dev\/(?:sd|hd|vd|xvd|nvme|mmcblk|loop|mapper)/i,
|
|
12
|
-
/:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/,
|
|
13
|
-
/\bchmod\s+(?:-R\s+)?(?:0?777|a\+rwx)\s+\/(?!tmp)/i,
|
|
14
|
-
/\bchown\s+(?:-R\s+)?root\b/i,
|
|
15
|
-
/\b(?:shutdown|reboot|poweroff|halt)\s*(?:-|$)/i,
|
|
16
|
-
]);
|
|
17
|
-
|
|
18
|
-
export const DEFAULT_ALLOW_GLOBS = Object.freeze([
|
|
19
|
-
// Git inspection commands
|
|
20
|
-
"git status*",
|
|
21
|
-
"git diff*",
|
|
22
|
-
"git log*",
|
|
23
|
-
"git branch*",
|
|
24
|
-
"git show*",
|
|
25
|
-
"git remote*",
|
|
26
|
-
"git tag*",
|
|
27
|
-
"git rev-parse*",
|
|
28
|
-
|
|
29
|
-
// File viewing & analysis
|
|
30
|
-
"cat *",
|
|
31
|
-
"head *",
|
|
32
|
-
"tail *",
|
|
33
|
-
"more *",
|
|
34
|
-
"less *",
|
|
35
|
-
"wc *",
|
|
36
|
-
"file *",
|
|
37
|
-
"stat *",
|
|
38
|
-
"diff *",
|
|
39
|
-
|
|
40
|
-
// Search & finding
|
|
41
|
-
"grep *",
|
|
42
|
-
"fgrep *",
|
|
43
|
-
"egrep *",
|
|
44
|
-
"rg *",
|
|
45
|
-
"ag *",
|
|
46
|
-
"find *",
|
|
47
|
-
|
|
48
|
-
// Directory listing & environment info
|
|
49
|
-
"ls*",
|
|
50
|
-
"dir*",
|
|
51
|
-
"pwd*",
|
|
52
|
-
"which *",
|
|
53
|
-
"whereis *",
|
|
54
|
-
"whoami*",
|
|
55
|
-
"hostname*",
|
|
56
|
-
"uname*",
|
|
57
|
-
"date*",
|
|
58
|
-
"uptime*",
|
|
59
|
-
"echo *",
|
|
60
|
-
"printf *",
|
|
61
|
-
|
|
62
|
-
// Language / runtime versions & test runners
|
|
63
|
-
"node --version*",
|
|
64
|
-
"node -v*",
|
|
65
|
-
"npm test*",
|
|
66
|
-
"npm --version*",
|
|
67
|
-
"npm -v*",
|
|
68
|
-
"pnpm test*",
|
|
69
|
-
"pnpm --version*",
|
|
70
|
-
"pnpm -v*",
|
|
71
|
-
"yarn test*",
|
|
72
|
-
"cargo test*",
|
|
73
|
-
"cargo check*",
|
|
74
|
-
"cargo --version*",
|
|
75
|
-
"pytest*",
|
|
76
|
-
"python --version*",
|
|
77
|
-
"python3 --version*",
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
"
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
*
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
|
|
1
|
+
// lib/bands.js
|
|
2
|
+
// Two-band deterministic filtering engine for dsh-approval-gate.
|
|
3
|
+
// Band 0: Hard Deny (fast regexes for exfiltration, fork-bombs, disk wipes)
|
|
4
|
+
// Band 1: Safe Allow (prefix globs & curated safe read-only inspection commands and tools)
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_DENY_PATTERNS = Object.freeze([
|
|
7
|
+
/\b(?:nc|ncat|netcat)\s+.*-e\s+/i,
|
|
8
|
+
/\/dev\/tcp\/\d+\.\d+\.\d+\.\d+\/\d+/i,
|
|
9
|
+
/\/dev\/udp\/\d+\.\d+\.\d+\.\d+\/\d+/i,
|
|
10
|
+
/\bmkfs(?:\.[a-z0-9]+)?\s+/i,
|
|
11
|
+
/\bdd\s+.*of=\/dev\/(?:sd|hd|vd|xvd|nvme|mmcblk|loop|mapper)/i,
|
|
12
|
+
/:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/,
|
|
13
|
+
/\bchmod\s+(?:-R\s+)?(?:0?777|a\+rwx)\s+\/(?!tmp)/i,
|
|
14
|
+
/\bchown\s+(?:-R\s+)?root\b/i,
|
|
15
|
+
/\b(?:shutdown|reboot|poweroff|halt)\s*(?:-|$)/i,
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_ALLOW_GLOBS = Object.freeze([
|
|
19
|
+
// Git inspection commands
|
|
20
|
+
"git status*",
|
|
21
|
+
"git diff*",
|
|
22
|
+
"git log*",
|
|
23
|
+
"git branch*",
|
|
24
|
+
"git show*",
|
|
25
|
+
"git remote*",
|
|
26
|
+
"git tag*",
|
|
27
|
+
"git rev-parse*",
|
|
28
|
+
|
|
29
|
+
// File viewing & analysis
|
|
30
|
+
"cat *",
|
|
31
|
+
"head *",
|
|
32
|
+
"tail *",
|
|
33
|
+
"more *",
|
|
34
|
+
"less *",
|
|
35
|
+
"wc *",
|
|
36
|
+
"file *",
|
|
37
|
+
"stat *",
|
|
38
|
+
"diff *",
|
|
39
|
+
|
|
40
|
+
// Search & finding
|
|
41
|
+
"grep *",
|
|
42
|
+
"fgrep *",
|
|
43
|
+
"egrep *",
|
|
44
|
+
"rg *",
|
|
45
|
+
"ag *",
|
|
46
|
+
"find *",
|
|
47
|
+
|
|
48
|
+
// Directory listing & environment info
|
|
49
|
+
"ls*",
|
|
50
|
+
"dir*",
|
|
51
|
+
"pwd*",
|
|
52
|
+
"which *",
|
|
53
|
+
"whereis *",
|
|
54
|
+
"whoami*",
|
|
55
|
+
"hostname*",
|
|
56
|
+
"uname*",
|
|
57
|
+
"date*",
|
|
58
|
+
"uptime*",
|
|
59
|
+
"echo *",
|
|
60
|
+
"printf *",
|
|
61
|
+
|
|
62
|
+
// Language / runtime versions & test runners
|
|
63
|
+
"node --version*",
|
|
64
|
+
"node -v*",
|
|
65
|
+
"npm test*",
|
|
66
|
+
"npm --version*",
|
|
67
|
+
"npm -v*",
|
|
68
|
+
"pnpm test*",
|
|
69
|
+
"pnpm --version*",
|
|
70
|
+
"pnpm -v*",
|
|
71
|
+
"yarn test*",
|
|
72
|
+
"cargo test*",
|
|
73
|
+
"cargo check*",
|
|
74
|
+
"cargo --version*",
|
|
75
|
+
"pytest*",
|
|
76
|
+
"python --version*",
|
|
77
|
+
"python3 --version*",
|
|
78
|
+
|
|
79
|
+
// Service status inspection
|
|
80
|
+
"systemctl is-active*",
|
|
81
|
+
"systemctl status*",
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
export const DEFAULT_READ_ONLY_TOOLS = Object.freeze([
|
|
85
|
+
"read",
|
|
86
|
+
"read_file",
|
|
87
|
+
"readFile",
|
|
88
|
+
"read_file_content",
|
|
89
|
+
"view_file",
|
|
90
|
+
"view",
|
|
91
|
+
"show",
|
|
92
|
+
"glob",
|
|
93
|
+
"grep",
|
|
94
|
+
"find_files",
|
|
95
|
+
"find_by_name",
|
|
96
|
+
"search_code",
|
|
97
|
+
"file_search",
|
|
98
|
+
"list_dir",
|
|
99
|
+
"list_directory",
|
|
100
|
+
"directory_list",
|
|
101
|
+
"tree",
|
|
102
|
+
"read_url_content",
|
|
103
|
+
"fetch_web_page",
|
|
104
|
+
"web_search",
|
|
105
|
+
"search_web",
|
|
106
|
+
"list_resources",
|
|
107
|
+
"read_resource",
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Compile a prefix-glob into an anchored RegExp.
|
|
112
|
+
* @param {string} pattern
|
|
113
|
+
* @returns {RegExp}
|
|
114
|
+
*/
|
|
115
|
+
export function compileGlob(pattern) {
|
|
116
|
+
let source = "";
|
|
117
|
+
for (const ch of pattern) {
|
|
118
|
+
if (ch === "*") source += ".*";
|
|
119
|
+
else if (ch === "?") source += ".";
|
|
120
|
+
else source += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
121
|
+
}
|
|
122
|
+
return new RegExp(`^${source}$`, "i");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export class BandsEngine {
|
|
126
|
+
/**
|
|
127
|
+
* @param {{
|
|
128
|
+
* denyPatterns?: Array<RegExp|string>,
|
|
129
|
+
* allowGlobs?: Array<RegExp|string>,
|
|
130
|
+
* readOnlyTools?: Array<string>
|
|
131
|
+
* }} [options]
|
|
132
|
+
*/
|
|
133
|
+
constructor(options = {}) {
|
|
134
|
+
this.denyPatterns = (options.denyPatterns || DEFAULT_DENY_PATTERNS).map((p) =>
|
|
135
|
+
typeof p === "string" ? new RegExp(p, "i") : p
|
|
136
|
+
);
|
|
137
|
+
this.allowGlobs = (options.allowGlobs || DEFAULT_ALLOW_GLOBS).map((g) =>
|
|
138
|
+
typeof g === "string" ? compileGlob(g) : g
|
|
139
|
+
);
|
|
140
|
+
this.readOnlyTools = new Set(options.readOnlyTools || DEFAULT_READ_ONLY_TOOLS);
|
|
141
|
+
|
|
142
|
+
// Derive read-only tools dynamically from host tool registry if provided (#156)
|
|
143
|
+
if (options.toolRegistry) {
|
|
144
|
+
try {
|
|
145
|
+
const tools = typeof options.toolRegistry.getTools === "function"
|
|
146
|
+
? options.toolRegistry.getTools()
|
|
147
|
+
: (Array.isArray(options.toolRegistry) ? options.toolRegistry : []);
|
|
148
|
+
for (const t of tools) {
|
|
149
|
+
if (t && (t.readOnly || t.readonly || t.isReadOnly) && typeof t.name === "string") {
|
|
150
|
+
this.readOnlyTools.add(t.name);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
// ignore
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Check if a tool is configured as an unconditional read-only tool.
|
|
161
|
+
* @param {string} tool
|
|
162
|
+
* @returns {boolean}
|
|
163
|
+
*/
|
|
164
|
+
isReadOnlyTool(tool) {
|
|
165
|
+
return typeof tool === "string" && this.readOnlyTools.has(tool.trim());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check if a command matches safe inspection patterns.
|
|
170
|
+
* @param {string} command
|
|
171
|
+
* @returns {boolean}
|
|
172
|
+
*/
|
|
173
|
+
isSafeInspectionCommand(command) {
|
|
174
|
+
if (typeof command !== "string" || command.trim() === "") return false;
|
|
175
|
+
const text = command.trim();
|
|
176
|
+
// Chained commands, subshells, substitutions, and redirections cannot match whole-string safe-allow
|
|
177
|
+
if (/[\n;&|`<>]|\$\([\s\S]*\)|\$\{[\s\S]*\}/.test(text)) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
for (const pattern of this.denyPatterns) {
|
|
181
|
+
if (pattern.test(text)) return false;
|
|
182
|
+
}
|
|
183
|
+
for (const glob of this.allowGlobs) {
|
|
184
|
+
if (glob.test(text)) return true;
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Register an additional read-only tool name.
|
|
191
|
+
* @param {string} tool
|
|
192
|
+
*/
|
|
193
|
+
addReadOnlyTool(tool) {
|
|
194
|
+
if (typeof tool === "string" && tool.trim()) {
|
|
195
|
+
this.readOnlyTools.add(tool.trim());
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Register an additional allow-glob pattern.
|
|
201
|
+
* @param {string} globPattern
|
|
202
|
+
*/
|
|
203
|
+
addAllowGlob(globPattern) {
|
|
204
|
+
if (typeof globPattern === "string" && globPattern.trim()) {
|
|
205
|
+
this.allowGlobs.push(compileGlob(globPattern.trim()));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Evaluates an execution request through Band 0 and Band 1.
|
|
211
|
+
* @param {string} tool - Tool name
|
|
212
|
+
* @param {string} [command] - Normalized command text (for bash)
|
|
213
|
+
* @returns {"deny" | "allow" | "unmatched"}
|
|
214
|
+
*/
|
|
215
|
+
evaluate(tool, command) {
|
|
216
|
+
if (this.isReadOnlyTool(tool)) {
|
|
217
|
+
return "allow";
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (typeof command === "string" && command.trim().length > 0) {
|
|
221
|
+
const text = command.trim();
|
|
222
|
+
|
|
223
|
+
// Band 0: Hard Deny
|
|
224
|
+
for (const pattern of this.denyPatterns) {
|
|
225
|
+
if (pattern.test(text)) {
|
|
226
|
+
return "deny";
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Band 1: Safe Allow (applies strictly to single commands without chaining, subshells or redirects)
|
|
231
|
+
if (!/[\n;&|`<>]|\$\([\s\S]*\)|\$\{[\s\S]*\}/.test(text)) {
|
|
232
|
+
for (const glob of this.allowGlobs) {
|
|
233
|
+
if (glob.test(text)) {
|
|
234
|
+
return "allow";
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return "unmatched";
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
package/lib/engine.js
CHANGED
|
@@ -13,13 +13,15 @@ import { DeclarativeRulesEngine } from "./rules-loader.js";
|
|
|
13
13
|
import { BlockCounters } from "./counters.js";
|
|
14
14
|
import { ContractGuard } from "./contract-guard.js";
|
|
15
15
|
import { OverengineeringGuard } from "./overengineering.js";
|
|
16
|
-
import { extractFilePath, DEFAULT_FILE_WRITE_TOOLS } from "./dispatcher.js";
|
|
16
|
+
import { extractFilePath, DEFAULT_BASH_TOOL, DEFAULT_FILE_WRITE_TOOLS } from "./dispatcher.js";
|
|
17
17
|
import { categorizeRiskBucket, extractRiskTags } from "./risk.js";
|
|
18
18
|
import { ClassifierLatencyTracker } from "./latency.js";
|
|
19
19
|
import { ArtifactRegistry } from "./artifacts.js";
|
|
20
20
|
import { isRmCommand, extractRmTargets, suggestTrashAlternative } from "./trash.js";
|
|
21
21
|
import { VersionGuard } from "./version-guard.js";
|
|
22
22
|
import { loadAgentsRules, evaluateAgentsRules } from "./agents-parser.js";
|
|
23
|
+
import { classifyMeasureKind } from "./measure-kinds.js";
|
|
24
|
+
import { SnapshotManager } from "./snapshots.js";
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* Determine whether an operation belongs to a critical risk category requiring lockedAsk (#52).
|
|
@@ -79,7 +81,7 @@ export class SecurityEngine {
|
|
|
79
81
|
this.classifier = options.classifier || null;
|
|
80
82
|
this.timeoutPolicy = options.timeoutPolicy === "permissive" ? "permissive" : "strict";
|
|
81
83
|
this.grantRegistry = options.grantRegistry || new SessionGrantStore(options);
|
|
82
|
-
this.defaultVerdict = options.defaultVerdict || "
|
|
84
|
+
this.defaultVerdict = options.defaultVerdict || "ask";
|
|
83
85
|
|
|
84
86
|
this.dryRun = Boolean(options.dryRun || options.shadowMode);
|
|
85
87
|
this.auditFailClosed = options.auditFailClosed === true;
|
|
@@ -87,33 +89,16 @@ export class SecurityEngine {
|
|
|
87
89
|
this.retryGuard = options.retryGuard !== undefined ? options.retryGuard : new RetryExhaustionGuard();
|
|
88
90
|
this.ledger = options.ledger !== undefined ? options.ledger : new QualityLedger();
|
|
89
91
|
this.counters = options.counters || new BlockCounters();
|
|
90
|
-
this.auditLogger = options.auditLogger !== undefined ? options.auditLogger : new AuditLogger({
|
|
91
|
-
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
this.declarativeRules = options.declarativeRules || new DeclarativeRulesEngine({
|
|
97
|
-
workspaceDir: this.inspectConfig?.workspaceDir,
|
|
98
|
-
});
|
|
99
|
-
this.contractGuard = options.contractGuard !== undefined
|
|
100
|
-
? options.contractGuard
|
|
101
|
-
: (options.taskContract ? new ContractGuard(options.taskContract) : null);
|
|
102
|
-
this.overengineeringGuard = options.overengineeringGuard !== undefined
|
|
103
|
-
? options.overengineeringGuard
|
|
104
|
-
: (options.maxFilesPerSession !== undefined || options.maxCreatedFilesPerSession !== undefined
|
|
105
|
-
? new OverengineeringGuard(options)
|
|
106
|
-
: null);
|
|
92
|
+
this.auditLogger = options.auditLogger !== undefined ? options.auditLogger : new AuditLogger({ failClosed: this.auditFailClosed });
|
|
93
|
+
if (this.auditFailClosed && this.auditLogger) this.auditLogger.failClosed = true;
|
|
94
|
+
this.declarativeRules = options.declarativeRules || new DeclarativeRulesEngine({ workspaceDir: this.inspectConfig?.workspaceDir });
|
|
95
|
+
this.contractGuard = options.contractGuard !== undefined ? options.contractGuard : (options.taskContract ? new ContractGuard(options.taskContract) : null);
|
|
96
|
+
this.overengineeringGuard = options.overengineeringGuard !== undefined ? options.overengineeringGuard : (options.maxFilesPerSession !== undefined || options.maxCreatedFilesPerSession !== undefined ? new OverengineeringGuard(options) : null);
|
|
107
97
|
this.latencyTracker = options.latencyTracker || new ClassifierLatencyTracker(options);
|
|
108
|
-
this.artifactRegistry = options.artifactRegistry !== undefined
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
this.
|
|
112
|
-
? options.versionGuard
|
|
113
|
-
: new VersionGuard({ workspaceDir: this.inspectConfig?.workspaceDir });
|
|
114
|
-
this.agentsRules = options.agentsRules !== undefined
|
|
115
|
-
? options.agentsRules
|
|
116
|
-
: (loadAgentsRules(this.inspectConfig?.workspaceDir)?.rules || null);
|
|
98
|
+
this.artifactRegistry = options.artifactRegistry !== undefined ? options.artifactRegistry : new ArtifactRegistry({ workspaceDir: this.inspectConfig?.workspaceDir });
|
|
99
|
+
this.versionGuard = options.versionGuard !== undefined ? options.versionGuard : new VersionGuard({ workspaceDir: this.inspectConfig?.workspaceDir });
|
|
100
|
+
this.agentsRules = options.agentsRules !== undefined ? options.agentsRules : (loadAgentsRules(this.inspectConfig?.workspaceDir)?.rules || null);
|
|
101
|
+
this.snapshotManager = options.snapshotManager !== undefined ? options.snapshotManager : new SnapshotManager({ workspaceDir: this.inspectConfig?.workspaceDir });
|
|
117
102
|
}
|
|
118
103
|
|
|
119
104
|
/**
|
|
@@ -493,8 +478,10 @@ export class SecurityEngine {
|
|
|
493
478
|
}
|
|
494
479
|
}
|
|
495
480
|
|
|
496
|
-
// Default verdict for unclassified commands (#163)
|
|
497
|
-
|
|
481
|
+
// Default verdict for unclassified commands (#163, #172)
|
|
482
|
+
const bashTool = this.inspectConfig?.toolName || DEFAULT_BASH_TOOL;
|
|
483
|
+
const isBashTool = toolName === bashTool || !toolName;
|
|
484
|
+
if (isBashTool && this.defaultVerdict === "ask") {
|
|
498
485
|
const askHit = { ask: true, reason: "unmatched-ask", snippet: command || toolName };
|
|
499
486
|
const res = { verdict: "ask", stage: "P4", reason: "unmatched-ask", hit: askHit };
|
|
500
487
|
this._applyLockedAskIfCritical(res, toolName, command, execution);
|
|
@@ -566,6 +553,7 @@ export class SecurityEngine {
|
|
|
566
553
|
if (!decision) return decision;
|
|
567
554
|
if (!decision.riskBucket) decision.riskBucket = categorizeRiskBucket(execution, context);
|
|
568
555
|
if (!decision.riskTags || !Array.isArray(decision.riskTags)) decision.riskTags = extractRiskTags(execution, context);
|
|
556
|
+
if (!decision.measureKind) decision.measureKind = classifyMeasureKind(execution, context);
|
|
569
557
|
const command = execution && execution.arguments && execution.arguments.command;
|
|
570
558
|
if (command && isRmCommand(command)) {
|
|
571
559
|
decision.trashAlternative = suggestTrashAlternative(command, this.inspectConfig?.workspaceDir, context?.sessionId);
|
|
@@ -573,21 +561,15 @@ export class SecurityEngine {
|
|
|
573
561
|
return decision;
|
|
574
562
|
}
|
|
575
563
|
|
|
576
|
-
getArtifacts(sessionId) {
|
|
577
|
-
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
return this.latencyTracker ? this.latencyTracker.getMetrics() : null;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
resetLatencyTracker() {
|
|
589
|
-
if (this.latencyTracker) this.latencyTracker.reset();
|
|
590
|
-
}
|
|
564
|
+
getArtifacts(sessionId) { return this.artifactRegistry ? this.artifactRegistry.getArtifacts(sessionId) : []; }
|
|
565
|
+
clearArtifacts(sessionId) { if (this.artifactRegistry) this.artifactRegistry.clearSession(sessionId); }
|
|
566
|
+
getLatencyMetrics() { return this.latencyTracker ? this.latencyTracker.getMetrics() : null; }
|
|
567
|
+
resetLatencyTracker() { if (this.latencyTracker) this.latencyTracker.reset(); }
|
|
568
|
+
takeSnapshot(targetPath, meta) { return this.snapshotManager ? this.snapshotManager.takeSnapshot(targetPath, meta) : null; }
|
|
569
|
+
rollbackSnapshot(id) { return this.snapshotManager ? this.snapshotManager.rollbackSnapshot(id) : { success: false, error: "no-snapshot-manager" }; }
|
|
570
|
+
getSnapshots(sessionId) { return this.snapshotManager ? this.snapshotManager.getSnapshots(sessionId) : []; }
|
|
571
|
+
clearSnapshots(sessionId) { return this.snapshotManager ? this.snapshotManager.clearSnapshots(sessionId) : { cleared: 0 }; }
|
|
572
|
+
clearAllSnapshots() { return this.snapshotManager ? this.snapshotManager.clearAllSnapshots() : { cleared: 0 }; }
|
|
591
573
|
}
|
|
592
574
|
|
|
593
575
|
|
package/lib/index.js
CHANGED
|
@@ -5,6 +5,9 @@ import { DeclarativeRulesEngine } from "./rules-loader.js";
|
|
|
5
5
|
import { LlmClassifier } from "./classifier.js";
|
|
6
6
|
import { ClassifierLatencyTracker } from "./latency.js";
|
|
7
7
|
import { assertHarnessCompatibility, provideServicePolyfill } from "./compat.js";
|
|
8
|
+
import { extractFilePath } from "./dispatcher.js";
|
|
9
|
+
import { SnapshotManager } from "./snapshots.js";
|
|
10
|
+
import { MEASURE_KINDS, classifyMeasureKind } from "./measure-kinds.js";
|
|
8
11
|
import Schema from "@deepseek-ai/schemastery";
|
|
9
12
|
|
|
10
13
|
if (typeof Schema.prototype?.volatile !== "function") {
|
|
@@ -208,8 +211,12 @@ export const Config = Schema.object({
|
|
|
208
211
|
unattended: Schema.boolean().default(false).volatile(),
|
|
209
212
|
/** Workspace directory for path traversal boundary checks. */
|
|
210
213
|
workspaceDir: Schema.string().default("").volatile(),
|
|
211
|
-
/** Default verdict for unclassified commands: '
|
|
212
|
-
defaultVerdict: Schema.union(["allow", "ask"]).default("
|
|
214
|
+
/** Default verdict for unclassified commands: 'ask' (default, fail-closed) or 'allow' (#163, #172). */
|
|
215
|
+
defaultVerdict: Schema.union(["allow", "ask"]).default("ask").volatile(),
|
|
216
|
+
/** Maximum retained file snapshots per session (#78). */
|
|
217
|
+
maxSnapshotsPerSession: Schema.number().default(50).volatile(),
|
|
218
|
+
/** Maximum file size in bytes for pre-execution snapshots (#77). */
|
|
219
|
+
maxSnapshotBytes: Schema.number().default(1048576).volatile(),
|
|
213
220
|
/** Dry-run / shadow mode for safe testing of rules on production (#128). */
|
|
214
221
|
dryRun: Schema.boolean().default(false).volatile(),
|
|
215
222
|
/** Shadow mode alias for dryRun (#128). */
|
|
@@ -313,7 +320,8 @@ export function apply(ctx, config) {
|
|
|
313
320
|
latencyTracker: new ClassifierLatencyTracker({
|
|
314
321
|
slaMs: cfg.classifierSlaMs !== undefined ? cfg.classifierSlaMs : 2000,
|
|
315
322
|
}),
|
|
316
|
-
defaultVerdict: cfg.defaultVerdict || "
|
|
323
|
+
defaultVerdict: cfg.defaultVerdict || "ask",
|
|
324
|
+
snapshotManager: new SnapshotManager({ workspaceDir: cfg.workspaceDir || "", maxSnapshotsPerSession: cfg.maxSnapshotsPerSession, maxSnapshotBytes: cfg.maxSnapshotBytes }),
|
|
317
325
|
dryRun: Boolean(cfg.dryRun || cfg.shadowMode),
|
|
318
326
|
auditFailClosed: Boolean(cfg.auditFailClosed),
|
|
319
327
|
auditLogger: new AuditLogger({
|
|
@@ -342,6 +350,12 @@ export function apply(ctx, config) {
|
|
|
342
350
|
resetLatencyTracker: () => (engine.resetLatencyTracker ? engine.resetLatencyTracker() : undefined),
|
|
343
351
|
getArtifacts: (sessionId) => (engine.getArtifacts ? engine.getArtifacts(sessionId) : []),
|
|
344
352
|
clearArtifacts: (sessionId) => (engine.clearArtifacts ? engine.clearArtifacts(sessionId) : undefined),
|
|
353
|
+
takeSnapshot: (targetPath, meta) => (engine.takeSnapshot ? engine.takeSnapshot(targetPath, meta) : null),
|
|
354
|
+
rollbackSnapshot: (id) => (engine.rollbackSnapshot ? engine.rollbackSnapshot(id) : { success: false, error: "no-engine" }),
|
|
355
|
+
getSnapshots: (sessionId) => (engine.getSnapshots ? engine.getSnapshots(sessionId) : []),
|
|
356
|
+
clearSnapshots: (sessionId) => (engine.clearSnapshots ? engine.clearSnapshots(sessionId) : { cleared: 0 }),
|
|
357
|
+
clearAllSnapshots: () => (engine.clearAllSnapshots ? engine.clearAllSnapshots() : { cleared: 0 }),
|
|
358
|
+
getSnapshotStats: () => (engine.snapshotManager ? engine.snapshotManager.getStats() : null),
|
|
345
359
|
getSnapshot: () => ({
|
|
346
360
|
active: true,
|
|
347
361
|
dryRun: Boolean(cfg.dryRun || cfg.shadowMode),
|
|
@@ -353,6 +367,7 @@ export function apply(ctx, config) {
|
|
|
353
367
|
cacheStats: engine.cache ? engine.cache.stats() : null,
|
|
354
368
|
breakerState: engine.breaker ? engine.breaker.getState() : null,
|
|
355
369
|
latencyMetrics: engine.getLatencyMetrics ? engine.getLatencyMetrics() : null,
|
|
370
|
+
snapshots: engine.snapshotManager ? engine.snapshotManager.getStats() : null,
|
|
356
371
|
harnessCompat: compat.capabilities,
|
|
357
372
|
}),
|
|
358
373
|
};
|
|
@@ -418,6 +433,10 @@ export function apply(ctx, config) {
|
|
|
418
433
|
};
|
|
419
434
|
return { kind: "ask", reason: askMessage(hit, who, translate, ctx && ctx.logger) };
|
|
420
435
|
}
|
|
436
|
+
if (engine.snapshotManager) {
|
|
437
|
+
const targetPath = extractFilePath(execution && execution.arguments);
|
|
438
|
+
if (targetPath) engine.takeSnapshot(targetPath, { sessionId, callId: execution && (execution.callId || execution.id), tool: execution && execution.name });
|
|
439
|
+
}
|
|
421
440
|
return next();
|
|
422
441
|
});
|
|
423
442
|
}
|
|
@@ -455,6 +474,10 @@ export function apply(ctx, config) {
|
|
|
455
474
|
logBlocked(ctx, hit, who);
|
|
456
475
|
return denyMessage(hit, who, translate, ctx && ctx.logger);
|
|
457
476
|
}
|
|
477
|
+
if (engine.snapshotManager) {
|
|
478
|
+
const targetPath = extractFilePath(execution && execution.arguments);
|
|
479
|
+
if (targetPath) engine.takeSnapshot(targetPath, { sessionId, callId: execution && (execution.callId || execution.id), tool: execution && execution.name });
|
|
480
|
+
}
|
|
458
481
|
return undefined;
|
|
459
482
|
};
|
|
460
483
|
|
|
@@ -476,96 +499,27 @@ export {
|
|
|
476
499
|
CIRCUIT_STATE_TRIPPED,
|
|
477
500
|
CIRCUIT_STATE_RESUMED,
|
|
478
501
|
} from "./breaker.js";
|
|
479
|
-
export {
|
|
480
|
-
|
|
481
|
-
DEFAULT_GRANT_TTL_MS,
|
|
482
|
-
DEFAULT_GRANT_MAX_USES,
|
|
483
|
-
matchGrantCriteria,
|
|
484
|
-
} from "./grants.js";
|
|
485
|
-
export {
|
|
486
|
-
canonicalizeCall,
|
|
487
|
-
canonicalString,
|
|
488
|
-
hashCall,
|
|
489
|
-
sortKeys,
|
|
490
|
-
} from "./canonical.js";
|
|
502
|
+
export { SessionGrantStore, DEFAULT_GRANT_TTL_MS, DEFAULT_GRANT_MAX_USES, matchGrantCriteria } from "./grants.js";
|
|
503
|
+
export { canonicalizeCall, canonicalString, hashCall, sortKeys } from "./canonical.js";
|
|
491
504
|
export { expandHome, resolveSafePath, isPathTraversal, canonicalizeFsPath } from "./paths.js";
|
|
492
505
|
export { splitSubcommands } from "./tokenizer.js";
|
|
493
506
|
export { redactCredentials, redactUrl } from "./redact.js";
|
|
494
|
-
export {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
} from "./dispatcher.js";
|
|
504
|
-
export {
|
|
505
|
-
parseGitArgv,
|
|
506
|
-
classifyGitOperation,
|
|
507
|
-
inspectGitCommand,
|
|
508
|
-
GIT_CATEGORY_READ,
|
|
509
|
-
GIT_CATEGORY_MUTATE_LOCAL,
|
|
510
|
-
GIT_CATEGORY_REMOTE,
|
|
511
|
-
GIT_CATEGORY_DESTRUCTIVE,
|
|
512
|
-
} from "./parsers/git.js";
|
|
513
|
-
export {
|
|
514
|
-
isDevicePath,
|
|
515
|
-
extractRedirects,
|
|
516
|
-
inspectRedirectSecurity,
|
|
517
|
-
inspectSilentErrors,
|
|
518
|
-
inspectPipelines,
|
|
519
|
-
} from "./parsers/shell-cmds.js";
|
|
520
|
-
export {
|
|
521
|
-
isLoopbackHost,
|
|
522
|
-
isLoopbackUrl,
|
|
523
|
-
isLoopbackCommand,
|
|
524
|
-
} from "./loopback.js";
|
|
525
|
-
export {
|
|
526
|
-
computeFingerprint,
|
|
527
|
-
computeFingerprintHash,
|
|
528
|
-
normalizeCommandString,
|
|
529
|
-
} from "./fingerprint.js";
|
|
530
|
-
export {
|
|
531
|
-
VerdictCache,
|
|
532
|
-
DEFAULT_CACHE_MAX_SIZE,
|
|
533
|
-
DEFAULT_CACHE_TTL_MS,
|
|
534
|
-
} from "./cache.js";
|
|
535
|
-
export {
|
|
536
|
-
RetryExhaustionGuard,
|
|
537
|
-
DEFAULT_MAX_RETRY_ATTEMPTS,
|
|
538
|
-
} from "./retry-exhaustion.js";
|
|
539
|
-
export {
|
|
540
|
-
QualityLedger,
|
|
541
|
-
MAX_LEDGER_EVENTS_PER_SESSION,
|
|
542
|
-
} from "./ledger.js";
|
|
543
|
-
export {
|
|
544
|
-
AuditLogger,
|
|
545
|
-
DEFAULT_AUDIT_LOG_RELATIVE,
|
|
546
|
-
DEFAULT_MAX_AUDIT_BYTES,
|
|
547
|
-
DEFAULT_MAX_AUDIT_LINES,
|
|
548
|
-
DEFAULT_RETAIN_LINES,
|
|
549
|
-
auditRotateContent,
|
|
550
|
-
} from "./audit-log.js";
|
|
507
|
+
export { CommandDispatcher, isReviewerAgent, extractFilePath, TOOL_KIND_SHELL, TOOL_KIND_FILE_WRITE, TOOL_KIND_FILE_READ, TOOL_KIND_NETWORK, TOOL_KIND_GENERIC } from "./dispatcher.js";
|
|
508
|
+
export { parseGitArgv, classifyGitOperation, inspectGitCommand, GIT_CATEGORY_READ, GIT_CATEGORY_MUTATE_LOCAL, GIT_CATEGORY_REMOTE, GIT_CATEGORY_DESTRUCTIVE } from "./parsers/git.js";
|
|
509
|
+
export { isDevicePath, extractRedirects, inspectRedirectSecurity, inspectSilentErrors, inspectPipelines } from "./parsers/shell-cmds.js";
|
|
510
|
+
export { isLoopbackHost, isLoopbackUrl, isLoopbackCommand } from "./loopback.js";
|
|
511
|
+
export { computeFingerprint, computeFingerprintHash, normalizeCommandString } from "./fingerprint.js";
|
|
512
|
+
export { VerdictCache, DEFAULT_CACHE_MAX_SIZE, DEFAULT_CACHE_TTL_MS } from "./cache.js";
|
|
513
|
+
export { RetryExhaustionGuard, DEFAULT_MAX_RETRY_ATTEMPTS } from "./retry-exhaustion.js";
|
|
514
|
+
export { QualityLedger, MAX_LEDGER_EVENTS_PER_SESSION } from "./ledger.js";
|
|
515
|
+
export { AuditLogger, DEFAULT_AUDIT_LOG_RELATIVE, DEFAULT_MAX_AUDIT_BYTES, DEFAULT_MAX_AUDIT_LINES, DEFAULT_RETAIN_LINES, auditRotateContent } from "./audit-log.js";
|
|
551
516
|
export { BlockCounters, categorizeReason } from "./counters.js";
|
|
552
517
|
export { isCriticalOperation } from "./engine.js";
|
|
553
518
|
export { parseYaml, parseScalar, stripComment } from "./yaml-parser.js";
|
|
554
519
|
export { matchConditionValue, evaluateWhen } from "./when-matcher.js";
|
|
555
520
|
export { HotwordsMatcher, DEFAULT_HOTWORDS, extractWords } from "./hotwords.js";
|
|
556
521
|
export { DeclarativeRulesEngine, BUILTIN_RULES_FILE, WORKSPACE_RULES_REL, USER_RULES_REL } from "./rules-loader.js";
|
|
557
|
-
export {
|
|
558
|
-
normalizeChatCompletionsUrl,
|
|
559
|
-
withLlmRetry,
|
|
560
|
-
sanitizeClassifierInput,
|
|
561
|
-
truncateClassifierContext,
|
|
562
|
-
isLowRiskExecution,
|
|
563
|
-
isShapeRejectError,
|
|
564
|
-
extractJsonPayload,
|
|
565
|
-
ClassifierVerdictSchema,
|
|
566
|
-
validateClassifierResponse,
|
|
567
|
-
LlmClassifier,
|
|
568
|
-
} from "./classifier.js";
|
|
522
|
+
export { normalizeChatCompletionsUrl, withLlmRetry, sanitizeClassifierInput, truncateClassifierContext, isLowRiskExecution, isShapeRejectError, extractJsonPayload, ClassifierVerdictSchema, validateClassifierResponse, LlmClassifier } from "./classifier.js";
|
|
569
523
|
export { ContractGuard } from "./contract-guard.js";
|
|
570
524
|
export { OverengineeringGuard, DEFAULT_MAX_FILES_PER_SESSION } from "./overengineering.js";
|
|
571
525
|
export { resolveTempRoot, isGlobalTempPath, checkTempIsolation } from "./paths.js";
|
|
@@ -577,5 +531,8 @@ export { ArtifactRegistry, normalizeArtifactPath } from "./artifacts.js";
|
|
|
577
531
|
export { parseAgentsSecurityRules, loadAgentsRules, evaluateAgentsRules } from "./agents-parser.js";
|
|
578
532
|
export { detectHarnessCapabilities, assertHarnessCompatibility, provideServicePolyfill, registerHookPolyfill } from "./compat.js";
|
|
579
533
|
export { parseSemver, isMajorBump, inspectPackageJsonDiff, VersionGuard } from "./version-guard.js";
|
|
534
|
+
export { SnapshotManager, DEFAULT_MAX_SNAPSHOTS_PER_SESSION, DEFAULT_MAX_SNAPSHOT_BYTES } from "./snapshots.js";
|
|
535
|
+
export { MEASURE_KINDS, classifyMeasureKind, isSystemLevelExecution, isWorkspaceWideExecution } from "./measure-kinds.js";
|
|
536
|
+
|
|
580
537
|
|
|
581
538
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// lib/measure-kinds.js
|
|
2
|
+
// Multi-level operational impact scale categorization (#97).
|
|
3
|
+
// Classifies execution into:
|
|
4
|
+
// - 'local-single': operation affects exactly one file or inspects local state
|
|
5
|
+
// - 'workspace-wide': operation affects the entire workspace/repository or multiple files
|
|
6
|
+
// - 'system-level': operation accesses or mutates OS-level, network, or environment state
|
|
7
|
+
|
|
8
|
+
export const MEASURE_KINDS = Object.freeze({
|
|
9
|
+
LOCAL_SINGLE: "local-single",
|
|
10
|
+
WORKSPACE_WIDE: "workspace-wide",
|
|
11
|
+
SYSTEM_LEVEL: "system-level",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const SYSTEM_BINARIES = new Set([
|
|
15
|
+
"sudo", "su", "doas",
|
|
16
|
+
"systemctl", "service", "journalctl", "init",
|
|
17
|
+
"apt", "apt-get", "dpkg", "apk", "dnf", "yum", "pacman", "zypper",
|
|
18
|
+
"curl", "wget", "nc", "netcat", "ncat", "telnet", "ssh", "scp", "sftp", "rsync",
|
|
19
|
+
"iptables", "nftables", "ufw", "firewall-cmd",
|
|
20
|
+
"crontab", "at",
|
|
21
|
+
"reboot", "shutdown", "poweroff", "halt",
|
|
22
|
+
"mount", "umount", "fdisk", "parted", "mkfs", "dd",
|
|
23
|
+
"insmod", "rmmod", "modprobe",
|
|
24
|
+
"docker", "podman", "containerd", "runc", "crictl", "kubectl",
|
|
25
|
+
"passwd", "useradd", "userdel", "usermod", "groupadd",
|
|
26
|
+
"kill", "killall", "pkill", "xkill",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const SYSTEM_PATH_REGEX = /((\/|\b)(etc|boot|sys|proc|dev|usr|var\/log|root)(\/|\b)|~?\/\.(ssh|gnupg|bashrc|zshrc|profile)(\/|\b))/i;
|
|
30
|
+
|
|
31
|
+
const WORKSPACE_COMMAND_PATTERNS = [
|
|
32
|
+
/^(npm|pnpm|yarn|bun)\s+(install|add|remove|uninstall|update|upgrade|ci|build|pack|publish|run)/i,
|
|
33
|
+
/^(cargo|go|dotnet|mvn|gradle)\s+(build|test|clean|install|run)/i,
|
|
34
|
+
/^git\s+(checkout|switch|merge|rebase|pull|push|reset|clean|branch|tag|stash|restore|clone)/i,
|
|
35
|
+
/^(find|grep|ripgrep|rg|ack|ag)\s+/i,
|
|
36
|
+
/\b(make|ninja|cmake|meson)\b/i,
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const SINGLE_FILE_TOOLS = new Set([
|
|
40
|
+
"view_file", "read_file", "view",
|
|
41
|
+
"write_to_file", "create_file", "write",
|
|
42
|
+
"replace_file_content", "edit_file", "patch_file",
|
|
43
|
+
"inspect_file",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const SYSTEM_LEVEL_TOOLS = new Set([
|
|
47
|
+
"read_url_content", "search_web", "web_search",
|
|
48
|
+
"browse", "http_request", "network_fetch",
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Classifies the operational impact scale for a tool call execution.
|
|
53
|
+
* @param {Record<string, any>} execution
|
|
54
|
+
* @param {Record<string, any>} [context]
|
|
55
|
+
* @returns {'local-single' | 'workspace-wide' | 'system-level'}
|
|
56
|
+
*/
|
|
57
|
+
export function classifyMeasureKind(execution, context = {}) {
|
|
58
|
+
if (!execution) return MEASURE_KINDS.LOCAL_SINGLE;
|
|
59
|
+
|
|
60
|
+
const toolName = execution.name || execution.tool || "";
|
|
61
|
+
const args = execution.arguments || {};
|
|
62
|
+
|
|
63
|
+
// 1. Tool-level system categorization
|
|
64
|
+
if (SYSTEM_LEVEL_TOOLS.has(toolName)) {
|
|
65
|
+
return MEASURE_KINDS.SYSTEM_LEVEL;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 2. Tool-level single-file mutations / reads
|
|
69
|
+
if (SINGLE_FILE_TOOLS.has(toolName)) {
|
|
70
|
+
const target = args.TargetFile || args.targetPath || args.path || args.file || "";
|
|
71
|
+
if (typeof target === "string" && SYSTEM_PATH_REGEX.test(target)) {
|
|
72
|
+
return MEASURE_KINDS.SYSTEM_LEVEL;
|
|
73
|
+
}
|
|
74
|
+
return MEASURE_KINDS.LOCAL_SINGLE;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 3. Shell / command inspection
|
|
78
|
+
const command = typeof args.command === "string" ? args.command.trim() : "";
|
|
79
|
+
if (!command) {
|
|
80
|
+
return MEASURE_KINDS.LOCAL_SINGLE;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Fast check: first word
|
|
84
|
+
const firstWord = command.split(/\s+/)[0]?.toLowerCase();
|
|
85
|
+
if (SYSTEM_BINARIES.has(firstWord)) {
|
|
86
|
+
return MEASURE_KINDS.SYSTEM_LEVEL;
|
|
87
|
+
}
|
|
88
|
+
if ((command.includes("/") || command.includes(".")) && SYSTEM_PATH_REGEX.test(command)) {
|
|
89
|
+
return MEASURE_KINDS.SYSTEM_LEVEL;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check subcommands or pipes for system binaries only if operators present
|
|
93
|
+
if (/[|;&]/.test(command)) {
|
|
94
|
+
const tokens = command.split(/[\s|;&]+/);
|
|
95
|
+
for (const token of tokens) {
|
|
96
|
+
if (SYSTEM_BINARIES.has(token.toLowerCase())) {
|
|
97
|
+
return MEASURE_KINDS.SYSTEM_LEVEL;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check workspace-wide patterns
|
|
103
|
+
for (const pattern of WORKSPACE_COMMAND_PATTERNS) {
|
|
104
|
+
if (pattern.test(command)) {
|
|
105
|
+
return MEASURE_KINDS.WORKSPACE_WIDE;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Multi-target rm or recursive rm
|
|
110
|
+
if (/^rm\s+/i.test(command)) {
|
|
111
|
+
if (/\s-[a-zA-Z]*r/i.test(command) || command.includes("*") || command.split(/\s+/).length > 3) {
|
|
112
|
+
return MEASURE_KINDS.WORKSPACE_WIDE;
|
|
113
|
+
}
|
|
114
|
+
return MEASURE_KINDS.LOCAL_SINGLE;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Simple single-target commands
|
|
118
|
+
if (/^(cat|head|tail|sed|awk|echo|touch|ls|stat|file)\b/i.test(command)) {
|
|
119
|
+
if (command.includes("*") || command.includes(";")) {
|
|
120
|
+
return MEASURE_KINDS.WORKSPACE_WIDE;
|
|
121
|
+
}
|
|
122
|
+
return MEASURE_KINDS.LOCAL_SINGLE;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Default fallback for unrecognized operations
|
|
126
|
+
return MEASURE_KINDS.LOCAL_SINGLE;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function isSystemLevelExecution(execution, context) {
|
|
130
|
+
return classifyMeasureKind(execution, context) === MEASURE_KINDS.SYSTEM_LEVEL;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function isWorkspaceWideExecution(execution, context) {
|
|
134
|
+
return classifyMeasureKind(execution, context) === MEASURE_KINDS.WORKSPACE_WIDE;
|
|
135
|
+
}
|
package/lib/messages.js
CHANGED
|
@@ -22,6 +22,7 @@ export const MESSAGES = Object.freeze({
|
|
|
22
22
|
detailUnsupported: "unsupported shell syntax",
|
|
23
23
|
approvalRequired: "Approval required",
|
|
24
24
|
askUnknown: "The command could not be fully inspected; review it before allowing",
|
|
25
|
+
askUnmatched: "The operation was not recognized by safety rules; human confirmation is required by default",
|
|
25
26
|
askUnparseable: "Shell syntax could not be fully inspected; review it before allowing",
|
|
26
27
|
askUnknownFormat: "The tool call format could not be inspected; review it before allowing",
|
|
27
28
|
askUnknownWriteTarget: "The file write target could not be identified; review it before allowing",
|
|
@@ -107,6 +108,7 @@ export const MESSAGES = Object.freeze({
|
|
|
107
108
|
hintTrashAlternative: "Move files to reversible .dsh/trash/<session>/ directory instead of rm",
|
|
108
109
|
hintAgentsMdProhibited: "Review project-specific guidelines and constraints defined in AGENTS.md",
|
|
109
110
|
hintMajorVersionBump: "Verify compatibility before bumping major dependency versions in package.json",
|
|
111
|
+
hintUnmatchedAsk: "Unrecognized operations default to fail-closed approval under defaultVerdict='ask'",
|
|
110
112
|
},
|
|
111
113
|
zh: {
|
|
112
114
|
blockedPrefix: "dsh-approval-gate 规则已阻止",
|
|
@@ -128,6 +130,7 @@ export const MESSAGES = Object.freeze({
|
|
|
128
130
|
detailUnsupported: "不支持的 shell 语法",
|
|
129
131
|
approvalRequired: "需要批准",
|
|
130
132
|
askUnknown: "命令无法完整检查;请在允许前进行审查",
|
|
133
|
+
askUnmatched: "该操作未被安全规则识别;默认需要人工确认",
|
|
131
134
|
askUnparseable: "Shell 语法无法完整检查;请在允许前进行审查",
|
|
132
135
|
askUnknownFormat: "工具调用格式无法检查;请在允许前进行审查",
|
|
133
136
|
askUnknownWriteTarget: "无法识别文件写入目标;请在允许前进行审查",
|
|
@@ -213,6 +216,7 @@ export const MESSAGES = Object.freeze({
|
|
|
213
216
|
hintTrashAlternative: "建议将文件移动到可逆的 .dsh/trash/<session>/ 目录,避免直接 rm",
|
|
214
217
|
hintAgentsMdProhibited: "请核对项目根目录 AGENTS.md 中定义的安全约束和禁止指令",
|
|
215
218
|
hintMajorVersionBump: "在升级 package.json 依赖主版本前请确认破坏性变更与兼容性",
|
|
219
|
+
hintUnmatchedAsk: "在 defaultVerdict='ask' 模式下,未识别的操作默认需要人工审核确认",
|
|
216
220
|
},
|
|
217
221
|
});
|
|
218
222
|
|
|
@@ -317,12 +321,14 @@ const HINT_KEYS = Object.freeze({
|
|
|
317
321
|
"agents-md-prohibited-command": "hintAgentsMdProhibited",
|
|
318
322
|
"agents-md-prohibited-path": "hintAgentsMdProhibited",
|
|
319
323
|
"major-version-bump": "hintMajorVersionBump",
|
|
324
|
+
"unmatched-ask": "hintUnmatchedAsk",
|
|
320
325
|
});
|
|
321
326
|
|
|
322
327
|
const ASK_KEYS = Object.freeze({
|
|
323
328
|
unparseable: "askUnparseable",
|
|
324
329
|
"unknown-format": "askUnknownFormat",
|
|
325
330
|
"unknown-write-target": "askUnknownWriteTarget",
|
|
331
|
+
"unmatched-ask": "askUnmatched",
|
|
326
332
|
"dynamic-command": "askDynamicCommand",
|
|
327
333
|
"dynamic-arguments": "askDynamicArguments",
|
|
328
334
|
"dynamic-redirect": "askDynamicRedirect",
|
package/lib/snapshots.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// lib/snapshots.js
|
|
2
|
+
// Pre-execution Snapshots (#77), Undo/Rollback (#76), and Dual-level Cleanup (#78).
|
|
3
|
+
// Zero external dependencies; safe filesystem operations with in-memory metadata.
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import crypto from "node:crypto";
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_MAX_SNAPSHOTS_PER_SESSION = 50;
|
|
10
|
+
export const DEFAULT_MAX_SNAPSHOT_BYTES = 1024 * 1024; // 1 MB
|
|
11
|
+
|
|
12
|
+
export class SnapshotManager {
|
|
13
|
+
/**
|
|
14
|
+
* @param {Record<string, any>} [options]
|
|
15
|
+
*/
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.maxSnapshotsPerSession = Number(options.maxSnapshotsPerSession) || DEFAULT_MAX_SNAPSHOTS_PER_SESSION;
|
|
18
|
+
this.maxSnapshotBytes = Number(options.maxSnapshotBytes) || DEFAULT_MAX_SNAPSHOT_BYTES;
|
|
19
|
+
this.workspaceDir = options.workspaceDir ? path.resolve(options.workspaceDir) : "";
|
|
20
|
+
|
|
21
|
+
/** @type {Map<string, Record<string, any>>} snapshotId -> Snapshot */
|
|
22
|
+
this.snapshots = new Map();
|
|
23
|
+
/** @type {Map<string, string[]>} sessionId -> [snapshotId, ...] */
|
|
24
|
+
this.sessionIndex = new Map();
|
|
25
|
+
/** @type {Map<string, string>} callId -> snapshotId */
|
|
26
|
+
this.callIndex = new Map();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolves absolute file path within workspace or host.
|
|
31
|
+
* @param {string} targetPath
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
resolvePath(targetPath) {
|
|
35
|
+
if (!targetPath || typeof targetPath !== "string") return "";
|
|
36
|
+
if (path.isAbsolute(targetPath)) return path.normalize(targetPath);
|
|
37
|
+
if (this.workspaceDir) return path.resolve(this.workspaceDir, targetPath);
|
|
38
|
+
return path.resolve(targetPath);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Captures a pre-execution snapshot before file mutation (#77).
|
|
43
|
+
* @param {string} targetPath - Relative or absolute path to target file
|
|
44
|
+
* @param {Record<string, any>} [meta] - Metadata (sessionId, callId, tool)
|
|
45
|
+
* @returns {Record<string, any>} Created snapshot record
|
|
46
|
+
*/
|
|
47
|
+
takeSnapshot(targetPath, meta = {}) {
|
|
48
|
+
const resolved = this.resolvePath(targetPath);
|
|
49
|
+
if (!resolved) {
|
|
50
|
+
return { success: false, error: "invalid-target-path" };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const sessionId = String(meta.sessionId || "default");
|
|
54
|
+
const callId = meta.callId ? String(meta.callId) : null;
|
|
55
|
+
const tool = meta.tool ? String(meta.tool) : "unknown";
|
|
56
|
+
|
|
57
|
+
let existed = false;
|
|
58
|
+
let content = null;
|
|
59
|
+
let tooLarge = false;
|
|
60
|
+
let size = 0;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
if (fs.existsSync(resolved)) {
|
|
64
|
+
existed = true;
|
|
65
|
+
const stat = fs.statSync(resolved);
|
|
66
|
+
size = stat.size;
|
|
67
|
+
if (size > this.maxSnapshotBytes) {
|
|
68
|
+
tooLarge = true;
|
|
69
|
+
} else {
|
|
70
|
+
content = fs.readFileSync(resolved, "utf8");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// If file unreadable or permission error, proceed safely
|
|
75
|
+
existed = false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const id = "snap_" + Date.now() + "_" + crypto.randomBytes(4).toString("hex");
|
|
79
|
+
const snapshot = {
|
|
80
|
+
id,
|
|
81
|
+
sessionId,
|
|
82
|
+
callId,
|
|
83
|
+
tool,
|
|
84
|
+
targetPath: resolved,
|
|
85
|
+
originalPath: targetPath,
|
|
86
|
+
existed,
|
|
87
|
+
content,
|
|
88
|
+
size,
|
|
89
|
+
tooLarge,
|
|
90
|
+
createdAt: Date.now(),
|
|
91
|
+
rolledBack: false,
|
|
92
|
+
rolledBackAt: null,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
this.snapshots.set(id, snapshot);
|
|
96
|
+
|
|
97
|
+
if (callId) {
|
|
98
|
+
this.callIndex.set(callId, id);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!this.sessionIndex.has(sessionId)) {
|
|
102
|
+
this.sessionIndex.set(sessionId, []);
|
|
103
|
+
}
|
|
104
|
+
const sessionList = this.sessionIndex.get(sessionId);
|
|
105
|
+
sessionList.push(id);
|
|
106
|
+
|
|
107
|
+
// Evict oldest if exceeding maxSnapshotsPerSession
|
|
108
|
+
if (sessionList.length > this.maxSnapshotsPerSession) {
|
|
109
|
+
const evictedId = sessionList.shift();
|
|
110
|
+
if (evictedId) {
|
|
111
|
+
const evicted = this.snapshots.get(evictedId);
|
|
112
|
+
if (evicted && evicted.callId) {
|
|
113
|
+
this.callIndex.delete(evicted.callId);
|
|
114
|
+
}
|
|
115
|
+
this.snapshots.delete(evictedId);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return snapshot;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Retrieves a snapshot by ID.
|
|
124
|
+
* @param {string} id
|
|
125
|
+
* @returns {Record<string, any> | null}
|
|
126
|
+
*/
|
|
127
|
+
getSnapshot(id) {
|
|
128
|
+
return this.snapshots.get(id) || null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Retrieves a snapshot by tool callId.
|
|
133
|
+
* @param {string} callId
|
|
134
|
+
* @returns {Record<string, any> | null}
|
|
135
|
+
*/
|
|
136
|
+
getSnapshotByCallId(callId) {
|
|
137
|
+
const id = this.callIndex.get(String(callId));
|
|
138
|
+
return id ? this.getSnapshot(id) : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Retrieves all snapshots for a session.
|
|
143
|
+
* @param {string} sessionId
|
|
144
|
+
* @returns {Record<string, any>[]}
|
|
145
|
+
*/
|
|
146
|
+
getSnapshots(sessionId) {
|
|
147
|
+
const ids = this.sessionIndex.get(String(sessionId)) || [];
|
|
148
|
+
const res = [];
|
|
149
|
+
for (const id of ids) {
|
|
150
|
+
const snap = this.snapshots.get(id);
|
|
151
|
+
if (snap) res.push(snap);
|
|
152
|
+
}
|
|
153
|
+
return res;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Undoes a file change by rolling back to its pre-execution snapshot (#76).
|
|
158
|
+
* @param {string} snapshotIdOrCallback - Snapshot ID or Call ID
|
|
159
|
+
* @returns {{ success: boolean, error?: string, snapshotId?: string, targetPath?: string, restoredExisted?: boolean }}
|
|
160
|
+
*/
|
|
161
|
+
rollbackSnapshot(snapshotIdOrCallback) {
|
|
162
|
+
if (!snapshotIdOrCallback) {
|
|
163
|
+
return { success: false, error: "snapshot-id-required" };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let snap = this.snapshots.get(snapshotIdOrCallback);
|
|
167
|
+
if (!snap) {
|
|
168
|
+
const id = this.callIndex.get(String(snapshotIdOrCallback));
|
|
169
|
+
if (id) snap = this.snapshots.get(id);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!snap) {
|
|
173
|
+
return { success: false, error: "snapshot-not-found" };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (snap.rolledBack) {
|
|
177
|
+
return { success: false, error: "snapshot-already-rolled-back", snapshotId: snap.id };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (snap.tooLarge) {
|
|
181
|
+
return { success: false, error: "snapshot-size-limit-exceeded", snapshotId: snap.id };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
if (snap.existed && snap.content !== null) {
|
|
186
|
+
// Restore previous content
|
|
187
|
+
const dir = path.dirname(snap.targetPath);
|
|
188
|
+
if (!fs.existsSync(dir)) {
|
|
189
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
190
|
+
}
|
|
191
|
+
fs.writeFileSync(snap.targetPath, snap.content, "utf8");
|
|
192
|
+
} else if (!snap.existed) {
|
|
193
|
+
// File was newly created by this call: delete it to undo
|
|
194
|
+
if (fs.existsSync(snap.targetPath)) {
|
|
195
|
+
fs.unlinkSync(snap.targetPath);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
snap.rolledBack = true;
|
|
200
|
+
snap.rolledBackAt = Date.now();
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
success: true,
|
|
204
|
+
snapshotId: snap.id,
|
|
205
|
+
targetPath: snap.targetPath,
|
|
206
|
+
restoredExisted: snap.existed,
|
|
207
|
+
rolledBackAt: snap.rolledBackAt,
|
|
208
|
+
};
|
|
209
|
+
} catch (err) {
|
|
210
|
+
return {
|
|
211
|
+
success: false,
|
|
212
|
+
error: "rollback-io-error",
|
|
213
|
+
detail: String(err && err.message || err),
|
|
214
|
+
snapshotId: snap.id,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Cleans up snapshots for a single session safely (#78).
|
|
221
|
+
* @param {string} sessionId
|
|
222
|
+
* @returns {{ cleared: number, sessionId: string }}
|
|
223
|
+
*/
|
|
224
|
+
clearSnapshots(sessionId) {
|
|
225
|
+
const sId = String(sessionId || "default");
|
|
226
|
+
const ids = this.sessionIndex.get(sId) || [];
|
|
227
|
+
let cleared = 0;
|
|
228
|
+
for (const id of ids) {
|
|
229
|
+
const snap = this.snapshots.get(id);
|
|
230
|
+
if (snap && snap.callId) {
|
|
231
|
+
this.callIndex.delete(snap.callId);
|
|
232
|
+
}
|
|
233
|
+
if (this.snapshots.delete(id)) {
|
|
234
|
+
cleared++;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
this.sessionIndex.delete(sId);
|
|
238
|
+
return { cleared, sessionId: sId };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Cleans up snapshots globally across all sessions (#78).
|
|
243
|
+
* @returns {{ cleared: number }}
|
|
244
|
+
*/
|
|
245
|
+
clearAllSnapshots() {
|
|
246
|
+
const total = this.snapshots.size;
|
|
247
|
+
this.snapshots.clear();
|
|
248
|
+
this.sessionIndex.clear();
|
|
249
|
+
this.callIndex.clear();
|
|
250
|
+
return { cleared: total };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Returns snapshot usage metrics and statistics.
|
|
255
|
+
* @returns {{ totalSnapshots: number, sessionCount: number, totalBytes: number }}
|
|
256
|
+
*/
|
|
257
|
+
getStats() {
|
|
258
|
+
let totalBytes = 0;
|
|
259
|
+
for (const snap of this.snapshots.values()) {
|
|
260
|
+
totalBytes += snap.size || 0;
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
totalSnapshots: this.snapshots.size,
|
|
264
|
+
sessionCount: this.sessionIndex.size,
|
|
265
|
+
totalBytes,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-approval-gate",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"description": "Host-only command safety gate for DeepSeek Harness: blocks recognized dangerous operations and requests DSH approval when a command cannot be inspected.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|