@goodandready/dsh-approval-gate 0.1.16 → 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 +162 -146
- package/lib/classifier.js +392 -0
- package/lib/contract-guard.js +147 -0
- package/lib/dispatcher.js +209 -207
- package/lib/engine.js +512 -407
- package/lib/index.js +57 -1
- package/lib/inspect.js +573 -520
- package/lib/messages.js +57 -0
- package/lib/overengineering.js +97 -0
- package/lib/paths.js +378 -320
- package/package.json +55 -55
package/lib/dispatcher.js
CHANGED
|
@@ -1,208 +1,210 @@
|
|
|
1
|
-
// lib/dispatcher.js
|
|
2
|
-
// Command Dispatcher: architectural isolation of command parsing and syntactic dispatch
|
|
3
|
-
// from the safety evaluation engine.
|
|
4
|
-
// Resolves: #133, #65
|
|
5
|
-
|
|
6
|
-
import { tokenize, splitSubcommands } from "./tokenizer.js";
|
|
7
|
-
import { parseGitArgv, classifyGitOperation } from "./parsers/git.js";
|
|
8
|
-
import { extractRedirects, inspectRedirectSecurity, inspectSilentErrors, inspectPipelines } from "./parsers/shell-cmds.js";
|
|
9
|
-
import { isLoopbackCommand, isLoopbackUrl } from "./loopback.js";
|
|
10
|
-
import { resolveSafePath, isPathTraversal, isProtectedConfigPath } from "./paths.js";
|
|
11
|
-
|
|
12
|
-
export const TOOL_KIND_SHELL = "shell";
|
|
13
|
-
export const TOOL_KIND_FILE_WRITE = "file_write";
|
|
14
|
-
export const TOOL_KIND_FILE_READ = "file_read";
|
|
15
|
-
export const TOOL_KIND_NETWORK = "network";
|
|
16
|
-
export const TOOL_KIND_GENERIC = "generic";
|
|
17
|
-
|
|
18
|
-
export const DEFAULT_BASH_TOOL = "bash";
|
|
19
|
-
export const DEFAULT_FILE_WRITE_TOOLS = Object.freeze([
|
|
20
|
-
"write",
|
|
21
|
-
"edit",
|
|
22
|
-
"Write",
|
|
23
|
-
"Edit",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* @
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
this.
|
|
108
|
-
this.
|
|
109
|
-
this.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (
|
|
122
|
-
if (this.
|
|
123
|
-
if (this.
|
|
124
|
-
return
|
|
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
|
-
const
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
descriptor.
|
|
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
|
-
descriptor.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
1
|
+
// lib/dispatcher.js
|
|
2
|
+
// Command Dispatcher: architectural isolation of command parsing and syntactic dispatch
|
|
3
|
+
// from the safety evaluation engine.
|
|
4
|
+
// Resolves: #133, #65
|
|
5
|
+
|
|
6
|
+
import { tokenize, splitSubcommands } from "./tokenizer.js";
|
|
7
|
+
import { parseGitArgv, classifyGitOperation } from "./parsers/git.js";
|
|
8
|
+
import { extractRedirects, inspectRedirectSecurity, inspectSilentErrors, inspectPipelines } from "./parsers/shell-cmds.js";
|
|
9
|
+
import { isLoopbackCommand, isLoopbackUrl } from "./loopback.js";
|
|
10
|
+
import { resolveSafePath, isPathTraversal, isProtectedConfigPath } from "./paths.js";
|
|
11
|
+
|
|
12
|
+
export const TOOL_KIND_SHELL = "shell";
|
|
13
|
+
export const TOOL_KIND_FILE_WRITE = "file_write";
|
|
14
|
+
export const TOOL_KIND_FILE_READ = "file_read";
|
|
15
|
+
export const TOOL_KIND_NETWORK = "network";
|
|
16
|
+
export const TOOL_KIND_GENERIC = "generic";
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_BASH_TOOL = "bash";
|
|
19
|
+
export const DEFAULT_FILE_WRITE_TOOLS = Object.freeze([
|
|
20
|
+
"write",
|
|
21
|
+
"edit",
|
|
22
|
+
"Write",
|
|
23
|
+
"Edit",
|
|
24
|
+
"write_to_file",
|
|
25
|
+
"replace_file_content",
|
|
26
|
+
"str_replace_editor",
|
|
27
|
+
"str_replace",
|
|
28
|
+
"apply_patch",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
export const DEFAULT_READ_ONLY_TOOLS = Object.freeze([
|
|
32
|
+
"read_file",
|
|
33
|
+
"view_file",
|
|
34
|
+
"read_url_content",
|
|
35
|
+
"search_web",
|
|
36
|
+
"list_dir",
|
|
37
|
+
"glob",
|
|
38
|
+
"grep",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
export const DEFAULT_NETWORK_TOOLS = Object.freeze([
|
|
42
|
+
"fetch",
|
|
43
|
+
"web_fetch",
|
|
44
|
+
"http_request",
|
|
45
|
+
"read_url_content",
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Extracts target file path from various tool argument schemas.
|
|
50
|
+
*
|
|
51
|
+
* @param {any} args
|
|
52
|
+
* @returns {string | null}
|
|
53
|
+
*/
|
|
54
|
+
export function extractFilePath(args) {
|
|
55
|
+
if (typeof args === "string") return args;
|
|
56
|
+
if (!args || typeof args !== "object") return null;
|
|
57
|
+
const keys = [
|
|
58
|
+
"path",
|
|
59
|
+
"file_path",
|
|
60
|
+
"filePath",
|
|
61
|
+
"file",
|
|
62
|
+
"filename",
|
|
63
|
+
"target",
|
|
64
|
+
"to",
|
|
65
|
+
"TargetFile",
|
|
66
|
+
"AbsolutePath",
|
|
67
|
+
];
|
|
68
|
+
for (const k of keys) {
|
|
69
|
+
if (typeof args[k] === "string" && args[k].trim()) {
|
|
70
|
+
return args[k].trim();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Checks whether an execution originates from a designated read-only reviewer agent.
|
|
78
|
+
* Resolves: #65 (strict read-only reviewer subagent isolation).
|
|
79
|
+
*
|
|
80
|
+
* @param {any} execution
|
|
81
|
+
* @param {any} [options]
|
|
82
|
+
* @returns {boolean}
|
|
83
|
+
*/
|
|
84
|
+
export function isReviewerAgent(execution, options = {}) {
|
|
85
|
+
if (options && options.reviewerMode === true) return true;
|
|
86
|
+
const agent = execution && execution.agent;
|
|
87
|
+
if (!agent) return false;
|
|
88
|
+
|
|
89
|
+
const role = typeof agent.role === "string" ? agent.role.toLowerCase() : "";
|
|
90
|
+
const name = typeof agent.name === "string" ? agent.name.toLowerCase() : "";
|
|
91
|
+
const type = typeof agent.type === "string" ? agent.type.toLowerCase() : "";
|
|
92
|
+
|
|
93
|
+
return role.includes("review") || name.includes("review") || type.includes("review");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class CommandDispatcher {
|
|
97
|
+
/**
|
|
98
|
+
* @param {{
|
|
99
|
+
* toolName?: string,
|
|
100
|
+
* fileWriteTools?: string[] | Set<string>,
|
|
101
|
+
* readOnlyTools?: string[] | Set<string>,
|
|
102
|
+
* networkTools?: string[] | Set<string>,
|
|
103
|
+
* workspaceDir?: string
|
|
104
|
+
* }} [options]
|
|
105
|
+
*/
|
|
106
|
+
constructor(options = {}) {
|
|
107
|
+
this.toolName = options.toolName || DEFAULT_BASH_TOOL;
|
|
108
|
+
this.fileWriteTools = new Set(options.fileWriteTools || DEFAULT_FILE_WRITE_TOOLS);
|
|
109
|
+
this.readOnlyTools = new Set(options.readOnlyTools || DEFAULT_READ_ONLY_TOOLS);
|
|
110
|
+
this.networkTools = new Set(options.networkTools || DEFAULT_NETWORK_TOOLS);
|
|
111
|
+
this.workspaceDir = options.workspaceDir || process.cwd();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Categorizes the tool name into functional kind.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} name
|
|
118
|
+
* @returns {"shell" | "file_write" | "file_read" | "network" | "generic"}
|
|
119
|
+
*/
|
|
120
|
+
classifyTool(name) {
|
|
121
|
+
if (!name) return TOOL_KIND_GENERIC;
|
|
122
|
+
if (name === this.toolName) return TOOL_KIND_SHELL;
|
|
123
|
+
if (this.fileWriteTools.has(name)) return TOOL_KIND_FILE_WRITE;
|
|
124
|
+
if (this.readOnlyTools.has(name)) return TOOL_KIND_FILE_READ;
|
|
125
|
+
if (this.networkTools.has(name)) return TOOL_KIND_NETWORK;
|
|
126
|
+
return TOOL_KIND_GENERIC;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Dispatches and normalizes a tool execution request into a structured descriptor.
|
|
131
|
+
*
|
|
132
|
+
* @param {any} execution - The tool execution event carrying name, arguments, agent, session
|
|
133
|
+
* @param {any} [options] - Dynamic context overrides
|
|
134
|
+
* @returns {{
|
|
135
|
+
* tool: string,
|
|
136
|
+
* kind: "shell" | "file_write" | "file_read" | "network" | "generic",
|
|
137
|
+
* isMutating: boolean,
|
|
138
|
+
* reviewerViolation: boolean,
|
|
139
|
+
* command?: string,
|
|
140
|
+
* tokens?: any[],
|
|
141
|
+
* subcommands?: string[],
|
|
142
|
+
* git?: any,
|
|
143
|
+
* silentError?: any,
|
|
144
|
+
* loopback?: any,
|
|
145
|
+
* filePath?: string,
|
|
146
|
+
* pathSafety?: any
|
|
147
|
+
* }}
|
|
148
|
+
*/
|
|
149
|
+
dispatch(execution, options = {}) {
|
|
150
|
+
const name = execution && execution.name ? String(execution.name).trim() : "";
|
|
151
|
+
const args = execution ? execution.arguments : undefined;
|
|
152
|
+
const kind = this.classifyTool(name);
|
|
153
|
+
const isReviewer = isReviewerAgent(execution, options);
|
|
154
|
+
|
|
155
|
+
const isMutating = kind === TOOL_KIND_SHELL || kind === TOOL_KIND_FILE_WRITE;
|
|
156
|
+
const reviewerViolation = isReviewer && isMutating;
|
|
157
|
+
|
|
158
|
+
const descriptor = {
|
|
159
|
+
tool: name,
|
|
160
|
+
kind,
|
|
161
|
+
isMutating,
|
|
162
|
+
reviewerViolation,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
if (kind === TOOL_KIND_SHELL) {
|
|
166
|
+
const rawCommand = args && typeof args === "object" ? args.command : (typeof args === "string" ? args : "");
|
|
167
|
+
descriptor.command = typeof rawCommand === "string" ? rawCommand : "";
|
|
168
|
+
|
|
169
|
+
if (descriptor.command) {
|
|
170
|
+
const tokenized = tokenize(descriptor.command);
|
|
171
|
+
descriptor.tokens = tokenized.tokens || [];
|
|
172
|
+
descriptor.tokenizerOk = tokenized.ok;
|
|
173
|
+
descriptor.tokenizerReason = tokenized.reason;
|
|
174
|
+
|
|
175
|
+
// Subcommands breakdown
|
|
176
|
+
descriptor.subcommands = splitSubcommands(descriptor.command);
|
|
177
|
+
|
|
178
|
+
// Silent errors inspection (#90)
|
|
179
|
+
descriptor.silentError = inspectSilentErrors(descriptor.command);
|
|
180
|
+
|
|
181
|
+
// Git inspection (#125, #103)
|
|
182
|
+
if (/\bgit\b/.test(descriptor.command)) {
|
|
183
|
+
const firstWords = descriptor.command.trim().split(/\s+/);
|
|
184
|
+
descriptor.git = parseGitArgv(firstWords);
|
|
185
|
+
descriptor.gitClassification = classifyGitOperation(descriptor.git.verb, descriptor.git.args);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Loopback detection (#40)
|
|
189
|
+
descriptor.loopback = isLoopbackCommand(descriptor.command.trim().split(/\s+/));
|
|
190
|
+
}
|
|
191
|
+
} else if (kind === TOOL_KIND_FILE_WRITE) {
|
|
192
|
+
const filePath = extractFilePath(args);
|
|
193
|
+
descriptor.filePath = filePath;
|
|
194
|
+
if (filePath) {
|
|
195
|
+
const ws = options.workspaceDir || this.workspaceDir;
|
|
196
|
+
descriptor.pathSafety = resolveSafePath(filePath, ws);
|
|
197
|
+
descriptor.isProtected = isProtectedConfigPath(descriptor.pathSafety.canonical || filePath);
|
|
198
|
+
descriptor.isTraversal = !descriptor.pathSafety.safe;
|
|
199
|
+
}
|
|
200
|
+
} else if (kind === TOOL_KIND_NETWORK) {
|
|
201
|
+
const url = args && typeof args === "object" ? (args.url || args.Url || args.uri || args.Uri) : "";
|
|
202
|
+
descriptor.url = typeof url === "string" ? url : "";
|
|
203
|
+
if (descriptor.url) {
|
|
204
|
+
descriptor.isLoopback = isLoopbackUrl(descriptor.url);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return descriptor;
|
|
209
|
+
}
|
|
208
210
|
}
|