agent-relay-runner 0.123.1 → 0.123.2
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.
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Bash",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "node \"${HOOK_DIR}/rg-r-guard.cjs\"",
|
|
10
|
+
"timeout": 5,
|
|
11
|
+
"statusMessage": "Checking ripgrep replace guard",
|
|
12
|
+
"suppressOutput": true
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
let input = "";
|
|
5
|
+
process.stdin.setEncoding("utf8");
|
|
6
|
+
process.stdin.on("data", (chunk) => {
|
|
7
|
+
input += chunk;
|
|
8
|
+
});
|
|
9
|
+
process.stdin.on("end", () => {
|
|
10
|
+
let payload;
|
|
11
|
+
try {
|
|
12
|
+
payload = JSON.parse(input || "{}");
|
|
13
|
+
} catch {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const toolName = String(payload.tool_name || payload.toolName || "").toLowerCase();
|
|
18
|
+
const command = String(payload.tool_input?.command || payload.toolInput?.command || "");
|
|
19
|
+
if (toolName !== "bash" || !command || !containsRipgrepReplaceFootgun(command)) return;
|
|
20
|
+
|
|
21
|
+
const reason = "Blocked: `rg -r` is ripgrep replace, not recursive search. Use `rg PATTERN PATH` for search, or `rg --replace REPLACEMENT PATTERN` only when replacement output is intentional.";
|
|
22
|
+
process.stdout.write(JSON.stringify({
|
|
23
|
+
hookSpecificOutput: {
|
|
24
|
+
hookEventName: "PreToolUse",
|
|
25
|
+
permissionDecision: "deny",
|
|
26
|
+
permissionDecisionReason: reason,
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
process.stdout.write("\n");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function containsRipgrepReplaceFootgun(command) {
|
|
33
|
+
for (const segment of splitShellSegments(command)) {
|
|
34
|
+
const words = shellWords(segment);
|
|
35
|
+
for (let i = 0; i < words.length; i += 1) {
|
|
36
|
+
const word = basename(words[i] || "");
|
|
37
|
+
if (word !== "rg" && word !== "ripgrep") continue;
|
|
38
|
+
if (ripgrepArgsUseReplace(words.slice(i + 1))) return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function ripgrepArgsUseReplace(args) {
|
|
45
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
46
|
+
const arg = args[i] || "";
|
|
47
|
+
if (arg === "--") return false;
|
|
48
|
+
if (arg === "--replace" || arg.startsWith("--replace=")) return true;
|
|
49
|
+
if (arg === "-r") return true;
|
|
50
|
+
if (/^-([A-Za-z]*r[A-Za-z]*|[A-Za-z]*r=.+)$/.test(arg)) return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function splitShellSegments(text) {
|
|
56
|
+
const segments = [];
|
|
57
|
+
let current = "";
|
|
58
|
+
let quote = "";
|
|
59
|
+
let escaped = false;
|
|
60
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
61
|
+
const ch = text[i];
|
|
62
|
+
if (escaped) {
|
|
63
|
+
current += ch;
|
|
64
|
+
escaped = false;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (ch === "\\") {
|
|
68
|
+
current += ch;
|
|
69
|
+
escaped = true;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (quote) {
|
|
73
|
+
current += ch;
|
|
74
|
+
if (ch === quote) quote = "";
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (ch === "'" || ch === "\"") {
|
|
78
|
+
quote = ch;
|
|
79
|
+
current += ch;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
// Command-substitution + grouping boundaries (`$(rg -r …)`, backtick `` `rg -r …` ``,
|
|
83
|
+
// subshells) split into their own segments so the inner command is scanned on its own
|
|
84
|
+
// (basename of `$(rg` ≠ rg otherwise) and its args never bleed into the outer command's
|
|
85
|
+
// scan. Only fires OUTSIDE quotes — a single/double-quoted literal is left intact, matching
|
|
86
|
+
// the tokenizer's existing quote handling. Known gap: substitution inside double quotes
|
|
87
|
+
// ("$(rg -r x)") is still treated as an opaque quoted word.
|
|
88
|
+
if (ch === "(" || ch === ")" || ch === "`") {
|
|
89
|
+
if (current.trim()) segments.push(current);
|
|
90
|
+
current = "";
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (ch === ";" || ch === "|" || ch === "&" || ch === "\n") {
|
|
94
|
+
if (current.trim()) segments.push(current);
|
|
95
|
+
current = "";
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
current += ch;
|
|
99
|
+
}
|
|
100
|
+
if (current.trim()) segments.push(current);
|
|
101
|
+
return segments;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function shellWords(text) {
|
|
105
|
+
const words = [];
|
|
106
|
+
let current = "";
|
|
107
|
+
let quote = "";
|
|
108
|
+
let escaped = false;
|
|
109
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
110
|
+
const ch = text[i];
|
|
111
|
+
if (escaped) {
|
|
112
|
+
current += ch;
|
|
113
|
+
escaped = false;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (ch === "\\") {
|
|
117
|
+
escaped = true;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (quote) {
|
|
121
|
+
if (ch === quote) quote = "";
|
|
122
|
+
else current += ch;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (ch === "'" || ch === "\"") {
|
|
126
|
+
quote = ch;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (/\s/.test(ch)) {
|
|
130
|
+
if (current) {
|
|
131
|
+
words.push(current);
|
|
132
|
+
current = "";
|
|
133
|
+
}
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
current += ch;
|
|
137
|
+
}
|
|
138
|
+
if (current) words.push(current);
|
|
139
|
+
return words;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function basename(value) {
|
|
143
|
+
return value.split("/").pop() || value;
|
|
144
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-runner",
|
|
3
|
-
"version": "0.123.
|
|
3
|
+
"version": "0.123.2",
|
|
4
4
|
"description": "Unified provider lifecycle runner for Agent Relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"src/**/*.ts",
|
|
12
12
|
"plugins/**",
|
|
13
|
-
"
|
|
13
|
+
"hooks/**",
|
|
14
|
+
"!src/**/*.test.ts",
|
|
15
|
+
"!hooks/**/*.test.ts"
|
|
14
16
|
],
|
|
15
17
|
"license": "AGPL-3.0-or-later",
|
|
16
18
|
"author": "Edin Mujkanovic <edin@exelerus.com>",
|