@khoavu882/doflow 2.0.0 → 2.0.1
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/README.md +4 -5
- package/bin/doflow.js +1 -1
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +4 -1
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +4 -1
- package/core/harnesses/claude/settings/settings.json +8 -9
- package/core/harnesses/codex/hooks/hooks.json +0 -10
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +4 -1
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +4 -1
- package/core/harnesses/copilot/hooks/doflow-lib.sh +4 -3
- package/core/harnesses/copilot/hooks/doflow.json +0 -6
- package/core/harnesses/kiro/agents/doflow.json +0 -10
- package/core/harnesses/kiro/hooks/doflow.json +0 -10
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +7 -3
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +7 -3
- package/core/harnesses/pi/extensions/doflow-guard.js +3 -2
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +1 -1
- package/core/harnesses/shared/hooks/adapters/antigravity.js +1 -1
- package/core/harnesses/shared/hooks/bridge/bridge.sh +35 -25
- package/core/harnesses/shared/hooks/bridge/envelope.jq +1 -1
- package/core/harnesses/shared/hooks/bridge/respond.sh +6 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +1 -1
- package/core/harnesses/shared/hooks/policies/deny-json.sh +23 -1
- package/core/harnesses/shared/hooks/policies/mcp-policy.conf +1 -1
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +4 -6
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +20 -21
- package/core/harnesses/shared/hooks/policies/stop-check.sh +7 -6
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +1 -1
- package/core/harnesses/shared/hooks/stream-hook-runner.js +11 -12
- package/core/registry/contracts.json +1 -1
- package/core/registry/contribution-points.json +5 -2
- package/core/registry/gates.json +5 -5
- package/core/registry/harnesses.json +9 -9
- package/core/registry/intake.json +53 -0
- package/core/registry/lifecycle.json +133 -711
- package/core/registry/package-templates/intake-category/files/category.json +8 -0
- package/core/registry/package-templates/intake-category/template.json +4 -0
- package/core/registry/packages/doflow/doflow-package.json +2 -2
- package/core/registry/rules.json +8 -8
- package/core/registry/settings.json +0 -4
- package/core/registry/storage.json +8 -0
- package/core/registry/telemetry.json +1 -1
- package/core/registry/verification.json +0 -5
- package/core/shared/guidance/references/DOFLOW_CHAIN.md +4 -4
- package/core/shared/guidance/references/ORCHESTRATION.md +2 -0
- package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +19 -1
- package/core/shared/guidance/rules/RULES.md +2 -2
- package/core/shared/scripts/doflow/bash/validate-artifacts.sh +1 -1
- package/core/shared/scripts/doflow/bin/doflow-run +5 -2
- package/core/shared/skills/do/SKILL.md +3 -3
- package/core/shared/skills/do-brainstorm/SKILL.md +26 -24
- package/core/shared/skills/do-brainstorm/references/handoff_branches.md +2 -1
- package/core/shared/skills/do-code-review/SKILL.md +7 -4
- package/core/shared/skills/do-design/SKILL.md +5 -3
- package/core/shared/skills/do-diagnose/SKILL.md +4 -2
- package/core/shared/skills/do-document/SKILL.md +1 -0
- package/core/shared/skills/do-document/references/embedded-research.md +1 -1
- package/core/shared/skills/do-execute-plan/SKILL.md +4 -2
- package/core/shared/skills/do-execute-plan/references/run_position.md +3 -1
- package/core/shared/skills/do-execute-plan/references/scaffold.md +1 -1
- package/core/shared/skills/do-flow/SKILL.md +4 -4
- package/core/shared/skills/do-flow/references/task_classes.md +5 -5
- package/core/shared/skills/do-implement/SKILL.md +9 -7
- package/core/shared/skills/do-plan/SKILL.md +9 -6
- package/core/shared/workflows/do-flow-chain.md +2 -0
- package/package.json +1 -1
- package/src/adapters/claude/index.js +23 -2
- package/src/adapters/codex/hooks.js +39 -3
- package/src/adapters/codex/index.js +13 -0
- package/src/adapters/copy-tree.js +1 -2
- package/src/cli/commands/status.js +1 -1
- package/src/cli/index.js +20 -8
- package/src/cli/runtime-commands.js +12 -4
- package/src/lifecycle/coverage.js +1 -30
- package/src/lifecycle/hook-tier.js +2 -2
- package/src/release/docs-blocks.js +32 -52
- package/src/runtime/context/pointer.js +67 -12
- package/src/runtime/context-pack.js +4 -1
- package/src/runtime/evidence-ledger.js +18 -0
- package/src/runtime/health.js +59 -1
- package/src/runtime/implementation-gate.js +4 -4
- package/src/runtime/intake/engine.js +677 -0
- package/src/runtime/intake/index.js +136 -0
- package/src/runtime/intake/manifest.js +140 -0
- package/src/runtime/intake/taxonomy.js +206 -0
- package/src/runtime/lifecycle/cli.js +73 -5
- package/src/runtime/lifecycle/followup.js +32 -6
- package/src/runtime/lifecycle/overview.js +2 -1
- package/src/runtime/packages/points.js +10 -0
- package/src/runtime/roadmap/attendance.js +137 -0
- package/src/runtime/roadmap/gates.js +6 -3
- package/src/runtime/roadmap/loop.js +113 -30
- package/src/runtime/roadmap/merge.js +17 -8
- package/src/runtime/roadmap/rows.js +13 -4
- package/src/runtime/roadmap/seed.js +153 -41
- package/src/runtime/roadmap/worktree-cleanup.js +147 -0
- package/src/runtime/settings/registry.js +1 -1
- package/src/runtime/slots.js +1 -1
- package/src/runtime/task-classifier.js +39 -4
- package/src/runtime/task-state.js +2 -3
- package/src/runtime/verb-view.js +267 -0
- package/src/runtime/verification/engine.js +0 -45
- package/src/runtime/worker/launch.js +5 -37
- package/src/runtime/worker/readiness.js +2 -2
- package/src/runtime/workflow-orchestrator.js +33 -5
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/copilot/hooks/doflow-pre-bash-guard.sh +0 -9
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +0 -25
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +0 -105
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +0 -579
|
@@ -1,579 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
case "${OSTYPE:-}" in msys*|cygwin*|win32*) if command -v jq >/dev/null 2>&1; then jq() { command jq -b "$@"; }; fi ;; esac # native jq.exe ends lines with CRLF unless -b
|
|
3
|
-
_DOFLOW_SRC=${0//\\//}; case $_DOFLOW_SRC in */*) _DOFLOW_POLDIR=${_DOFLOW_SRC%/*} ;; *) _DOFLOW_POLDIR=. ;; esac
|
|
4
|
-
# pre-bash-guard.sh — Canonical Policy Library: PreToolUse(shell-command) hook
|
|
5
|
-
#
|
|
6
|
-
# Intercepts every shell-command tool call and blocks dangerous commands by
|
|
7
|
-
# matching against POSIX ERE patterns in blocked-patterns.conf (grep -E).
|
|
8
|
-
#
|
|
9
|
-
# Multi-session safe: reads the conf and canonical worker/cwd directories only.
|
|
10
|
-
# Marked reset/clean rules and an exact DROP DATABASE launch snapshot may exempt a dispatch worker;
|
|
11
|
-
# every failed scope/command check retains the ordinary pattern verdict.
|
|
12
|
-
# Must complete in <50ms.
|
|
13
|
-
#
|
|
14
|
-
# Canonical Policy Script Contract (design.md §4):
|
|
15
|
-
# env DOFLOW_PROJECT_DIR, DOFLOW_AGENT (both optional; not required to
|
|
16
|
-
# evaluate this policy)
|
|
17
|
-
# stdin a native payload with envelope v1 under doflow_event; only the
|
|
18
|
-
# envelope's operation, tool and command and native root cwd are used.
|
|
19
|
-
# exit exit 2 + doflow_decide deny = deny; other faults per IC-011.
|
|
20
|
-
#
|
|
21
|
-
# Reconciliation note (022-normalize-hooks, Phase A / D1): this also absorbs
|
|
22
|
-
# stream-hook-runner.js's inline DESTRUCTIVE_COMMAND_PATTERN
|
|
23
|
-
# (`\brm\s+-rf\s+[/~]`, since narrowed to the recursive-rm-of-catastrophic-target
|
|
24
|
-
# rule below), which was the only pre-bash-guard protection
|
|
25
|
-
# Antigravity sessions routed through that runner actually had — it
|
|
26
|
-
# duplicated, rather than delegated to, this policy's fuller
|
|
27
|
-
# blocked-patterns.conf coverage (git force-push, git reset --hard, git clean
|
|
28
|
-
# -fd, SQL DROP/DELETE/TRUNCATE, curl|wget-pipe-to-shell, chmod -R 777, dd
|
|
29
|
-
# from a block device). That pattern is kept here as a hardcoded floor for
|
|
30
|
-
# the case blocked-patterns.conf itself is missing, so a harness that had
|
|
31
|
-
# only the hardcoded regex before this change does not lose even that
|
|
32
|
-
# minimum floor of protection; once blocked-patterns.conf is present (the
|
|
33
|
-
# normal case), the full config-driven pattern list applies as it already
|
|
34
|
-
# does for claude/codex today, which is a real, intended widening of
|
|
35
|
-
# coverage for Antigravity once B.5 wires this policy for them (see
|
|
36
|
-
# design.md R2/R3), not a silent side effect.
|
|
37
|
-
|
|
38
|
-
set -uo pipefail
|
|
39
|
-
|
|
40
|
-
INPUT=$(cat)
|
|
41
|
-
if [ -f "$_DOFLOW_POLDIR/contract.sh" ]; then
|
|
42
|
-
. "$_DOFLOW_POLDIR/contract.sh"
|
|
43
|
-
else
|
|
44
|
-
doflow_decision_init() { :; }
|
|
45
|
-
doflow_decide() { :; }
|
|
46
|
-
doflow_read_envelope() { DF_ENV_CLASS=missing; DF_ENV_CODE=contract-missing; }
|
|
47
|
-
( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure pre-bash-guard contract-missing ) >/dev/null 2>&1 || true
|
|
48
|
-
if [[ "${DOFLOW_AGENT:-}" =~ ^[a-z]+$ ]]; then
|
|
49
|
-
printf '[pre-bash-guard] DoFlow hooks out of step; run: doflow update -t %s\n' "$DOFLOW_AGENT" >&2
|
|
50
|
-
else
|
|
51
|
-
printf '[pre-bash-guard] DoFlow hooks out of step; run: doflow update\n' >&2
|
|
52
|
-
fi
|
|
53
|
-
doflow_decide deny contract-missing
|
|
54
|
-
exit 2
|
|
55
|
-
fi
|
|
56
|
-
doflow_decision_init pre-bash-guard 1
|
|
57
|
-
command -v jq >/dev/null 2>&1 || {
|
|
58
|
-
doflow_decide error dependency-missing:jq
|
|
59
|
-
( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure pre-bash-guard dependency-missing:jq ) >/dev/null 2>&1 || true
|
|
60
|
-
exit 0
|
|
61
|
-
}
|
|
62
|
-
DF_ENV_CLASS=missing DF_ENV_CODE=envelope-missing
|
|
63
|
-
doflow_read_envelope || true
|
|
64
|
-
|
|
65
|
-
TOOL_NAME=""
|
|
66
|
-
COMMAND=""
|
|
67
|
-
if [ "$DF_ENV_CLASS" = v1 ]; then
|
|
68
|
-
[ "$DF_ENV_OP" = command ] || exit 0
|
|
69
|
-
TOOL_NAME=$DF_ENV_TOOL
|
|
70
|
-
COMMAND=$DF_ENV_COMMAND
|
|
71
|
-
elif [ "$DF_ENV_CLASS" = unsupported ]; then
|
|
72
|
-
doflow_decide unsupported envelope-version
|
|
73
|
-
( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure pre-bash-guard unsupported:envelope-version ) >/dev/null 2>&1 || true
|
|
74
|
-
exit 0
|
|
75
|
-
else
|
|
76
|
-
if [ "${DF_ENV_CODE:-envelope-missing}" != contract-missing ]; then
|
|
77
|
-
( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure pre-bash-guard envelope-missing ) >/dev/null 2>&1 || true
|
|
78
|
-
fi
|
|
79
|
-
if [[ "${DOFLOW_AGENT:-}" =~ ^[a-z]+$ ]]; then printf '[pre-bash-guard] DoFlow hooks out of step; run: doflow update -t %s\n' "$DOFLOW_AGENT" >&2; else printf '[pre-bash-guard] DoFlow hooks out of step; run: doflow update\n' >&2; fi
|
|
80
|
-
doflow_decide deny "${DF_ENV_CODE:-envelope-missing}"
|
|
81
|
-
exit 2
|
|
82
|
-
fi
|
|
83
|
-
|
|
84
|
-
# Nothing to check if command is empty
|
|
85
|
-
[ -z "$COMMAND" ] && exit 0
|
|
86
|
-
|
|
87
|
-
PATTERNS_FILE="$_DOFLOW_POLDIR/blocked-patterns.conf"
|
|
88
|
-
|
|
89
|
-
# ASCII-only patterns: the C locale keeps bash's substring/pattern operations
|
|
90
|
-
# byte-wise (much faster than multibyte-aware in a UTF-8 locale).
|
|
91
|
-
export LC_ALL=C
|
|
92
|
-
|
|
93
|
-
# Shape checks accept one terminal line ending, never an additional command.
|
|
94
|
-
# Keep COMMAND raw for all ordinary pattern verdicts.
|
|
95
|
-
SHAPE_COMMAND=$COMMAND
|
|
96
|
-
case $SHAPE_COMMAND in
|
|
97
|
-
*$'\r\n') SHAPE_COMMAND=${SHAPE_COMMAND%$'\r\n'} ;;
|
|
98
|
-
*$'\n') SHAPE_COMMAND=${SHAPE_COMMAND%$'\n'} ;;
|
|
99
|
-
esac
|
|
100
|
-
|
|
101
|
-
# Lazy and cached: shape checks precede the shared scope's only two subshells
|
|
102
|
-
# (cd/pwd builtins), then one git probe for reset/clean only.
|
|
103
|
-
# Never infer worker identity from command text.
|
|
104
|
-
_WORKER_EXEMPT=
|
|
105
|
-
_worker_exempt() {
|
|
106
|
-
case $_WORKER_EXEMPT in allow) return 0 ;; deny) return 1 ;; esac
|
|
107
|
-
_WORKER_EXEMPT=deny
|
|
108
|
-
local reset_re='^[[:blank:]]*git[[:blank:]]+reset([[:blank:]]+(--hard|-q|--quiet))+([[:blank:]]+[A-Za-z0-9_@][A-Za-z0-9_./^~@{}-]*)?[[:blank:]]*$'
|
|
109
|
-
local clean_re='^[[:blank:]]*git[[:blank:]]+clean([[:blank:]]+(-[dfqxX]+|--force|--quiet))+[[:blank:]]*$'
|
|
110
|
-
local forces command=$SHAPE_COMMAND
|
|
111
|
-
if [[ $command =~ $reset_re ]]; then
|
|
112
|
-
case " $command " in *[[:blank:]]--hard[[:blank:]]*) ;; *) return 1 ;; esac
|
|
113
|
-
elif [[ $command =~ $clean_re ]]; then
|
|
114
|
-
forces=${command//[!f]/}
|
|
115
|
-
[ "${#forces}" -eq 1 ] || return 1
|
|
116
|
-
else
|
|
117
|
-
return 1
|
|
118
|
-
fi
|
|
119
|
-
_worker_scope || return 1
|
|
120
|
-
local project=${_WORKER_DIR%/.doflow/state/dispatch-worktrees/*}
|
|
121
|
-
local name=${_WORKER_DIR##*/} info git_dir top suffix
|
|
122
|
-
[ -d "$project/.git" ] || return 1
|
|
123
|
-
# No inherited GIT_* / config environment can redirect this read-only probe.
|
|
124
|
-
info=$(env -i PATH="$PATH" HOME="${HOME:-}" git -C "$_WORKER_CWD" rev-parse --path-format=absolute --git-dir --show-toplevel 2>/dev/null) || return 1
|
|
125
|
-
git_dir=${info%%$'\n'*}; top=${info#*$'\n'}
|
|
126
|
-
[ "$top" = "$_WORKER_DIR" ] || return 1
|
|
127
|
-
case $git_dir in "$project/.git/worktrees/$name"*) ;; *) return 1 ;; esac
|
|
128
|
-
suffix=${git_dir#"$project/.git/worktrees/$name"}
|
|
129
|
-
case $suffix in *[!0-9]*) return 1 ;; esac
|
|
130
|
-
_WORKER_EXEMPT=allow
|
|
131
|
-
return 0
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
# Both exemptions use exactly the same worker, dispatch-path, cwd and GIT_* checks.
|
|
135
|
-
_WORKER_SCOPE=
|
|
136
|
-
_worker_scope() {
|
|
137
|
-
case $_WORKER_SCOPE in allow) return 0 ;; deny) return 1 ;; esac
|
|
138
|
-
_WORKER_SCOPE=deny
|
|
139
|
-
local worktree cwd config_key
|
|
140
|
-
[ "${DOFLOW_SESSION_ROLE:-}" = worker ] || return 1
|
|
141
|
-
[ -z "${GIT_DIR+x}${GIT_WORK_TREE+x}${GIT_INDEX_FILE+x}${GIT_COMMON_DIR+x}${GIT_CONFIG_PARAMETERS+x}${GIT_CONFIG_COUNT+x}" ] || return 1
|
|
142
|
-
for config_key in ${!GIT_CONFIG_KEY_@}; do return 1; done
|
|
143
|
-
case ${DOFLOW_WORKTREE:-} in /*) ;; *) return 1 ;; esac
|
|
144
|
-
[ -d "$DOFLOW_WORKTREE" ] || return 1
|
|
145
|
-
# A sentinel preserves path-ending newlines across command substitution.
|
|
146
|
-
worktree=$(CDPATH= cd -P -- "$DOFLOW_WORKTREE" 2>/dev/null && pwd -P && printf '.') || return 1
|
|
147
|
-
worktree=${worktree%.}; worktree=${worktree%$'\n'}
|
|
148
|
-
local dispatch_re='/\.doflow/state/dispatch-worktrees/d-[0-9a-f]{12}$'
|
|
149
|
-
[[ $worktree =~ $dispatch_re ]] || return 1
|
|
150
|
-
[ -n "${DF_ENV_CWD:-}" ] || return 1
|
|
151
|
-
cwd=$(CDPATH= cd -P -- "$DF_ENV_CWD" 2>/dev/null && pwd -P && printf '.') || return 1
|
|
152
|
-
cwd=${cwd%.}; cwd=${cwd%$'\n'}
|
|
153
|
-
case $cwd in "$worktree"|"$worktree"/*) ;; *) return 1 ;; esac
|
|
154
|
-
_WORKER_DIR=$worktree _WORKER_CWD=$cwd
|
|
155
|
-
_WORKER_SCOPE=allow
|
|
156
|
-
return 0
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
# 112: no settings reads here. Re-validate only the launcher's snapshot, then
|
|
160
|
-
# recognise one full bare client invocation and one statement. No shell eval,
|
|
161
|
-
# substitutions, variables, wrappers, redirects, additional args or statements.
|
|
162
|
-
_worker_dropdb_exempt() {
|
|
163
|
-
local list=${DOFLOW_GUARD_DROP_DATABASES:-} list_re='^[a-z][a-z0-9_]{0,62}( [a-z][a-z0-9_]{0,62})*$'
|
|
164
|
-
[ "${#list}" -le 1024 ] && [[ $list =~ $list_re ]] || return 1
|
|
165
|
-
local names=() name rest client quote statement id_re sql_re command=$SHAPE_COMMAND
|
|
166
|
-
IFS=' ' read -r -a names <<< "$list"
|
|
167
|
-
[ "${#names[@]}" -le 32 ] || return 1
|
|
168
|
-
local psql_re='^[[:blank:]]*psql([[:blank:]]+(-[hpUd][[:blank:]]+[A-Za-z0-9_.:-]+|--(host|port|username|dbname)=[A-Za-z0-9_.:-]+|-w))*[[:blank:]]+-c[[:blank:]]+'
|
|
169
|
-
local mysql_re='^[[:blank:]]*mysql([[:blank:]]+(-[hPu][[:blank:]]+[A-Za-z0-9_.:-]+|--(host|port|user)=[A-Za-z0-9_.:-]+))*[[:blank:]]+-e[[:blank:]]+'
|
|
170
|
-
if [[ $command =~ $psql_re ]]; then client=psql
|
|
171
|
-
elif [[ $command =~ $mysql_re ]]; then client=mysql
|
|
172
|
-
else return 1
|
|
173
|
-
fi
|
|
174
|
-
rest=${command:${#BASH_REMATCH[0]}}
|
|
175
|
-
local single_re="^'([^']*)'[[:blank:]]*$" double_re='^"([^"]*)"[[:blank:]]*$'
|
|
176
|
-
if [[ $rest =~ $single_re ]]; then quote=single
|
|
177
|
-
elif [[ $rest =~ $double_re ]]; then quote=double
|
|
178
|
-
else return 1
|
|
179
|
-
fi
|
|
180
|
-
statement=${BASH_REMATCH[1]}
|
|
181
|
-
id_re='[a-z][a-z0-9_]*'
|
|
182
|
-
if [ "$quote" = single ]; then
|
|
183
|
-
if [ "$client" = psql ]; then id_re='([a-z][a-z0-9_]*|"[a-z][a-z0-9_]*")'
|
|
184
|
-
else id_re='([a-z][a-z0-9_]*|`[a-z][a-z0-9_]*`)'
|
|
185
|
-
fi
|
|
186
|
-
fi
|
|
187
|
-
sql_re="^[[:blank:]]*[Dd][Rr][Oo][Pp][[:blank:]]+[Dd][Aa][Tt][Aa][Bb][Aa][Ss][Ee][[:blank:]]+([Ii][Ff][[:blank:]]+[Ee][Xx][Ii][Ss][Tt][Ss][[:blank:]]+)?($id_re)[[:blank:]]*;?[[:blank:]]*$"
|
|
188
|
-
[[ $statement =~ $sql_re ]] || return 1
|
|
189
|
-
name=${BASH_REMATCH[2]}
|
|
190
|
-
case $name in \"*\"|\`*\`) name=${name#?}; name=${name%?} ;; esac
|
|
191
|
-
local listed found=false
|
|
192
|
-
for listed in "${names[@]}"; do
|
|
193
|
-
if [ "$name" = "$listed" ]; then found=true; break; fi
|
|
194
|
-
done
|
|
195
|
-
[ "$found" = true ] || return 1
|
|
196
|
-
_worker_scope
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
# ── Quoted text, comments and heredoc bodies are not commands ────────────────
|
|
200
|
-
# scrub_quotes <text> -> sets SCRUBBED. Anchored shell-command patterns must
|
|
201
|
-
# not fire on text that is merely data (a commit message, an echo, a comment,
|
|
202
|
-
# a heredoc body). One linear pass, line by line, pure bash, no forks:
|
|
203
|
-
# - quoted text containing whitespace or ; & | is blanked to "" (it could
|
|
204
|
-
# otherwise fake a command position, e.g. echo "done && rm -rf /x");
|
|
205
|
-
# a quoted word with none of those ("/", "$HOME", $'/') is kept, unquoted,
|
|
206
|
-
# so rm -rf "/" is still seen as a target
|
|
207
|
-
# - the argument of sh|bash|zsh -c, eval and a here-string fed to a shell IS executed: it is kept and put
|
|
208
|
-
# in command position (wrapped in ";"), scrubbed again inside
|
|
209
|
-
# - inside double quotes, $( ... ) and `...` bodies also run: kept the same way
|
|
210
|
-
# - a # that starts a word comments out the rest of the line
|
|
211
|
-
# - a heredoc body (<<WORD, <<-WORD, quoted or not) is data and is dropped,
|
|
212
|
-
# except when the heredoc feeds a shell (bash <<EOF), whose body runs
|
|
213
|
-
# - a backslash-newline joins the two lines (outside a heredoc body)
|
|
214
|
-
# - a backslash-escaped character is literal; an unterminated quote keeps the
|
|
215
|
-
# rest of the text as is (errs toward blocking)
|
|
216
|
-
# Nesting depth is capped; beyond the cap the raw text is used (errs toward
|
|
217
|
-
# blocking). Text with no quote, # or << (or no command word at all) is
|
|
218
|
-
# returned unchanged without scanning.
|
|
219
|
-
_EXEC_CTX='(^|[[:space:];&|(])((([^[:space:]]*/)?(ba|z|da|k)?sh[[:space:]]+(-[[:alnum:]]+[[:space:]]+)*-[[:alnum:]]*c|eval)[[:space:]]+|([^[:space:]]*/)?(ba|z|da|k)?sh([[:space:]]+-[-[:alnum:]]*)*[[:space:]]*[0-9]*<<<[[:space:]]*)$'
|
|
220
|
-
_SH_HERE='(^|[[:space:];&|(])([^[:space:]]*/)?(ba|z|da|k)?sh([[:space:]]+-[[:alnum:]]+)*[[:space:]]*$'
|
|
221
|
-
_HD_WORD="^(-?)[[:space:]]*[\"']?\\\\?([A-Za-z_][A-Za-z0-9_.-]*)"
|
|
222
|
-
SCRUB_DEPTH=0
|
|
223
|
-
|
|
224
|
-
# Closes the quote scrub_quotes just read (uses its locals: out qc qin qexec).
|
|
225
|
-
_scrub_close_quote() {
|
|
226
|
-
local rest pre body acc depth ch
|
|
227
|
-
if [ "$qexec" = 1 ]; then
|
|
228
|
-
[ "$qc" = '"' ] && qin=${qin//\\\"/\"} # \" inside "..." is a quote of the inner command
|
|
229
|
-
scrub_quotes "$qin"; cur+="; $SCRUBBED ; "
|
|
230
|
-
return
|
|
231
|
-
fi
|
|
232
|
-
case $qin in
|
|
233
|
-
*[[:space:]\;\&\|]*) ;;
|
|
234
|
-
*) cur+=$qin; return ;;
|
|
235
|
-
esac
|
|
236
|
-
cur+=$qc$qc
|
|
237
|
-
[ "$qc" = '"' ] || return
|
|
238
|
-
case $qin in *'$('*|*\`*) ;; *) return ;; esac
|
|
239
|
-
rest=$qin
|
|
240
|
-
while :; do
|
|
241
|
-
pre=${rest%%[\`\$\\]*}
|
|
242
|
-
[ "$pre" = "$rest" ] && break
|
|
243
|
-
rest=${rest#"$pre"}
|
|
244
|
-
case ${rest:0:1} in
|
|
245
|
-
'\') rest=${rest:2} ;;
|
|
246
|
-
'`')
|
|
247
|
-
body=${rest:1}
|
|
248
|
-
case $body in *\`*) acc=${body%%\`*}; rest=${body#*\`} ;; *) acc=$body; rest="" ;; esac
|
|
249
|
-
scrub_quotes "$acc"; cur+="; $SCRUBBED ; " ;;
|
|
250
|
-
*)
|
|
251
|
-
if [ "${rest:1:1}" = "(" ]; then
|
|
252
|
-
body=${rest:2}; acc=""; depth=1
|
|
253
|
-
while [ -n "$body" ]; do
|
|
254
|
-
pre=${body%%[()]*}
|
|
255
|
-
if [ "$pre" = "$body" ]; then acc+=$body; body=""; break; fi
|
|
256
|
-
acc+=$pre; ch=${body:${#pre}:1}; body=${body:$((${#pre} + 1))}
|
|
257
|
-
if [ "$ch" = "(" ]; then depth=$((depth + 1)); acc+=$ch
|
|
258
|
-
else depth=$((depth - 1)); [ "$depth" -eq 0 ] && break; acc+=$ch; fi
|
|
259
|
-
done
|
|
260
|
-
rest=$body
|
|
261
|
-
scrub_quotes "$acc"; cur+="; $SCRUBBED ; "
|
|
262
|
-
else
|
|
263
|
-
rest=${rest:1}
|
|
264
|
-
fi ;;
|
|
265
|
-
esac
|
|
266
|
-
done
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
scrub_quotes() {
|
|
270
|
-
local text=$1
|
|
271
|
-
case $text in
|
|
272
|
-
*[\'\"#]*|*'<<'*|*'\'$'\n'*) ;;
|
|
273
|
-
*) SCRUBBED=$text; return ;;
|
|
274
|
-
esac
|
|
275
|
-
case $text in
|
|
276
|
-
*rm[[:space:]]*|*git[[:space:]]*|*curl[[:space:]]*|*wget[[:space:]]*|*chmod[[:space:]]*|*dd[[:space:]]*|*find[[:space:]]*|*eval*|*sh[[:space:]]*|*echo[[:space:]]*|*printf[[:space:]]*|*cat[[:space:]]*|*tee[[:space:]]*|*mkdir[[:space:]]*|*cd[[:space:]]*|*true[[:space:]]*|*:[[:space:]]*|*gh[[:space:]]*) ;;
|
|
277
|
-
*) SCRUBBED=$text; return ;;
|
|
278
|
-
esac
|
|
279
|
-
if [ "$SCRUB_DEPTH" -ge 8 ]; then SCRUBBED=$text; return; fi
|
|
280
|
-
SCRUB_DEPTH=$((SCRUB_DEPTH + 1))
|
|
281
|
-
|
|
282
|
-
local out="" cur="" line L first=1 qc="" qin="" qexec=0 qansi=0 hd_word="" hd_dash=0
|
|
283
|
-
local pre c tail w d
|
|
284
|
-
while IFS= read -r line; do
|
|
285
|
-
if [ -z "$hd_word" ]; then # backslash-newline joins lines
|
|
286
|
-
while [[ $line == *[!\\]'\' || $line == '\' ]] && IFS= read -r w; do line="${line%?} $w"; done
|
|
287
|
-
fi
|
|
288
|
-
L=$line
|
|
289
|
-
if [ -n "$qc" ]; then
|
|
290
|
-
qin+=$'\n'
|
|
291
|
-
else
|
|
292
|
-
[ "$first" = 1 ] || cur+=$'\n'
|
|
293
|
-
if [ -n "$hd_word" ]; then # inside a heredoc body: skip the line
|
|
294
|
-
if [ "$hd_dash" = 1 ]; then while [ "${L:0:1}" = $'\t' ]; do L=${L:1}; done; fi
|
|
295
|
-
[ "$L" = "$hd_word" ] && hd_word=""
|
|
296
|
-
first=0; continue
|
|
297
|
-
fi
|
|
298
|
-
fi
|
|
299
|
-
first=0
|
|
300
|
-
# A very long line is scanned in ~1 KB segments (quote state carries over),
|
|
301
|
-
# so the per-token slicing below never copies a huge remainder: stays linear.
|
|
302
|
-
while [ -n "$line" ]; do
|
|
303
|
-
if [ ${#line} -gt 2048 ]; then
|
|
304
|
-
L=${line:0:1024}; line=${line:1024}
|
|
305
|
-
while [[ $L == *'\' || $L == *'<' ]] && [ -n "$line" ]; do L+=${line:0:1}; line=${line:1}; done
|
|
306
|
-
else
|
|
307
|
-
L=$line; line=""
|
|
308
|
-
fi
|
|
309
|
-
while [ -n "$L" ]; do
|
|
310
|
-
if [ ${#cur} -gt 512 ]; then # keep the tail buffer small
|
|
311
|
-
out+=${cur:0:$((${#cur} - 128))}; cur=${cur: -128}
|
|
312
|
-
fi
|
|
313
|
-
if [ -n "$qc" ]; then # inside a quote: look for the close
|
|
314
|
-
if [ "$qc" = "'" ] && [ "$qansi" = 0 ]; then
|
|
315
|
-
case $L in
|
|
316
|
-
*\'*) qin+=${L%%\'*}; L=${L#*\'}; _scrub_close_quote; qc="" ;;
|
|
317
|
-
*) qin+=$L; L="" ;;
|
|
318
|
-
esac
|
|
319
|
-
else
|
|
320
|
-
if [ "$qc" = '"' ]; then pre=${L%%[\"\\]*}; else pre=${L%%[\'\\]*}; fi
|
|
321
|
-
if [ "$pre" = "$L" ]; then
|
|
322
|
-
qin+=$L; L=""
|
|
323
|
-
else
|
|
324
|
-
qin+=$pre; L=${L#"$pre"}
|
|
325
|
-
if [ "${L:0:1}" = '\' ]; then
|
|
326
|
-
qin+=${L:0:2}; L=${L:2}
|
|
327
|
-
else
|
|
328
|
-
L=${L:1}; _scrub_close_quote; qc=""
|
|
329
|
-
fi
|
|
330
|
-
fi
|
|
331
|
-
fi
|
|
332
|
-
continue
|
|
333
|
-
fi
|
|
334
|
-
pre=${L%%[\'\"\\#<]*}
|
|
335
|
-
if [ "$pre" = "$L" ]; then cur+=$L; L=""; break; fi
|
|
336
|
-
cur+=$pre; L=${L#"$pre"}
|
|
337
|
-
c=${L:0:1}
|
|
338
|
-
case $c in
|
|
339
|
-
'\') cur+=${L:0:2}; L=${L:2} ;;
|
|
340
|
-
'#')
|
|
341
|
-
case ${cur: -1} in
|
|
342
|
-
""|[[:space:]\;\&\|\(]) L="" ;;
|
|
343
|
-
*) cur+=$c; L=${L:1} ;;
|
|
344
|
-
esac ;;
|
|
345
|
-
'<')
|
|
346
|
-
if [ "${L:0:3}" = '<<<' ]; then
|
|
347
|
-
cur+='<<<'; L=${L:3}
|
|
348
|
-
elif [ "${L:0:2}" = '<<' ]; then
|
|
349
|
-
cur+='<<'; L=${L:2}
|
|
350
|
-
case $out$cur in
|
|
351
|
-
*'(('*) ;;
|
|
352
|
-
*)
|
|
353
|
-
if [[ $L =~ $_HD_WORD ]]; then
|
|
354
|
-
d=${BASH_REMATCH[1]}; w=${BASH_REMATCH[2]}
|
|
355
|
-
if [ ${#cur} -gt 122 ]; then tail=${cur: -122}; else tail=$cur; fi
|
|
356
|
-
tail=${tail%??}
|
|
357
|
-
[[ $tail =~ $_SH_HERE ]] || { hd_word=$w; [ -n "$d" ] && hd_dash=1 || hd_dash=0; }
|
|
358
|
-
fi ;;
|
|
359
|
-
esac
|
|
360
|
-
else
|
|
361
|
-
cur+='<'; L=${L:1}
|
|
362
|
-
fi ;;
|
|
363
|
-
*) # opening quote
|
|
364
|
-
qc=$c; qin=""; qexec=0; qansi=0; L=${L:1}
|
|
365
|
-
if [ "$c" = "'" ] && [ "${cur: -1}" = '$' ]; then
|
|
366
|
-
cur=${cur%?}; qansi=1
|
|
367
|
-
fi
|
|
368
|
-
if [ ${#cur} -gt 100 ]; then tail=${cur: -100}; else tail=$cur; fi
|
|
369
|
-
[[ $tail =~ $_EXEC_CTX ]] && qexec=1 ;;
|
|
370
|
-
esac
|
|
371
|
-
done
|
|
372
|
-
done
|
|
373
|
-
done <<< "$text"
|
|
374
|
-
[ -n "$qc" ] && cur+=$qc$qin # unterminated quote: keep as is
|
|
375
|
-
SCRUB_DEPTH=$((SCRUB_DEPTH - 1))
|
|
376
|
-
SCRUBBED=$out$cur
|
|
377
|
-
}
|
|
378
|
-
scrub_quotes "$COMMAND"
|
|
379
|
-
SHELL_TEXT=$SCRUBBED
|
|
380
|
-
|
|
381
|
-
# Positive SQL text-only allow-list, ALWAYS on raw COMMAND, never SHELL_TEXT.
|
|
382
|
-
# Whole-command matches permit only commit messages, echo/printf literals,
|
|
383
|
-
# cat with a single-quoted heredoc, and gh pr/issue create/comment/edit bodies.
|
|
384
|
-
# Double quotes forbid $, backticks and backslashes; file paths have no dot-led
|
|
385
|
-
# segment. Heredoc delimiters are single-quoted identifiers, close exactly once
|
|
386
|
-
# on the final line, and allow one trailing newline. The sole substitution is
|
|
387
|
-
# the exact Claude commit $(cat <<'WORD' ... WORD) idiom. No eval or shell parser:
|
|
388
|
-
# every non-match keeps the original raw SQL verdict, including malformed input.
|
|
389
|
-
_text_only_command() {
|
|
390
|
-
local text=${COMMAND%$'\n'} b='[[:blank:]]' single="'[^']*'" literal path git gh args re header body word line closed=false
|
|
391
|
-
literal="($single|\"[^\"\$\`\\\\]*\")"
|
|
392
|
-
path='/?[A-Za-z0-9_-][A-Za-z0-9_.-]*(/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*'
|
|
393
|
-
git="git${b}+commit(${b}+(-a|-q|--no-verify))*"
|
|
394
|
-
args="(${b}+([A-Za-z0-9_./:=,@%+-]+|$literal))*"
|
|
395
|
-
gh="gh${b}+(pr|issue)${b}+(create|comment|edit)$args"
|
|
396
|
-
re="^${b}*$git(${b}+-m${b}+$literal(${b}+(-a|-q|--no-verify))*)+${b}*$"
|
|
397
|
-
[[ $text =~ $re ]] && return 0
|
|
398
|
-
re="^${b}*(echo|printf)${b}+$literal(${b}*>>?${b}*$path)?${b}*$"
|
|
399
|
-
[[ $text =~ $re ]] && return 0
|
|
400
|
-
re="^${b}*$gh${b}+--body${b}+$single$args${b}*$"
|
|
401
|
-
[[ $text =~ $re ]] && return 0
|
|
402
|
-
|
|
403
|
-
# Split only the first line. The body is inert only if its first delimiter
|
|
404
|
-
# occurrence is also its last line; otherwise a hidden second command runs.
|
|
405
|
-
case $text in *$'\n'*) ;; *) return 1 ;; esac
|
|
406
|
-
header=${text%%$'\n'*}; body=${text#*$'\n'}
|
|
407
|
-
local hd="<<'([A-Za-z_][A-Za-z0-9_]*)'" cat_start='"\$\(cat' cat_end=$'\n)"'
|
|
408
|
-
local redir=">>?${b}*$path" tee_flag="(-a|--append)"
|
|
409
|
-
local re_git="^${b}*($git${b}+-F${b}+-${b}+|$gh${b}+--body-file${b}+-${b}+)${hd}${b}*$"
|
|
410
|
-
local re_cat_before="^${b}*cat(${b}+${redir})?${b}+${hd}${b}*$"
|
|
411
|
-
local re_cat_after="^${b}*cat${b}+${hd}(${b}*${redir})?${b}*$"
|
|
412
|
-
local re_tee_before="^${b}*tee(${b}+${tee_flag})?${b}+${path}${b}+${hd}${b}*$"
|
|
413
|
-
local re_tee_after="^${b}*tee(${b}+${tee_flag})?${b}+${hd}${b}+(${tee_flag}${b}+)?${path}${b}*$"
|
|
414
|
-
if [[ $header =~ $re_git ]] || [[ $header =~ $re_cat_before ]] || [[ $header =~ $re_cat_after ]] || [[ $header =~ $re_tee_before ]] || [[ $header =~ $re_tee_after ]]; then
|
|
415
|
-
# Match the delimiter separately so prefix capture groups cannot change its index.
|
|
416
|
-
[[ $header =~ $hd ]] || return 1
|
|
417
|
-
word=${BASH_REMATCH[1]}
|
|
418
|
-
else
|
|
419
|
-
re="^${b}*$git${b}+-m${b}+$cat_start${b}+${hd}${b}*$"
|
|
420
|
-
[[ $header =~ $re ]] || return 1
|
|
421
|
-
[[ $header =~ $hd ]] || return 1
|
|
422
|
-
word=${BASH_REMATCH[1]}
|
|
423
|
-
[[ $body == *"$cat_end" ]] || return 1
|
|
424
|
-
body=${body%"$cat_end"}
|
|
425
|
-
fi
|
|
426
|
-
while IFS= read -r line; do
|
|
427
|
-
[ "$closed" = false ] || return 1
|
|
428
|
-
[ "$line" != "$word" ] || closed=true
|
|
429
|
-
done <<< "$body"
|
|
430
|
-
[ "$closed" = true ]
|
|
431
|
-
}
|
|
432
|
-
DATA_ONLY=false
|
|
433
|
-
_text_only_command && DATA_ONLY=true
|
|
434
|
-
|
|
435
|
-
# Project-specific unanchored patterns keep their verdicts too: only these
|
|
436
|
-
# three shipped SQL families participate in the text-only exception.
|
|
437
|
-
_sql_pattern() {
|
|
438
|
-
case $1 in
|
|
439
|
-
'DROP[[:space:]]+(TABLE|DATABASE|SCHEMA)[[:space:]]'|\
|
|
440
|
-
'DELETE[[:space:]]+FROM[[:space:]]+[[:alnum:]_]+[[:space:]]*;'|\
|
|
441
|
-
'TRUNCATE[[:space:]]+TABLE') return 0 ;;
|
|
442
|
-
esac
|
|
443
|
-
return 1
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
# Hardcoded floor, used only when blocked-patterns.conf is missing: recursive
|
|
447
|
-
# rm (any flag spelling, flag before or after the target) of a catastrophic
|
|
448
|
-
# target — the same rule as the rm lines in blocked-patterns.conf. Subpaths
|
|
449
|
-
# such as /tmp/x are not blocked.
|
|
450
|
-
_CMDPOS='(^|[;&|(`])[[:space:]]*'
|
|
451
|
-
_WRAP='((sudo([[:space:]]+(-[ugChprtUDR][[:space:]]+[^-[:space:];&|][^[:space:];&|]*|--(user|group|host|prompt|role|type|chdir)[[:space:]]+[^-[:space:];&|][^[:space:];&|]*|-[^[:space:];&|]*|[A-Za-z_][A-Za-z0-9_]*=[^[:space:];&|]*))*|xargs([[:space:]]+(-[nILPsdEa][[:space:]]+[^-[:space:];&|][^[:space:];&|]*|-[^[:space:];&|]*|[A-Za-z_][A-Za-z0-9_]*=[^[:space:];&|]*))*|env([[:space:]]+(-[uCS][[:space:]]+[^-[:space:];&|][^[:space:];&|]*|-[^[:space:];&|]*|[A-Za-z_][A-Za-z0-9_]*=[^[:space:];&|]*))*|[A-Za-z_][A-Za-z0-9_]*=[^[:space:];&|]*|(command|time|nohup|exec|eval|then|do|else)([[:space:]]+(-[^[:space:];&|]*|[A-Za-z_][A-Za-z0-9_]*=[^[:space:];&|]*))*)[[:space:]]+)*'
|
|
452
|
-
_PATHRM='(\\|/(usr/)?bin/)?rm'
|
|
453
|
-
_RM_FLAG='(-[a-zA-Z]*[rR][a-zA-Z]*|--recursive)'
|
|
454
|
-
_RM_ARGS='([[:space:]]+[^[:space:];&|]+)*'
|
|
455
|
-
_END='([[:space:];&|)`]|$)'
|
|
456
|
-
_RM_PREFIX="${_CMDPOS}${_WRAP}${_PATHRM}${_RM_ARGS}[[:space:]]+"
|
|
457
|
-
# _floor_rm_hits <target-regex> : does the scrubbed command recursively rm it?
|
|
458
|
-
_floor_rm_hits() {
|
|
459
|
-
grep -qiE -- "${_RM_PREFIX}(${_RM_FLAG}${_RM_ARGS}[[:space:]]+($1)${_END}|($1)${_RM_ARGS}[[:space:]]+${_RM_FLAG}${_END})" <<<"$SHELL_TEXT" 2>/dev/null
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
if [ ! -f "$PATTERNS_FILE" ]; then
|
|
463
|
-
# The pattern file ships with DoFlow, so a missing one means a broken install: record it on this
|
|
464
|
-
# machine (feature 046, IC-018) and go on exactly as before. Subshell, output discarded: nothing
|
|
465
|
-
# the helper does can reach this policy's options, variables, output or exit status.
|
|
466
|
-
( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure pre-bash-guard patterns-missing ) >/dev/null 2>&1 || true
|
|
467
|
-
doflow_decide error patterns-missing
|
|
468
|
-
if _floor_rm_hits '/+\*{0,2}'; then
|
|
469
|
-
echo "[pre-bash-guard] Catastrophic delete blocked — recursive rm of the root directory (/)" >&2
|
|
470
|
-
doflow_decide deny catastrophic-rm-root
|
|
471
|
-
exit 2
|
|
472
|
-
fi
|
|
473
|
-
if _floor_rm_hits '(~[A-Za-z_][A-Za-z0-9_.-]*|~|\$HOME|\$\{HOME\})(/\*{0,2})?'; then
|
|
474
|
-
echo "[pre-bash-guard] Catastrophic delete blocked — recursive rm of the home directory" >&2
|
|
475
|
-
doflow_decide deny catastrophic-rm-home
|
|
476
|
-
exit 2
|
|
477
|
-
fi
|
|
478
|
-
if _floor_rm_hits '/(Users|home|etc|usr|bin|sbin|var|opt|System|Library|Applications|Volumes|private|root|boot|lib|dev|proc)(/\*{0,2})?'; then
|
|
479
|
-
echo "[pre-bash-guard] Catastrophic delete blocked — recursive rm of a system directory" >&2
|
|
480
|
-
doflow_decide deny catastrophic-rm-system
|
|
481
|
-
exit 2
|
|
482
|
-
fi
|
|
483
|
-
# No patterns file beyond the hardcoded floor — allow everything else
|
|
484
|
-
# (fail open — don't block the agent).
|
|
485
|
-
exit 0
|
|
486
|
-
fi
|
|
487
|
-
|
|
488
|
-
# One combined ERE cheaply rules out commands that match no live pattern. A
|
|
489
|
-
# grep match or regex error falls through to the original per-line loop, which
|
|
490
|
-
# still owns the reason and optional exclude semantics.
|
|
491
|
-
_anchored_patterns="" _full_patterns="" _has_backreference=false
|
|
492
|
-
_has_worker_marker=false
|
|
493
|
-
while IFS=$'\t' read -r pattern _reason _exclude || [ -n "$pattern" ]; do
|
|
494
|
-
[ -z "$pattern" ] && continue
|
|
495
|
-
case "$pattern" in
|
|
496
|
-
'#@exempt-next worker-worktree') _has_worker_marker=true; continue ;;
|
|
497
|
-
\#*) continue ;;
|
|
498
|
-
esac
|
|
499
|
-
if [ "$DATA_ONLY" = true ] && _sql_pattern "$pattern"; then continue; fi
|
|
500
|
-
if [[ "$pattern" =~ \\[1-9] ]]; then _has_backreference=true; fi
|
|
501
|
-
case "$pattern" in
|
|
502
|
-
'(^|'*) _anchored_patterns="${_anchored_patterns:+$_anchored_patterns|}($pattern)" ;;
|
|
503
|
-
*) _full_patterns="${_full_patterns:+$_full_patterns|}($pattern)" ;;
|
|
504
|
-
esac
|
|
505
|
-
done < "$PATTERNS_FILE"
|
|
506
|
-
_anchored_status=1 _full_status=1
|
|
507
|
-
if [ -n "$_anchored_patterns" ]; then
|
|
508
|
-
grep -qiE -- "$_anchored_patterns" <<<"$SHELL_TEXT" 2>/dev/null
|
|
509
|
-
_anchored_status=$?
|
|
510
|
-
fi
|
|
511
|
-
if [ -n "$_full_patterns" ]; then
|
|
512
|
-
grep -qiE -- "$_full_patterns" <<<"$COMMAND" 2>/dev/null
|
|
513
|
-
_full_status=$?
|
|
514
|
-
fi
|
|
515
|
-
if [ "$_anchored_status" -eq 1 ] && [ "$_full_status" -eq 1 ] && [ "$_has_backreference" != true ]; then
|
|
516
|
-
# Some accepted worker shapes (reset -q --hard, clean -f) already match no
|
|
517
|
-
# deny rule. Give those allows the same scope code without widening a rule.
|
|
518
|
-
if [ "$_has_worker_marker" = true ] && _worker_exempt; then doflow_decide allow worker-worktree; fi
|
|
519
|
-
exit 0
|
|
520
|
-
fi
|
|
521
|
-
|
|
522
|
-
# ── Pattern matching ──────────────────────────────────────────────────────────
|
|
523
|
-
|
|
524
|
-
_exempt_next= _worker_skipped=false
|
|
525
|
-
while IFS=$'\t' read -r pattern reason exclude || [ -n "$pattern" ]; do
|
|
526
|
-
# Skip comments and empty lines
|
|
527
|
-
[ -z "$pattern" ] && continue
|
|
528
|
-
case "$pattern" in
|
|
529
|
-
'#@exempt-next worker-worktree') _exempt_next=worker-worktree; continue ;;
|
|
530
|
-
'#@exempt-next worker-dropdb') _exempt_next=worker-dropdb; continue ;;
|
|
531
|
-
\#*) continue ;;
|
|
532
|
-
esac
|
|
533
|
-
exemption=$_exempt_next
|
|
534
|
-
_exempt_next=
|
|
535
|
-
if [ "$DATA_ONLY" = true ] && _sql_pattern "$pattern"; then continue; fi
|
|
536
|
-
|
|
537
|
-
# Match pattern against command (case-insensitive, POSIX extended regex: the default macOS
|
|
538
|
-
# filesystem is case-insensitive, so RM and GIT run there).
|
|
539
|
-
# A bad regex makes grep exit 2, which "if" reads as no match (fail open).
|
|
540
|
-
# "--" stops grep from treating a pattern beginning with '-' (e.g. an
|
|
541
|
-
# exclude pattern like "--force-with-lease") as an option flag.
|
|
542
|
-
# Anchored shell-command patterns (they begin with the "(^|" command-position
|
|
543
|
-
# anchor) match the scrubbed text; every other pattern (the SQL ones)
|
|
544
|
-
# matches the full text, so statements inside quoted psql -c / heredocs are
|
|
545
|
-
# still caught.
|
|
546
|
-
case "$pattern" in '(^|'*) target=$SHELL_TEXT ;; *) target=$COMMAND ;; esac
|
|
547
|
-
matched=false
|
|
548
|
-
if grep -qiE -- "$pattern" <<<"$target" 2>/dev/null; then
|
|
549
|
-
matched=true
|
|
550
|
-
fi
|
|
551
|
-
|
|
552
|
-
# Optional third column: if the command also matches the exclude pattern,
|
|
553
|
-
# this pattern line is skipped entirely (approximates negative lookahead,
|
|
554
|
-
# which POSIX ERE cannot express — e.g. "--force-with-lease" excludes the
|
|
555
|
-
# "git push --force" block).
|
|
556
|
-
if [ "$matched" = "true" ] && [ -n "$exclude" ]; then
|
|
557
|
-
if grep -qiE -- "$exclude" <<<"$target" 2>/dev/null; then
|
|
558
|
-
matched=false
|
|
559
|
-
fi
|
|
560
|
-
fi
|
|
561
|
-
|
|
562
|
-
if [ "$matched" = "true" ]; then
|
|
563
|
-
if [ "$exemption" = worker-worktree ] && _worker_exempt; then
|
|
564
|
-
_worker_skipped=worker-worktree
|
|
565
|
-
continue
|
|
566
|
-
fi
|
|
567
|
-
if [ "$exemption" = worker-dropdb ] && _worker_dropdb_exempt; then
|
|
568
|
-
_worker_skipped=worker-dropdb
|
|
569
|
-
continue
|
|
570
|
-
fi
|
|
571
|
-
echo "[pre-bash-guard] ${reason:-Command blocked by pre-bash-guard}" >&2
|
|
572
|
-
doflow_decide deny blocked-pattern
|
|
573
|
-
exit 2
|
|
574
|
-
fi
|
|
575
|
-
done < "$PATTERNS_FILE"
|
|
576
|
-
|
|
577
|
-
# No match — allow
|
|
578
|
-
[ "$_worker_skipped" != false ] && doflow_decide allow "$_worker_skipped"
|
|
579
|
-
exit 0
|