@bookedsolid/rea 0.34.0 → 0.36.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.
@@ -1,26 +1,58 @@
1
- #!/usr/bin/env bash
1
+ #!/bin/bash
2
2
  # PreToolUse hook: blocked-paths-bash-gate.sh
3
+ # 0.35.0+ — Node-binary shim for `rea hook blocked-paths-bash-gate`.
3
4
  #
4
- # 0.23.0+ thin shim. Forwards stdin to `rea hook scan-bash --mode blocked`.
5
- # See protected-paths-bash-gate.sh for the architectural rationale + CLI
6
- # resolution strategy + verdict-verification model; this shim differs
7
- # only in the --mode flag.
5
+ # Pre-0.35.0 this was a thin bash shim over `rea hook scan-bash --mode
6
+ # blocked` (the parser-backed AST walker that closes 9 bypass classes
7
+ # from helix-023 + discord-ops Round 13 see `src/hooks/bash-scanner/`).
8
+ # The full bash body is preserved at
9
+ # `__tests__/hooks/parity/baselines/blocked-paths-bash-gate.sh.pre-0.35.0`.
8
10
  #
9
- # Codex round 4 Finding 2: 2-tier sandboxed resolver (drops PATH lookup
10
- # and node_modules/.bin/rea symlink). See protected-paths-bash-gate.sh
11
- # for rationale.
11
+ # This shim now resolves the CLI through the same 2-tier sandboxed
12
+ # resolver as the 0.32.0+ pilots and calls `rea hook blocked-paths-
13
+ # bash-gate` directly — eliminating the shim → CLI → scanner-module
14
+ # subprocess hop entirely.
12
15
  #
13
- # Codex round 2 R2-3: REA_NODE_CLI env-var honoring REMOVED.
16
+ # Behavioral contract is preserved byte-for-byte: exit 0 on allow,
17
+ # exit 2 on HALT / verdict block / malformed payload / sandbox fail.
14
18
  #
15
- # Exit codes:
16
- # 0 = allow
17
- # 2 = block (verdict, missing CLI, malformed payload, verdict mismatch)
19
+ # # CLI-resolution trust boundary
20
+ #
21
+ # Mirrors the 0.32.0 final shim shape. The resolved CLI MUST live
22
+ # INSIDE realpath(CLAUDE_PROJECT_DIR) AND have an ancestor
23
+ # `package.json` whose `name` is `@bookedsolid/rea`. Defends against
24
+ # symlink-out and tarball-replacement attacks on the resolved CLI.
25
+ #
26
+ # # Fail-closed posture
27
+ #
28
+ # blocked-paths-bash-gate is a Tier-1 security gate (PreToolUse Bash).
29
+ # The pre-0.35.0 bash body refused on uncertainty for every failure
30
+ # class. Early-exit branches (CLI missing, node missing, sandbox failed,
31
+ # version skew) fail closed AFTER the relevance pre-gate passes.
32
+ # Irrelevant Bash calls exit 0 regardless of CLI state.
33
+ #
34
+ # # Relevance pre-gate
35
+ #
36
+ # Same posture as 0.34.0 dangerous-bash + secret-scanner. When the CLI
37
+ # is missing, refuse only when the extracted command MENTIONS a path
38
+ # from `policy.blocked_paths`. Empty policy → no enforcement, exit 0.
39
+ # This unblocks the install path itself: `npx rea init`, pre-`pnpm build`
40
+ # checkouts can still run benign Bash like `ls`/`mkdir`/`pnpm install`.
18
41
 
19
42
  set -uo pipefail
20
43
 
21
- proj="${CLAUDE_PROJECT_DIR:-$(pwd)}"
44
+ # 1. HALT check.
45
+ # shellcheck source=_lib/halt-check.sh
46
+ source "$(dirname "$0")/_lib/halt-check.sh"
47
+ check_halt
48
+ REA_ROOT=$(rea_root)
49
+
50
+ proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
22
51
 
23
- # 2-tier sandboxed CLI resolver. NO PATH lookup, NO env-var override.
52
+ # 2. Capture stdin once.
53
+ INPUT=$(cat)
54
+
55
+ # 3. Resolve the rea CLI through the fixed 2-tier sandboxed order.
24
56
  REA_ARGV=()
25
57
  RESOLVED_CLI_PATH=""
26
58
  if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
@@ -31,49 +63,83 @@ elif [ -f "$proj/dist/cli/index.js" ]; then
31
63
  RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
32
64
  fi
33
65
 
66
+ # 3b. Relevance pre-gate. Only used when the CLI is missing.
34
67
  if [ "${#REA_ARGV[@]}" -eq 0 ]; then
35
- printf 'rea: CLI not found at sandboxed tiers (node_modules/@bookedsolid/rea/dist or dist/).\n' >&2
36
- printf 'Install @bookedsolid/rea via npm/pnpm and run `rea doctor`.\n' >&2
37
- printf 'Refusing the Bash command on uncertainty.\n' >&2
68
+ CLI_MISSING_CMD=""
69
+ if command -v jq >/dev/null 2>&1; then
70
+ CLI_MISSING_CMD=$(printf '%s' "$INPUT" | jq -r '
71
+ (.tool_input.command // "") | tostring
72
+ ' 2>/dev/null || true)
73
+ else
74
+ CLI_MISSING_CMD="$INPUT"
75
+ fi
76
+ if [ -z "$CLI_MISSING_CMD" ]; then
77
+ # Empty/non-Bash payload → pre-0.35.0 body would have exited 0.
78
+ exit 0
79
+ fi
80
+ # Empty policy.blocked_paths → no enforcement, exit 0.
81
+ POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
82
+ if [ ! -f "$POLICY_FILE" ]; then
83
+ exit 0
84
+ fi
85
+ # Substring scan: does the command mention any blocked_paths entry?
86
+ # Coarse — over-trigger is fine, under-trigger is the bypass we MUST
87
+ # avoid. Strip YAML quotes/comments via a minimal awk filter.
88
+ CLI_MISSING_RELEVANT=0
89
+ while IFS= read -r entry; do
90
+ [ -z "$entry" ] && continue
91
+ case "$CLI_MISSING_CMD" in
92
+ *"$entry"*) CLI_MISSING_RELEVANT=1; break ;;
93
+ esac
94
+ done < <(awk '
95
+ /^blocked_paths:/ { in_block=1; next }
96
+ in_block && /^[[:space:]]*-/ {
97
+ sub(/^[[:space:]]*-[[:space:]]*/, "")
98
+ gsub(/^["'\'']/, "")
99
+ gsub(/["'\'']$/, "")
100
+ print
101
+ next
102
+ }
103
+ in_block && /^[^[:space:]-]/ { in_block=0 }
104
+ ' "$POLICY_FILE" 2>/dev/null)
105
+ if [ "$CLI_MISSING_RELEVANT" -eq 0 ]; then
106
+ exit 0
107
+ fi
108
+ printf 'rea: blocked-paths-bash-gate cannot run — the rea CLI is not built.\n' >&2
109
+ printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
110
+ printf 'This shim fails closed because the pre-0.35.0 bash body enforced blocked_paths refusal without a CLI.\n' >&2
38
111
  exit 2
39
112
  fi
40
113
 
41
- # Codex round 4 Finding 2 + round 5 F2 tier defense: realpath the
42
- # resolved CLI; PRIMARY check is project-root containment, SECONDARY
43
- # is ancestor `package.json` with the protected name. See
44
- # protected-paths-bash-gate.sh for the full rationale.
114
+ # 4. Realpath sandbox check.
45
115
  if ! command -v node >/dev/null 2>&1; then
46
- printf 'rea: node not on PATH (required to realpath verify scan-bash CLI). Refusing.\n' >&2
116
+ printf 'rea: blocked-paths-bash-gate cannot run `node` is not on PATH.\n' >&2
117
+ printf 'Install Node 22+ (engines.node) to restore blocked_paths refusal.\n' >&2
47
118
  exit 2
48
119
  fi
120
+
49
121
  sandbox_check=$(node -e '
50
122
  const fs = require("fs");
51
123
  const path = require("path");
52
124
  const cli = process.argv[1];
53
125
  const projDir = process.argv[2];
54
- let real;
126
+ let real, realProj;
55
127
  try { real = fs.realpathSync(cli); } catch (e) {
56
- process.stdout.write("bad:realpath:" + (e && e.message ? e.message : String(e)));
57
- process.exit(1);
128
+ process.stdout.write("bad:realpath"); process.exit(1);
58
129
  }
59
- // PRIMARY (round 5 F2): realCli must live INSIDE realProj. Catches
60
- // node_modules/@bookedsolid/rea -> /tmp/sym-attacker symlink-out.
61
- let realProj;
62
130
  try { realProj = fs.realpathSync(projDir); } catch (e) {
63
- process.stdout.write("bad:realpath-proj:" + (e && e.message ? e.message : String(e)));
64
- process.exit(1);
131
+ process.stdout.write("bad:realpath-proj"); process.exit(1);
65
132
  }
66
- const projWithSep = realProj.endsWith(path.sep) ? realProj : realProj + path.sep;
133
+ const sep = path.sep;
134
+ const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
67
135
  if (!(real === realProj || real.startsWith(projWithSep))) {
68
- process.stdout.write("bad:cli-escapes-project:" + real + ":proj=" + realProj);
69
- process.exit(1);
136
+ process.stdout.write("bad:cli-escapes-project"); process.exit(1);
70
137
  }
71
- // SECONDARY (round 4 #2): shape + ancestor `package.json` with
72
- // `@bookedsolid/rea`. Guards against intra-project hijack.
138
+ // Codex round-1 P1 fix: enforce dist/cli/index.js shape (see
139
+ // settings-protection.sh).
73
140
  const expectedEnd = path.join("dist", "cli", "index.js");
74
141
  if (!real.endsWith(path.sep + expectedEnd) && real !== "/" + expectedEnd) {
75
- process.stdout.write("bad:cli-shape:" + real);
76
- process.exit(1);
142
+ process.stdout.write("bad:cli-shape"); process.exit(1);
77
143
  }
78
144
  let cur = path.dirname(path.dirname(path.dirname(real)));
79
145
  let found = false;
@@ -82,94 +148,30 @@ sandbox_check=$(node -e '
82
148
  if (fs.existsSync(pj)) {
83
149
  try {
84
150
  const data = JSON.parse(fs.readFileSync(pj, "utf8"));
85
- if (data && data.name === "@bookedsolid/rea") {
86
- found = true;
87
- break;
88
- }
89
- } catch (e) {
90
- // Continue.
91
- }
151
+ if (data && data.name === "@bookedsolid/rea") { found = true; break; }
152
+ } catch (e) { /* keep walking */ }
92
153
  }
93
154
  cur = path.dirname(cur);
94
155
  }
95
- if (!found) {
96
- process.stdout.write("bad:no-rea-pkg:" + real);
97
- process.exit(1);
98
- }
156
+ if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
99
157
  process.stdout.write("ok");
100
- process.exit(0);
101
- ' "$RESOLVED_CLI_PATH" "$proj" 2>&1)
102
- sandbox_status=$?
103
- if [ "$sandbox_status" -ne 0 ] || [ "$sandbox_check" != "ok" ]; then
104
- printf 'rea: scan-bash CLI realpath escapes sandbox (%s). Refusing.\n' "$sandbox_check" >&2
158
+ ' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
159
+
160
+ if [ "$sandbox_check" != "ok" ]; then
161
+ printf 'rea: blocked-paths-bash-gate FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
105
162
  exit 2
106
163
  fi
107
164
 
108
- # 0.28.0 helix-027 (bash total-lockout postmortem) — version-probe per
109
- # shim. See protected-paths-bash-gate.sh for the full rationale; this
110
- # shim mirrors the behavior to detect a stale CLI before payload reach.
111
- probe_out=$("${REA_ARGV[@]}" hook scan-bash --help 2>&1)
165
+ # 5. Version-probe.
166
+ probe_out=$("${REA_ARGV[@]}" hook blocked-paths-bash-gate --help 2>&1)
112
167
  probe_status=$?
113
- if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'scan-bash' -e '--mode'; then
114
- printf 'rea: this shim requires the `rea hook scan-bash` subcommand (introduced in 0.23.0).\n' >&2
168
+ if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'blocked-paths-bash-gate'; then
169
+ printf 'rea: this shim requires the `rea hook blocked-paths-bash-gate` subcommand (introduced in 0.35.0).\n' >&2
115
170
  printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
116
- printf 'Run `pnpm install` (or `npm install`) to sync the CLI to the version this shim expects.\n' >&2
171
+ printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
117
172
  exit 2
118
173
  fi
119
174
 
120
- payload=$(cat)
121
- if [ -z "$payload" ]; then
122
- exit 0
123
- fi
124
-
125
- verdict=$(printf '%s' "$payload" | "${REA_ARGV[@]}" hook scan-bash --mode blocked)
126
- status=$?
127
-
128
- verifier='try {
129
- const raw = require("fs").readFileSync(0, "utf8");
130
- if (raw.trim().length === 0) { process.stdout.write("bad:empty"); process.exit(1); }
131
- const v = JSON.parse(raw);
132
- if (typeof v !== "object" || v === null || Array.isArray(v)) {
133
- process.stdout.write("bad:non-object"); process.exit(1);
134
- }
135
- if (v.verdict !== "allow" && v.verdict !== "block") {
136
- process.stdout.write("bad:verdict-shape:" + String(v.verdict)); process.exit(1);
137
- }
138
- process.stdout.write("ok:" + v.verdict); process.exit(0);
139
- } catch (e) {
140
- process.stdout.write("bad:" + (e && e.message ? e.message : String(e))); process.exit(1);
141
- }'
142
-
143
- verdict_check=$(printf '%s' "$verdict" | node -e "$verifier" 2>&1)
144
- verdict_check_status=$?
145
-
146
- case "$status" in
147
- 0)
148
- if [ "$verdict_check_status" -ne 0 ]; then
149
- printf 'rea: scan-bash exited 0 but verdict JSON is malformed (%s). Refusing on uncertainty.\n' "$verdict_check" >&2
150
- exit 2
151
- fi
152
- if [ "$verdict_check" != "ok:allow" ]; then
153
- printf 'rea: scan-bash exit 0 but verdict says %s. Refusing on uncertainty.\n' "$verdict_check" >&2
154
- exit 2
155
- fi
156
- exit 0
157
- ;;
158
- 2)
159
- if [ "$verdict_check_status" -ne 0 ]; then
160
- exit 2
161
- fi
162
- if [ "$verdict_check" != "ok:block" ]; then
163
- printf 'rea: scan-bash exit 2 but verdict says %s. Refusing on uncertainty.\n' "$verdict_check" >&2
164
- exit 2
165
- fi
166
- exit 2
167
- ;;
168
- *)
169
- printf 'rea: scan-bash exited %d (expected 0/2). Refusing on uncertainty.\n' "$status" >&2
170
- if [ -n "$verdict" ]; then
171
- printf 'rea: scan-bash stdout was: %s\n' "$verdict" >&2
172
- fi
173
- exit 2
174
- ;;
175
- esac
175
+ # 6. Forward stdin (already captured up-front).
176
+ printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook blocked-paths-bash-gate
177
+ exit $?