@bookedsolid/rea 0.9.3 → 0.10.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/README.md +105 -0
- package/THREAT_MODEL.md +19 -1
- package/dist/cli/audit.d.ts +31 -0
- package/dist/cli/audit.js +71 -0
- package/dist/cli/cache.d.ts +33 -1
- package/dist/cli/cache.js +40 -2
- package/dist/cli/index.js +40 -2
- package/dist/config/tier-map.d.ts +1 -0
- package/dist/config/tier-map.js +210 -0
- package/dist/gateway/middleware/blocked-paths.js +38 -0
- package/dist/gateway/middleware/policy.js +68 -3
- package/hooks/_lib/common.sh +6 -1
- package/hooks/_lib/push-review-core.sh +115 -19
- package/hooks/commit-review-gate.sh +119 -7
- package/hooks/settings-protection.sh +297 -64
- package/package.json +1 -1
|
@@ -376,6 +376,44 @@ function matchesBlockedPattern(value, pattern) {
|
|
|
376
376
|
}
|
|
377
377
|
return false;
|
|
378
378
|
}
|
|
379
|
+
// Defect H (rea#79): dot-anchored patterns. A pattern whose base starts with
|
|
380
|
+
// `.` (e.g. `.rea/`, `.env`, `.husky/`) is meant to block ONLY leading-dot
|
|
381
|
+
// filesystem entries — never any path segment that happens to be spelled
|
|
382
|
+
// similarly without the dot. The previous suffix-based match let pattern
|
|
383
|
+
// `.rea/` trip on `Projects/rea/Bug Reports` (any project folder named
|
|
384
|
+
// `rea`) because `suffix.startsWith(base)` was false but the final
|
|
385
|
+
// `segs.includes(base)` fallback conflated `.rea` with `rea` through
|
|
386
|
+
// normalization downstream in some code paths. By explicitly requiring
|
|
387
|
+
// leading-dot segment equality, dot-prefixed patterns cannot bleed across
|
|
388
|
+
// the dot/no-dot boundary regardless of normalization rule drift.
|
|
389
|
+
const dotAnchored = base.startsWith('.');
|
|
390
|
+
if (dotAnchored) {
|
|
391
|
+
// Dot-anchored: segment must equal base exactly. Dir patterns also match
|
|
392
|
+
// "<base>/..." via the trailing slash marker. Never scans non-dot
|
|
393
|
+
// segments, so `Projects/rea/...` can never match `.rea/`.
|
|
394
|
+
for (let i = 0; i < segs.length; i++) {
|
|
395
|
+
const seg = segs[i];
|
|
396
|
+
if (seg === base) {
|
|
397
|
+
// Exact segment match — for a non-dir pattern this matches a FILE
|
|
398
|
+
// named exactly `.env`; for a dir pattern it matches the directory
|
|
399
|
+
// entry itself (the trailing-slash below covers its contents).
|
|
400
|
+
if (!dirPattern && i !== segs.length - 1)
|
|
401
|
+
continue;
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
if (dirPattern && seg === base)
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
if (dirPattern) {
|
|
408
|
+
// Dir pattern: any suffix that starts with `<base>/` matches.
|
|
409
|
+
for (let i = 0; i < segs.length; i++) {
|
|
410
|
+
const suffix = segs.slice(i).join('/');
|
|
411
|
+
if (suffix === base || suffix.startsWith(`${base}/`))
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
379
417
|
for (let i = 0; i < segs.length; i++) {
|
|
380
418
|
const suffix = segs.slice(i).join('/');
|
|
381
419
|
if (suffix === base)
|
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
import { AutonomyLevel, InvocationStatus, Tier } from '../../policy/types.js';
|
|
2
|
-
import { classifyTool, isToolBlocked } from '../../config/tier-map.js';
|
|
2
|
+
import { classifyTool, isToolBlocked, reaCommandTier } from '../../config/tier-map.js';
|
|
3
3
|
import { loadPolicyAsync } from '../../policy/loader.js';
|
|
4
|
+
const BASH_DISPLAY_MAX_LEN = 80;
|
|
5
|
+
/** Extract the `rea <subcommand>` head from a Bash command string for display
|
|
6
|
+
* in deny messages. Returns `null` when the command is not a rea invocation. */
|
|
7
|
+
function extractReaSubcommand(command) {
|
|
8
|
+
const tokens = command.trim().split(/\s+/);
|
|
9
|
+
if (tokens.length === 0)
|
|
10
|
+
return null;
|
|
11
|
+
const first = tokens[0];
|
|
12
|
+
if (first === undefined)
|
|
13
|
+
return null;
|
|
14
|
+
let idx = 0;
|
|
15
|
+
if (first === 'npx' && tokens.length >= 2 && (tokens[1] === 'rea' || tokens[1] === '@bookedsolid/rea')) {
|
|
16
|
+
idx = 2;
|
|
17
|
+
}
|
|
18
|
+
else if (first === 'rea' || first.endsWith('/rea')) {
|
|
19
|
+
idx = 1;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const sub = tokens[idx];
|
|
25
|
+
if (sub === undefined)
|
|
26
|
+
return 'rea';
|
|
27
|
+
const sub2 = tokens[idx + 1];
|
|
28
|
+
if (sub2 !== undefined && /^[a-z][a-z-]*$/.test(sub2)) {
|
|
29
|
+
return `rea ${sub} ${sub2}`;
|
|
30
|
+
}
|
|
31
|
+
return `rea ${sub}`;
|
|
32
|
+
}
|
|
33
|
+
/** Build a readable `Bash: <head>` display string for deny messages. Caller
|
|
34
|
+
* is responsible for only invoking this for tool_name === 'Bash'. Uses
|
|
35
|
+
* JSON.stringify to escape hostile characters (newlines, control chars). */
|
|
36
|
+
function formatBashDisplay(command, reaDisplay) {
|
|
37
|
+
if (reaDisplay !== null) {
|
|
38
|
+
return `Bash (${reaDisplay})`;
|
|
39
|
+
}
|
|
40
|
+
const trimmed = command.trim();
|
|
41
|
+
const truncated = trimmed.length > BASH_DISPLAY_MAX_LEN
|
|
42
|
+
? `${trimmed.slice(0, BASH_DISPLAY_MAX_LEN - 1)}…`
|
|
43
|
+
: trimmed;
|
|
44
|
+
return `Bash (${JSON.stringify(truncated)})`;
|
|
45
|
+
}
|
|
4
46
|
/**
|
|
5
47
|
* Autonomy level tier permissions:
|
|
6
48
|
* - L0: Read only
|
|
@@ -48,7 +90,23 @@ export function createPolicyMiddleware(initialPolicy, gatewayConfig, baseDir) {
|
|
|
48
90
|
}
|
|
49
91
|
// SECURITY: Re-derive tier from tool_name — do NOT trust ctx.tier from prior middleware.
|
|
50
92
|
// This prevents a rogue middleware from downgrading a destructive tool to read-tier.
|
|
51
|
-
|
|
93
|
+
let tier = classifyTool(ctx.tool_name, ctx.server_name, gatewayConfig);
|
|
94
|
+
// Defect E (rea#78): when the invocation is a `Bash` call whose command
|
|
95
|
+
// parses as `rea <subcommand>`, classify by subcommand instead of the
|
|
96
|
+
// generic `Write` Bash default. REA's own CLI must not be denied by REA's
|
|
97
|
+
// own middleware at the autonomy level the gate's remediation text
|
|
98
|
+
// targets. Returns null on non-rea commands so the generic tier stands.
|
|
99
|
+
let reaSubcommandDisplay = null;
|
|
100
|
+
if (ctx.tool_name === 'Bash') {
|
|
101
|
+
const command = ctx.arguments['command'];
|
|
102
|
+
if (typeof command === 'string') {
|
|
103
|
+
const subTier = reaCommandTier(command);
|
|
104
|
+
if (subTier !== null) {
|
|
105
|
+
tier = subTier;
|
|
106
|
+
reaSubcommandDisplay = extractReaSubcommand(command);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
52
110
|
ctx.tier = tier; // Overwrite with authoritative classification
|
|
53
111
|
// Validate autonomy level is known
|
|
54
112
|
const allowed = TIER_ALLOWED[policy.autonomy_level];
|
|
@@ -60,7 +118,14 @@ export function createPolicyMiddleware(initialPolicy, gatewayConfig, baseDir) {
|
|
|
60
118
|
// Check autonomy level vs tier (fail-closed: deny if tier unknown)
|
|
61
119
|
if (!allowed.has(tier)) {
|
|
62
120
|
ctx.status = InvocationStatus.Denied;
|
|
63
|
-
|
|
121
|
+
// Defect E composition: when the denial is a Bash invocation, include
|
|
122
|
+
// the command head so the deny-reason is actionable. `Bash` alone tells
|
|
123
|
+
// the operator nothing about WHICH shell command tripped the gate.
|
|
124
|
+
const toolDisplay = ctx.tool_name === 'Bash' && typeof ctx.arguments['command'] === 'string'
|
|
125
|
+
? formatBashDisplay(ctx.arguments['command'], reaSubcommandDisplay)
|
|
126
|
+
: ctx.tool_name;
|
|
127
|
+
ctx.error = `Autonomy level ${policy.autonomy_level} does not allow ${tier}-tier tools. Tool: ${toolDisplay}`;
|
|
128
|
+
ctx.metadata['reason_code'] = 'tier_exceeds_autonomy';
|
|
64
129
|
return;
|
|
65
130
|
}
|
|
66
131
|
// Store current autonomy level in metadata for audit middleware
|
package/hooks/_lib/common.sh
CHANGED
|
@@ -87,7 +87,12 @@ triage_score() {
|
|
|
87
87
|
local diff_input
|
|
88
88
|
diff_input=$(cat)
|
|
89
89
|
local line_count
|
|
90
|
-
|
|
90
|
+
# Defect K (rea#62) sibling: see `hooks/commit-review-gate.sh` for the
|
|
91
|
+
# full bug rationale. `|| echo "0"` captures "0\n0" on no-match, which
|
|
92
|
+
# breaks arithmetic comparisons downstream. `|| true` + bash default keeps
|
|
93
|
+
# the branch arithmetic-safe.
|
|
94
|
+
line_count=$(printf '%s' "$diff_input" | grep -cE '^\+[^+]|^-[^-]' 2>/dev/null || true)
|
|
95
|
+
line_count="${line_count:-0}"
|
|
91
96
|
|
|
92
97
|
# Check for sensitive paths
|
|
93
98
|
local sensitive=0
|
|
@@ -435,8 +435,8 @@ pr_core_run() {
|
|
|
435
435
|
--arg os_uid "$SKIP_OS_UID" \
|
|
436
436
|
--arg os_whoami "$SKIP_OS_WHOAMI" \
|
|
437
437
|
--arg os_hostname "$SKIP_OS_HOST" \
|
|
438
|
-
--
|
|
439
|
-
--
|
|
438
|
+
--argjson os_pid "$SKIP_OS_PID" \
|
|
439
|
+
--argjson os_ppid "$SKIP_OS_PPID" \
|
|
440
440
|
--arg os_ppid_cmd "$SKIP_OS_PPID_CMD" \
|
|
441
441
|
--arg os_tty "$SKIP_OS_TTY" \
|
|
442
442
|
--arg os_ci "$SKIP_OS_CI" \
|
|
@@ -924,17 +924,26 @@ pr_core_run() {
|
|
|
924
924
|
fi
|
|
925
925
|
done
|
|
926
926
|
|
|
927
|
+
# Defect J (rea#61): branch-deletion guard MUST fail closed regardless of
|
|
928
|
+
# whether another refspec in the same push resolved a SOURCE_SHA. A mixed
|
|
929
|
+
# push like `git push origin safe:safe :main` iterates both refspecs; the
|
|
930
|
+
# safe refspec sets SOURCE_SHA from its local_sha, and the deletion refspec
|
|
931
|
+
# sets only HAS_DELETE=1 via its `continue` branch. If we check HAS_DELETE
|
|
932
|
+
# INSIDE the `-z SOURCE_SHA` fallback, the delete slips through unchecked.
|
|
933
|
+
# Hoist the check above the fallback so any deletion anywhere in the push
|
|
934
|
+
# blocks the entire push.
|
|
935
|
+
if [[ "$HAS_DELETE" -eq 1 ]]; then
|
|
936
|
+
{
|
|
937
|
+
printf 'PUSH BLOCKED: refspec is a branch deletion.\n'
|
|
938
|
+
printf '\n'
|
|
939
|
+
printf ' Branch deletions are sensitive operations and require explicit\n'
|
|
940
|
+
printf ' human action outside the agent. Perform the deletion manually.\n'
|
|
941
|
+
printf '\n'
|
|
942
|
+
} >&2
|
|
943
|
+
exit 2
|
|
944
|
+
fi
|
|
945
|
+
|
|
927
946
|
if [[ -z "$SOURCE_SHA" || -z "$MERGE_BASE" ]]; then
|
|
928
|
-
if [[ "$HAS_DELETE" -eq 1 ]]; then
|
|
929
|
-
{
|
|
930
|
-
printf 'PUSH BLOCKED: refspec is a branch deletion.\n'
|
|
931
|
-
printf '\n'
|
|
932
|
-
printf ' Branch deletions are sensitive operations and require explicit\n'
|
|
933
|
-
printf ' human action outside the agent. Perform the deletion manually.\n'
|
|
934
|
-
printf '\n'
|
|
935
|
-
} >&2
|
|
936
|
-
exit 2
|
|
937
|
-
fi
|
|
938
947
|
{
|
|
939
948
|
printf 'PUSH BLOCKED: could not resolve a merge-base for any push refspec.\n'
|
|
940
949
|
printf '\n'
|
|
@@ -969,8 +978,13 @@ pr_core_run() {
|
|
|
969
978
|
exit 0
|
|
970
979
|
fi
|
|
971
980
|
|
|
981
|
+
# Defect K (rea#62): `grep -c ... || echo "0"` captures `0\n0` when grep
|
|
982
|
+
# exits non-zero on no-match — grep still prints its own `0` to stdout before
|
|
983
|
+
# exiting, and the `|| echo "0"` branch appends another. `|| true` swallows
|
|
984
|
+
# the non-zero exit, and `${LINE_COUNT:-0}` defaults an empty result to 0.
|
|
972
985
|
local LINE_COUNT
|
|
973
|
-
LINE_COUNT=$(printf '%s' "$DIFF_FULL" | grep -cE '^\+[^+]|^-[^-]' 2>/dev/null ||
|
|
986
|
+
LINE_COUNT=$(printf '%s' "$DIFF_FULL" | grep -cE '^\+[^+]|^-[^-]' 2>/dev/null || true)
|
|
987
|
+
LINE_COUNT="${LINE_COUNT:-0}"
|
|
974
988
|
|
|
975
989
|
# ── 7a. Protected-path Codex adversarial review gate ──────────────────────
|
|
976
990
|
# The per-refspec check runs inside the main loop (section 7, above) so
|
|
@@ -981,8 +995,36 @@ pr_core_run() {
|
|
|
981
995
|
# refspec was either clean or had an acceptable audit.
|
|
982
996
|
|
|
983
997
|
# ── 8. Check review cache ─────────────────────────────────────────────────
|
|
984
|
-
|
|
985
|
-
|
|
998
|
+
# Defect L (rea#63): `shasum` is not installed on Alpine, distroless, or
|
|
999
|
+
# most minimal Linux CI images — only `sha256sum` is. The prior `shasum -a
|
|
1000
|
+
# 256 ... || echo ""` chain silently produced an empty PUSH_SHA, which the
|
|
1001
|
+
# rest of the gate treats as "no cache entry" rather than "hasher missing".
|
|
1002
|
+
# Combined with the silent-cache-miss fallback (Defect F), every push from
|
|
1003
|
+
# such a runner burned a full fresh codex review invisibly.
|
|
1004
|
+
#
|
|
1005
|
+
# Portable chain: sha256sum → shasum → openssl. The openssl branch uses
|
|
1006
|
+
# `awk '{print $NF}'` WITHOUT `-r` — `-r` was added in OpenSSL 3.0 /
|
|
1007
|
+
# LibreSSL 3.3+; on OpenSSL 1.1.1 (Debian 11, Ubuntu 20.04, RHEL 8,
|
|
1008
|
+
# Amazon Linux 2, Alpine 3.13–3.14) `-r` is rejected and stdout is empty.
|
|
1009
|
+
# `$NF` handles BOTH default output shapes: `(stdin)= <hex>` (1.1.x) and
|
|
1010
|
+
# `<hex> *stdin` (3.x/LibreSSL coreutils-style).
|
|
1011
|
+
#
|
|
1012
|
+
# Hex-64 validation catches broken pipes, partial reads, or unexpected
|
|
1013
|
+
# hasher output that would otherwise be silently cached as garbage.
|
|
1014
|
+
local PUSH_SHA=""
|
|
1015
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
1016
|
+
PUSH_SHA=$(printf '%s' "$DIFF_FULL" | sha256sum 2>/dev/null | awk '{print $1}')
|
|
1017
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
1018
|
+
PUSH_SHA=$(printf '%s' "$DIFF_FULL" | shasum -a 256 2>/dev/null | awk '{print $1}')
|
|
1019
|
+
elif command -v openssl >/dev/null 2>&1; then
|
|
1020
|
+
PUSH_SHA=$(printf '%s' "$DIFF_FULL" | openssl dgst -sha256 2>/dev/null | awk '{print $NF}')
|
|
1021
|
+
else
|
|
1022
|
+
printf 'rea push-review: WARN no sha256 hasher found (sha256sum/shasum/openssl); cache disabled\n' >&2
|
|
1023
|
+
fi
|
|
1024
|
+
if [[ -n "$PUSH_SHA" && ! "$PUSH_SHA" =~ ^[0-9a-f]{64}$ ]]; then
|
|
1025
|
+
printf 'rea push-review: WARN hasher returned invalid output; cache disabled\n' >&2
|
|
1026
|
+
PUSH_SHA=""
|
|
1027
|
+
fi
|
|
986
1028
|
|
|
987
1029
|
local -a REA_CLI_ARGS
|
|
988
1030
|
REA_CLI_ARGS=()
|
|
@@ -1017,8 +1059,45 @@ pr_core_run() {
|
|
|
1017
1059
|
fi
|
|
1018
1060
|
|
|
1019
1061
|
if [[ -n "$PUSH_SHA" ]] && [[ ${#REA_CLI_ARGS[@]} -gt 0 ]]; then
|
|
1062
|
+
# Defect F (rea#75): distinguish cache-miss from cache-error. Prior version
|
|
1063
|
+
# swallowed all non-zero exits and stderr into a silent `{hit:false}`, which
|
|
1064
|
+
# masked Defect A (0.9.2 `node <shim>` SyntaxError) for weeks. Now we
|
|
1065
|
+
# capture stderr + exit code separately and emit a visible WARN with an
|
|
1066
|
+
# actionable filename when the CLI failed.
|
|
1020
1067
|
local CACHE_RESULT
|
|
1021
|
-
|
|
1068
|
+
local CACHE_STDOUT=""
|
|
1069
|
+
local CACHE_STDERR_FILE
|
|
1070
|
+
# SECURITY (Codex LOW 4): require mktemp. A predictable /tmp path like
|
|
1071
|
+
# /tmp/rea-cache-err.$PID is a TOCTOU attack surface on shared hosts —
|
|
1072
|
+
# another user can pre-create a symlink from that name to a file they
|
|
1073
|
+
# want us to clobber. If mktemp is unavailable, fail loudly rather than
|
|
1074
|
+
# silently falling back to a predictable path.
|
|
1075
|
+
if ! CACHE_STDERR_FILE=$(mktemp -t rea-cache-err.XXXXXX 2>/dev/null); then
|
|
1076
|
+
printf 'rea push-review: mktemp unavailable; cannot capture cache-check stderr. Aborting.\n' >&2
|
|
1077
|
+
return 2
|
|
1078
|
+
fi
|
|
1079
|
+
local CACHE_EXIT=0
|
|
1080
|
+
CACHE_STDOUT=$("${REA_CLI_ARGS[@]}" cache check "$PUSH_SHA" --branch "$SOURCE_BRANCH" --base "$TARGET_BRANCH" 2>"$CACHE_STDERR_FILE") || CACHE_EXIT=$?
|
|
1081
|
+
local CACHE_STDERR=""
|
|
1082
|
+
CACHE_STDERR=$(cat "$CACHE_STDERR_FILE" 2>/dev/null || true)
|
|
1083
|
+
rm -f "$CACHE_STDERR_FILE"
|
|
1084
|
+
if [[ "$CACHE_EXIT" -ne 0 ]]; then
|
|
1085
|
+
# SECURITY (Codex LOW 5): strip C0/C1 control characters from CLI
|
|
1086
|
+
# stderr before echoing to the terminal. A tampered dist/ or hostile
|
|
1087
|
+
# CLI could otherwise emit OSC/CSI sequences that rewrite lines above
|
|
1088
|
+
# the deny message and mislead the operator. We strip both C0 + DEL
|
|
1089
|
+
# AND C1 (0x80-0x9F) — some terminal emulators still honor bare C1
|
|
1090
|
+
# bytes as CSI introducers (0x9B) or OSC (0x9D).
|
|
1091
|
+
local CACHE_STDERR_SAFE
|
|
1092
|
+
CACHE_STDERR_SAFE=$(printf '%s' "$CACHE_STDERR" | LC_ALL=C tr -d '\000-\037\177\200-\237')
|
|
1093
|
+
printf 'rea push-review: CACHE CHECK FAILED (exit=%d): %s\n' "$CACHE_EXIT" "$CACHE_STDERR_SAFE" >&2
|
|
1094
|
+
printf 'rea push-review: treating as miss; file bookedsolidtech/rea issue if unexpected.\n' >&2
|
|
1095
|
+
CACHE_RESULT='{"hit":false,"reason":"query_error"}'
|
|
1096
|
+
elif [[ -z "$CACHE_STDOUT" ]]; then
|
|
1097
|
+
CACHE_RESULT='{"hit":false,"reason":"cold"}'
|
|
1098
|
+
else
|
|
1099
|
+
CACHE_RESULT="$CACHE_STDOUT"
|
|
1100
|
+
fi
|
|
1022
1101
|
# Require BOTH hit == true AND result == "pass". A cached `fail` verdict
|
|
1023
1102
|
# (Codex 0.8.0 pass-2 finding #1) must NOT satisfy the gate — cache.ts
|
|
1024
1103
|
# serializes `result` verbatim, so a negative verdict would otherwise
|
|
@@ -1037,8 +1116,10 @@ pr_core_run() {
|
|
|
1037
1116
|
fi
|
|
1038
1117
|
|
|
1039
1118
|
# ── 9. Block and request review ───────────────────────────────────────────
|
|
1119
|
+
# Defect K (rea#62): same `0\n0` bug as LINE_COUNT above.
|
|
1040
1120
|
local FILE_COUNT
|
|
1041
|
-
FILE_COUNT=$(printf '%s' "$DIFF_FULL" | grep -c '^\+\+\+ ' 2>/dev/null ||
|
|
1121
|
+
FILE_COUNT=$(printf '%s' "$DIFF_FULL" | grep -c '^\+\+\+ ' 2>/dev/null || true)
|
|
1122
|
+
FILE_COUNT="${FILE_COUNT:-0}"
|
|
1042
1123
|
|
|
1043
1124
|
{
|
|
1044
1125
|
printf 'PUSH REVIEW GATE: Review required before pushing\n'
|
|
@@ -1050,8 +1131,23 @@ pr_core_run() {
|
|
|
1050
1131
|
printf ' Action required:\n'
|
|
1051
1132
|
printf ' 1. Spawn a code-reviewer agent to review: git diff %s..%s\n' "$MERGE_BASE" "$SOURCE_SHA"
|
|
1052
1133
|
printf ' 2. Spawn a security-engineer agent for security review\n'
|
|
1053
|
-
|
|
1054
|
-
|
|
1134
|
+
# Defect L (rea#63) follow-up: when no sha256 hasher is available the
|
|
1135
|
+
# cache is disabled and PUSH_SHA is empty. Emitting `rea cache set <blank>
|
|
1136
|
+
# pass ...` would be a dead-end — the CLI rejects the empty positional.
|
|
1137
|
+
# Print an alternate completion path in that case. The Codex-adversarial
|
|
1138
|
+
# review concerns list flagged this UX cliff in the 0.9.4 pass.
|
|
1139
|
+
if [[ -n "$PUSH_SHA" ]]; then
|
|
1140
|
+
printf ' 3. After both pass, cache the result:\n'
|
|
1141
|
+
printf ' rea cache set %s pass --branch %s --base %s\n' "$PUSH_SHA" "$SOURCE_BRANCH" "$TARGET_BRANCH"
|
|
1142
|
+
else
|
|
1143
|
+
printf ' 3. Cache is DISABLED on this host (no sha256 hasher found).\n'
|
|
1144
|
+
printf ' After both reviews pass, bypass the push-review gate with:\n'
|
|
1145
|
+
printf ' REA_SKIP_PUSH_REVIEW="<reason>" git push ...\n'
|
|
1146
|
+
printf ' The bypass is audited as push.review.skipped — this is the\n'
|
|
1147
|
+
printf ' documented escape hatch when cache is unavailable.\n'
|
|
1148
|
+
printf ' To restore the cache path, install one of: sha256sum,\n'
|
|
1149
|
+
printf ' shasum (Perl Digest::SHA), or openssl.\n'
|
|
1150
|
+
fi
|
|
1055
1151
|
printf '\n'
|
|
1056
1152
|
} >&2
|
|
1057
1153
|
exit 2
|
|
@@ -114,7 +114,16 @@ if [[ -z "$DIFF_OUTPUT" ]]; then
|
|
|
114
114
|
fi
|
|
115
115
|
|
|
116
116
|
# Count changed lines (additions + deletions)
|
|
117
|
-
|
|
117
|
+
# Defect K (rea#62) sibling: `|| echo "0"` captures "0\n0" into LINE_COUNT
|
|
118
|
+
# when grep exits non-zero on a no-match — grep still prints its own `0` and
|
|
119
|
+
# `echo "0"` appends another. At this site the concatenated `"0\n0"` is then
|
|
120
|
+
# evaluated as arithmetic (`-gt $SIGNIFICANT_THRESHOLD`, `-ge $TRIVIAL_THRESHOLD`
|
|
121
|
+
# below) and bash emits a "syntax error in expression" at runtime on any
|
|
122
|
+
# rename-only / mode-only / empty-file-add diff. `|| true` + bash-default
|
|
123
|
+
# expansion fixes both the banner cosmetic and the arithmetic-unsafe control
|
|
124
|
+
# flow in one shot.
|
|
125
|
+
LINE_COUNT=$(printf '%s' "$DIFF_FULL" | grep -cE '^\+[^+]|^-[^-]' 2>/dev/null || true)
|
|
126
|
+
LINE_COUNT="${LINE_COUNT:-0}"
|
|
118
127
|
|
|
119
128
|
# Check for sensitive paths
|
|
120
129
|
SENSITIVE=0
|
|
@@ -162,17 +171,103 @@ fi
|
|
|
162
171
|
|
|
163
172
|
# ── 10. Check review cache for all non-trivial commits ────────────────────────
|
|
164
173
|
# Compute SHA and branch here so both standard and significant tiers share them.
|
|
165
|
-
|
|
174
|
+
#
|
|
175
|
+
# Defect L (rea#63) sibling: `shasum` is not installed on Alpine, distroless,
|
|
176
|
+
# or most minimal Linux CI images — only `sha256sum` is. The prior chain
|
|
177
|
+
# silently produced an empty STAGED_SHA, which the cache block then skipped
|
|
178
|
+
# AND the banner at §11 rendered as `rea cache set pass` — a dead-end the
|
|
179
|
+
# agent cannot execute. Portable chain mirrors push-review-core.sh §8:
|
|
180
|
+
# sha256sum → shasum → openssl. The openssl branch uses `awk '{print $NF}'`
|
|
181
|
+
# WITHOUT `-r` to stay compatible with OpenSSL 1.1.x (Debian 11, Ubuntu
|
|
182
|
+
# 20.04, RHEL 8, Amazon Linux 2, Alpine 3.13–3.14).
|
|
183
|
+
STAGED_SHA=""
|
|
184
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
185
|
+
STAGED_SHA=$(printf '%s' "$DIFF_FULL" | sha256sum 2>/dev/null | awk '{print $1}')
|
|
186
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
187
|
+
STAGED_SHA=$(printf '%s' "$DIFF_FULL" | shasum -a 256 2>/dev/null | awk '{print $1}')
|
|
188
|
+
elif command -v openssl >/dev/null 2>&1; then
|
|
189
|
+
STAGED_SHA=$(printf '%s' "$DIFF_FULL" | openssl dgst -sha256 2>/dev/null | awk '{print $NF}')
|
|
190
|
+
else
|
|
191
|
+
printf 'rea commit-review: WARN no sha256 hasher found (sha256sum/shasum/openssl); cache disabled\n' >&2
|
|
192
|
+
fi
|
|
193
|
+
if [[ -n "$STAGED_SHA" && ! "$STAGED_SHA" =~ ^[0-9a-f]{64}$ ]]; then
|
|
194
|
+
printf 'rea commit-review: WARN hasher returned invalid output; cache disabled\n' >&2
|
|
195
|
+
STAGED_SHA=""
|
|
196
|
+
fi
|
|
166
197
|
BRANCH=$(cd "$REA_ROOT" && git branch --show-current 2>/dev/null || echo "")
|
|
167
198
|
CACHE_FILE="${REA_ROOT}/.rea/review-cache.json"
|
|
168
199
|
|
|
200
|
+
# Codex pass-3 finding #1: `rea cache check` and `rea cache set` both declare
|
|
201
|
+
# `--base` as a `requiredOption` in src/cli/index.ts. Prior versions of this
|
|
202
|
+
# gate omitted `--base`, so (a) the CLI path exited non-zero and the
|
|
203
|
+
# `|| echo '{"hit":false}'` fallback quietly masked the contract error, and
|
|
204
|
+
# (b) the section-11 banner instructed the agent to run `rea cache set <sha>
|
|
205
|
+
# pass` — also missing `--base`, rejected by the CLI on every retry. A
|
|
206
|
+
# successful cache flow was unreachable.
|
|
207
|
+
#
|
|
208
|
+
# Resolve BASE_BRANCH by the same preference order the push-gate uses in
|
|
209
|
+
# push-review-core.sh §7 (lines 778-794): origin/HEAD → origin/main →
|
|
210
|
+
# origin/master → empty. If nothing resolves, disable the cache (the
|
|
211
|
+
# alternative is emitting a cache command the CLI rejects on every call).
|
|
212
|
+
BASE_BRANCH=""
|
|
213
|
+
_origin_head=$(cd "$REA_ROOT" && git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || true)
|
|
214
|
+
if [[ -n "$_origin_head" ]]; then
|
|
215
|
+
BASE_BRANCH="${_origin_head#refs/remotes/origin/}"
|
|
216
|
+
fi
|
|
217
|
+
if [[ -z "$BASE_BRANCH" ]]; then
|
|
218
|
+
# Use `git -C` so the current-shell cwd is never mutated — matches the
|
|
219
|
+
# cross-repo guard at §1a and keeps the file's dominant idiom. Raw
|
|
220
|
+
# `cd "$REA_ROOT" && git …` would leave the hook process sitting in
|
|
221
|
+
# $REA_ROOT, which is safe today but breaks silently if a future edit
|
|
222
|
+
# adds a relative-path command downstream.
|
|
223
|
+
if git -C "$REA_ROOT" rev-parse --verify --quiet refs/remotes/origin/main >/dev/null 2>&1; then
|
|
224
|
+
BASE_BRANCH="main"
|
|
225
|
+
elif git -C "$REA_ROOT" rev-parse --verify --quiet refs/remotes/origin/master >/dev/null 2>&1; then
|
|
226
|
+
BASE_BRANCH="master"
|
|
227
|
+
fi
|
|
228
|
+
fi
|
|
229
|
+
if [[ -z "$BASE_BRANCH" && -n "$STAGED_SHA" ]]; then
|
|
230
|
+
printf 'rea commit-review: WARN could not resolve base branch (no origin/HEAD, no origin/main, no origin/master); cache disabled\n' >&2
|
|
231
|
+
STAGED_SHA=""
|
|
232
|
+
fi
|
|
233
|
+
unset _origin_head
|
|
234
|
+
|
|
169
235
|
if [[ -n "$STAGED_SHA" ]]; then
|
|
170
236
|
CACHE_HIT=false
|
|
171
237
|
|
|
172
|
-
# Primary: use CLI when available — handles TTL, expiry, and branch-scoped keys
|
|
238
|
+
# Primary: use CLI when available — handles TTL, expiry, and branch-scoped keys.
|
|
239
|
+
# Cache predicate must require BOTH `.hit == true` AND `.result == "pass"` —
|
|
240
|
+
# a cached `fail` verdict would otherwise satisfy `.hit == true` and let the
|
|
241
|
+
# commit proceed despite a recorded negative review. Mirrors the push-gate
|
|
242
|
+
# predicate at push-review-core.sh §8; the §218-226 direct-cache fallback
|
|
243
|
+
# already enforces `result == "pass"`, so the two paths must agree.
|
|
173
244
|
if [[ ${#REA_CLI_ARGS[@]} -gt 0 ]]; then
|
|
174
|
-
|
|
175
|
-
|
|
245
|
+
# Defect F (rea#75): surface cache-query errors instead of treating them as
|
|
246
|
+
# legitimate misses. See hooks/_lib/push-review-core.sh for the rationale.
|
|
247
|
+
# SECURITY (Codex LOW 4): require mktemp. Predictable /tmp paths are a
|
|
248
|
+
# TOCTOU surface on shared hosts; fall-loud instead of fall-back.
|
|
249
|
+
if ! CACHE_STDERR_FILE=$(mktemp -t rea-commit-cache-err.XXXXXX 2>/dev/null); then
|
|
250
|
+
printf 'rea commit-review: mktemp unavailable; cannot capture cache-check stderr. Aborting.\n' >&2
|
|
251
|
+
exit 2
|
|
252
|
+
fi
|
|
253
|
+
CACHE_EXIT=0
|
|
254
|
+
CACHE_STDOUT=$("${REA_CLI_ARGS[@]}" cache check "$STAGED_SHA" --branch "$BRANCH" --base "$BASE_BRANCH" 2>"$CACHE_STDERR_FILE") || CACHE_EXIT=$?
|
|
255
|
+
CACHE_STDERR=$(cat "$CACHE_STDERR_FILE" 2>/dev/null || true)
|
|
256
|
+
rm -f "$CACHE_STDERR_FILE"
|
|
257
|
+
if [[ "$CACHE_EXIT" -ne 0 ]]; then
|
|
258
|
+
# SECURITY (Codex LOW 5): strip C0/C1 control chars before echoing CLI
|
|
259
|
+
# stderr. Includes 0x80-0x9F because some terminals interpret bare C1
|
|
260
|
+
# bytes (CSI 0x9B, OSC 0x9D) as escape introducers.
|
|
261
|
+
CACHE_STDERR_SAFE=$(printf '%s' "$CACHE_STDERR" | LC_ALL=C tr -d '\000-\037\177\200-\237')
|
|
262
|
+
printf 'rea commit-review: CACHE CHECK FAILED (exit=%d): %s\n' "$CACHE_EXIT" "$CACHE_STDERR_SAFE" >&2
|
|
263
|
+
printf 'rea commit-review: treating as miss; file bookedsolidtech/rea issue if unexpected.\n' >&2
|
|
264
|
+
CACHE_RESULT='{"hit":false,"reason":"query_error"}'
|
|
265
|
+
elif [[ -z "$CACHE_STDOUT" ]]; then
|
|
266
|
+
CACHE_RESULT='{"hit":false,"reason":"cold"}'
|
|
267
|
+
else
|
|
268
|
+
CACHE_RESULT="$CACHE_STDOUT"
|
|
269
|
+
fi
|
|
270
|
+
if printf '%s' "$CACHE_RESULT" | jq -e '.hit == true and .result == "pass"' >/dev/null 2>&1; then
|
|
176
271
|
CACHE_HIT=true
|
|
177
272
|
fi
|
|
178
273
|
fi
|
|
@@ -210,8 +305,25 @@ fi
|
|
|
210
305
|
printf ' 1. Inspect: git diff --cached\n'
|
|
211
306
|
printf ' 2. Decide: Is this safe to commit? (initial commits, refactors, and\n'
|
|
212
307
|
printf ' feature work are normal — use judgement, not ceremony)\n'
|
|
213
|
-
|
|
214
|
-
|
|
308
|
+
# Defect L follow-up: when no sha256 hasher is available STAGED_SHA is empty
|
|
309
|
+
# and `rea cache set pass` is a dead-end the CLI rejects. Branch the banner
|
|
310
|
+
# to surface an actionable path instead. Unlike push-review-core.sh there is
|
|
311
|
+
# no `REA_SKIP_COMMIT_REVIEW` env escape hatch (the commit gate only fires
|
|
312
|
+
# under Claude Code's Bash `PreToolUse` matcher, so a human direct-shell
|
|
313
|
+
# commit bypasses it entirely). The only remediation is to install a sha256
|
|
314
|
+
# hasher or ask the user to commit directly.
|
|
315
|
+
if [[ -n "$STAGED_SHA" ]]; then
|
|
316
|
+
printf ' 3. Approve: rea cache set %s pass --branch %s --base %s\n' \
|
|
317
|
+
"$STAGED_SHA" "$BRANCH" "$BASE_BRANCH"
|
|
318
|
+
printf ' 4. Retry the git commit command\n'
|
|
319
|
+
else
|
|
320
|
+
printf ' 3. Cache is DISABLED on this host (no sha256 hasher or no base\n'
|
|
321
|
+
printf ' branch resolvable). Install one of: sha256sum (Linux coreutils),\n'
|
|
322
|
+
printf ' shasum (perl-core), or openssl; or ensure origin/HEAD is set so\n'
|
|
323
|
+
printf ' the gate can identify the merge target. Without these the cache\n'
|
|
324
|
+
printf ' path cannot complete — escalate to the user if neither can be\n'
|
|
325
|
+
printf ' provided.\n'
|
|
326
|
+
fi
|
|
215
327
|
printf '\n'
|
|
216
328
|
printf ' Only escalate to the user if you find a genuine problem in the diff.\n'
|
|
217
329
|
} >&2
|