@reddb-io/red-skills 4.4.1 → 4.5.0
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/bin/build-info.mjs +87 -0
- package/bin/cli-frontdoor.mjs +14 -0
- package/bin/red-skills-brain.mjs +17 -22
- package/bin/red-skills-content.mjs +17 -0
- package/bin/red-skills-hook.mjs +45 -0
- package/bin/red-skills-memory.mjs +17 -22
- package/bin/red-skills-redskilled-mcp.mjs +2 -0
- package/bin/red-skills-resource.mjs +21 -0
- package/bin/runtime-compatibility.mjs +17 -0
- package/bin/runtime-paths.mjs +13 -0
- package/dist/code-nav.bundle.min.mjs +1 -1
- package/dist/opencode-host.bundle.min.mjs +1 -1
- package/dist/redskilled-mcp.bundle.min.mjs +36 -36
- package/dist/redskilled.bundle.min.mjs +1 -1
- package/dist/release.bundle.min.mjs +1 -1
- package/dist/rsp-core.bundle.min.mjs +1 -1
- package/dist/rsp.bundle.min.mjs +1 -1
- package/dist/statusline.bundle.min.mjs +1 -1
- package/package.json +9 -4
- package/runtime/hook-routes.json +125 -0
- package/runtime/plugins/brain/scripts/bootstrap.mjs +656 -0
- package/runtime/plugins/brain/scripts/structural-smoke.sh +7 -0
- package/runtime/plugins/dev/hooks/branch-lock-codex.sh +105 -0
- package/runtime/plugins/dev/hooks/code-nav-mcp.sh +63 -0
- package/runtime/plugins/dev/hooks/command-guard.sh +893 -0
- package/runtime/plugins/dev/hooks/ensure-codex-statusline.mjs +144 -0
- package/runtime/plugins/dev/hooks/red-fetch.mjs +38 -0
- package/runtime/plugins/dev/hooks/redskilled-mcp.sh +64 -0
- package/runtime/plugins/dev/hooks/rsp-hook.sh +343 -0
- package/runtime/plugins/dev/hooks/tests/command-guard.test.sh +378 -0
- package/runtime/plugins/dev/hooks/tests/rsp-hook.test.sh +229 -0
- package/runtime/plugins/dev/scripts/memory-bridge.sh +230 -0
- package/runtime/plugins/dev/scripts/structural-smoke.sh +7 -0
- package/runtime/plugins/dev/scripts/tests/memory-bridge.test.sh +555 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/cargo-pre-worktree.sh +37 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/envelope-post-attempt.sh +47 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/gradle-pre-worktree.sh +45 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/heartbeat-post-attempt.sh +43 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/validation-post-merge.sh +77 -0
- package/runtime/plugins/dev/skills/engineering/afk/detectors/cargo.sh +27 -0
- package/runtime/plugins/dev/skills/engineering/afk/detectors/gradle.sh +38 -0
- package/runtime/plugins/dev/skills/engineering/afk/examples/only-mine.sh +32 -0
- package/runtime/plugins/dev/skills/engineering/afk/examples/red-afk-attempt-action.yml +53 -0
- package/runtime/plugins/dev/skills/engineering/afk/examples/rs-afk-attempt.yml +76 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-cargo +38 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-envelope +50 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-gradle +42 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-heartbeat +46 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-validation +79 -0
- package/runtime/plugins/dev/skills/engineering/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/runtime/plugins/dev/skills/engineering/red-setup/scripts/install-runtime-shim.sh +181 -0
- package/runtime/plugins/dev/skills/engineering/red-setup/workflows/red-issues-needs-triage.yml +36 -0
- package/runtime/plugins/dev/skills/engineering/wizard/template.sh +211 -0
- package/runtime/plugins/dev/skills/engineering/zoom-out/scripts/graph-mode-plumbing.sh +36 -0
- package/runtime/plugins/dev/skills/engineering/zoom-out/scripts/tests/contract.test.sh +168 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock-hook.sh +113 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock-session-start.sh +81 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock.sh +68 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/dev-config.sh +126 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/git-command-classifier.sh +198 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/lock-store.sh +67 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/scope-resolver.sh +43 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/branch-lock-cli.test.sh +114 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/claude-plugin-hook.test.sh +117 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/codex-hook.test.sh +230 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/dev-config.test.sh +99 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/git-command-classifier.test.sh +187 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/lock-store.test.sh +90 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/scope-resolver.test.sh +63 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/session-start.test.sh +123 -0
- package/runtime/plugins/dev/skills/misc/git-guardrails-claude-code/scripts/block-dangerous-git.sh +231 -0
- package/runtime/plugins/dev/skills/misc/git-guardrails-claude-code/scripts/tests/block-dangerous-git.test.sh +143 -0
- package/runtime/plugins/internal/scripts/structural-smoke.sh +7 -0
- package/runtime/plugins/internal/skills/maintainer/create-plugin/scripts/create-plugin.sh +308 -0
- package/runtime/plugins/memory/scripts/bootstrap.mjs +659 -0
- package/runtime/plugins/memory/scripts/structural-smoke.sh +7 -0
- package/scripts/check-skills-boundary.mjs +32 -0
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Dev PreToolUse shell-command guard.
|
|
3
|
+
#
|
|
4
|
+
# Reads a Claude/Codex/OpenCode-style PreToolUse payload on stdin, extracts the
|
|
5
|
+
# shell command, and blocks it when it matches a deny rule from .red/config.yaml:
|
|
6
|
+
#
|
|
7
|
+
# command_guard:
|
|
8
|
+
# global:
|
|
9
|
+
# - "sudo *"
|
|
10
|
+
# - "rm -rf *"
|
|
11
|
+
# main:
|
|
12
|
+
# - "git rebase"
|
|
13
|
+
# worktree:
|
|
14
|
+
# - "git clean"
|
|
15
|
+
#
|
|
16
|
+
# plugins:
|
|
17
|
+
# dev:
|
|
18
|
+
# enabled: true
|
|
19
|
+
#
|
|
20
|
+
# The hook is dormant unless plugins.dev.enabled is true. Once enabled, the dev
|
|
21
|
+
# invariant is built in: agent-created git worktrees must live under .red/tmp/,
|
|
22
|
+
# and branch-moving commands are blocked in the primary checkout. Missing/empty
|
|
23
|
+
# deny rules still allow every other command. `global` rules apply in every
|
|
24
|
+
# scope, `main` rules apply outside RedSkills runtime worktrees, and `worktree`
|
|
25
|
+
# rules apply inside `/afk` and `/go` worktrees. Deny rules accept explicit
|
|
26
|
+
# modes: `regex:<pattern>`, `prefix:<literal>`, `suffix:<literal>`,
|
|
27
|
+
# `exact:<literal>`, and `glob:<pattern>`. Bare rules with glob metacharacters
|
|
28
|
+
# are treated as globs; every other bare rule matches the exact command, a
|
|
29
|
+
# command prefix, or a command suffix at a shell-command boundary.
|
|
30
|
+
|
|
31
|
+
set -uo pipefail
|
|
32
|
+
|
|
33
|
+
INPUT="$(timeout "${RED_SKILLS_HOOK_STDIN_TIMEOUT_S:-5s}" cat 2>/dev/null || true)"
|
|
34
|
+
|
|
35
|
+
allow() {
|
|
36
|
+
printf '{}'
|
|
37
|
+
exit 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
deny() {
|
|
41
|
+
local reason="$1"
|
|
42
|
+
printf '%s\n' "$reason" >&2
|
|
43
|
+
jq -nc --arg reason "$reason" '{
|
|
44
|
+
decision: "block",
|
|
45
|
+
reason: $reason,
|
|
46
|
+
hookSpecificOutput: {
|
|
47
|
+
hookEventName: "PreToolUse",
|
|
48
|
+
permissionDecision: "deny",
|
|
49
|
+
permissionDecisionReason: $reason
|
|
50
|
+
}
|
|
51
|
+
}'
|
|
52
|
+
exit 0
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
command -v jq >/dev/null 2>&1 || allow
|
|
56
|
+
|
|
57
|
+
COMMAND="$(
|
|
58
|
+
jq -r '
|
|
59
|
+
def command_value:
|
|
60
|
+
.tool_input.command? //
|
|
61
|
+
.tool_input.cmd? //
|
|
62
|
+
.tool_input.args.command? //
|
|
63
|
+
.input.command? //
|
|
64
|
+
.input.cmd? //
|
|
65
|
+
.arguments.command? //
|
|
66
|
+
.arguments.cmd? //
|
|
67
|
+
.command? //
|
|
68
|
+
.cmd? //
|
|
69
|
+
empty;
|
|
70
|
+
command_value
|
|
71
|
+
| if type == "array" then map(tostring) | join(" ")
|
|
72
|
+
elif type == "string" then .
|
|
73
|
+
else ""
|
|
74
|
+
end
|
|
75
|
+
' <<<"$INPUT" 2>/dev/null
|
|
76
|
+
)"
|
|
77
|
+
[[ -n "$COMMAND" && "$COMMAND" != "null" ]] || allow
|
|
78
|
+
|
|
79
|
+
ROOT="$(jq -r '.cwd // .workspace.current_dir // empty' <<<"$INPUT" 2>/dev/null)"
|
|
80
|
+
[[ -z "$ROOT" || "$ROOT" == "null" ]] && ROOT="${CLAUDE_PROJECT_DIR:-${CODEX_PROJECT_DIR:-}}"
|
|
81
|
+
[[ -z "$ROOT" || "$ROOT" == "null" ]] && ROOT="$(pwd)"
|
|
82
|
+
|
|
83
|
+
if [[ -n "$ROOT" && ! -d "$ROOT/.git" ]]; then
|
|
84
|
+
ROOT="$(git -C "$ROOT" rev-parse --show-toplevel 2>/dev/null || printf '%s' "$ROOT")"
|
|
85
|
+
fi
|
|
86
|
+
[[ -n "$ROOT" ]] || allow
|
|
87
|
+
|
|
88
|
+
find_config() {
|
|
89
|
+
local dir="$1"
|
|
90
|
+
local i
|
|
91
|
+
for ((i = 0; i < 64; i++)); do
|
|
92
|
+
if [[ -f "$dir/.red/config.yaml" ]]; then
|
|
93
|
+
printf '%s\n' "$dir/.red/config.yaml"
|
|
94
|
+
return 0
|
|
95
|
+
fi
|
|
96
|
+
[[ "$dir" == "/" ]] && break
|
|
97
|
+
dir="$(dirname "$dir")"
|
|
98
|
+
done
|
|
99
|
+
return 1
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
CONFIG="$(find_config "$ROOT" || true)"
|
|
103
|
+
[[ -n "$CONFIG" ]] || allow
|
|
104
|
+
REPO_ROOT="${CONFIG%/.red/config.yaml}"
|
|
105
|
+
|
|
106
|
+
trim() {
|
|
107
|
+
local s="$1"
|
|
108
|
+
s="${s#"${s%%[![:space:]]*}"}"
|
|
109
|
+
s="${s%"${s##*[![:space:]]}"}"
|
|
110
|
+
printf '%s' "$s"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
strip_comment() {
|
|
114
|
+
local s="$1"
|
|
115
|
+
if [[ "$s" != *\"* && "$s" != *\'* ]]; then
|
|
116
|
+
s="${s%%#*}"
|
|
117
|
+
fi
|
|
118
|
+
trim "$s"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
strip_line_comment() {
|
|
122
|
+
local s="$1"
|
|
123
|
+
if [[ "$s" != *\"* && "$s" != *\'* ]]; then
|
|
124
|
+
s="${s%%#*}"
|
|
125
|
+
fi
|
|
126
|
+
s="${s%"${s##*[![:space:]]}"}"
|
|
127
|
+
printf '%s' "$s"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
unquote_scalar() {
|
|
131
|
+
local s
|
|
132
|
+
s="$(strip_comment "$1")"
|
|
133
|
+
local q="${s:0:1}"
|
|
134
|
+
if [[ "$q" == '"' || "$q" == "'" ]]; then
|
|
135
|
+
local rest="${s:1}"
|
|
136
|
+
local before="${rest%%"$q"*}"
|
|
137
|
+
if [[ "$rest" == *"$q"* ]]; then
|
|
138
|
+
local close_len=$((1 + ${#before} + 1))
|
|
139
|
+
local tail="${s:$close_len}"
|
|
140
|
+
if [[ "$tail" =~ ^[[:space:]]*(#.*)?$ ]]; then
|
|
141
|
+
s="${s:1:${#before}}"
|
|
142
|
+
fi
|
|
143
|
+
fi
|
|
144
|
+
fi
|
|
145
|
+
trim "$s"
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
join_stack() {
|
|
149
|
+
local joined="" item
|
|
150
|
+
for item in "${STACK[@]}"; do
|
|
151
|
+
[[ -n "$joined" ]] && joined+="."
|
|
152
|
+
joined+="$item"
|
|
153
|
+
done
|
|
154
|
+
printf '%s' "$joined"
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
read_config_scalar() {
|
|
158
|
+
local wanted="$1"
|
|
159
|
+
local -a STACK=()
|
|
160
|
+
local -a INDENTS=()
|
|
161
|
+
local raw line indent_str indent rest key value full
|
|
162
|
+
|
|
163
|
+
while IFS= read -r raw || [[ -n "$raw" ]]; do
|
|
164
|
+
raw="${raw%$'\r'}"
|
|
165
|
+
line="$(strip_line_comment "$raw")"
|
|
166
|
+
[[ -z "${line//[[:space:]]/}" ]] && continue
|
|
167
|
+
indent_str="${raw%%[![:space:]]*}"
|
|
168
|
+
indent=${#indent_str}
|
|
169
|
+
((indent % 2 == 0)) || return 1
|
|
170
|
+
rest="${line:$indent}"
|
|
171
|
+
|
|
172
|
+
[[ "$rest" =~ ^[A-Za-z_][A-Za-z0-9_-]*: ]] || continue
|
|
173
|
+
key="${rest%%:*}"
|
|
174
|
+
value="${rest#*:}"
|
|
175
|
+
value="$(trim "$value")"
|
|
176
|
+
|
|
177
|
+
while ((${#INDENTS[@]} > 0 && INDENTS[${#INDENTS[@]} - 1] >= indent)); do
|
|
178
|
+
unset "STACK[$((${#STACK[@]} - 1))]"
|
|
179
|
+
unset "INDENTS[$((${#INDENTS[@]} - 1))]"
|
|
180
|
+
done
|
|
181
|
+
|
|
182
|
+
full="$(join_stack)"
|
|
183
|
+
[[ -n "$full" ]] && full+="."
|
|
184
|
+
full+="$key"
|
|
185
|
+
|
|
186
|
+
if [[ -z "$value" ]]; then
|
|
187
|
+
STACK+=("$key")
|
|
188
|
+
INDENTS+=("$indent")
|
|
189
|
+
continue
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
if [[ "$full" == "$wanted" ]]; then
|
|
193
|
+
unquote_scalar "$value"
|
|
194
|
+
return 0
|
|
195
|
+
fi
|
|
196
|
+
done <"$CONFIG"
|
|
197
|
+
|
|
198
|
+
return 1
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
enabled="$(read_config_scalar "plugins.dev.enabled" || true)"
|
|
202
|
+
[[ "$enabled" == "true" ]] || allow
|
|
203
|
+
|
|
204
|
+
scope_is_command_guard_worktree() {
|
|
205
|
+
local root="$1"
|
|
206
|
+
case "$root" in
|
|
207
|
+
*/.red/tmp/work-* | */.red/tmp/work-*/*) return 0 ;;
|
|
208
|
+
*/.red/tmp/workers/*/worktree | */.red/tmp/workers/*/worktree/*) return 0 ;;
|
|
209
|
+
*) return 1 ;;
|
|
210
|
+
esac
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
COMMAND_GUARD_SCOPE="main"
|
|
214
|
+
scope_is_command_guard_worktree "$ROOT" && COMMAND_GUARD_SCOPE="worktree"
|
|
215
|
+
|
|
216
|
+
canonical_path() {
|
|
217
|
+
local base="$1"
|
|
218
|
+
local path="$2"
|
|
219
|
+
if [[ "$path" == /* ]]; then
|
|
220
|
+
realpath -m -- "$path" 2>/dev/null || printf '%s\n' "$path"
|
|
221
|
+
else
|
|
222
|
+
realpath -m -- "$base/$path" 2>/dev/null || printf '%s/%s\n' "$base" "$path"
|
|
223
|
+
fi
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
git_subcommand_index() {
|
|
227
|
+
local -n tokens_ref="$1"
|
|
228
|
+
local git_index="$2"
|
|
229
|
+
local -n cwd_ref="$3"
|
|
230
|
+
local k=$((git_index + 1))
|
|
231
|
+
local len=${#tokens_ref[@]}
|
|
232
|
+
while ((k < len)); do
|
|
233
|
+
local tok="${tokens_ref[k]}"
|
|
234
|
+
case "$tok" in
|
|
235
|
+
-C)
|
|
236
|
+
if ((k + 1 < len)); then
|
|
237
|
+
cwd_ref="$(canonical_path "$cwd_ref" "${tokens_ref[k + 1]}")"
|
|
238
|
+
fi
|
|
239
|
+
k=$((k + 2))
|
|
240
|
+
continue
|
|
241
|
+
;;
|
|
242
|
+
-c|--git-dir|--work-tree|--namespace|--exec-path|--super-prefix)
|
|
243
|
+
k=$((k + 2))
|
|
244
|
+
continue
|
|
245
|
+
;;
|
|
246
|
+
--*=*|-*)
|
|
247
|
+
k=$((k + 1))
|
|
248
|
+
continue
|
|
249
|
+
;;
|
|
250
|
+
*)
|
|
251
|
+
break
|
|
252
|
+
;;
|
|
253
|
+
esac
|
|
254
|
+
done
|
|
255
|
+
printf '%s\n' "$k"
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
skip_git_worktree_add_option() {
|
|
259
|
+
local -n tokens_ref="$1"
|
|
260
|
+
local -n index_ref="$2"
|
|
261
|
+
local len=${#tokens_ref[@]}
|
|
262
|
+
local tok="${tokens_ref[index_ref]}"
|
|
263
|
+
case "$tok" in
|
|
264
|
+
-b|-B|--orphan|--reason)
|
|
265
|
+
index_ref=$((index_ref + 2))
|
|
266
|
+
return 0
|
|
267
|
+
;;
|
|
268
|
+
--reason=*|--orphan=*)
|
|
269
|
+
index_ref=$((index_ref + 1))
|
|
270
|
+
return 0
|
|
271
|
+
;;
|
|
272
|
+
--detach|--checkout|--no-checkout|--guess-remote|--no-guess-remote|--force|-f|--lock)
|
|
273
|
+
index_ref=$((index_ref + 1))
|
|
274
|
+
return 0
|
|
275
|
+
;;
|
|
276
|
+
--)
|
|
277
|
+
index_ref=$((index_ref + 1))
|
|
278
|
+
return 1
|
|
279
|
+
;;
|
|
280
|
+
--*)
|
|
281
|
+
index_ref=$((index_ref + 1))
|
|
282
|
+
return 0
|
|
283
|
+
;;
|
|
284
|
+
-*)
|
|
285
|
+
# Unknown short option. Treat it as a flag so the destination is still the
|
|
286
|
+
# first non-option token; if the option actually consumed an argument, the
|
|
287
|
+
# guard fails closed on a non-.red/tmp "destination".
|
|
288
|
+
index_ref=$((index_ref + 1))
|
|
289
|
+
return 0
|
|
290
|
+
;;
|
|
291
|
+
*)
|
|
292
|
+
((index_ref < len))
|
|
293
|
+
return 1
|
|
294
|
+
;;
|
|
295
|
+
esac
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
worktree_add_destination() {
|
|
299
|
+
local tokens_name="$1"
|
|
300
|
+
local -n tokens_ref="$tokens_name"
|
|
301
|
+
local git_index="$2"
|
|
302
|
+
local cwd="$ROOT"
|
|
303
|
+
local s
|
|
304
|
+
s="$(git_subcommand_index "$tokens_name" "$git_index" cwd)"
|
|
305
|
+
[[ "${tokens_ref[s]:-}" == "worktree" && "${tokens_ref[s + 1]:-}" == "add" ]] || return 1
|
|
306
|
+
|
|
307
|
+
local j=$((s + 2))
|
|
308
|
+
local len=${#tokens_ref[@]}
|
|
309
|
+
while ((j < len)); do
|
|
310
|
+
if skip_git_worktree_add_option "$tokens_name" j; then
|
|
311
|
+
continue
|
|
312
|
+
fi
|
|
313
|
+
[[ -n "${tokens_ref[j]:-}" ]] || return 1
|
|
314
|
+
printf '%s\t%s\n' "$cwd" "${tokens_ref[j]}"
|
|
315
|
+
return 0
|
|
316
|
+
done
|
|
317
|
+
return 1
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
primary_branch_movement_reason() {
|
|
321
|
+
local tokens_name="$1"
|
|
322
|
+
local -n tokens_ref="$tokens_name"
|
|
323
|
+
local n=${#tokens_ref[@]}
|
|
324
|
+
local i j s sub
|
|
325
|
+
|
|
326
|
+
for ((i = 0; i < n; i++)); do
|
|
327
|
+
if [[ "${tokens_ref[i]}" == "gh" && "${tokens_ref[i + 1]:-}" == "pr" && "${tokens_ref[i + 2]:-}" == "checkout" ]]; then
|
|
328
|
+
printf 'gh pr checkout changes the primary checkout; create or reuse a .red/tmp worktree for the PR branch instead'
|
|
329
|
+
return 0
|
|
330
|
+
fi
|
|
331
|
+
|
|
332
|
+
[[ "${tokens_ref[i]}" == "git" ]] || continue
|
|
333
|
+
local cwd="$ROOT"
|
|
334
|
+
s="$(git_subcommand_index "$tokens_name" "$i" cwd)"
|
|
335
|
+
sub="${tokens_ref[s]:-}"
|
|
336
|
+
case "$sub" in
|
|
337
|
+
checkout|switch)
|
|
338
|
+
local target="" saw_doubledash=0
|
|
339
|
+
for ((j = s + 1; j < n; j++)); do
|
|
340
|
+
local t="${tokens_ref[j]}"
|
|
341
|
+
case "$t" in
|
|
342
|
+
--)
|
|
343
|
+
saw_doubledash=1
|
|
344
|
+
continue
|
|
345
|
+
;;
|
|
346
|
+
-b|-B|-c|-C|--create|--force-create|--orphan|--track)
|
|
347
|
+
printf 'git %s creates or checks out a branch in the primary checkout; use git worktree add .red/tmp/work-<slug> -b <branch> ... instead' "$sub"
|
|
348
|
+
return 0
|
|
349
|
+
;;
|
|
350
|
+
--create=*|--force-create=*|--orphan=*)
|
|
351
|
+
printf 'git %s creates a branch in the primary checkout; use git worktree add .red/tmp/work-<slug> -b <branch> ... instead' "$sub"
|
|
352
|
+
return 0
|
|
353
|
+
;;
|
|
354
|
+
-*)
|
|
355
|
+
continue
|
|
356
|
+
;;
|
|
357
|
+
*)
|
|
358
|
+
target="$t"
|
|
359
|
+
break
|
|
360
|
+
;;
|
|
361
|
+
esac
|
|
362
|
+
done
|
|
363
|
+
((saw_doubledash)) && continue
|
|
364
|
+
[[ -z "$target" || "$target" == "." ]] && continue
|
|
365
|
+
printf 'git %s changes the primary checkout branch; create or enter a .red/tmp worktree instead' "$sub"
|
|
366
|
+
return 0
|
|
367
|
+
;;
|
|
368
|
+
esac
|
|
369
|
+
done
|
|
370
|
+
|
|
371
|
+
return 1
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
enforce_dev_worktree_policy() {
|
|
375
|
+
local -a tokens
|
|
376
|
+
read -ra tokens <<<"$COMMAND"
|
|
377
|
+
local n=${#tokens[@]}
|
|
378
|
+
((n > 0)) || return 0
|
|
379
|
+
|
|
380
|
+
local i
|
|
381
|
+
for ((i = 0; i < n; i++)); do
|
|
382
|
+
[[ "${tokens[i]}" == "git" ]] || continue
|
|
383
|
+
local pair cwd dest abs allowed_root
|
|
384
|
+
pair="$(worktree_add_destination tokens "$i" || true)"
|
|
385
|
+
[[ -n "$pair" ]] || continue
|
|
386
|
+
cwd="${pair%%$'\t'*}"
|
|
387
|
+
dest="${pair#*$'\t'}"
|
|
388
|
+
abs="$(canonical_path "$cwd" "$dest")"
|
|
389
|
+
allowed_root="$(canonical_path "$REPO_ROOT" ".red/tmp")"
|
|
390
|
+
case "$abs" in
|
|
391
|
+
"$allowed_root"/*)
|
|
392
|
+
;;
|
|
393
|
+
*)
|
|
394
|
+
deny "$(cat <<EOF
|
|
395
|
+
BLOCKED by RedSkills dev worktree guard.
|
|
396
|
+
plugins.dev.enabled is true, so agent-created git worktrees must live under .red/tmp/.
|
|
397
|
+
|
|
398
|
+
Requested worktree path: $dest
|
|
399
|
+
Resolved worktree path: $abs
|
|
400
|
+
Allowed root: $allowed_root/
|
|
401
|
+
|
|
402
|
+
Use: git worktree add .red/tmp/work-<slug> -b <branch> origin/main
|
|
403
|
+
EOF
|
|
404
|
+
)"
|
|
405
|
+
;;
|
|
406
|
+
esac
|
|
407
|
+
done
|
|
408
|
+
|
|
409
|
+
if [[ "$COMMAND_GUARD_SCOPE" == "main" ]]; then
|
|
410
|
+
local reason
|
|
411
|
+
reason="$(primary_branch_movement_reason tokens || true)"
|
|
412
|
+
if [[ -n "$reason" ]]; then
|
|
413
|
+
deny "$(cat <<EOF
|
|
414
|
+
BLOCKED by RedSkills dev primary-checkout guard.
|
|
415
|
+
plugins.dev.enabled is true, so agents must not create or switch branches in the primary checkout.
|
|
416
|
+
|
|
417
|
+
$reason.
|
|
418
|
+
EOF
|
|
419
|
+
)"
|
|
420
|
+
fi
|
|
421
|
+
fi
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
enforce_dev_worktree_policy
|
|
425
|
+
|
|
426
|
+
is_gh_write_command() {
|
|
427
|
+
local upper
|
|
428
|
+
upper="$(printf '%s' "$COMMAND" | tr '[:lower:]' '[:upper:]')"
|
|
429
|
+
[[ "$upper" =~ (^|[[:space:]\;\&\|])GH[[:space:]]+ISSUE[[:space:]]+(COMMENT|CREATE|EDIT)([[:space:]]|$) ]] && return 0
|
|
430
|
+
[[ "$upper" =~ (^|[[:space:]\;\&\|])GH[[:space:]]+PR[[:space:]]+(COMMENT|CREATE|EDIT|REVIEW)([[:space:]]|$) ]] && return 0
|
|
431
|
+
[[ "$upper" =~ (^|[[:space:]\;\&\|])GH[[:space:]]+RELEASE[[:space:]]+(CREATE|EDIT)([[:space:]]|$) ]] && return 0
|
|
432
|
+
[[ "$upper" =~ (^|[[:space:]\;\&\|])GH[[:space:]]+GIST[[:space:]]+CREATE([[:space:]]|$) ]] && return 0
|
|
433
|
+
[[ "$upper" =~ (^|[[:space:]\;\&\|])RSP[[:space:]]+GH[[:space:]]+ISSUE[[:space:]]+(COMMENT|CREATE|EDIT)([[:space:]]|$) ]] && return 0
|
|
434
|
+
[[ "$upper" =~ (^|[[:space:]\;\&\|])RSP[[:space:]]+GH[[:space:]]+PR[[:space:]]+(COMMENT|CREATE|EDIT|REVIEW)([[:space:]]|$) ]] && return 0
|
|
435
|
+
return 1
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
has_gh_body_home_path() {
|
|
439
|
+
[[ "$COMMAND" =~ (^|[[:space:]])(--body|-b)(=|[[:space:]]+).*(/home/[^[:space:]\'\"]+|/Users/[^[:space:]\'\"]+) ]]
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
has_sensitive_assignment() {
|
|
443
|
+
local upper
|
|
444
|
+
upper="$(printf '%s' "$COMMAND" | tr '[:lower:]' '[:upper:]')"
|
|
445
|
+
[[ "$upper" =~ [A-Z0-9_]*(TOKEN|SECRET|PASSWORD|APIKEY|API_KEY|API-KEY)[A-Z0-9_-]*=[^[:space:]\'\"]+ ]]
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
enforce_no_leak_gh_write_policy() {
|
|
449
|
+
is_gh_write_command || return 0
|
|
450
|
+
local reason=""
|
|
451
|
+
if [[ "$COMMAND" == *"claude.ai/code/session_"* ]]; then
|
|
452
|
+
reason="Claude session link"
|
|
453
|
+
elif has_gh_body_home_path; then
|
|
454
|
+
reason="absolute home path in gh body text"
|
|
455
|
+
elif has_sensitive_assignment; then
|
|
456
|
+
reason="sensitive KEY=value assignment"
|
|
457
|
+
fi
|
|
458
|
+
|
|
459
|
+
[[ -n "$reason" ]] || return 0
|
|
460
|
+
deny "$(cat <<EOF
|
|
461
|
+
BLOCKED by RedSkills no-leak command guard.
|
|
462
|
+
The gh write command contains a public-output leak pattern: $reason.
|
|
463
|
+
|
|
464
|
+
Redact and retry. Use placeholders such as [REDACTED_HOME], [REDACTED_SECRET], or [REDACTED_CLAUDE_SESSION] when the reference is unavoidable.
|
|
465
|
+
EOF
|
|
466
|
+
)"
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
enforce_no_leak_gh_write_policy
|
|
470
|
+
|
|
471
|
+
# Returns the filename component if path targets the .red/tmp/ root directly
|
|
472
|
+
# (no named-lane subdirectory after the root). Returns nothing otherwise.
|
|
473
|
+
tmp_root_blocked_path() {
|
|
474
|
+
local raw="$1"
|
|
475
|
+
local s="$raw"
|
|
476
|
+
[[ "$s" == \'* ]] && s="${s:1}" && s="${s%\'}"
|
|
477
|
+
[[ "$s" == \"* ]] && s="${s:1}" && s="${s%\"}"
|
|
478
|
+
local rel
|
|
479
|
+
case "$s" in
|
|
480
|
+
"$REPO_ROOT/.red/tmp/"*)
|
|
481
|
+
rel="${s#$REPO_ROOT/.red/tmp/}"
|
|
482
|
+
;;
|
|
483
|
+
".red/tmp/"*)
|
|
484
|
+
rel="${s#.red/tmp/}"
|
|
485
|
+
;;
|
|
486
|
+
*)
|
|
487
|
+
return 1
|
|
488
|
+
;;
|
|
489
|
+
esac
|
|
490
|
+
[[ -n "$rel" && "$rel" != */* ]] || return 1
|
|
491
|
+
printf '%s' "$rel"
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
deny_tmp_root_write() {
|
|
495
|
+
local path="$1"
|
|
496
|
+
deny "$(cat <<EOF
|
|
497
|
+
BLOCKED by RedSkills tmp-root write guard.
|
|
498
|
+
plugins.dev.enabled is true, so files must not be created directly at .red/tmp/.
|
|
499
|
+
|
|
500
|
+
Requested path: $path
|
|
501
|
+
Correct lanes: .red/tmp/logs/<YYYY-MM-DD>/ for session logs
|
|
502
|
+
.red/tmp/scratch/ for ad-hoc scratch files
|
|
503
|
+
|
|
504
|
+
Write to the appropriate named lane instead.
|
|
505
|
+
EOF
|
|
506
|
+
)"
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
enforce_tmp_root_write_policy() {
|
|
510
|
+
local -a tokens
|
|
511
|
+
read -ra tokens <<<"$COMMAND"
|
|
512
|
+
local n=${#tokens[@]}
|
|
513
|
+
((n > 0)) || return 0
|
|
514
|
+
|
|
515
|
+
local i j leaf
|
|
516
|
+
|
|
517
|
+
for ((i = 0; i < n; i++)); do
|
|
518
|
+
local tok="${tokens[i]}"
|
|
519
|
+
local redir_target=""
|
|
520
|
+
|
|
521
|
+
# Standalone or concatenated redirection operators
|
|
522
|
+
case "$tok" in
|
|
523
|
+
'>'|'>>'|[0-9]'>'|[0-9]'>>'|'&>'|'&>>')
|
|
524
|
+
redir_target="${tokens[i+1]:-}"
|
|
525
|
+
;;
|
|
526
|
+
'>'?*|'>>'?*)
|
|
527
|
+
if [[ "$tok" == '>>'* ]]; then
|
|
528
|
+
redir_target="${tok#>>}"
|
|
529
|
+
else
|
|
530
|
+
redir_target="${tok#>}"
|
|
531
|
+
fi
|
|
532
|
+
;;
|
|
533
|
+
[0-9]'>'?*|[0-9]'>>'?*)
|
|
534
|
+
local re_fd_append='^[0-9]+>>(.+)'
|
|
535
|
+
local re_fd_write='^[0-9]+>(.+)'
|
|
536
|
+
if [[ "$tok" =~ $re_fd_append ]]; then
|
|
537
|
+
redir_target="${BASH_REMATCH[1]}"
|
|
538
|
+
elif [[ "$tok" =~ $re_fd_write ]]; then
|
|
539
|
+
redir_target="${BASH_REMATCH[1]}"
|
|
540
|
+
fi
|
|
541
|
+
;;
|
|
542
|
+
'&>'?*|'&>>'?*)
|
|
543
|
+
if [[ "$tok" == '&>>'* ]]; then
|
|
544
|
+
redir_target="${tok#&>>}"
|
|
545
|
+
else
|
|
546
|
+
redir_target="${tok#&>}"
|
|
547
|
+
fi
|
|
548
|
+
;;
|
|
549
|
+
esac
|
|
550
|
+
|
|
551
|
+
if [[ -n "$redir_target" ]]; then
|
|
552
|
+
leaf="$(tmp_root_blocked_path "$redir_target" || true)"
|
|
553
|
+
[[ -n "$leaf" ]] && deny_tmp_root_write "$redir_target"
|
|
554
|
+
fi
|
|
555
|
+
|
|
556
|
+
# File-creating commands: touch, tee, cp, mv
|
|
557
|
+
case "$tok" in
|
|
558
|
+
touch|tee)
|
|
559
|
+
for ((j = i + 1; j < n; j++)); do
|
|
560
|
+
local arg="${tokens[j]}"
|
|
561
|
+
case "$arg" in '>'|'>>'|'|'|'&&'|'||'|';') break ;; esac
|
|
562
|
+
[[ "${arg:0:1}" == "-" ]] && continue
|
|
563
|
+
leaf="$(tmp_root_blocked_path "$arg" || true)"
|
|
564
|
+
[[ -n "$leaf" ]] && deny_tmp_root_write "$arg"
|
|
565
|
+
done
|
|
566
|
+
;;
|
|
567
|
+
cp|mv)
|
|
568
|
+
local -a positional=()
|
|
569
|
+
for ((j = i + 1; j < n; j++)); do
|
|
570
|
+
local arg="${tokens[j]}"
|
|
571
|
+
case "$arg" in '>'|'>>'|'|'|'&&'|'||'|';') break ;; esac
|
|
572
|
+
[[ "${arg:0:1}" == "-" ]] && continue
|
|
573
|
+
positional+=("$arg")
|
|
574
|
+
done
|
|
575
|
+
if [[ ${#positional[@]} -ge 2 ]]; then
|
|
576
|
+
local dest="${positional[${#positional[@]}-1]}"
|
|
577
|
+
leaf="$(tmp_root_blocked_path "$dest" || true)"
|
|
578
|
+
[[ -n "$leaf" ]] && deny_tmp_root_write "$dest"
|
|
579
|
+
fi
|
|
580
|
+
;;
|
|
581
|
+
esac
|
|
582
|
+
done
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
enforce_tmp_root_write_policy
|
|
586
|
+
|
|
587
|
+
strip_path_token() {
|
|
588
|
+
local s="$1"
|
|
589
|
+
[[ "$s" == \'* ]] && s="${s:1}" && s="${s%\'}"
|
|
590
|
+
[[ "$s" == \"* ]] && s="${s:1}" && s="${s%\"}"
|
|
591
|
+
printf '%s' "$s"
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
pid_file_live() {
|
|
595
|
+
local pid_file="$1" raw
|
|
596
|
+
[[ -f "$pid_file" ]] || return 1
|
|
597
|
+
raw="$(tr -d '[:space:]' <"$pid_file" 2>/dev/null || true)"
|
|
598
|
+
[[ "$raw" =~ ^[1-9][0-9]*$ ]] || return 1
|
|
599
|
+
kill -0 "$raw" 2>/dev/null
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
supervisor_delete_reason() {
|
|
603
|
+
local abs="$1" dir
|
|
604
|
+
for dir in \
|
|
605
|
+
"$(canonical_path "$REPO_ROOT" ".red/state/afk")" \
|
|
606
|
+
"$(canonical_path "$REPO_ROOT" ".red/tmp")"
|
|
607
|
+
do
|
|
608
|
+
case "$abs" in
|
|
609
|
+
"$dir"/afk-supervisor.*|"$dir")
|
|
610
|
+
if pid_file_live "$dir/afk-supervisor.pid"; then
|
|
611
|
+
printf 'refusing to delete live supervisor artifacts under %s' "$dir"
|
|
612
|
+
return 0
|
|
613
|
+
fi
|
|
614
|
+
;;
|
|
615
|
+
esac
|
|
616
|
+
done
|
|
617
|
+
return 1
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
worker_delete_reason() {
|
|
621
|
+
local abs="$1" tmp_root ns prefix rel worker worker_dir
|
|
622
|
+
tmp_root="$(canonical_path "$REPO_ROOT" ".red/tmp")"
|
|
623
|
+
for ns in workers go-workers scout-workers; do
|
|
624
|
+
prefix="$tmp_root/$ns/"
|
|
625
|
+
case "$abs" in
|
|
626
|
+
"$prefix"*)
|
|
627
|
+
rel="${abs#$prefix}"
|
|
628
|
+
worker="${rel%%/*}"
|
|
629
|
+
[[ -n "$worker" ]] || continue
|
|
630
|
+
worker_dir="$prefix$worker"
|
|
631
|
+
if pid_file_live "$worker_dir/worker.pid"; then
|
|
632
|
+
printf 'refusing to delete live worker artifacts under %s' "$worker_dir"
|
|
633
|
+
return 0
|
|
634
|
+
fi
|
|
635
|
+
;;
|
|
636
|
+
esac
|
|
637
|
+
done
|
|
638
|
+
return 1
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
delete_target_reason() {
|
|
642
|
+
local raw="$1" s abs tmp_root rel reason
|
|
643
|
+
s="$(strip_path_token "$raw")"
|
|
644
|
+
[[ -n "$s" ]] || return 1
|
|
645
|
+
abs="$(canonical_path "$ROOT" "$s")"
|
|
646
|
+
tmp_root="$(canonical_path "$REPO_ROOT" ".red/tmp")"
|
|
647
|
+
|
|
648
|
+
if [[ "$abs" == "$tmp_root" ]]; then
|
|
649
|
+
printf 'refusing to delete the .red/tmp root'
|
|
650
|
+
return 0
|
|
651
|
+
fi
|
|
652
|
+
|
|
653
|
+
case "$abs" in
|
|
654
|
+
"$tmp_root"/*)
|
|
655
|
+
rel="${abs#$tmp_root/}"
|
|
656
|
+
if [[ "$rel" == "*" ]]; then
|
|
657
|
+
printf 'refusing to delete every entry under .red/tmp'
|
|
658
|
+
return 0
|
|
659
|
+
fi
|
|
660
|
+
;;
|
|
661
|
+
esac
|
|
662
|
+
|
|
663
|
+
reason="$(supervisor_delete_reason "$abs" || true)"
|
|
664
|
+
if [[ -n "$reason" ]]; then
|
|
665
|
+
printf '%s' "$reason"
|
|
666
|
+
return 0
|
|
667
|
+
fi
|
|
668
|
+
|
|
669
|
+
reason="$(worker_delete_reason "$abs" || true)"
|
|
670
|
+
if [[ -n "$reason" ]]; then
|
|
671
|
+
printf '%s' "$reason"
|
|
672
|
+
return 0
|
|
673
|
+
fi
|
|
674
|
+
|
|
675
|
+
return 1
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
deny_tmp_delete() {
|
|
679
|
+
local path="$1" reason="$2"
|
|
680
|
+
deny "$(cat <<EOF
|
|
681
|
+
BLOCKED by RedSkills tmp deletion guard.
|
|
682
|
+
plugins.dev.enabled is true, so cleanup commands must delete a named lane under .red/tmp/, not the tmp root or live liveness anchors.
|
|
683
|
+
|
|
684
|
+
Requested path: $path
|
|
685
|
+
Reason: $reason
|
|
686
|
+
|
|
687
|
+
Target a specific owned lane or stop the live supervisor/worker before removing its anchors.
|
|
688
|
+
EOF
|
|
689
|
+
)"
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
enforce_tmp_delete_policy() {
|
|
693
|
+
local -a tokens
|
|
694
|
+
read -ra tokens <<<"$COMMAND"
|
|
695
|
+
local n=${#tokens[@]}
|
|
696
|
+
((n > 0)) || return 0
|
|
697
|
+
|
|
698
|
+
local i j arg reason
|
|
699
|
+
for ((i = 0; i < n; i++)); do
|
|
700
|
+
case "${tokens[i]}" in
|
|
701
|
+
rm|/bin/rm|/usr/bin/rm)
|
|
702
|
+
for ((j = i + 1; j < n; j++)); do
|
|
703
|
+
arg="${tokens[j]}"
|
|
704
|
+
case "$arg" in '>'|'>>'|'|'|'&&'|'||'|';') break ;; esac
|
|
705
|
+
[[ "$arg" == -- ]] && continue
|
|
706
|
+
[[ "${arg:0:1}" == "-" ]] && continue
|
|
707
|
+
reason="$(delete_target_reason "$arg" || true)"
|
|
708
|
+
[[ -n "$reason" ]] && deny_tmp_delete "$arg" "$reason"
|
|
709
|
+
done
|
|
710
|
+
;;
|
|
711
|
+
esac
|
|
712
|
+
done
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
enforce_tmp_delete_policy
|
|
716
|
+
|
|
717
|
+
guard_scope_for_path() {
|
|
718
|
+
local path="$1"
|
|
719
|
+
case "$path" in
|
|
720
|
+
command_guard.global | \
|
|
721
|
+
command_guard.deny | \
|
|
722
|
+
plugins.dev.command_guard.global | \
|
|
723
|
+
plugins.dev.command_guard.deny | \
|
|
724
|
+
dev.command_guard.global | \
|
|
725
|
+
dev.command_guard.deny)
|
|
726
|
+
printf 'global'
|
|
727
|
+
;;
|
|
728
|
+
command_guard.main | plugins.dev.command_guard.main | dev.command_guard.main)
|
|
729
|
+
printf 'main'
|
|
730
|
+
;;
|
|
731
|
+
command_guard.worktree | plugins.dev.command_guard.worktree | dev.command_guard.worktree)
|
|
732
|
+
printf 'worktree'
|
|
733
|
+
;;
|
|
734
|
+
*)
|
|
735
|
+
return 1
|
|
736
|
+
;;
|
|
737
|
+
esac
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
emit_deny_pattern() {
|
|
741
|
+
local path="$1"
|
|
742
|
+
local raw_value="$2"
|
|
743
|
+
local scope item
|
|
744
|
+
scope="$(guard_scope_for_path "$path" || true)"
|
|
745
|
+
[[ -n "$scope" ]] || return 0
|
|
746
|
+
[[ "$scope" == "global" || "$scope" == "$COMMAND_GUARD_SCOPE" ]] || return 0
|
|
747
|
+
item="$(unquote_scalar "$raw_value")"
|
|
748
|
+
[[ -n "$item" ]] && printf '%s\t%s\n' "$scope" "$item"
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
read_deny_patterns() {
|
|
752
|
+
local -a STACK=()
|
|
753
|
+
local -a INDENTS=()
|
|
754
|
+
local raw line indent_str indent rest parent key value full
|
|
755
|
+
|
|
756
|
+
while IFS= read -r raw || [[ -n "$raw" ]]; do
|
|
757
|
+
raw="${raw%$'\r'}"
|
|
758
|
+
line="$(strip_line_comment "$raw")"
|
|
759
|
+
[[ -z "${line//[[:space:]]/}" ]] && continue
|
|
760
|
+
indent_str="${raw%%[![:space:]]*}"
|
|
761
|
+
indent=${#indent_str}
|
|
762
|
+
((indent % 2 == 0)) || return 0
|
|
763
|
+
rest="${line:$indent}"
|
|
764
|
+
|
|
765
|
+
while ((${#INDENTS[@]} > 0 && INDENTS[${#INDENTS[@]} - 1] >= indent)); do
|
|
766
|
+
unset "STACK[$((${#STACK[@]} - 1))]"
|
|
767
|
+
unset "INDENTS[$((${#INDENTS[@]} - 1))]"
|
|
768
|
+
done
|
|
769
|
+
|
|
770
|
+
if [[ "$rest" =~ ^-([[:space:]]|$) ]]; then
|
|
771
|
+
parent="$(join_stack)"
|
|
772
|
+
emit_deny_pattern "$parent" "${rest#-}"
|
|
773
|
+
continue
|
|
774
|
+
fi
|
|
775
|
+
|
|
776
|
+
[[ "$rest" =~ ^[A-Za-z_][A-Za-z0-9_-]*: ]] || continue
|
|
777
|
+
key="${rest%%:*}"
|
|
778
|
+
value="${rest#*:}"
|
|
779
|
+
value="$(trim "$value")"
|
|
780
|
+
full="$(join_stack)"
|
|
781
|
+
[[ -n "$full" ]] && full+="."
|
|
782
|
+
full+="$key"
|
|
783
|
+
|
|
784
|
+
if [[ -z "$value" ]]; then
|
|
785
|
+
STACK+=("$key")
|
|
786
|
+
INDENTS+=("$indent")
|
|
787
|
+
continue
|
|
788
|
+
fi
|
|
789
|
+
|
|
790
|
+
emit_deny_pattern "$full" "$value"
|
|
791
|
+
done <"$CONFIG"
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
has_leading_command_boundary() {
|
|
795
|
+
local value="$1"
|
|
796
|
+
local first="${value:0:1}"
|
|
797
|
+
[[ -z "$value" ||
|
|
798
|
+
"$first" == ";" ||
|
|
799
|
+
"$first" == "&" ||
|
|
800
|
+
"$first" == "|" ||
|
|
801
|
+
"$first" =~ [[:space:]] ]]
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
has_trailing_command_boundary() {
|
|
805
|
+
local value="$1"
|
|
806
|
+
local last="${value: -1}"
|
|
807
|
+
[[ -z "$value" ||
|
|
808
|
+
"$last" == ";" ||
|
|
809
|
+
"$last" == "&" ||
|
|
810
|
+
"$last" == "|" ||
|
|
811
|
+
"$last" =~ [[:space:]] ]]
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
matches_rule() {
|
|
815
|
+
local command="$1"
|
|
816
|
+
local rule="$2"
|
|
817
|
+
local mode="" pattern="$rule"
|
|
818
|
+
case "$rule" in
|
|
819
|
+
regex:*|re:*)
|
|
820
|
+
pattern="${rule#*:}"
|
|
821
|
+
[[ -n "$pattern" && "$command" =~ $pattern ]]
|
|
822
|
+
return
|
|
823
|
+
;;
|
|
824
|
+
prefix:*)
|
|
825
|
+
mode="prefix"
|
|
826
|
+
pattern="${rule#prefix:}"
|
|
827
|
+
;;
|
|
828
|
+
suffix:*)
|
|
829
|
+
mode="suffix"
|
|
830
|
+
pattern="${rule#suffix:}"
|
|
831
|
+
;;
|
|
832
|
+
exact:*)
|
|
833
|
+
mode="exact"
|
|
834
|
+
pattern="${rule#exact:}"
|
|
835
|
+
;;
|
|
836
|
+
glob:*)
|
|
837
|
+
mode="glob"
|
|
838
|
+
pattern="${rule#glob:}"
|
|
839
|
+
;;
|
|
840
|
+
esac
|
|
841
|
+
|
|
842
|
+
local left right
|
|
843
|
+
case "$mode" in
|
|
844
|
+
prefix)
|
|
845
|
+
right="${command#"$pattern"}"
|
|
846
|
+
[[ "$right" != "$command" ]] && has_leading_command_boundary "$right"
|
|
847
|
+
return
|
|
848
|
+
;;
|
|
849
|
+
suffix)
|
|
850
|
+
left="${command%"$pattern"}"
|
|
851
|
+
[[ "$left" != "$command" ]] && has_trailing_command_boundary "$left"
|
|
852
|
+
return
|
|
853
|
+
;;
|
|
854
|
+
exact)
|
|
855
|
+
[[ "$command" == "$pattern" ]]
|
|
856
|
+
return
|
|
857
|
+
;;
|
|
858
|
+
glob)
|
|
859
|
+
[[ "$command" == $pattern ]]
|
|
860
|
+
return
|
|
861
|
+
;;
|
|
862
|
+
esac
|
|
863
|
+
|
|
864
|
+
case "$rule" in
|
|
865
|
+
*[\*\?\[]*)
|
|
866
|
+
[[ "$command" == $rule ]]
|
|
867
|
+
;;
|
|
868
|
+
*)
|
|
869
|
+
if [[ "$command" == "$rule" ]]; then
|
|
870
|
+
return 0
|
|
871
|
+
fi
|
|
872
|
+
right="${command#"$rule"}"
|
|
873
|
+
left="${command%"$rule"}"
|
|
874
|
+
([[ "$right" != "$command" ]] && has_leading_command_boundary "$right") ||
|
|
875
|
+
([[ "$left" != "$command" ]] && has_trailing_command_boundary "$left")
|
|
876
|
+
;;
|
|
877
|
+
esac
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
while IFS=$'\t' read -r rule_scope rule; do
|
|
881
|
+
[[ -n "$rule" ]] || continue
|
|
882
|
+
if matches_rule "$COMMAND" "$rule"; then
|
|
883
|
+
deny "$(cat <<EOF
|
|
884
|
+
BLOCKED by RedSkills command guard.
|
|
885
|
+
The command '$COMMAND' matched command_guard.$rule_scope rule '$rule' from .red/config.yaml.
|
|
886
|
+
|
|
887
|
+
Remove or narrow command_guard.$rule_scope if this command is intentional.
|
|
888
|
+
EOF
|
|
889
|
+
)"
|
|
890
|
+
fi
|
|
891
|
+
done < <(read_deny_patterns)
|
|
892
|
+
|
|
893
|
+
allow
|