@goodandready/dsh-approval-gate 0.1.12 → 0.1.13
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 +11 -0
- package/lib/dispatcher.js +208 -0
- package/lib/index.js +31 -0
- package/lib/inspect.js +103 -160
- package/lib/loopback.js +85 -0
- package/lib/messages.js +43 -1
- package/lib/parsers/git.js +321 -0
- package/lib/parsers/shell-cmds.js +267 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.13
|
|
4
|
+
|
|
5
|
+
- Architecture: выделение `CommandDispatcher` (`lib/dispatcher.js`) для полной изоляции синтаксического разбора аргументов и классификации типов инструментов от движка принятия решений `SecurityEngine` (Refs: #133).
|
|
6
|
+
- Git: специализированный парсер семантики команд Git (`lib/parsers/git.js`) с разграничением безопасных операций чтения (`status`, `log`, `diff`, `rev-parse`), локальных мутаций (`commit`, `checkout -b`, `add`, `branch`) и деструктивных действий (`reset --hard`, `clean -f`, `branch -D`) (Refs: #125).
|
|
7
|
+
- Git: разделение локальных операций и сетевой публикации — автоматическое разрешение локальных коммитов и веток при обязательном запросе подтверждения (`git-remote-push`) для `git push` в удаленный репозиторий (Refs: #103).
|
|
8
|
+
- Shell: специализированный парсер конструкций оболочки (`lib/parsers/shell-cmds.js`) для детекции опасных перенаправлений в системные блочные устройства (`/dev/sd*`), защищенные файлы секретов и предотвращения обхода через traversal (Refs: #126).
|
|
9
|
+
- Security: `SilentErrorGuard` — детекция маскировки критических ошибок компиляции и сбоев команд через подавление потока stderr (`2>/dev/null`, `&>/dev/null`), принудительный нулевой код возврата (`|| true`, `|| :`, `|| exit 0`) и отключение проверки ошибок (`set +e`) (Refs: #90).
|
|
10
|
+
- Security: строгая изоляция агента-ревьюера (`Reviewer`) в режиме read-only без возможности запуска shell-процессов или записи/модификации файлов (Refs: #65).
|
|
11
|
+
- Security: предотвращение рекурсивной или системной модификации прав и владельцев (`chmod -R`, `chown -R`, манипуляции над `/`, `/etc`, `/var`, `/usr`, `/root`) (Refs: #65).
|
|
12
|
+
- Network: безопасный loopback bypass (`lib/loopback.js`) с нулевой задержкой и автоматическим разрешением запросов к `localhost`, `127.0.0.1`, `::1`, `0.0.0.0` для локальных dev-серверов и тестов (Refs: #40).
|
|
13
|
+
|
|
3
14
|
## 0.1.12
|
|
4
15
|
|
|
5
16
|
- Fix: корректная регистрация сервиса `approvalGate` в Cordis через `ctx.provide('approvalGate', ...)` и экспорт `export const provide = ['approvalGate']`, устраняющая ошибку `cannot set property "approvalGate" without provide` при старте хоста DSH.
|
|
@@ -0,0 +1,208 @@
|
|
|
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
|
+
"str_replace_editor",
|
|
25
|
+
"str_replace",
|
|
26
|
+
"apply_patch",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export const DEFAULT_READ_ONLY_TOOLS = Object.freeze([
|
|
30
|
+
"read_file",
|
|
31
|
+
"view_file",
|
|
32
|
+
"read_url_content",
|
|
33
|
+
"search_web",
|
|
34
|
+
"list_dir",
|
|
35
|
+
"glob",
|
|
36
|
+
"grep",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
export const DEFAULT_NETWORK_TOOLS = Object.freeze([
|
|
40
|
+
"fetch",
|
|
41
|
+
"web_fetch",
|
|
42
|
+
"http_request",
|
|
43
|
+
"read_url_content",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Extracts target file path from various tool argument schemas.
|
|
48
|
+
*
|
|
49
|
+
* @param {any} args
|
|
50
|
+
* @returns {string | null}
|
|
51
|
+
*/
|
|
52
|
+
export function extractFilePath(args) {
|
|
53
|
+
if (typeof args === "string") return args;
|
|
54
|
+
if (!args || typeof args !== "object") return null;
|
|
55
|
+
const keys = [
|
|
56
|
+
"path",
|
|
57
|
+
"file_path",
|
|
58
|
+
"filePath",
|
|
59
|
+
"file",
|
|
60
|
+
"filename",
|
|
61
|
+
"target",
|
|
62
|
+
"to",
|
|
63
|
+
"TargetFile",
|
|
64
|
+
"AbsolutePath",
|
|
65
|
+
];
|
|
66
|
+
for (const k of keys) {
|
|
67
|
+
if (typeof args[k] === "string" && args[k].trim()) {
|
|
68
|
+
return args[k].trim();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Checks whether an execution originates from a designated read-only reviewer agent.
|
|
76
|
+
* Resolves: #65 (strict read-only reviewer subagent isolation).
|
|
77
|
+
*
|
|
78
|
+
* @param {any} execution
|
|
79
|
+
* @param {any} [options]
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
export function isReviewerAgent(execution, options = {}) {
|
|
83
|
+
if (options && options.reviewerMode === true) return true;
|
|
84
|
+
const agent = execution && execution.agent;
|
|
85
|
+
if (!agent) return false;
|
|
86
|
+
|
|
87
|
+
const role = typeof agent.role === "string" ? agent.role.toLowerCase() : "";
|
|
88
|
+
const name = typeof agent.name === "string" ? agent.name.toLowerCase() : "";
|
|
89
|
+
const type = typeof agent.type === "string" ? agent.type.toLowerCase() : "";
|
|
90
|
+
|
|
91
|
+
return role.includes("review") || name.includes("review") || type.includes("review");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class CommandDispatcher {
|
|
95
|
+
/**
|
|
96
|
+
* @param {{
|
|
97
|
+
* toolName?: string,
|
|
98
|
+
* fileWriteTools?: string[] | Set<string>,
|
|
99
|
+
* readOnlyTools?: string[] | Set<string>,
|
|
100
|
+
* networkTools?: string[] | Set<string>,
|
|
101
|
+
* workspaceDir?: string
|
|
102
|
+
* }} [options]
|
|
103
|
+
*/
|
|
104
|
+
constructor(options = {}) {
|
|
105
|
+
this.toolName = options.toolName || DEFAULT_BASH_TOOL;
|
|
106
|
+
this.fileWriteTools = new Set(options.fileWriteTools || DEFAULT_FILE_WRITE_TOOLS);
|
|
107
|
+
this.readOnlyTools = new Set(options.readOnlyTools || DEFAULT_READ_ONLY_TOOLS);
|
|
108
|
+
this.networkTools = new Set(options.networkTools || DEFAULT_NETWORK_TOOLS);
|
|
109
|
+
this.workspaceDir = options.workspaceDir || process.cwd();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Categorizes the tool name into functional kind.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} name
|
|
116
|
+
* @returns {"shell" | "file_write" | "file_read" | "network" | "generic"}
|
|
117
|
+
*/
|
|
118
|
+
classifyTool(name) {
|
|
119
|
+
if (!name) return TOOL_KIND_GENERIC;
|
|
120
|
+
if (name === this.toolName) return TOOL_KIND_SHELL;
|
|
121
|
+
if (this.fileWriteTools.has(name)) return TOOL_KIND_FILE_WRITE;
|
|
122
|
+
if (this.readOnlyTools.has(name)) return TOOL_KIND_FILE_READ;
|
|
123
|
+
if (this.networkTools.has(name)) return TOOL_KIND_NETWORK;
|
|
124
|
+
return TOOL_KIND_GENERIC;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Dispatches and normalizes a tool execution request into a structured descriptor.
|
|
129
|
+
*
|
|
130
|
+
* @param {any} execution - The tool execution event carrying name, arguments, agent, session
|
|
131
|
+
* @param {any} [options] - Dynamic context overrides
|
|
132
|
+
* @returns {{
|
|
133
|
+
* tool: string,
|
|
134
|
+
* kind: "shell" | "file_write" | "file_read" | "network" | "generic",
|
|
135
|
+
* isMutating: boolean,
|
|
136
|
+
* reviewerViolation: boolean,
|
|
137
|
+
* command?: string,
|
|
138
|
+
* tokens?: any[],
|
|
139
|
+
* subcommands?: string[],
|
|
140
|
+
* git?: any,
|
|
141
|
+
* silentError?: any,
|
|
142
|
+
* loopback?: any,
|
|
143
|
+
* filePath?: string,
|
|
144
|
+
* pathSafety?: any
|
|
145
|
+
* }}
|
|
146
|
+
*/
|
|
147
|
+
dispatch(execution, options = {}) {
|
|
148
|
+
const name = execution && execution.name ? String(execution.name).trim() : "";
|
|
149
|
+
const args = execution ? execution.arguments : undefined;
|
|
150
|
+
const kind = this.classifyTool(name);
|
|
151
|
+
const isReviewer = isReviewerAgent(execution, options);
|
|
152
|
+
|
|
153
|
+
const isMutating = kind === TOOL_KIND_SHELL || kind === TOOL_KIND_FILE_WRITE;
|
|
154
|
+
const reviewerViolation = isReviewer && isMutating;
|
|
155
|
+
|
|
156
|
+
const descriptor = {
|
|
157
|
+
tool: name,
|
|
158
|
+
kind,
|
|
159
|
+
isMutating,
|
|
160
|
+
reviewerViolation,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
if (kind === TOOL_KIND_SHELL) {
|
|
164
|
+
const rawCommand = args && typeof args === "object" ? args.command : (typeof args === "string" ? args : "");
|
|
165
|
+
descriptor.command = typeof rawCommand === "string" ? rawCommand : "";
|
|
166
|
+
|
|
167
|
+
if (descriptor.command) {
|
|
168
|
+
const tokenized = tokenize(descriptor.command);
|
|
169
|
+
descriptor.tokens = tokenized.tokens || [];
|
|
170
|
+
descriptor.tokenizerOk = tokenized.ok;
|
|
171
|
+
descriptor.tokenizerReason = tokenized.reason;
|
|
172
|
+
|
|
173
|
+
// Subcommands breakdown
|
|
174
|
+
descriptor.subcommands = splitSubcommands(descriptor.command);
|
|
175
|
+
|
|
176
|
+
// Silent errors inspection (#90)
|
|
177
|
+
descriptor.silentError = inspectSilentErrors(descriptor.command);
|
|
178
|
+
|
|
179
|
+
// Git inspection (#125, #103)
|
|
180
|
+
if (/\bgit\b/.test(descriptor.command)) {
|
|
181
|
+
const firstWords = descriptor.command.trim().split(/\s+/);
|
|
182
|
+
descriptor.git = parseGitArgv(firstWords);
|
|
183
|
+
descriptor.gitClassification = classifyGitOperation(descriptor.git.verb, descriptor.git.args);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Loopback detection (#40)
|
|
187
|
+
descriptor.loopback = isLoopbackCommand(descriptor.command.trim().split(/\s+/));
|
|
188
|
+
}
|
|
189
|
+
} else if (kind === TOOL_KIND_FILE_WRITE) {
|
|
190
|
+
const filePath = extractFilePath(args);
|
|
191
|
+
descriptor.filePath = filePath;
|
|
192
|
+
if (filePath) {
|
|
193
|
+
const ws = options.workspaceDir || this.workspaceDir;
|
|
194
|
+
descriptor.pathSafety = resolveSafePath(filePath, ws);
|
|
195
|
+
descriptor.isProtected = isProtectedConfigPath(descriptor.pathSafety.canonical || filePath);
|
|
196
|
+
descriptor.isTraversal = !descriptor.pathSafety.safe;
|
|
197
|
+
}
|
|
198
|
+
} else if (kind === TOOL_KIND_NETWORK) {
|
|
199
|
+
const url = args && typeof args === "object" ? (args.url || args.Url || args.uri || args.Uri) : "";
|
|
200
|
+
descriptor.url = typeof url === "string" ? url : "";
|
|
201
|
+
if (descriptor.url) {
|
|
202
|
+
descriptor.isLoopback = isLoopbackUrl(descriptor.url);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return descriptor;
|
|
207
|
+
}
|
|
208
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -370,3 +370,34 @@ export {
|
|
|
370
370
|
export { expandHome, resolveSafePath, isPathTraversal, canonicalizeFsPath } from "./paths.js";
|
|
371
371
|
export { splitSubcommands } from "./tokenizer.js";
|
|
372
372
|
export { redactCredentials, redactUrl } from "./redact.js";
|
|
373
|
+
export {
|
|
374
|
+
CommandDispatcher,
|
|
375
|
+
isReviewerAgent,
|
|
376
|
+
extractFilePath,
|
|
377
|
+
TOOL_KIND_SHELL,
|
|
378
|
+
TOOL_KIND_FILE_WRITE,
|
|
379
|
+
TOOL_KIND_FILE_READ,
|
|
380
|
+
TOOL_KIND_NETWORK,
|
|
381
|
+
TOOL_KIND_GENERIC,
|
|
382
|
+
} from "./dispatcher.js";
|
|
383
|
+
export {
|
|
384
|
+
parseGitArgv,
|
|
385
|
+
classifyGitOperation,
|
|
386
|
+
inspectGitCommand,
|
|
387
|
+
GIT_CATEGORY_READ,
|
|
388
|
+
GIT_CATEGORY_MUTATE_LOCAL,
|
|
389
|
+
GIT_CATEGORY_REMOTE,
|
|
390
|
+
GIT_CATEGORY_DESTRUCTIVE,
|
|
391
|
+
} from "./parsers/git.js";
|
|
392
|
+
export {
|
|
393
|
+
isDevicePath,
|
|
394
|
+
extractRedirects,
|
|
395
|
+
inspectRedirectSecurity,
|
|
396
|
+
inspectSilentErrors,
|
|
397
|
+
inspectPipelines,
|
|
398
|
+
} from "./parsers/shell-cmds.js";
|
|
399
|
+
export {
|
|
400
|
+
isLoopbackHost,
|
|
401
|
+
isLoopbackUrl,
|
|
402
|
+
isLoopbackCommand,
|
|
403
|
+
} from "./loopback.js";
|