@goodandready/dsh-approval-gate 0.1.17 → 0.1.18
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/contract-guard.js +147 -0
- package/lib/dispatcher.js +209 -207
- package/lib/engine.js +48 -0
- package/lib/index.js +21 -1
- package/lib/inspect.js +573 -520
- package/lib/messages.js +39 -0
- package/lib/overengineering.js +97 -0
- package/lib/paths.js +378 -320
- package/package.json +1 -1
package/lib/messages.js
CHANGED
|
@@ -67,6 +67,14 @@ export const MESSAGES = Object.freeze({
|
|
|
67
67
|
ruleClassifierTimeoutPermissive: "Semantic classifier timed out; allowed by low-risk fallback policy",
|
|
68
68
|
askClassifierTimeout: "Semantic classifier request timed out; human confirmation required",
|
|
69
69
|
askClassifierError: "Semantic classifier error; human confirmation required",
|
|
70
|
+
askPayloadTooLarge: "Command arguments exceed maximum payload size limit; review before allowing",
|
|
71
|
+
ruleReviewerInjectionAttempt: "Prompt instruction injection detected in tool arguments",
|
|
72
|
+
askContractViolation: "Operation falls outside task contract boundaries; review before allowing",
|
|
73
|
+
ruleContractViolation: "Execution violated task contract boundaries",
|
|
74
|
+
askUnisolatedTempPath: "Target path uses global /tmp instead of isolated project temp directory; review before allowing",
|
|
75
|
+
ruleUnisolatedTempPath: "Target path uses unisolated global temp directory",
|
|
76
|
+
askOverengineeringLimit: "Session file creation limit exceeded; potential overengineering requires review before allowing",
|
|
77
|
+
ruleOverengineeringLimit: "Session exceeded maximum allowed created files limit",
|
|
70
78
|
hintRecursiveRm: "Specify exact target files instead of recursive deletion",
|
|
71
79
|
hintPathTraversal: "Keep operations strictly within the project directory",
|
|
72
80
|
hintSecretWrite: "Modify configuration via settings or environment variables, not raw file overwrite",
|
|
@@ -83,6 +91,11 @@ export const MESSAGES = Object.freeze({
|
|
|
83
91
|
hintFailClosedAudit: "Ensure the audit log directory is writable and disk space is available",
|
|
84
92
|
hintLockedAskUnattended: "Switch to an interactive session or request explicit human confirmation",
|
|
85
93
|
hintClassifierTimeout: "Check external LLM connectivity or review timeout policy settings",
|
|
94
|
+
hintPayloadTooLarge: "Reduce input payload size or chunk data into smaller batches",
|
|
95
|
+
hintReviewerInjectionAttempt: "Remove adversarial prompt override directives from input",
|
|
96
|
+
hintContractViolation: "Confine operations to permitted task tools and project scope",
|
|
97
|
+
hintUnisolatedTempPath: "Use workspace-isolated temporary directory (<workspace>/.dsh/tmp)",
|
|
98
|
+
hintOverengineeringLimit: "Reuse existing files and avoid creating redundant abstractions or boilerplate",
|
|
86
99
|
},
|
|
87
100
|
zh: {
|
|
88
101
|
blockedPrefix: "dsh-approval-gate 规则已阻止",
|
|
@@ -149,6 +162,14 @@ export const MESSAGES = Object.freeze({
|
|
|
149
162
|
ruleClassifierTimeoutPermissive: "语义分类器超时;根据低风险容错策略放行",
|
|
150
163
|
askClassifierTimeout: "语义分类器请求超时;需要人工审查",
|
|
151
164
|
askClassifierError: "语义分类器发生异常;需要人工审查",
|
|
165
|
+
askPayloadTooLarge: "命令参数超过最大负载大小限制;请在允许前进行审查",
|
|
166
|
+
ruleReviewerInjectionAttempt: "在工具参数中检测到提示词指令注入尝试",
|
|
167
|
+
askContractViolation: "操作超出任务契约范围;请在允许前进行审查",
|
|
168
|
+
ruleContractViolation: "执行违反了任务契约边界",
|
|
169
|
+
askUnisolatedTempPath: "目标路径使用全局 /tmp 而非隔离的项目临时目录;请在允许前进行审查",
|
|
170
|
+
ruleUnisolatedTempPath: "目标路径使用未隔离的全局临时目录",
|
|
171
|
+
askOverengineeringLimit: "会话创建文件数超过限制;潜在的过度工程需要人工审查",
|
|
172
|
+
ruleOverengineeringLimit: "会话超出最大允许创建文件数限制",
|
|
152
173
|
hintRecursiveRm: "请指定具体目标文件,避免递归删除",
|
|
153
174
|
hintPathTraversal: "请将操作限制在项目根目录内",
|
|
154
175
|
hintSecretWrite: "请通过设置或环境变量修改配置,不要直接覆盖敏感配置文件",
|
|
@@ -165,6 +186,11 @@ export const MESSAGES = Object.freeze({
|
|
|
165
186
|
hintFailClosedAudit: "请确保审计日志目录可写且磁盘空间充足",
|
|
166
187
|
hintLockedAskUnattended: "请切换到交互式会话或请求人工显式确认",
|
|
167
188
|
hintClassifierTimeout: "请检查外部 LLM 服务连通性或调整分类器超时容错策略",
|
|
189
|
+
hintPayloadTooLarge: "减小输入参数大小或分批传输数据",
|
|
190
|
+
hintReviewerInjectionAttempt: "从输入中移除对抗性指令覆盖指令",
|
|
191
|
+
hintContractViolation: "将操作限制在允许的任务工具和项目范围内",
|
|
192
|
+
hintUnisolatedTempPath: "使用工作区隔离的临时目录 (<workspace>/.dsh/tmp)",
|
|
193
|
+
hintOverengineeringLimit: "复用现有文件,避免创建多余的抽象或样板代码",
|
|
168
194
|
},
|
|
169
195
|
});
|
|
170
196
|
|
|
@@ -230,6 +256,10 @@ const RULE_KEYS = Object.freeze({
|
|
|
230
256
|
"classifier-timeout-strict": "ruleClassifierTimeoutStrict",
|
|
231
257
|
"classifier-timeout-fallback-strict": "ruleClassifierTimeoutStrict",
|
|
232
258
|
"classifier-timeout-fallback-permissive": "ruleClassifierTimeoutPermissive",
|
|
259
|
+
"reviewer-injection-attempt": "ruleReviewerInjectionAttempt",
|
|
260
|
+
"contract-violation": "ruleContractViolation",
|
|
261
|
+
"unisolated-temp-path": "ruleUnisolatedTempPath",
|
|
262
|
+
"overengineering-file-limit": "ruleOverengineeringLimit",
|
|
233
263
|
});
|
|
234
264
|
|
|
235
265
|
const HINT_KEYS = Object.freeze({
|
|
@@ -250,6 +280,11 @@ const HINT_KEYS = Object.freeze({
|
|
|
250
280
|
"locked-ask-unattended-denied": "hintLockedAskUnattended",
|
|
251
281
|
"classifier-timeout-strict": "hintClassifierTimeout",
|
|
252
282
|
"classifier-timeout-fallback-strict": "hintClassifierTimeout",
|
|
283
|
+
"payload-too-large": "hintPayloadTooLarge",
|
|
284
|
+
"reviewer-injection-attempt": "hintReviewerInjectionAttempt",
|
|
285
|
+
"contract-violation": "hintContractViolation",
|
|
286
|
+
"unisolated-temp-path": "hintUnisolatedTempPath",
|
|
287
|
+
"overengineering-file-limit": "hintOverengineeringLimit",
|
|
253
288
|
});
|
|
254
289
|
|
|
255
290
|
const ASK_KEYS = Object.freeze({
|
|
@@ -268,6 +303,10 @@ const ASK_KEYS = Object.freeze({
|
|
|
268
303
|
"classifier-timeout-strict": "askClassifierTimeout",
|
|
269
304
|
"classifier-timeout-fallback-strict": "askClassifierTimeout",
|
|
270
305
|
"classifier-error": "askClassifierError",
|
|
306
|
+
"payload-too-large": "askPayloadTooLarge",
|
|
307
|
+
"contract-violation": "askContractViolation",
|
|
308
|
+
"unisolated-temp-path": "askUnisolatedTempPath",
|
|
309
|
+
"overengineering-file-limit": "askOverengineeringLimit",
|
|
271
310
|
});
|
|
272
311
|
|
|
273
312
|
const DETAIL_KEYS = Object.freeze({
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// lib/overengineering.js
|
|
2
|
+
// Overengineering guard limiting excessive file creation per session (#89).
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_MAX_FILES_PER_SESSION = 10;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tracks and limits file creation within agent sessions to prevent codebase bloat and overengineering (#89).
|
|
8
|
+
*/
|
|
9
|
+
export class OverengineeringGuard {
|
|
10
|
+
/**
|
|
11
|
+
* @param {{ maxFilesPerSession?: number }} [options]
|
|
12
|
+
*/
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.maxFilesPerSession =
|
|
15
|
+
typeof options.maxFilesPerSession === "number" && options.maxFilesPerSession > 0
|
|
16
|
+
? options.maxFilesPerSession
|
|
17
|
+
: (typeof options.maxCreatedFilesPerSession === "number" && options.maxCreatedFilesPerSession > 0
|
|
18
|
+
? options.maxCreatedFilesPerSession
|
|
19
|
+
: DEFAULT_MAX_FILES_PER_SESSION);
|
|
20
|
+
this.maxCreatedFilesPerSession = this.maxFilesPerSession;
|
|
21
|
+
this._sessionFiles = new Map();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Gets or initializes the Set of files created in this session.
|
|
26
|
+
* @private
|
|
27
|
+
*/
|
|
28
|
+
_getSessionSet(sessionId) {
|
|
29
|
+
const key = sessionId || "default";
|
|
30
|
+
if (!this._sessionFiles.has(key)) {
|
|
31
|
+
this._sessionFiles.set(key, new Set());
|
|
32
|
+
}
|
|
33
|
+
return this._sessionFiles.get(key);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Checks file creation against threshold and records new files.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} sessionId
|
|
40
|
+
* @param {string} filePath
|
|
41
|
+
* @returns {{ allowed: boolean, reason?: string, count: number, limit: number, detail?: string }}
|
|
42
|
+
*/
|
|
43
|
+
checkAndRecord(sessionId, filePath) {
|
|
44
|
+
if (!filePath || typeof filePath !== "string") {
|
|
45
|
+
return { allowed: true, count: this.getCount(sessionId), limit: this.maxFilesPerSession };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const cleanPath = filePath.trim().replace(/\\/g, "/");
|
|
49
|
+
const set = this._getSessionSet(sessionId);
|
|
50
|
+
|
|
51
|
+
// If file was already created/tracked in this session, modifying it is permitted
|
|
52
|
+
if (set.has(cleanPath)) {
|
|
53
|
+
return { allowed: true, count: set.size, limit: this.maxFilesPerSession };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check if adding this file exceeds limit
|
|
57
|
+
if (set.size >= this.maxCreatedFilesPerSession) {
|
|
58
|
+
return {
|
|
59
|
+
allowed: false,
|
|
60
|
+
reason: "overengineering-file-limit",
|
|
61
|
+
count: set.size + 1,
|
|
62
|
+
limit: this.maxCreatedFilesPerSession,
|
|
63
|
+
detail: `Created ${set.size + 1} files in session, exceeding limit of ${this.maxCreatedFilesPerSession} files (#89)`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
set.add(cleanPath);
|
|
68
|
+
return { allowed: true, count: set.size, limit: this.maxFilesPerSession };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Returns current count of created files in session.
|
|
73
|
+
* @param {string} sessionId
|
|
74
|
+
* @returns {number}
|
|
75
|
+
*/
|
|
76
|
+
getCount(sessionId) {
|
|
77
|
+
const key = sessionId || "default";
|
|
78
|
+
const set = this._sessionFiles.get(key);
|
|
79
|
+
return set ? set.size : 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Clears session tracked files.
|
|
84
|
+
* @param {string} sessionId
|
|
85
|
+
*/
|
|
86
|
+
reset(sessionId) {
|
|
87
|
+
const key = sessionId || "default";
|
|
88
|
+
this._sessionFiles.delete(key);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Clears all session tracking.
|
|
93
|
+
*/
|
|
94
|
+
clear() {
|
|
95
|
+
this._sessionFiles.clear();
|
|
96
|
+
}
|
|
97
|
+
}
|