@bookedsolid/rea 0.1.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/LICENSE +21 -0
- package/README.md +339 -0
- package/SECURITY.md +104 -0
- package/THREAT_MODEL.md +245 -0
- package/agents/accessibility-engineer.md +101 -0
- package/agents/backend-engineer.md +126 -0
- package/agents/code-reviewer.md +144 -0
- package/agents/codex-adversarial.md +107 -0
- package/agents/frontend-specialist.md +84 -0
- package/agents/qa-engineer.md +138 -0
- package/agents/rea-orchestrator.md +101 -0
- package/agents/security-engineer.md +108 -0
- package/agents/technical-writer.md +140 -0
- package/agents/typescript-specialist.md +111 -0
- package/commands/codex-review.md +104 -0
- package/commands/freeze.md +81 -0
- package/commands/halt-check.md +120 -0
- package/commands/rea.md +52 -0
- package/commands/review.md +79 -0
- package/dist/cli/check.d.ts +1 -0
- package/dist/cli/check.js +66 -0
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.js +93 -0
- package/dist/cli/freeze.d.ts +8 -0
- package/dist/cli/freeze.js +61 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +65 -0
- package/dist/cli/init.d.ts +6 -0
- package/dist/cli/init.js +237 -0
- package/dist/cli/serve.d.ts +1 -0
- package/dist/cli/serve.js +19 -0
- package/dist/cli/utils.d.ts +23 -0
- package/dist/cli/utils.js +51 -0
- package/dist/config/tier-map.d.ts +11 -0
- package/dist/config/tier-map.js +108 -0
- package/dist/config/types.d.ts +24 -0
- package/dist/config/types.js +1 -0
- package/dist/gateway/circuit-breaker.d.ts +43 -0
- package/dist/gateway/circuit-breaker.js +86 -0
- package/dist/gateway/middleware/audit-types.d.ts +16 -0
- package/dist/gateway/middleware/audit-types.js +1 -0
- package/dist/gateway/middleware/audit.d.ts +12 -0
- package/dist/gateway/middleware/audit.js +98 -0
- package/dist/gateway/middleware/blocked-paths.d.ts +12 -0
- package/dist/gateway/middleware/blocked-paths.js +117 -0
- package/dist/gateway/middleware/chain.d.ts +28 -0
- package/dist/gateway/middleware/chain.js +40 -0
- package/dist/gateway/middleware/circuit-breaker.d.ts +11 -0
- package/dist/gateway/middleware/circuit-breaker.js +43 -0
- package/dist/gateway/middleware/injection.d.ts +22 -0
- package/dist/gateway/middleware/injection.js +128 -0
- package/dist/gateway/middleware/kill-switch.d.ts +10 -0
- package/dist/gateway/middleware/kill-switch.js +58 -0
- package/dist/gateway/middleware/policy.d.ts +12 -0
- package/dist/gateway/middleware/policy.js +70 -0
- package/dist/gateway/middleware/rate-limit.d.ts +12 -0
- package/dist/gateway/middleware/rate-limit.js +31 -0
- package/dist/gateway/middleware/redact.d.ts +16 -0
- package/dist/gateway/middleware/redact.js +128 -0
- package/dist/gateway/middleware/result-size-cap.d.ts +13 -0
- package/dist/gateway/middleware/result-size-cap.js +48 -0
- package/dist/gateway/middleware/session.d.ts +10 -0
- package/dist/gateway/middleware/session.js +18 -0
- package/dist/gateway/middleware/tier.d.ts +6 -0
- package/dist/gateway/middleware/tier.js +10 -0
- package/dist/gateway/rate-limiter.d.ts +36 -0
- package/dist/gateway/rate-limiter.js +75 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/policy/loader.d.ts +80 -0
- package/dist/policy/loader.js +146 -0
- package/dist/policy/types.d.ts +34 -0
- package/dist/policy/types.js +19 -0
- package/hooks/_lib/common.sh +105 -0
- package/hooks/_lib/halt-check.sh +39 -0
- package/hooks/_lib/policy-read.sh +79 -0
- package/hooks/architecture-review-gate.sh +84 -0
- package/hooks/attribution-advisory.sh +126 -0
- package/hooks/blocked-paths-enforcer.sh +176 -0
- package/hooks/changeset-security-gate.sh +143 -0
- package/hooks/commit-review-gate.sh +166 -0
- package/hooks/dangerous-bash-interceptor.sh +362 -0
- package/hooks/dependency-audit-gate.sh +118 -0
- package/hooks/env-file-protection.sh +110 -0
- package/hooks/pr-issue-link-gate.sh +65 -0
- package/hooks/push-review-gate.sh +120 -0
- package/hooks/secret-scanner.sh +229 -0
- package/hooks/security-disclosure-gate.sh +146 -0
- package/hooks/settings-protection.sh +147 -0
- package/package.json +93 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: dangerous-bash-interceptor.sh
|
|
3
|
+
# Fires BEFORE every Bash tool call.
|
|
4
|
+
# Detects destructive shell commands and blocks them (exit 2) or warns (exit 0).
|
|
5
|
+
#
|
|
6
|
+
# Compatible with: interactive sessions + headless Docker (no TTY required).
|
|
7
|
+
# All diagnostic output goes to stderr only.
|
|
8
|
+
#
|
|
9
|
+
# Content extraction:
|
|
10
|
+
# Bash tool → tool_input.command
|
|
11
|
+
#
|
|
12
|
+
# Exit codes:
|
|
13
|
+
# 0 = safe or advisory-only — allow the command to run
|
|
14
|
+
# 2 = HIGH severity danger detected — block the command with feedback
|
|
15
|
+
|
|
16
|
+
set -uo pipefail
|
|
17
|
+
|
|
18
|
+
# ── 1. Read ALL stdin immediately before doing anything else ──────────────────
|
|
19
|
+
INPUT=$(cat)
|
|
20
|
+
|
|
21
|
+
# ── 2. Dependency check ───────────────────────────────────────────────────────
|
|
22
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
23
|
+
printf 'REA ERROR: jq is required but not installed.\n' >&2
|
|
24
|
+
printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
|
|
25
|
+
exit 2
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# ── 3. HALT check ─────────────────────────────────────────────────────────────
|
|
29
|
+
REA_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
30
|
+
HALT_FILE="${REA_ROOT}/.rea/HALT"
|
|
31
|
+
if [ -f "$HALT_FILE" ]; then
|
|
32
|
+
printf 'REA HALT: %s\nAll agent operations suspended. Run: rea unfreeze\n' \
|
|
33
|
+
"$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
|
|
34
|
+
exit 2
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# ── 4. Parse tool_input.command from the hook payload ─────────────────────────
|
|
38
|
+
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
39
|
+
|
|
40
|
+
if [[ -z "$CMD" ]]; then
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# ── 5. Helper: truncate command for display ────────────────────────────────────
|
|
45
|
+
truncate_cmd() {
|
|
46
|
+
local STR="$1"
|
|
47
|
+
local MAX=200
|
|
48
|
+
if [[ ${#STR} -gt $MAX ]]; then
|
|
49
|
+
printf '%s' "${STR:0:$MAX}..."
|
|
50
|
+
else
|
|
51
|
+
printf '%s' "$STR"
|
|
52
|
+
fi
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# ── 6. Violation accumulators ──────────────────────────────────────────────────
|
|
56
|
+
HIGH_FILE=$(mktemp "${TMPDIR:-/tmp}/rea-bash-high-XXXXXX")
|
|
57
|
+
MEDIUM_FILE=$(mktemp "${TMPDIR:-/tmp}/rea-bash-medium-XXXXXX")
|
|
58
|
+
|
|
59
|
+
cleanup_violations() {
|
|
60
|
+
rm -f "$HIGH_FILE" "$MEDIUM_FILE"
|
|
61
|
+
}
|
|
62
|
+
trap cleanup_violations EXIT
|
|
63
|
+
|
|
64
|
+
add_high() {
|
|
65
|
+
local LABEL="$1"
|
|
66
|
+
local DETAIL="$2"
|
|
67
|
+
shift 2
|
|
68
|
+
printf 'HIGH|%s|%s\n' "$LABEL" "$DETAIL" >> "$HIGH_FILE"
|
|
69
|
+
for ALT in "$@"; do
|
|
70
|
+
printf 'ALT:%s\n' "$ALT" >> "$HIGH_FILE"
|
|
71
|
+
done
|
|
72
|
+
printf 'END_VIOLATION\n' >> "$HIGH_FILE"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
add_medium() {
|
|
76
|
+
local LABEL="$1"
|
|
77
|
+
local DETAIL="$2"
|
|
78
|
+
shift 2
|
|
79
|
+
printf 'MEDIUM|%s|%s\n' "$LABEL" "$DETAIL" >> "$MEDIUM_FILE"
|
|
80
|
+
for ALT in "$@"; do
|
|
81
|
+
printf 'ALT:%s\n' "$ALT" >> "$MEDIUM_FILE"
|
|
82
|
+
done
|
|
83
|
+
printf 'END_VIOLATION\n' >> "$MEDIUM_FILE"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# ── 7. Per-segment evaluation helper ──────────────────────────────────────────
|
|
87
|
+
# Split on &&, ||, ;, and newlines and test a pattern against each segment.
|
|
88
|
+
# Returns 0 if ANY segment matches the pattern.
|
|
89
|
+
any_segment_matches() {
|
|
90
|
+
local PATTERN="$1"
|
|
91
|
+
while IFS= read -r SEG; do
|
|
92
|
+
if printf '%s' "$SEG" | grep -qiE "$PATTERN"; then
|
|
93
|
+
return 0
|
|
94
|
+
fi
|
|
95
|
+
done < <(printf '%s' "$CMD" | sed 's/&&/\n/g; s/||/\n/g; s/;/\n/g')
|
|
96
|
+
return 1
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# ── 8. Smart exclusion flags ──────────────────────────────────────────────────
|
|
100
|
+
CMD_IS_REBASE_SAFE=0
|
|
101
|
+
if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+(rebase)[[:space:]].*(--abort|--continue)'; then
|
|
102
|
+
CMD_IS_REBASE_SAFE=1
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
CMD_IS_CLEAN_DRY=0
|
|
106
|
+
if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+clean.*([ \t]-n|--dry-run)'; then
|
|
107
|
+
CMD_IS_CLEAN_DRY=1
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# ── 9. HIGH severity checks ────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
# H1: git push --force or -f (per-segment — prevents --force-with-lease poisoning)
|
|
113
|
+
# A segment containing --force-with-lease is excluded; other segments are not.
|
|
114
|
+
while IFS= read -r SEGMENT; do
|
|
115
|
+
SEGMENT=$(printf '%s' "$SEGMENT" | sed 's/^[[:space:]]*//')
|
|
116
|
+
[[ -z "$SEGMENT" ]] && continue
|
|
117
|
+
# Skip segments that use the safe --force-with-lease
|
|
118
|
+
if printf '%s' "$SEGMENT" | grep -qiE 'git[[:space:]]+push.*--force-with-lease'; then
|
|
119
|
+
continue
|
|
120
|
+
fi
|
|
121
|
+
if printf '%s' "$SEGMENT" | grep -qiE 'git[[:space:]]+push.*(--force|-f[[:space:]])' || \
|
|
122
|
+
printf '%s' "$SEGMENT" | grep -qiE 'git[[:space:]]+push.*(--force|-f)$'; then
|
|
123
|
+
add_high \
|
|
124
|
+
"git push --force — force push detected" \
|
|
125
|
+
"Force-pushing rewrites public history and breaks collaborators' local copies." \
|
|
126
|
+
"Alt: Use 'git push --force-with-lease' — blocks if upstream has new commits you haven't pulled."
|
|
127
|
+
break
|
|
128
|
+
fi
|
|
129
|
+
done < <(printf '%s' "$CMD" | sed 's/&&/\n/g; s/||/\n/g; s/;/\n/g')
|
|
130
|
+
|
|
131
|
+
# H2: git rebase — advisory (MEDIUM)
|
|
132
|
+
if [[ $CMD_IS_REBASE_SAFE -eq 0 ]]; then
|
|
133
|
+
if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+rebase([[:space:]]|$)'; then
|
|
134
|
+
add_medium \
|
|
135
|
+
"git rebase — rewrites commit history (advisory)" \
|
|
136
|
+
"Rebase changes commit SHAs. Safe on local feature branches; dangerous on shared/published branches." \
|
|
137
|
+
"Alt: 'git merge origin/main' preserves history (creates merge commit)." \
|
|
138
|
+
" 'git rebase --abort' to cancel if in progress."
|
|
139
|
+
fi
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
# H3: git checkout -- .
|
|
143
|
+
if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+checkout[[:space:]]+--[[:space:]]+\.'; then
|
|
144
|
+
add_high \
|
|
145
|
+
"git checkout -- . — discards all uncommitted changes" \
|
|
146
|
+
"Overwrites working tree changes with HEAD. Uncommitted work is lost permanently." \
|
|
147
|
+
"Alt: 'git stash' to temporarily shelve changes, 'git restore <file>' for individual files."
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
# H4: git restore . (any form — with or without --staged flag)
|
|
151
|
+
if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+restore[[:space:]].*[[:space:]]\.([[:space:]]|$)' || \
|
|
152
|
+
printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+restore[[:space:]]+\.[[:space:]]*$'; then
|
|
153
|
+
add_high \
|
|
154
|
+
"git restore . — discards all uncommitted changes" \
|
|
155
|
+
"Restores every tracked file to HEAD, permanently discarding all working tree modifications." \
|
|
156
|
+
"Alt: 'git stash' to save changes temporarily, or restore individual files: 'git restore <file>'."
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
# H5: git clean -f
|
|
160
|
+
if [[ $CMD_IS_CLEAN_DRY -eq 0 ]]; then
|
|
161
|
+
if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+clean[[:space:]]+-[a-zA-Z]*f'; then
|
|
162
|
+
add_high \
|
|
163
|
+
"git clean -f — removes untracked files" \
|
|
164
|
+
"Permanently deletes untracked files from the working tree. Cannot be undone via git." \
|
|
165
|
+
"Alt: 'git clean -n' (dry-run) to preview what would be deleted before committing."
|
|
166
|
+
fi
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
# H6: DROP TABLE or DROP DATABASE in psql
|
|
170
|
+
if printf '%s' "$CMD" | grep -qiE '(psql|pgcli)[^|&;]*DROP[[:space:]]+(TABLE|DATABASE|SCHEMA)'; then
|
|
171
|
+
add_high \
|
|
172
|
+
"DROP TABLE/DATABASE via psql — destructive DDL" \
|
|
173
|
+
"Running destructive DDL directly in psql bypasses migration pipeline safety checks." \
|
|
174
|
+
"Alt: Use your project's migration tool. Never run DROP via ad-hoc psql."
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
# H7: kill -9 with pgrep subshell
|
|
178
|
+
if printf '%s' "$CMD" | grep -qiE 'kill[[:space:]]+-9[[:space:]]+(\$\(|`)'; then
|
|
179
|
+
add_high \
|
|
180
|
+
"kill -9 with pgrep subshell — aggressive process termination" \
|
|
181
|
+
"Sends SIGKILL to processes matched by name, which may kill unintended processes." \
|
|
182
|
+
"Alt: 'kill -15 <pid>' (SIGTERM) for graceful shutdown."
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
# H8: killall -9
|
|
186
|
+
if printf '%s' "$CMD" | grep -qiE 'killall[[:space:]]+-9[[:space:]]+\S'; then
|
|
187
|
+
add_high \
|
|
188
|
+
"killall -9 — SIGKILL all matching processes" \
|
|
189
|
+
"Immediately terminates all processes with the given name without cleanup." \
|
|
190
|
+
"Alt: 'killall -15 <name>' (SIGTERM) allows graceful shutdown."
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
# H9: git commit --no-verify
|
|
194
|
+
if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+commit.*--no-verify'; then
|
|
195
|
+
add_high \
|
|
196
|
+
"git commit --no-verify — skipping pre-commit hooks" \
|
|
197
|
+
"Bypasses all pre-commit safety gates including secret scanning and linting." \
|
|
198
|
+
"Alt: Fix the underlying hook failure rather than bypassing it."
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
# H10: HUSKY=0 bypass — suppresses all git hooks without --no-verify
|
|
202
|
+
if printf '%s' "$CMD" | grep -qiE '(^|[[:space:];]|&&|\|\|)HUSKY=0[[:space:]]+git[[:space:]]+(commit|push|tag)'; then
|
|
203
|
+
add_high \
|
|
204
|
+
"HUSKY=0 — bypasses all husky git hooks" \
|
|
205
|
+
"Setting HUSKY=0 disables pre-commit, commit-msg, and pre-push safety gates without --no-verify." \
|
|
206
|
+
"Alt: Fix the underlying hook failure rather than suppressing all hooks."
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
# H11: rm -rf with broad targets
|
|
210
|
+
# Covers combined flags (rm -rf, rm -fr), split flags (rm -r -f), and long flags (rm --recursive --force)
|
|
211
|
+
BROAD_TARGETS='(\/|~\/|\.\/\*|\*|\.|src|dist|build|node_modules)'
|
|
212
|
+
if printf '%s' "$CMD" | grep -qiE "rm[[:space:]]+-[a-zA-Z]*r[a-zA-Z]*f[[:space:]]+${BROAD_TARGETS}" || \
|
|
213
|
+
printf '%s' "$CMD" | grep -qiE "rm[[:space:]]+-[a-zA-Z]*f[a-zA-Z]*r[[:space:]]+${BROAD_TARGETS}" || \
|
|
214
|
+
printf '%s' "$CMD" | grep -qiE "rm[[:space:]]+-[a-zA-Z]*r[[:space:]]+-[a-zA-Z]*f[[:space:]]+${BROAD_TARGETS}" || \
|
|
215
|
+
printf '%s' "$CMD" | grep -qiE "rm[[:space:]]+-[a-zA-Z]*f[[:space:]]+-[a-zA-Z]*r[[:space:]]+${BROAD_TARGETS}" || \
|
|
216
|
+
printf '%s' "$CMD" | grep -qiE "rm[[:space:]]+--recursive[[:space:]]+--force[[:space:]]+${BROAD_TARGETS}" || \
|
|
217
|
+
printf '%s' "$CMD" | grep -qiE "rm[[:space:]]+--force[[:space:]]+--recursive[[:space:]]+${BROAD_TARGETS}"; then
|
|
218
|
+
add_high \
|
|
219
|
+
"rm -rf with broad target — mass file deletion" \
|
|
220
|
+
"Permanently deletes files and directories. Cannot be undone." \
|
|
221
|
+
"Alt: Move to a temp location first, or use 'rm -ri' for interactive deletion."
|
|
222
|
+
fi
|
|
223
|
+
|
|
224
|
+
# H12: curl/wget piped directly to shell (supply chain attack vector)
|
|
225
|
+
if printf '%s' "$CMD" | grep -qiE '(curl|wget)[^|]*\|[[:space:]]*(bash|sh|zsh|fish)'; then
|
|
226
|
+
add_high \
|
|
227
|
+
"curl/wget piped to shell — remote code execution" \
|
|
228
|
+
"Executing remote scripts without inspection is a major supply chain risk." \
|
|
229
|
+
"Alt: Download first, inspect the script, then execute: curl -o script.sh URL && cat script.sh && bash script.sh"
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
# H13: git push --no-verify — bypasses pre-push hooks
|
|
233
|
+
if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+push.*--no-verify'; then
|
|
234
|
+
add_high \
|
|
235
|
+
"git push --no-verify — skipping pre-push hooks" \
|
|
236
|
+
"Bypasses all pre-push safety gates including CI checks." \
|
|
237
|
+
"Alt: Fix the underlying hook failure rather than bypassing it."
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
# H14: git -c core.hooksPath= — redirects or disables hook execution
|
|
241
|
+
if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+-c[[:space:]]+core\.hookspath'; then
|
|
242
|
+
add_high \
|
|
243
|
+
"git -c core.hooksPath — overriding hooks directory" \
|
|
244
|
+
"Redirecting the hooks path can disable all safety hooks." \
|
|
245
|
+
"Alt: Fix the underlying hook issue. Do not bypass the hooks directory."
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
# H15: REA_BYPASS env var — attempted escape hatch
|
|
249
|
+
if printf '%s' "$CMD" | grep -qiE '(^|[[:space:];]|&&|\|\|)REA_BYPASS[[:space:]]*='; then
|
|
250
|
+
add_high \
|
|
251
|
+
"REA_BYPASS env var — unauthorized bypass attempt" \
|
|
252
|
+
"Setting REA_BYPASS is not a supported escape mechanism and indicates a bypass attempt." \
|
|
253
|
+
"Alt: If you need to override a gate, request human escalation."
|
|
254
|
+
fi
|
|
255
|
+
|
|
256
|
+
# H16: alias/function definitions containing bypass strings
|
|
257
|
+
if printf '%s' "$CMD" | grep -qiE '(alias|function)[[:space:]]+[a-zA-Z_]+.*(--(no-verify|force)|HUSKY=0|core\.hookspath)'; then
|
|
258
|
+
add_high \
|
|
259
|
+
"Alias/function definition with bypass — circumventing safety gates" \
|
|
260
|
+
"Defining aliases or functions that embed bypass flags defeats safety hooks." \
|
|
261
|
+
"Alt: Do not wrap bypass patterns in aliases or functions."
|
|
262
|
+
fi
|
|
263
|
+
|
|
264
|
+
# H17: context_protection — block commands that should be delegated to subagents
|
|
265
|
+
# Reads context_protection.delegate_to_subagent from .rea/policy.yaml.
|
|
266
|
+
# These commands produce excessive output that exhausts coordinator context windows.
|
|
267
|
+
POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
|
|
268
|
+
if [[ -f "$POLICY_FILE" ]]; then
|
|
269
|
+
DELEGATE_PATTERNS=()
|
|
270
|
+
IN_DELEGATE_BLOCK=0
|
|
271
|
+
while IFS= read -r line; do
|
|
272
|
+
if printf '%s' "$line" | grep -qE '^[[:space:]]*delegate_to_subagent:'; then
|
|
273
|
+
# Check for inline empty array
|
|
274
|
+
if printf '%s' "$line" | grep -qE 'delegate_to_subagent:[[:space:]]*\[\]'; then
|
|
275
|
+
break
|
|
276
|
+
fi
|
|
277
|
+
IN_DELEGATE_BLOCK=1
|
|
278
|
+
continue
|
|
279
|
+
fi
|
|
280
|
+
if [[ $IN_DELEGATE_BLOCK -eq 1 ]]; then
|
|
281
|
+
# Block sequence items start with " - "
|
|
282
|
+
if printf '%s' "$line" | grep -qE '^[[:space:]]*-[[:space:]]'; then
|
|
283
|
+
pattern=$(printf '%s' "$line" | sed "s/^[[:space:]]*-[[:space:]]*//; s/^[\"']//; s/[\"']$//")
|
|
284
|
+
if [[ -n "$pattern" ]]; then
|
|
285
|
+
DELEGATE_PATTERNS+=("$pattern")
|
|
286
|
+
fi
|
|
287
|
+
else
|
|
288
|
+
# Non-continuation line = end of block
|
|
289
|
+
break
|
|
290
|
+
fi
|
|
291
|
+
fi
|
|
292
|
+
done < "$POLICY_FILE"
|
|
293
|
+
|
|
294
|
+
for pattern in "${DELEGATE_PATTERNS[@]+"${DELEGATE_PATTERNS[@]}"}"; do
|
|
295
|
+
# Use fixed-string match — these are command prefixes, not regex
|
|
296
|
+
if printf '%s' "$CMD" | grep -qF "$pattern"; then
|
|
297
|
+
add_high \
|
|
298
|
+
"Context protection — command must run in a subagent" \
|
|
299
|
+
"This command produces excessive output that will exhaust the coordinator context window. Delegate it to a subagent instead of running it directly." \
|
|
300
|
+
"Alt: Use the Agent tool to delegate: Agent(subagent_type: 'qa-engineer-automation', prompt: 'Run $pattern and report pass/fail summary only.')" \
|
|
301
|
+
"Alt: The context_protection policy in .rea/policy.yaml lists commands that must be delegated."
|
|
302
|
+
break
|
|
303
|
+
fi
|
|
304
|
+
done
|
|
305
|
+
fi
|
|
306
|
+
|
|
307
|
+
# ── 10. MEDIUM severity checks ────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
# M1: npm install --force
|
|
310
|
+
if printf '%s' "$CMD" | grep -qiE 'npm[[:space:]]+(install|i)[[:space:]].*--force'; then
|
|
311
|
+
add_medium \
|
|
312
|
+
"npm install --force — bypasses dependency resolution" \
|
|
313
|
+
"--force skips conflict checks and can install incompatible package versions." \
|
|
314
|
+
"Alt: Resolve the dependency conflict explicitly. Use --legacy-peer-deps if needed."
|
|
315
|
+
fi
|
|
316
|
+
|
|
317
|
+
# ── 11. Evaluate and report ───────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
TRUNCATED_CMD=$(truncate_cmd "$CMD")
|
|
320
|
+
|
|
321
|
+
print_violations() {
|
|
322
|
+
local VF="$1"
|
|
323
|
+
local NOTE_LABEL="$2"
|
|
324
|
+
while IFS= read -r LINE; do
|
|
325
|
+
case "$LINE" in
|
|
326
|
+
HIGH\|*|MEDIUM\|*)
|
|
327
|
+
local SEV LABEL DETAIL
|
|
328
|
+
SEV=$(printf '%s' "$LINE" | cut -d'|' -f1)
|
|
329
|
+
LABEL=$(printf '%s' "$LINE" | cut -d'|' -f2)
|
|
330
|
+
DETAIL=$(printf '%s' "$LINE" | cut -d'|' -f3)
|
|
331
|
+
printf ' %s: %s\n' "$SEV" "$LABEL"
|
|
332
|
+
printf ' %s: %s\n' "$NOTE_LABEL" "$DETAIL"
|
|
333
|
+
;;
|
|
334
|
+
ALT:*)
|
|
335
|
+
printf ' %s\n' "${LINE#ALT:}"
|
|
336
|
+
;;
|
|
337
|
+
END_VIOLATION)
|
|
338
|
+
printf '\n'
|
|
339
|
+
;;
|
|
340
|
+
esac
|
|
341
|
+
done < "$VF"
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if [[ -s "$HIGH_FILE" ]]; then
|
|
345
|
+
{
|
|
346
|
+
printf 'BASH INTERCEPTED: Dangerous command blocked\n'
|
|
347
|
+
print_violations "$HIGH_FILE" "Reason"
|
|
348
|
+
printf ' BLOCKED COMMAND: %s\n' "$TRUNCATED_CMD"
|
|
349
|
+
} >&2
|
|
350
|
+
exit 2
|
|
351
|
+
fi
|
|
352
|
+
|
|
353
|
+
if [[ -s "$MEDIUM_FILE" ]]; then
|
|
354
|
+
{
|
|
355
|
+
printf 'BASH ADVISORY: Potentially risky command (not blocked)\n'
|
|
356
|
+
print_violations "$MEDIUM_FILE" "Note"
|
|
357
|
+
printf ' COMMAND: %s\n' "$TRUNCATED_CMD"
|
|
358
|
+
} >&2
|
|
359
|
+
exit 0
|
|
360
|
+
fi
|
|
361
|
+
|
|
362
|
+
exit 0
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: dependency-audit-gate.sh
|
|
3
|
+
# Fires BEFORE every Bash tool call.
|
|
4
|
+
# Detects package install commands (npm install, pnpm add, yarn add) and
|
|
5
|
+
# verifies the package exists on the registry before allowing the install.
|
|
6
|
+
#
|
|
7
|
+
# Exit codes:
|
|
8
|
+
# 0 = allow (not an install command, or package verified)
|
|
9
|
+
# 2 = block (package not found on registry)
|
|
10
|
+
|
|
11
|
+
set -uo pipefail
|
|
12
|
+
|
|
13
|
+
# ── 1. Read ALL stdin immediately ─────────────────────────────────────────────
|
|
14
|
+
INPUT=$(cat)
|
|
15
|
+
|
|
16
|
+
# ── 2. Dependency check ──────────────────────────────────────────────────────
|
|
17
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
18
|
+
printf 'REA ERROR: jq is required but not installed.\n' >&2
|
|
19
|
+
printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
|
|
20
|
+
exit 2
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
# ── 3. HALT check ────────────────────────────────────────────────────────────
|
|
24
|
+
REA_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
25
|
+
HALT_FILE="${REA_ROOT}/.rea/HALT"
|
|
26
|
+
if [ -f "$HALT_FILE" ]; then
|
|
27
|
+
printf 'REA HALT: %s\nAll agent operations suspended. Run: rea unfreeze\n' \
|
|
28
|
+
"$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
|
|
29
|
+
exit 2
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# ── 4. Parse command ──────────────────────────────────────────────────────────
|
|
33
|
+
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
34
|
+
|
|
35
|
+
if [[ -z "$CMD" ]]; then
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# ── 5. Detect package install commands ────────────────────────────────────────
|
|
40
|
+
# Match: npm install <pkg>, npm i <pkg>, pnpm add <pkg>, yarn add <pkg>
|
|
41
|
+
# Skip: npm install (no args), npm ci, npm install --save-dev (without new pkg)
|
|
42
|
+
|
|
43
|
+
extract_packages() {
|
|
44
|
+
local cmd="$1"
|
|
45
|
+
|
|
46
|
+
# npm install/add with packages (skip flags and local paths)
|
|
47
|
+
if printf '%s' "$cmd" | grep -qiE '(npm[[:space:]]+(install|i|add)|pnpm[[:space:]]+(add|install)|yarn[[:space:]]+add)[[:space:]]'; then
|
|
48
|
+
# Extract the part after the install command
|
|
49
|
+
local after_cmd
|
|
50
|
+
after_cmd=$(printf '%s' "$cmd" | sed -E 's/.*(npm[[:space:]]+(install|i|add)|pnpm[[:space:]]+(add|install)|yarn[[:space:]]+add)[[:space:]]+//')
|
|
51
|
+
|
|
52
|
+
# Split on spaces and filter
|
|
53
|
+
for token in $after_cmd; do
|
|
54
|
+
# Skip flags
|
|
55
|
+
if [[ "$token" == -* ]]; then continue; fi
|
|
56
|
+
# Skip local paths
|
|
57
|
+
if [[ "$token" == ./* || "$token" == /* || "$token" == ../* ]]; then continue; fi
|
|
58
|
+
# Skip empty
|
|
59
|
+
if [[ -z "$token" ]]; then continue; fi
|
|
60
|
+
# Strip version specifier for lookup
|
|
61
|
+
local pkg_name
|
|
62
|
+
pkg_name=$(printf '%s' "$token" | sed -E 's/@[^@/]+$//')
|
|
63
|
+
# Handle scoped packages (@scope/name)
|
|
64
|
+
if [[ -z "$pkg_name" ]]; then
|
|
65
|
+
pkg_name="$token"
|
|
66
|
+
fi
|
|
67
|
+
printf '%s\n' "$pkg_name"
|
|
68
|
+
done
|
|
69
|
+
fi
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
PACKAGES=$(extract_packages "$CMD")
|
|
73
|
+
|
|
74
|
+
if [[ -z "$PACKAGES" ]]; then
|
|
75
|
+
exit 0
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# ── 6. Verify packages exist on registry ──────────────────────────────────────
|
|
79
|
+
FAILED=""
|
|
80
|
+
CHECKED=0
|
|
81
|
+
|
|
82
|
+
while IFS= read -r pkg; do
|
|
83
|
+
[[ -z "$pkg" ]] && continue
|
|
84
|
+
CHECKED=$((CHECKED + 1))
|
|
85
|
+
|
|
86
|
+
# Cap at 5 packages per command to avoid slow hook
|
|
87
|
+
if [[ $CHECKED -gt 5 ]]; then
|
|
88
|
+
break
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# Use npm view to check if package exists
|
|
92
|
+
# macOS doesn't have `timeout` by default, use a background process with kill
|
|
93
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
94
|
+
if ! timeout 5 npm view "$pkg" name >/dev/null 2>&1; then
|
|
95
|
+
FAILED="${FAILED} - ${pkg}\n"
|
|
96
|
+
fi
|
|
97
|
+
else
|
|
98
|
+
# Fallback: run npm view without timeout (still fast for simple checks)
|
|
99
|
+
if ! npm view "$pkg" name >/dev/null 2>&1; then
|
|
100
|
+
FAILED="${FAILED} - ${pkg}\n"
|
|
101
|
+
fi
|
|
102
|
+
fi
|
|
103
|
+
done <<< "$PACKAGES"
|
|
104
|
+
|
|
105
|
+
if [[ -n "$FAILED" ]]; then
|
|
106
|
+
{
|
|
107
|
+
printf 'DEPENDENCY AUDIT: Package not found on npm registry\n'
|
|
108
|
+
printf '\n'
|
|
109
|
+
printf ' The following packages could not be verified:\n'
|
|
110
|
+
printf '%b' "$FAILED"
|
|
111
|
+
printf '\n'
|
|
112
|
+
printf ' Rule: All packages must exist on the npm registry before installation.\n'
|
|
113
|
+
printf ' Check: Is the package name spelled correctly? Does it exist on npmjs.com?\n'
|
|
114
|
+
} >&2
|
|
115
|
+
exit 2
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
exit 0
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: env-file-protection.sh
|
|
3
|
+
# Fires BEFORE every Bash tool call.
|
|
4
|
+
# Blocks commands that read .env* / .envrc files via shell text utilities.
|
|
5
|
+
#
|
|
6
|
+
# Rationale: .env files contain credentials. Reading them via Bash exposes
|
|
7
|
+
# the values in command output, logs, and agent transcripts. Load credentials
|
|
8
|
+
# in code only (process.env, os.environ, etc.) — never via shell reads.
|
|
9
|
+
#
|
|
10
|
+
# Trigger: command matches ALL of:
|
|
11
|
+
# 1. Uses a text-reading utility (list below)
|
|
12
|
+
# 2. References a .env* or .envrc filename
|
|
13
|
+
#
|
|
14
|
+
# Exit codes:
|
|
15
|
+
# 0 = allow
|
|
16
|
+
# 2 = block (env file read detected)
|
|
17
|
+
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
|
|
20
|
+
INPUT=$(cat)
|
|
21
|
+
|
|
22
|
+
# ── Dependency check ──────────────────────────────────────────────────────────
|
|
23
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
24
|
+
printf 'REA ERROR: jq is required but not installed.\n' >&2
|
|
25
|
+
printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
|
|
26
|
+
exit 2
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# ── HALT check ────────────────────────────────────────────────────────────────
|
|
30
|
+
REA_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
31
|
+
HALT_FILE="${REA_ROOT}/.rea/HALT"
|
|
32
|
+
if [ -f "$HALT_FILE" ]; then
|
|
33
|
+
printf 'REA HALT: %s\nAll agent operations suspended. Run: rea unfreeze\n' \
|
|
34
|
+
"$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
|
|
35
|
+
exit 2
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
39
|
+
|
|
40
|
+
if [[ -z "$CMD" ]]; then
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
truncate_cmd() {
|
|
45
|
+
local STR="$1"
|
|
46
|
+
local MAX=100
|
|
47
|
+
if [[ ${#STR} -gt $MAX ]]; then
|
|
48
|
+
printf '%s' "${STR:0:$MAX}..."
|
|
49
|
+
else
|
|
50
|
+
printf '%s' "$STR"
|
|
51
|
+
fi
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Text-reading utilities (shell and common alternatives)
|
|
55
|
+
# Defense-in-depth: this list catches the most common shell-based exfiltration
|
|
56
|
+
# vectors. It is NOT exhaustive. Known gaps include:
|
|
57
|
+
# - Docker volume mounts (docker run -v .env:/...) — separate concern
|
|
58
|
+
# - Editor commands (vim, nano, code) — not typically used by agents
|
|
59
|
+
# - Redirects/process substitution (< .env) without a listed utility
|
|
60
|
+
# - Network tools (curl file://, nc) — low-risk in agent context
|
|
61
|
+
# The goal is to block casual and accidental reads, not defeat a determined
|
|
62
|
+
# adversary with shell access.
|
|
63
|
+
PATTERN_UTILITY='(cat|head|tail|less|more|grep|sed|awk|bat|strings|printf|xargs|tee|jq|python3?[[:space:]]+-c|ruby[[:space:]]+-e)[[:space:]]'
|
|
64
|
+
# Also catch: source/., cp (reads then writes elsewhere)
|
|
65
|
+
PATTERN_SOURCE='(source|\.)[[:space:]]+[^;|&]*\.env'
|
|
66
|
+
PATTERN_CP_ENV='cp[[:space:]]+[^;|&]*\.env'
|
|
67
|
+
# .env* files or .envrc (direnv)
|
|
68
|
+
PATTERN_ENV_FILE='(\.env[a-zA-Z0-9._-]*|\.envrc)([[:space:]]|"|'"'"'|$)'
|
|
69
|
+
|
|
70
|
+
MATCHES_UTILITY=0
|
|
71
|
+
MATCHES_ENV_FILE=0
|
|
72
|
+
|
|
73
|
+
if printf '%s' "$CMD" | grep -qE "$PATTERN_UTILITY"; then
|
|
74
|
+
MATCHES_UTILITY=1
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
if printf '%s' "$CMD" | grep -qE "$PATTERN_ENV_FILE"; then
|
|
78
|
+
MATCHES_ENV_FILE=1
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# Direct source/cp of .env files — always block
|
|
82
|
+
if printf '%s' "$CMD" | grep -qE "$PATTERN_SOURCE" || \
|
|
83
|
+
printf '%s' "$CMD" | grep -qE "$PATTERN_CP_ENV"; then
|
|
84
|
+
TRUNCATED_CMD=$(truncate_cmd "$CMD")
|
|
85
|
+
{
|
|
86
|
+
printf 'ENV FILE PROTECTION: Direct sourcing or copying of .env files is blocked.\n'
|
|
87
|
+
printf '\n'
|
|
88
|
+
printf ' Command: %s\n' "$TRUNCATED_CMD"
|
|
89
|
+
printf '\n'
|
|
90
|
+
printf ' Rule: Load credentials in code only — never via shell source or cp.\n'
|
|
91
|
+
printf ' Use: process.env.VAR_NAME, os.environ["VAR_NAME"], etc.\n'
|
|
92
|
+
} >&2
|
|
93
|
+
exit 2
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
if [[ $MATCHES_UTILITY -eq 1 && $MATCHES_ENV_FILE -eq 1 ]]; then
|
|
97
|
+
TRUNCATED_CMD=$(truncate_cmd "$CMD")
|
|
98
|
+
{
|
|
99
|
+
printf 'ENV FILE PROTECTION: Reading .env files via Bash is blocked.\n'
|
|
100
|
+
printf '\n'
|
|
101
|
+
printf ' Command: %s\n' "$TRUNCATED_CMD"
|
|
102
|
+
printf '\n'
|
|
103
|
+
printf ' Rule: Load credentials in code only, never via shell.\n'
|
|
104
|
+
printf ' Use: process.env.VAR_NAME, os.environ["VAR_NAME"], etc.\n'
|
|
105
|
+
printf ' .env files must not be read via shell utilities in agent sessions.\n'
|
|
106
|
+
} >&2
|
|
107
|
+
exit 2
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
exit 0
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# pr-issue-link-gate.sh — PreToolUse: Bash
|
|
3
|
+
#
|
|
4
|
+
# Ensures every `gh pr create` command references at least one GitHub issue
|
|
5
|
+
# via closes/fixes/resolves #N syntax in the PR body. When the magic keyword
|
|
6
|
+
# is present, GitHub automatically closes the linked issue when the PR merges
|
|
7
|
+
# to the default branch and creates a cross-reference in the issue timeline.
|
|
8
|
+
#
|
|
9
|
+
# This gate is ADVISORY (exit 0) — it warns but does not block. Some PRs
|
|
10
|
+
# legitimately have no linked issue (chores, hotfixes, release PRs). The
|
|
11
|
+
# advisory gives the agent an opportunity to add the link before proceeding.
|
|
12
|
+
#
|
|
13
|
+
# Only active for Bash tool calls containing `gh pr create`.
|
|
14
|
+
# JSONL-only projects (no GitHub) are unaffected — gh is unavailable there.
|
|
15
|
+
#
|
|
16
|
+
# Triggered by: PreToolUse — Bash tool
|
|
17
|
+
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
# shellcheck source=_lib/common.sh
|
|
21
|
+
source "$(dirname "$0")/_lib/common.sh"
|
|
22
|
+
|
|
23
|
+
check_halt
|
|
24
|
+
|
|
25
|
+
INPUT="$(cat)"
|
|
26
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
27
|
+
|
|
28
|
+
if [[ "$TOOL_NAME" != "Bash" ]]; then
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
|
|
33
|
+
|
|
34
|
+
# Only intercept gh pr create
|
|
35
|
+
if ! echo "$COMMAND" | grep -qE 'gh\s+pr\s+create'; then
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
require_jq
|
|
40
|
+
|
|
41
|
+
# Check for closing keywords followed by an issue number
|
|
42
|
+
# Accepted: closes #N, fixes #N, resolves #N (case-insensitive, any spacing)
|
|
43
|
+
if echo "$COMMAND" | grep -qiE '(closes|fixes|resolves)\s+#[0-9]+'; then
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Advisory — warn but do not block.
|
|
48
|
+
# Chore PRs, release PRs, and hotfixes may legitimately have no linked issue.
|
|
49
|
+
printf 'PR ISSUE LINK ADVISORY: This PR does not reference a GitHub issue.\n' >&2
|
|
50
|
+
printf '\n' >&2
|
|
51
|
+
printf 'When a PR body includes a closing reference, GitHub automatically:\n' >&2
|
|
52
|
+
printf ' - Closes the issue when the PR merges to the default branch\n' >&2
|
|
53
|
+
printf ' - Creates a cross-reference in the issue timeline\n' >&2
|
|
54
|
+
printf ' - Links the PR in the CHANGELOG context\n' >&2
|
|
55
|
+
printf '\n' >&2
|
|
56
|
+
printf 'Add to the --body:\n' >&2
|
|
57
|
+
printf ' closes #N closes one issue\n' >&2
|
|
58
|
+
printf ' fixes #N same effect\n' >&2
|
|
59
|
+
printf ' resolves #N same effect\n' >&2
|
|
60
|
+
printf ' closes #N, closes #M closes multiple issues\n' >&2
|
|
61
|
+
printf '\n' >&2
|
|
62
|
+
printf 'If this is a chore, release, or hotfix PR with no upstream issue, you may proceed.\n' >&2
|
|
63
|
+
|
|
64
|
+
# Exit 0 — advisory only, does not block the PR creation
|
|
65
|
+
exit 0
|