@bookedsolid/rea 0.31.0 → 0.32.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/.husky/prepare-commit-msg +80 -6
- package/MIGRATING.md +24 -15
- package/dist/cli/hook.js +32 -22
- package/dist/hooks/_lib/halt-check.d.ts +78 -0
- package/dist/hooks/_lib/halt-check.js +106 -0
- package/dist/hooks/_lib/payload.d.ts +86 -0
- package/dist/hooks/_lib/payload.js +166 -0
- package/dist/hooks/_lib/segments.d.ts +100 -0
- package/dist/hooks/_lib/segments.js +444 -0
- package/dist/hooks/attribution-advisory/index.d.ts +72 -0
- package/dist/hooks/attribution-advisory/index.js +233 -0
- package/dist/hooks/bash-scanner/protected-scan.js +14 -2
- package/dist/hooks/pr-issue-link-gate/index.d.ts +91 -0
- package/dist/hooks/pr-issue-link-gate/index.js +127 -0
- package/dist/hooks/security-disclosure-gate/index.d.ts +91 -0
- package/dist/hooks/security-disclosure-gate/index.js +502 -0
- package/hooks/_lib/protected-paths.sh +10 -3
- package/hooks/attribution-advisory.sh +139 -131
- package/hooks/pr-issue-link-gate.sh +114 -45
- package/hooks/security-disclosure-gate.sh +148 -316
- package/hooks/settings-protection.sh +13 -9
- package/package.json +1 -1
- package/templates/attribution-advisory.dogfood-staged.sh +170 -0
- package/templates/pr-issue-link-gate.dogfood-staged.sh +134 -0
- package/templates/prepare-commit-msg.husky.sh +80 -6
- package/templates/security-disclosure-gate.dogfood-staged.sh +171 -0
- package/templates/settings-protection.dogfood.patch +58 -0
|
@@ -1,162 +1,170 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# PreToolUse hook: attribution-advisory.sh
|
|
3
|
-
#
|
|
3
|
+
# 0.32.0+ — Node-binary shim for `rea hook attribution-advisory`.
|
|
4
4
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
5
|
+
# Pre-0.32.0 the gate's full body lived here as bash (162 LOC,
|
|
6
|
+
# including the AI-attribution pattern catalog and segment-relevance
|
|
7
|
+
# gating). The migration to the parser-backed Node binary moves all
|
|
8
|
+
# of that into `src/hooks/attribution-advisory/index.ts`. This shim
|
|
9
|
+
# is the Claude Code dispatcher's view of the hook — it forwards
|
|
10
|
+
# stdin to the CLI and exits with whatever the CLI returns.
|
|
7
11
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
12
|
+
# Behavioral contract is preserved byte-for-byte: exit 0 on
|
|
13
|
+
# disabled-policy / non-relevant / clean-command, exit 2 on HALT /
|
|
14
|
+
# attribution detected / malformed payload (fail-closed).
|
|
11
15
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
16
|
+
# # CLI-resolution trust boundary
|
|
17
|
+
#
|
|
18
|
+
# Codex round 1 P1 (2026-05-15): realpath sandbox check + version
|
|
19
|
+
# probe. Mirrors delegation-advisory.sh §3. Defends against
|
|
20
|
+
# symlink-out + tarball-replacement attacks on the resolved CLI AND
|
|
21
|
+
# stale-node_modules version skew that would otherwise turn every
|
|
22
|
+
# Bash dispatch into a hard failure.
|
|
15
23
|
|
|
16
24
|
set -uo pipefail
|
|
17
25
|
|
|
18
|
-
#
|
|
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
|
-
# 0.16.0: HALT check sourced from shared _lib/halt-check.sh.
|
|
26
|
+
# 1. HALT check.
|
|
30
27
|
# shellcheck source=_lib/halt-check.sh
|
|
31
28
|
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
32
29
|
check_halt
|
|
33
30
|
REA_ROOT=$(rea_root)
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
|
|
37
|
-
if [ ! -f "$POLICY_FILE" ]; then
|
|
38
|
-
exit 0
|
|
39
|
-
fi
|
|
40
|
-
if ! grep -qE '^block_ai_attribution:[[:space:]]*true' "$POLICY_FILE" 2>/dev/null; then
|
|
41
|
-
exit 0
|
|
42
|
-
fi
|
|
43
|
-
|
|
44
|
-
# ── 5. Parse tool_input.command from the hook payload ─────────────────────────
|
|
45
|
-
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
46
|
-
|
|
47
|
-
if [[ -z "$CMD" ]]; then
|
|
48
|
-
exit 0
|
|
49
|
-
fi
|
|
32
|
+
proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
|
|
50
33
|
|
|
51
|
-
# 0.
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if
|
|
75
|
-
|
|
76
|
-
fi
|
|
77
|
-
|
|
78
|
-
if any_segment_starts_with "$CMD" 'git[[:space:]]+commit'; then
|
|
79
|
-
IS_RELEVANT=1
|
|
34
|
+
# 2. Relevance pre-gate (0.32.0 round-5 P1, round-6 fix). PreToolUse
|
|
35
|
+
# Bash matchers fire on EVERY shell command, but this hook only
|
|
36
|
+
# enforces against `git commit` / `gh pr create|edit`. Capture
|
|
37
|
+
# stdin + check relevance FIRST so unrelated commands (ls,
|
|
38
|
+
# pnpm test, …) exit 0 even when the CLI is missing/stale/
|
|
39
|
+
# sandboxed-out.
|
|
40
|
+
#
|
|
41
|
+
# Match the pattern ANYWHERE in the command string (after the
|
|
42
|
+
# opening quote, then `[^"]*` for any leading shell prefix —
|
|
43
|
+
# `sudo`, `time`, env assignments like `FOO=x git commit …`).
|
|
44
|
+
# Round-6 P1: prior round-5 pattern anchored at the start of the
|
|
45
|
+
# JSON value and missed all prefixed forms.
|
|
46
|
+
INPUT=$(cat)
|
|
47
|
+
# Substring scan (NOT JSON-aware). Round-7 P2: any JSON-aware regex
|
|
48
|
+
# anchored on `"command":"...` gets tripped by escaped quotes in
|
|
49
|
+
# quoted env prefixes (`FOO="two words" git commit …` → the payload
|
|
50
|
+
# carries `\"two words\"` and `[^"]*` stops at the escaped quote).
|
|
51
|
+
# Plain substring match has no such edge: it over-triggers only on
|
|
52
|
+
# the rare case where the pattern appears inside a quoted argument
|
|
53
|
+
# (`echo "gh pr create"`), and the Node body handles that correctly.
|
|
54
|
+
# This hook only fires on `tool_name=Bash`, so we don't risk matching
|
|
55
|
+
# unrelated payload shapes.
|
|
56
|
+
RELEVANT=0
|
|
57
|
+
if printf '%s' "$INPUT" | grep -qE '(git[[:space:]]+commit|gh[[:space:]]+pr[[:space:]]+(create|edit))'; then
|
|
58
|
+
RELEVANT=1
|
|
80
59
|
fi
|
|
81
|
-
|
|
82
|
-
|
|
60
|
+
if [ "$RELEVANT" -eq 0 ]; then
|
|
61
|
+
# Irrelevant Bash call — nothing the pre-0.32.0 body would have
|
|
62
|
+
# processed. Always exit 0 regardless of CLI state.
|
|
83
63
|
exit 0
|
|
84
64
|
fi
|
|
85
65
|
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
|
|
99
|
-
# either end-of-line, whitespace, `>`, or a domain that does NOT begin
|
|
100
|
-
# with `users.noreply.github.com`. Posix ERE has no lookarounds, so we
|
|
101
|
-
# enumerate the allowed-prefix shapes explicitly. The "AI names" branch
|
|
102
|
-
# below catches Co-Authored-By with named tools regardless of the email
|
|
103
|
-
# domain, so dropping `users.noreply.github.com` from the noreply
|
|
104
|
-
# pattern only relaxes the check for human collaborators — never for AI.
|
|
105
|
-
if any_segment_matches "$CMD" 'Co-Authored-By:.*noreply@(anthropic\.com|openai\.com|github-copilot|github\.com|claude\.ai|chatgpt\.com|googlemail\.com|google\.com|cursor\.com|codeium\.com|tabnine\.com|amazon\.com|amazonaws\.com|amazon-q\.amazonaws\.com|cody\.dev|sourcegraph\.com|mistral\.ai|xai-org|x\.ai|inflection\.ai|perplexity\.ai|replit\.com|jetbrains\.com|bito\.ai|pieces\.app|phind\.com|you\.com)'; then
|
|
106
|
-
FOUND=1
|
|
66
|
+
# 2b. Policy short-circuit (round-6 P2). The pre-0.32.0 bash body
|
|
67
|
+
# no-op'd when `block_ai_attribution` was absent or false. Without
|
|
68
|
+
# this check, an unbuilt/stale install would refuse `git commit`
|
|
69
|
+
# even on repos that DELIBERATELY disable the attribution gate.
|
|
70
|
+
# Read the policy via a simple grep — the canonical loader
|
|
71
|
+
# handles inline forms but we only need block form here, and a
|
|
72
|
+
# conservative "true-and-only-true counts" rule matches the
|
|
73
|
+
# intent (false / absent / inline-only all → no enforcement).
|
|
74
|
+
POLICY_FILE="$REA_ROOT/.rea/policy.yaml"
|
|
75
|
+
if [ ! -f "$POLICY_FILE" ] || ! grep -qE '^block_ai_attribution:[[:space:]]*true([[:space:]]|$)' "$POLICY_FILE"; then
|
|
76
|
+
# Attribution blocking disabled — pre-0.32.0 bash body would have
|
|
77
|
+
# exited 0 here. Don't refuse on stale-install grounds.
|
|
78
|
+
exit 0
|
|
107
79
|
fi
|
|
108
80
|
|
|
109
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
81
|
+
# 3. Resolve the rea CLI.
|
|
82
|
+
REA_ARGV=()
|
|
83
|
+
RESOLVED_CLI_PATH=""
|
|
84
|
+
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
85
|
+
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
86
|
+
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
87
|
+
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
88
|
+
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
89
|
+
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
112
90
|
fi
|
|
113
91
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
92
|
+
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
93
|
+
# 0.32.0 round-4 P2: when `block_ai_attribution: true`, this hook is
|
|
94
|
+
# blocking-tier — the pre-0.32.0 bash body enforced the policy
|
|
95
|
+
# without a compiled CLI. Falling through to exit 0 would silently
|
|
96
|
+
# let AI-attribution patterns through every git commit / gh pr
|
|
97
|
+
# create-or-edit until the operator rebuilds. Fail closed and tell
|
|
98
|
+
# the operator how to restore protection.
|
|
99
|
+
printf 'rea: attribution-advisory cannot run — the rea CLI is not built.\n' >&2
|
|
100
|
+
printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
|
|
101
|
+
printf 'This shim fails closed because the pre-0.32.0 bash body enforced attribution policy without a CLI.\n' >&2
|
|
102
|
+
exit 2
|
|
117
103
|
fi
|
|
118
104
|
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if any_segment_matches "$CMD" '\[Claude Code\]\(|\[GitHub Copilot\]\(|\[ChatGPT\]\(|\[Gemini\]\(|\[Cursor\]\('; then
|
|
125
|
-
FOUND=1
|
|
105
|
+
# 3. Realpath sandbox check.
|
|
106
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
107
|
+
printf 'rea: attribution-advisory cannot run — `node` is not on PATH.\n' >&2
|
|
108
|
+
printf 'Install Node 22+ (engines.node) to restore enforcement.\n' >&2
|
|
109
|
+
exit 2
|
|
126
110
|
fi
|
|
127
111
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
112
|
+
sandbox_check=$(node -e '
|
|
113
|
+
const fs = require("fs");
|
|
114
|
+
const path = require("path");
|
|
115
|
+
const cli = process.argv[1];
|
|
116
|
+
const projDir = process.argv[2];
|
|
117
|
+
let real, realProj;
|
|
118
|
+
try { real = fs.realpathSync(cli); } catch (e) {
|
|
119
|
+
process.stdout.write("bad:realpath"); process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
122
|
+
process.stdout.write("bad:realpath-proj"); process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
const sep = path.sep;
|
|
125
|
+
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
126
|
+
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
127
|
+
process.stdout.write("bad:cli-escapes-project"); process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
130
|
+
let found = false;
|
|
131
|
+
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
132
|
+
const pj = path.join(cur, "package.json");
|
|
133
|
+
if (fs.existsSync(pj)) {
|
|
134
|
+
try {
|
|
135
|
+
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
136
|
+
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
137
|
+
} catch (e) { /* keep walking */ }
|
|
138
|
+
}
|
|
139
|
+
cur = path.dirname(cur);
|
|
140
|
+
}
|
|
141
|
+
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
142
|
+
process.stdout.write("ok");
|
|
143
|
+
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
144
|
+
|
|
145
|
+
if [ "$sandbox_check" != "ok" ]; then
|
|
146
|
+
# 0.32.0 round-4 P2: fail closed (blocking-tier when policy enables —
|
|
147
|
+
# see top-of-file rationale). Sandbox failure means the CLI cannot
|
|
148
|
+
# be authenticated; refuse rather than silently bypass.
|
|
149
|
+
printf 'rea: attribution-advisory FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
|
|
150
|
+
exit 2
|
|
131
151
|
fi
|
|
132
152
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
printf ' - Markdown-linked tool names: [Claude Code](...)\n'
|
|
146
|
-
printf ' - Emoji attribution: 🤖 Generated...\n'
|
|
147
|
-
printf '\n'
|
|
148
|
-
printf ' What is ALLOWED (legitimate references):\n'
|
|
149
|
-
printf ' - "Fix Claude API integration"\n'
|
|
150
|
-
printf ' - "Update OpenAI SDK version"\n'
|
|
151
|
-
printf ' - "Add Copilot config"\n'
|
|
152
|
-
printf '\n'
|
|
153
|
-
printf ' Remove the attribution markers and rewrite the command.\n'
|
|
154
|
-
printf ' To disable: set block_ai_attribution: false in .rea/policy.yaml\n'
|
|
155
|
-
printf '═══════════════════════════════════════════════════════════════════\n'
|
|
156
|
-
printf '\n'
|
|
157
|
-
} >&2
|
|
153
|
+
# 4. Version-probe: confirm the resolved CLI implements
|
|
154
|
+
# `hook attribution-advisory`. Codex round 1 P1.
|
|
155
|
+
probe_out=$("${REA_ARGV[@]}" hook attribution-advisory --help 2>&1)
|
|
156
|
+
probe_status=$?
|
|
157
|
+
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'attribution-advisory'; then
|
|
158
|
+
# 0.32.0 round-4 P2: stale/older CLI without the new subcommand is
|
|
159
|
+
# NOT advisory-tier fall-through — the bash body it replaces
|
|
160
|
+
# enforced when policy enabled. Fail closed and tell the operator
|
|
161
|
+
# exactly how to fix.
|
|
162
|
+
printf 'rea: this shim requires the `rea hook attribution-advisory` subcommand (introduced in 0.32.0).\n' >&2
|
|
163
|
+
printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
|
|
164
|
+
printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
|
|
158
165
|
exit 2
|
|
159
166
|
fi
|
|
160
167
|
|
|
161
|
-
#
|
|
162
|
-
|
|
168
|
+
# 5. Forward stdin (already captured up-front for the relevance gate).
|
|
169
|
+
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook attribution-advisory
|
|
170
|
+
exit $?
|
|
@@ -1,65 +1,134 @@
|
|
|
1
|
-
#!/
|
|
2
|
-
# pr-issue-link-gate.sh
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: pr-issue-link-gate.sh
|
|
3
|
+
# 0.32.0+ — Node-binary shim for `rea hook pr-issue-link-gate`.
|
|
3
4
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# Pre-0.32.0 the gate's full body lived here as bash; the migration to
|
|
6
|
+
# the parser-backed Node binary moves the matching + advisory logic
|
|
7
|
+
# into `src/hooks/pr-issue-link-gate/index.ts`. This shim is the
|
|
8
|
+
# Claude Code dispatcher's view of the hook — it forwards stdin to the
|
|
9
|
+
# CLI and exits with whatever the CLI returns.
|
|
8
10
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# advisory gives the agent an opportunity to add the link before proceeding.
|
|
11
|
+
# Behavioral contract is preserved byte-for-byte: ALWAYS exit 0 except
|
|
12
|
+
# under HALT (exit 2) or a malformed payload (exit 2, fail-closed).
|
|
12
13
|
#
|
|
13
|
-
#
|
|
14
|
-
# JSONL-only projects (no GitHub) are unaffected — gh is unavailable there.
|
|
14
|
+
# # CLI-resolution trust boundary
|
|
15
15
|
#
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
# Codex round 1 P1 (2026-05-15): mirrors the realpath sandbox check
|
|
17
|
+
# from `delegation-advisory.sh` §3 and `protected-paths-bash-gate.sh`
|
|
18
|
+
# §6. The resolved CLI MUST live INSIDE realpath(CLAUDE_PROJECT_DIR)
|
|
19
|
+
# AND have an ancestor `package.json` whose `name` is
|
|
20
|
+
# `@bookedsolid/rea`. Pre-fix the shim executed
|
|
21
|
+
# `node_modules/@bookedsolid/rea/dist/cli/index.js` directly without
|
|
22
|
+
# realpathing the target, which would let an attacker who controlled
|
|
23
|
+
# `node_modules/@bookedsolid/rea` (symlink-out, postinstall script,
|
|
24
|
+
# tarball-replacement) ship forged review code that intercepts every
|
|
25
|
+
# Bash dispatch.
|
|
26
|
+
#
|
|
27
|
+
# Sandboxed resolution order (PATH is INTENTIONALLY OMITTED):
|
|
28
|
+
# 1. node_modules/@bookedsolid/rea/dist/cli/index.js (consumer-side)
|
|
29
|
+
# 2. dist/cli/index.js under CLAUDE_PROJECT_DIR (dogfood)
|
|
30
|
+
#
|
|
31
|
+
# When NO rea CLI is reachable through the sandboxed order, this hook
|
|
32
|
+
# falls through to allow (exit 0) — the advisory is a nudge, not a
|
|
33
|
+
# security claim. The bash-tier path gates fail-closed because they
|
|
34
|
+
# protect write surfaces; this gate only emits prose.
|
|
35
|
+
#
|
|
36
|
+
# # Version skew
|
|
37
|
+
#
|
|
38
|
+
# Codex round 1 P1 (2026-05-15): a fresh `rea init` against a stale
|
|
39
|
+
# `node_modules/@bookedsolid/rea` would deliver this 0.32.0 shim while
|
|
40
|
+
# the installed CLI lacks the `hook pr-issue-link-gate` subcommand —
|
|
41
|
+
# every Bash dispatch would then fail with `unknown command` (exit 1).
|
|
42
|
+
# Probe the subcommand's `--help` output before propagating the exit
|
|
43
|
+
# code; on probe failure, advise the operator to `pnpm install` and
|
|
44
|
+
# fall through silently so the workspace stays usable.
|
|
19
45
|
|
|
20
|
-
|
|
21
|
-
source "$(dirname "$0")/_lib/common.sh"
|
|
46
|
+
set -uo pipefail
|
|
22
47
|
|
|
48
|
+
# 1. HALT check. Even though the CLI re-checks for defense-in-depth,
|
|
49
|
+
# short-circuit here so we never spawn `node` while frozen.
|
|
50
|
+
# shellcheck source=_lib/halt-check.sh
|
|
51
|
+
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
23
52
|
check_halt
|
|
53
|
+
REA_ROOT=$(rea_root)
|
|
24
54
|
|
|
25
|
-
|
|
26
|
-
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
55
|
+
proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
|
|
27
56
|
|
|
28
|
-
|
|
29
|
-
|
|
57
|
+
# 2. Resolve the rea CLI through the fixed 2-tier sandboxed order.
|
|
58
|
+
REA_ARGV=()
|
|
59
|
+
RESOLVED_CLI_PATH=""
|
|
60
|
+
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
61
|
+
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
62
|
+
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
63
|
+
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
64
|
+
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
65
|
+
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
30
66
|
fi
|
|
31
67
|
|
|
32
|
-
|
|
68
|
+
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
33
71
|
|
|
34
|
-
#
|
|
35
|
-
if !
|
|
72
|
+
# 3. Realpath sandbox check — mirrors delegation-advisory.sh §3.
|
|
73
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
36
74
|
exit 0
|
|
37
75
|
fi
|
|
38
76
|
|
|
39
|
-
|
|
77
|
+
sandbox_check=$(node -e '
|
|
78
|
+
const fs = require("fs");
|
|
79
|
+
const path = require("path");
|
|
80
|
+
const cli = process.argv[1];
|
|
81
|
+
const projDir = process.argv[2];
|
|
82
|
+
let real, realProj;
|
|
83
|
+
try { real = fs.realpathSync(cli); } catch (e) {
|
|
84
|
+
process.stdout.write("bad:realpath"); process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
87
|
+
process.stdout.write("bad:realpath-proj"); process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
const sep = path.sep;
|
|
90
|
+
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
91
|
+
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
92
|
+
process.stdout.write("bad:cli-escapes-project"); process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
95
|
+
let found = false;
|
|
96
|
+
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
97
|
+
const pj = path.join(cur, "package.json");
|
|
98
|
+
if (fs.existsSync(pj)) {
|
|
99
|
+
try {
|
|
100
|
+
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
101
|
+
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
102
|
+
} catch (e) { /* keep walking */ }
|
|
103
|
+
}
|
|
104
|
+
cur = path.dirname(cur);
|
|
105
|
+
}
|
|
106
|
+
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
107
|
+
process.stdout.write("ok");
|
|
108
|
+
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
40
109
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if echo "$COMMAND" | grep -qiE '(closes|fixes|resolves)\s+#[0-9]+'; then
|
|
110
|
+
if [ "$sandbox_check" != "ok" ]; then
|
|
111
|
+
printf 'rea: pr-issue-link-gate skipped (sandbox check: %s)\n' "$sandbox_check" >&2
|
|
44
112
|
exit 0
|
|
45
113
|
fi
|
|
46
114
|
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
printf '
|
|
54
|
-
printf '
|
|
55
|
-
printf '
|
|
56
|
-
printf '
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
115
|
+
# 4. Version-probe: confirm the resolved CLI implements the
|
|
116
|
+
# `hook pr-issue-link-gate` subcommand. A stale node_modules from
|
|
117
|
+
# a fresh `rea init` against an older installed version would
|
|
118
|
+
# otherwise turn every Bash dispatch into a hard failure.
|
|
119
|
+
probe_out=$("${REA_ARGV[@]}" hook pr-issue-link-gate --help 2>&1)
|
|
120
|
+
probe_status=$?
|
|
121
|
+
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'pr-issue-link-gate'; then
|
|
122
|
+
printf 'rea: this shim requires the `rea hook pr-issue-link-gate` subcommand (introduced in 0.32.0).\n' >&2
|
|
123
|
+
printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
|
|
124
|
+
printf 'Run `pnpm install` (or `npm install`) to sync the CLI to the version this shim expects.\n' >&2
|
|
125
|
+
exit 0
|
|
126
|
+
fi
|
|
63
127
|
|
|
64
|
-
#
|
|
65
|
-
|
|
128
|
+
# 5. Forward stdin to the CLI synchronously. The advisory text must
|
|
129
|
+
# reach the operator's stderr before this hook returns; the CLI's
|
|
130
|
+
# own exit code is the hook's exit code (0 normally, 2 under HALT
|
|
131
|
+
# or malformed payload).
|
|
132
|
+
INPUT=$(cat)
|
|
133
|
+
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook pr-issue-link-gate
|
|
134
|
+
exit $?
|