@bookedsolid/rea 0.37.0 → 0.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hooks/_lib/shim-runtime.sh +423 -0
- package/hooks/architecture-review-gate.sh +11 -103
- package/hooks/attribution-advisory.sh +38 -209
- package/hooks/blocked-paths-bash-gate.sh +32 -146
- package/hooks/blocked-paths-enforcer.sh +32 -137
- package/hooks/changeset-security-gate.sh +26 -119
- package/hooks/dangerous-bash-interceptor.sh +46 -170
- package/hooks/delegation-advisory.sh +26 -144
- package/hooks/delegation-capture.sh +33 -139
- package/hooks/dependency-audit-gate.sh +29 -121
- package/hooks/env-file-protection.sh +30 -141
- package/hooks/local-review-gate.sh +117 -352
- package/hooks/pr-issue-link-gate.sh +16 -118
- package/hooks/protected-paths-bash-gate.sh +53 -152
- package/hooks/secret-scanner.sh +90 -213
- package/hooks/security-disclosure-gate.sh +32 -155
- package/hooks/settings-protection.sh +56 -176
- package/package.json +1 -1
- package/templates/_lib_shim-runtime.dogfood-staged.sh +423 -0
- package/templates/architecture-review-gate.dogfood-staged.sh +11 -103
- package/templates/attribution-advisory.dogfood-staged.sh +38 -209
- package/templates/blocked-paths-bash-gate.dogfood-staged.sh +32 -146
- package/templates/blocked-paths-enforcer.dogfood-staged.sh +32 -137
- package/templates/changeset-security-gate.dogfood-staged.sh +26 -119
- package/templates/dangerous-bash-interceptor.dogfood-staged.sh +46 -170
- package/templates/delegation-advisory.dogfood-staged.sh +44 -0
- package/templates/delegation-capture.dogfood-staged.sh +52 -0
- package/templates/dependency-audit-gate.dogfood-staged.sh +29 -121
- package/templates/env-file-protection.dogfood-staged.sh +30 -141
- package/templates/local-review-gate.dogfood-staged.sh +117 -352
- package/templates/pr-issue-link-gate.dogfood-staged.sh +16 -118
- package/templates/protected-paths-bash-gate.dogfood-staged.sh +53 -152
- package/templates/secret-scanner.dogfood-staged.sh +90 -213
- package/templates/security-disclosure-gate.dogfood-staged.sh +32 -155
- package/templates/settings-protection.dogfood-staged.sh +56 -176
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# hooks/_lib/shim-runtime.sh — shared Node-binary shim runtime.
|
|
3
|
+
# Introduced 0.38.0.
|
|
4
|
+
#
|
|
5
|
+
# Source via:
|
|
6
|
+
# source "$(dirname "$0")/_lib/shim-runtime.sh"
|
|
7
|
+
# shim_run
|
|
8
|
+
#
|
|
9
|
+
# # Problem this solves
|
|
10
|
+
#
|
|
11
|
+
# Releases 0.32.0 → 0.35.0 ported all 14 PreToolUse/PostToolUse hooks
|
|
12
|
+
# from bash to Node-binary CLIs. Each port left a ~120-LOC shell shim
|
|
13
|
+
# that does the same five things:
|
|
14
|
+
#
|
|
15
|
+
# 1. HALT check
|
|
16
|
+
# 2. Capture stdin
|
|
17
|
+
# 3. Resolve the rea CLI through the fixed 2-tier sandboxed order
|
|
18
|
+
# 4. Realpath sandbox check (cli inside CLAUDE_PROJECT_DIR + ancestor
|
|
19
|
+
# package.json with `name`=`@bookedsolid/rea`)
|
|
20
|
+
# 5. Version-probe `rea hook <NAME> --help`, then forward stdin
|
|
21
|
+
#
|
|
22
|
+
# Plus standardized fail-closed / fail-open banners. The duplication
|
|
23
|
+
# was the single largest source of drift bugs in the marathon — every
|
|
24
|
+
# round of codex review found at least one shim that had drifted (e.g.
|
|
25
|
+
# settings-protection.sh / blocked-paths-bash-gate.sh / blocked-paths-
|
|
26
|
+
# enforcer.sh gained the `dist/cli/index.js` shape check at codex
|
|
27
|
+
# round-1 of 0.35.0; pr-issue-link-gate / attribution-advisory got
|
|
28
|
+
# the sandbox-before-policy-read fix at codex round-2 of 0.37.0).
|
|
29
|
+
#
|
|
30
|
+
# 0.38.0 consolidates the duplicated infrastructure into this helper.
|
|
31
|
+
# Each shim becomes ~20 LOC of hook-specific customization plus a
|
|
32
|
+
# single `shim_run` invocation.
|
|
33
|
+
#
|
|
34
|
+
# # Public API
|
|
35
|
+
#
|
|
36
|
+
# Variables the shim sets BEFORE sourcing this lib + calling shim_run:
|
|
37
|
+
#
|
|
38
|
+
# SHIM_NAME (required) — subcommand name like
|
|
39
|
+
# "dangerous-bash-interceptor". Used in
|
|
40
|
+
# banners, the `rea hook <name>` invocation,
|
|
41
|
+
# and the version-probe content match.
|
|
42
|
+
#
|
|
43
|
+
# SHIM_INTRODUCED_IN (required) — version string like "0.34.0".
|
|
44
|
+
# Used in the version-skew banner ("requires
|
|
45
|
+
# the … subcommand (introduced in X)").
|
|
46
|
+
#
|
|
47
|
+
# SHIM_FAIL_OPEN (default 0) — 1 = advisory-tier (exit 0
|
|
48
|
+
# on every CLI-failure branch except HALT);
|
|
49
|
+
# 0 = blocking-tier (exit 2). Advisory shims
|
|
50
|
+
# (pr-issue-link-gate, architecture-review-
|
|
51
|
+
# gate, delegation-advisory, delegation-
|
|
52
|
+
# capture) set this to 1.
|
|
53
|
+
#
|
|
54
|
+
# SHIM_ENFORCE_CLI_SHAPE (default 0) — 1 = ALSO require that the
|
|
55
|
+
# resolved CLI's realpath ends in
|
|
56
|
+
# `dist/cli/index.js`. Closes the codex
|
|
57
|
+
# round-1 P1 finding from 0.35.0 (an attacker
|
|
58
|
+
# who repoints node_modules/@bookedsolid/rea
|
|
59
|
+
# → arbitrary in-project JS would otherwise
|
|
60
|
+
# execute that file as the trusted gate CLI).
|
|
61
|
+
# settings-protection, blocked-paths-bash-
|
|
62
|
+
# gate, blocked-paths-enforcer, protected-
|
|
63
|
+
# paths-bash-gate all set this to 1.
|
|
64
|
+
#
|
|
65
|
+
# SHIM_REFUSAL_NOUN (default "protection") — used in the
|
|
66
|
+
# fail-closed CLI-missing banner ("to restore
|
|
67
|
+
# $SHIM_REFUSAL_NOUN"). Per-shim wording.
|
|
68
|
+
#
|
|
69
|
+
# SHIM_NODE_MISSING_NOUN (default same as SHIM_REFUSAL_NOUN) — used
|
|
70
|
+
# in the "node not on PATH" banner.
|
|
71
|
+
#
|
|
72
|
+
# SHIM_SKIP_VERSION_PROBE (default 0) — 1 = skip the version-probe
|
|
73
|
+
# step entirely. delegation-capture sets this
|
|
74
|
+
# because the pre-port body had no probe (the
|
|
75
|
+
# forward is fire-and-forget; a stale CLI
|
|
76
|
+
# drops the signal silently rather than
|
|
77
|
+
# spamming the operator with a probe banner
|
|
78
|
+
# on every Agent/Skill dispatch).
|
|
79
|
+
#
|
|
80
|
+
# Optional shim-defined callbacks (functions). Each runs in the same
|
|
81
|
+
# process as the shim — they have access to INPUT, REA_ROOT, proj,
|
|
82
|
+
# REA_ARGV, RESOLVED_CLI_PATH. To take effect they MUST be defined
|
|
83
|
+
# BEFORE `shim_run` is called.
|
|
84
|
+
#
|
|
85
|
+
# shim_is_relevant Return 0 if the payload should pass through
|
|
86
|
+
# the gate; return 1 to exit 0 immediately
|
|
87
|
+
# (irrelevant Bash/Write call). Runs AFTER
|
|
88
|
+
# stdin capture, BEFORE any CLI work. Most
|
|
89
|
+
# shims define this for the relevance pre-
|
|
90
|
+
# gate.
|
|
91
|
+
#
|
|
92
|
+
# shim_cli_missing_relevant
|
|
93
|
+
# Called when the CLI is unreachable (no
|
|
94
|
+
# node_modules/@bookedsolid/rea AND no
|
|
95
|
+
# dist/cli/index.js). Return 0 to fail-closed
|
|
96
|
+
# (emit banner + exit 2 or exit 0 per
|
|
97
|
+
# FAIL_OPEN); return 1 to exit 0 silently
|
|
98
|
+
# (pre-bash-body behavior allowed the payload
|
|
99
|
+
# when no rule matched). When this hook is
|
|
100
|
+
# NOT defined, default behavior is:
|
|
101
|
+
# - SHIM_FAIL_OPEN=0 → emit banner, exit 2
|
|
102
|
+
# - SHIM_FAIL_OPEN=1 → exit 0 silently
|
|
103
|
+
# dangerous-bash-interceptor / secret-scanner
|
|
104
|
+
# / settings-protection define this to mirror
|
|
105
|
+
# the pre-port body's keyword-relevance scan.
|
|
106
|
+
#
|
|
107
|
+
# shim_policy_short_circuit
|
|
108
|
+
# Called AFTER sandbox-check, BEFORE version-
|
|
109
|
+
# probe. Return 0 to exit 0 cleanly (policy
|
|
110
|
+
# disabled the gate); return 1 to continue
|
|
111
|
+
# with version-probe + forward. Used by
|
|
112
|
+
# attribution-advisory (`block_ai_attribution`
|
|
113
|
+
# check) and security-disclosure-gate
|
|
114
|
+
# (`REA_DISCLOSURE_MODE=disabled` check).
|
|
115
|
+
# Can call `policy_reader_get` etc. since
|
|
116
|
+
# REA_ARGV is sandbox-validated by this point.
|
|
117
|
+
#
|
|
118
|
+
# shim_forward Override the final stdin-forward step.
|
|
119
|
+
# Default: `printf '%s' "$INPUT" |
|
|
120
|
+
# "${REA_ARGV[@]}" hook "$SHIM_NAME"; exit $?`.
|
|
121
|
+
# delegation-capture overrides this to detach
|
|
122
|
+
# (background + disown). Receives INPUT,
|
|
123
|
+
# REA_ARGV in env.
|
|
124
|
+
#
|
|
125
|
+
# # Bash 3.2 compatibility
|
|
126
|
+
#
|
|
127
|
+
# This lib targets macOS bash 3.2 (and POSIX-ish where possible).
|
|
128
|
+
# Avoid: `mapfile`, `read -d`, `${VAR^^}`, associative arrays.
|
|
129
|
+
# OK: arrays, indirect expansion (`${!VAR}`), `[[`.
|
|
130
|
+
#
|
|
131
|
+
# # Trust boundary
|
|
132
|
+
#
|
|
133
|
+
# `shim_run` is sourced into the same shell as the shim. It assumes
|
|
134
|
+
# the shim has set `set -uo pipefail` at the top. It does NOT
|
|
135
|
+
# re-source halt-check.sh — the shim does that explicitly so the
|
|
136
|
+
# REA_ROOT helper is visible BEFORE the lib is sourced.
|
|
137
|
+
|
|
138
|
+
set -uo pipefail
|
|
139
|
+
|
|
140
|
+
# -----------------------------------------------------------------------------
|
|
141
|
+
# Defaults — applied by `shim_run` when the shim hasn't set them. We use
|
|
142
|
+
# the `:=` operator to assign-if-unset so callers can override.
|
|
143
|
+
# -----------------------------------------------------------------------------
|
|
144
|
+
_shim_apply_defaults() {
|
|
145
|
+
: "${SHIM_NAME:?shim-runtime: SHIM_NAME must be set before shim_run}"
|
|
146
|
+
: "${SHIM_INTRODUCED_IN:?shim-runtime: SHIM_INTRODUCED_IN must be set before shim_run}"
|
|
147
|
+
: "${SHIM_FAIL_OPEN:=0}"
|
|
148
|
+
: "${SHIM_ENFORCE_CLI_SHAPE:=0}"
|
|
149
|
+
: "${SHIM_REFUSAL_NOUN:=protection}"
|
|
150
|
+
: "${SHIM_NODE_MISSING_NOUN:=$SHIM_REFUSAL_NOUN}"
|
|
151
|
+
: "${SHIM_SKIP_VERSION_PROBE:=0}"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# -----------------------------------------------------------------------------
|
|
155
|
+
# CLI resolution — fixed 2-tier sandboxed order. PATH is INTENTIONALLY
|
|
156
|
+
# OMITTED (agent-controlled $PATH would let a forged `rea` binary
|
|
157
|
+
# intercept every hook dispatch).
|
|
158
|
+
#
|
|
159
|
+
# Sets REA_ARGV (array) and RESOLVED_CLI_PATH (string) on success.
|
|
160
|
+
# When neither tier resolves, REA_ARGV stays empty and RESOLVED_CLI_PATH
|
|
161
|
+
# stays empty.
|
|
162
|
+
# -----------------------------------------------------------------------------
|
|
163
|
+
shim_resolve_cli() {
|
|
164
|
+
REA_ARGV=()
|
|
165
|
+
RESOLVED_CLI_PATH=""
|
|
166
|
+
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
167
|
+
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
168
|
+
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
169
|
+
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
170
|
+
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
171
|
+
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
172
|
+
fi
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# -----------------------------------------------------------------------------
|
|
176
|
+
# Realpath sandbox check — validates the resolved CLI:
|
|
177
|
+
# 1. realpath(CLI) lives INSIDE realpath(CLAUDE_PROJECT_DIR)
|
|
178
|
+
# 2. an ancestor package.json has `name`=`@bookedsolid/rea`
|
|
179
|
+
# 3. (when SHIM_ENFORCE_CLI_SHAPE=1) realpath ends in dist/cli/index.js
|
|
180
|
+
#
|
|
181
|
+
# Echoes "ok" on success or "bad:<reason>" on failure. Caller compares
|
|
182
|
+
# to "ok".
|
|
183
|
+
#
|
|
184
|
+
# Args:
|
|
185
|
+
# $1 — resolved CLI path
|
|
186
|
+
# $2 — CLAUDE_PROJECT_DIR
|
|
187
|
+
# $3 — "1" to enforce dist/cli/index.js shape, "0" otherwise
|
|
188
|
+
# -----------------------------------------------------------------------------
|
|
189
|
+
shim_sandbox_check() {
|
|
190
|
+
local cli_path="$1"
|
|
191
|
+
local proj_dir="$2"
|
|
192
|
+
local enforce_shape="${3:-0}"
|
|
193
|
+
node -e '
|
|
194
|
+
const fs = require("fs");
|
|
195
|
+
const path = require("path");
|
|
196
|
+
const cli = process.argv[1];
|
|
197
|
+
const projDir = process.argv[2];
|
|
198
|
+
const enforceShape = process.argv[3] === "1";
|
|
199
|
+
let real, realProj;
|
|
200
|
+
try { real = fs.realpathSync(cli); } catch (e) {
|
|
201
|
+
process.stdout.write("bad:realpath"); process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
204
|
+
process.stdout.write("bad:realpath-proj"); process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
const sep = path.sep;
|
|
207
|
+
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
208
|
+
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
209
|
+
process.stdout.write("bad:cli-escapes-project"); process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
if (enforceShape) {
|
|
212
|
+
// 0.35.0 codex round-1 P1 fix: enforce dist/cli/index.js shape so a
|
|
213
|
+
// workspace attacker who repoints node_modules/@bookedsolid/rea or
|
|
214
|
+
// dist at an arbitrary in-project JS file cannot execute it as the
|
|
215
|
+
// trusted gate CLI.
|
|
216
|
+
const expectedEnd = path.join("dist", "cli", "index.js");
|
|
217
|
+
if (!real.endsWith(path.sep + expectedEnd) && real !== "/" + expectedEnd) {
|
|
218
|
+
process.stdout.write("bad:cli-shape"); process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
222
|
+
let found = false;
|
|
223
|
+
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
224
|
+
const pj = path.join(cur, "package.json");
|
|
225
|
+
if (fs.existsSync(pj)) {
|
|
226
|
+
try {
|
|
227
|
+
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
228
|
+
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
229
|
+
} catch (e) { /* keep walking */ }
|
|
230
|
+
}
|
|
231
|
+
cur = path.dirname(cur);
|
|
232
|
+
}
|
|
233
|
+
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
234
|
+
process.stdout.write("ok");
|
|
235
|
+
' -- "$cli_path" "$proj_dir" "$enforce_shape" 2>/dev/null
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
# -----------------------------------------------------------------------------
|
|
239
|
+
# Standardized banners — keep stderr templates identical across shims.
|
|
240
|
+
# -----------------------------------------------------------------------------
|
|
241
|
+
shim_emit_cli_missing_banner() {
|
|
242
|
+
printf 'rea: %s cannot run — the rea CLI is not built.\n' "$SHIM_NAME" >&2
|
|
243
|
+
printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore %s.\n' "$SHIM_REFUSAL_NOUN" >&2
|
|
244
|
+
printf 'This shim fails closed because the pre-port bash body enforced %s refusal without a CLI.\n' "$SHIM_NAME" >&2
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
shim_emit_node_missing_banner() {
|
|
248
|
+
printf 'rea: %s cannot run — `node` is not on PATH.\n' "$SHIM_NAME" >&2
|
|
249
|
+
printf 'Install Node 22+ (engines.node) to restore %s.\n' "$SHIM_NODE_MISSING_NOUN" >&2
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
shim_emit_sandbox_failure_banner() {
|
|
253
|
+
local reason="$1"
|
|
254
|
+
printf 'rea: %s FAILED sandbox check (%s) — refusing.\n' "$SHIM_NAME" "$reason" >&2
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
shim_emit_sandbox_skip_banner() {
|
|
258
|
+
local reason="$1"
|
|
259
|
+
printf 'rea: %s skipped (sandbox check: %s)\n' "$SHIM_NAME" "$reason" >&2
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
shim_emit_version_skew_banner_blocking() {
|
|
263
|
+
printf 'rea: this shim requires the `rea hook %s` subcommand (introduced in %s).\n' "$SHIM_NAME" "$SHIM_INTRODUCED_IN" >&2
|
|
264
|
+
printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
|
|
265
|
+
printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
shim_emit_version_skew_banner_advisory() {
|
|
269
|
+
printf 'rea: this shim requires the `rea hook %s` subcommand (introduced in %s).\n' "$SHIM_NAME" "$SHIM_INTRODUCED_IN" >&2
|
|
270
|
+
printf 'Run `pnpm install` (or `npm install`) to sync the CLI; falling through silently.\n' >&2
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
# -----------------------------------------------------------------------------
|
|
274
|
+
# Default stdin forward. shim_forward can override (delegation-capture).
|
|
275
|
+
# -----------------------------------------------------------------------------
|
|
276
|
+
shim_default_forward() {
|
|
277
|
+
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook "$SHIM_NAME"
|
|
278
|
+
exit $?
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# -----------------------------------------------------------------------------
|
|
282
|
+
# Main entry point. Reads SHIM_* variables, runs the standard flow.
|
|
283
|
+
# -----------------------------------------------------------------------------
|
|
284
|
+
shim_run() {
|
|
285
|
+
_shim_apply_defaults
|
|
286
|
+
|
|
287
|
+
# 1. HALT check — the shim is expected to have sourced halt-check.sh
|
|
288
|
+
# and called `check_halt` BEFORE sourcing this lib, so REA_ROOT is
|
|
289
|
+
# already set. We just use it.
|
|
290
|
+
: "${REA_ROOT:?shim-runtime: REA_ROOT must be set (source halt-check.sh + call check_halt first)}"
|
|
291
|
+
proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
|
|
292
|
+
|
|
293
|
+
# 2. Capture stdin once.
|
|
294
|
+
INPUT=$(cat)
|
|
295
|
+
|
|
296
|
+
# 3. Relevance pre-gate. If the shim defined `shim_is_relevant`, call it.
|
|
297
|
+
if declare -F shim_is_relevant >/dev/null 2>&1; then
|
|
298
|
+
if ! shim_is_relevant; then
|
|
299
|
+
exit 0
|
|
300
|
+
fi
|
|
301
|
+
fi
|
|
302
|
+
|
|
303
|
+
# 4. Resolve CLI.
|
|
304
|
+
shim_resolve_cli
|
|
305
|
+
|
|
306
|
+
# 5. Sandbox check (when CLI was resolved). On failure clear REA_ARGV
|
|
307
|
+
# + stash the reason so the eventual CLI-required branch can emit
|
|
308
|
+
# the correct banner. Running the sandbox check BEFORE the policy
|
|
309
|
+
# short-circuit prevents an unsandboxed CLI from being invoked by
|
|
310
|
+
# Tier-1 of the policy reader (0.37.0 codex round-2 P1: applies to
|
|
311
|
+
# shims like attribution-advisory whose policy_short_circuit may
|
|
312
|
+
# use `policy_reader_get`).
|
|
313
|
+
#
|
|
314
|
+
# Advisory-tier: a sandbox failure exits 0 with the skip banner —
|
|
315
|
+
# nothing to enforce for nudges. Blocking-tier: deferred to the
|
|
316
|
+
# CLI-required branch below so we emit ONE banner per refusal
|
|
317
|
+
# (instead of double-emitting sandbox + cli-missing).
|
|
318
|
+
local sandbox_result=""
|
|
319
|
+
local sandbox_failed=0
|
|
320
|
+
local node_missing=0
|
|
321
|
+
if [ "${#REA_ARGV[@]}" -gt 0 ]; then
|
|
322
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
323
|
+
# 0.38.1 round-2 P2 fix: pre-fix this branch exited 0/2 IMMEDIATELY
|
|
324
|
+
# without ever calling shim_policy_short_circuit, so a blocking-
|
|
325
|
+
# tier shim whose policy said "disabled" still refused when node
|
|
326
|
+
# was absent (which contradicts the pre-port body's no-op-on-
|
|
327
|
+
# disabled posture). Clear REA_ARGV here so Tier 1 (rea CLI)
|
|
328
|
+
# can't fire — the policy reader degrades to Tier 2 (python3) /
|
|
329
|
+
# Tier 3 (awk), neither of which needs node. Track node-missing
|
|
330
|
+
# separately so the CLI-required branch below can emit the right
|
|
331
|
+
# banner if the policy did NOT short-circuit us out.
|
|
332
|
+
node_missing=1
|
|
333
|
+
REA_ARGV=()
|
|
334
|
+
else
|
|
335
|
+
sandbox_result=$(shim_sandbox_check "$RESOLVED_CLI_PATH" "$proj" "$SHIM_ENFORCE_CLI_SHAPE")
|
|
336
|
+
if [ "$sandbox_result" != "ok" ]; then
|
|
337
|
+
sandbox_failed=1
|
|
338
|
+
if [ "$SHIM_FAIL_OPEN" -eq 1 ]; then
|
|
339
|
+
shim_emit_sandbox_skip_banner "$sandbox_result"
|
|
340
|
+
exit 0
|
|
341
|
+
fi
|
|
342
|
+
# Blocking-tier: clear REA_ARGV so Tier-1 policy reads (in
|
|
343
|
+
# shim_policy_short_circuit) degrade to Tier 2 / Tier 3 instead
|
|
344
|
+
# of invoking the untrusted CLI.
|
|
345
|
+
REA_ARGV=()
|
|
346
|
+
fi
|
|
347
|
+
fi
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
# 6. Policy short-circuit. Runs BEFORE the CLI-missing / node-missing
|
|
351
|
+
# banners so a shim whose policy says "disabled" exits 0 cleanly
|
|
352
|
+
# even when the CLI is unbuilt OR node is absent (matches the
|
|
353
|
+
# pre-port body's no-op-on-disabled posture). The policy reader's
|
|
354
|
+
# 4-tier ladder produces correct answers when REA_ARGV is empty:
|
|
355
|
+
# falls back to Tier 2 python3 if available, or Tier 3 awk
|
|
356
|
+
# (block-form only) otherwise.
|
|
357
|
+
if declare -F shim_policy_short_circuit >/dev/null 2>&1; then
|
|
358
|
+
if shim_policy_short_circuit; then
|
|
359
|
+
exit 0
|
|
360
|
+
fi
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
# 6b. node-missing fail branch — only fires if shim_policy_short_circuit
|
|
364
|
+
# did NOT exit us out above. Emits the dedicated node-missing
|
|
365
|
+
# banner for blocking-tier; advisory-tier exits 0 silently.
|
|
366
|
+
if [ "$node_missing" -eq 1 ]; then
|
|
367
|
+
if [ "$SHIM_FAIL_OPEN" -eq 1 ]; then
|
|
368
|
+
exit 0
|
|
369
|
+
fi
|
|
370
|
+
shim_emit_node_missing_banner
|
|
371
|
+
exit 2
|
|
372
|
+
fi
|
|
373
|
+
|
|
374
|
+
# 7. CLI-required branch. If REA_ARGV is empty either (a) the CLI
|
|
375
|
+
# wasn't installed/built, OR (b) the sandbox check failed and we
|
|
376
|
+
# cleared it above. Distinguish.
|
|
377
|
+
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
378
|
+
if [ "$sandbox_failed" -eq 1 ]; then
|
|
379
|
+
shim_emit_sandbox_failure_banner "$sandbox_result"
|
|
380
|
+
exit 2
|
|
381
|
+
fi
|
|
382
|
+
if declare -F shim_cli_missing_relevant >/dev/null 2>&1; then
|
|
383
|
+
if ! shim_cli_missing_relevant; then
|
|
384
|
+
# CLI missing AND payload is not relevant per shim's keyword
|
|
385
|
+
# scan — the pre-port bash body would have allowed this.
|
|
386
|
+
exit 0
|
|
387
|
+
fi
|
|
388
|
+
fi
|
|
389
|
+
# Either no callback defined OR the callback said "yes, relevant".
|
|
390
|
+
if [ "$SHIM_FAIL_OPEN" -eq 1 ]; then
|
|
391
|
+
# Advisory tier — drop the gate silently. No banner; advisory
|
|
392
|
+
# hooks are nudges, not security claims.
|
|
393
|
+
exit 0
|
|
394
|
+
fi
|
|
395
|
+
shim_emit_cli_missing_banner
|
|
396
|
+
exit 2
|
|
397
|
+
fi
|
|
398
|
+
|
|
399
|
+
# 8. Version probe (skipped when SHIM_SKIP_VERSION_PROBE=1, used by
|
|
400
|
+
# delegation-capture whose pre-port body had no probe — a stale
|
|
401
|
+
# CLI drops the signal silently rather than spamming the operator
|
|
402
|
+
# on every Agent/Skill dispatch).
|
|
403
|
+
if [ "$SHIM_SKIP_VERSION_PROBE" -eq 0 ]; then
|
|
404
|
+
local probe_out probe_status
|
|
405
|
+
probe_out=$("${REA_ARGV[@]}" hook "$SHIM_NAME" --help 2>&1)
|
|
406
|
+
probe_status=$?
|
|
407
|
+
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e "$SHIM_NAME"; then
|
|
408
|
+
if [ "$SHIM_FAIL_OPEN" -eq 1 ]; then
|
|
409
|
+
shim_emit_version_skew_banner_advisory
|
|
410
|
+
exit 0
|
|
411
|
+
fi
|
|
412
|
+
shim_emit_version_skew_banner_blocking
|
|
413
|
+
exit 2
|
|
414
|
+
fi
|
|
415
|
+
fi
|
|
416
|
+
|
|
417
|
+
# 9. Forward stdin.
|
|
418
|
+
if declare -F shim_forward >/dev/null 2>&1; then
|
|
419
|
+
shim_forward
|
|
420
|
+
else
|
|
421
|
+
shim_default_forward
|
|
422
|
+
fi
|
|
423
|
+
}
|
|
@@ -1,116 +1,24 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# PostToolUse hook: architecture-review-gate.sh
|
|
3
3
|
# 0.33.0+ — Node-binary shim for `rea hook architecture-review-gate`.
|
|
4
|
+
# 0.38.0+ — migrated to `_lib/shim-runtime.sh` (shared runtime).
|
|
4
5
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# index.ts`.
|
|
9
|
-
#
|
|
10
|
-
# Behavioral contract is preserved byte-for-byte: ALWAYS exit 0
|
|
11
|
-
# (advisory-only) except under HALT (exit 2). The hook fires for ALL
|
|
12
|
-
# Write/Edit PostToolUse events, but the Node body short-circuits to
|
|
13
|
-
# exit 0 when patterns are unset/empty — so the cost of running the
|
|
14
|
-
# CLI on every write is bounded.
|
|
15
|
-
#
|
|
16
|
-
# # CLI-resolution trust boundary
|
|
17
|
-
#
|
|
18
|
-
# Realpath sandbox check + version probe. Same shape as the 0.32.0
|
|
19
|
-
# pilots.
|
|
20
|
-
#
|
|
21
|
-
# # Fail-OPEN posture
|
|
22
|
-
#
|
|
23
|
-
# architecture-review-gate is ADVISORY-only — the pre-0.33.0 bash body
|
|
24
|
-
# never refused (exit 0 only). The early-exit branches (CLI missing,
|
|
25
|
-
# node missing, sandbox failed, version skew) all exit 0 silently
|
|
26
|
-
# because there is nothing to "preserve protection" for. The HALT
|
|
27
|
-
# check is the only path to exit 2.
|
|
6
|
+
# Advisory-tier: ALWAYS exit 0 (except HALT). Fires on every Write/
|
|
7
|
+
# Edit PostToolUse; the Node body short-circuits when policy patterns
|
|
8
|
+
# are unset/empty so the cost on the hot path is bounded.
|
|
28
9
|
|
|
29
10
|
set -uo pipefail
|
|
30
11
|
|
|
31
|
-
# 1. HALT check.
|
|
32
12
|
# shellcheck source=_lib/halt-check.sh
|
|
33
13
|
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
34
14
|
check_halt
|
|
35
15
|
REA_ROOT=$(rea_root)
|
|
36
16
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
# policy, check patterns array, prefix-match) is well under the
|
|
42
|
-
# cost of a sandbox/probe pair. Capture stdin once.
|
|
43
|
-
INPUT=$(cat)
|
|
44
|
-
|
|
45
|
-
# 3. Resolve the rea CLI. Advisory-tier: exit 0 silently on missing
|
|
46
|
-
# CLI — nothing to enforce.
|
|
47
|
-
REA_ARGV=()
|
|
48
|
-
RESOLVED_CLI_PATH=""
|
|
49
|
-
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
50
|
-
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
51
|
-
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
52
|
-
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
53
|
-
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
54
|
-
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
55
|
-
fi
|
|
56
|
-
|
|
57
|
-
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
58
|
-
exit 0
|
|
59
|
-
fi
|
|
60
|
-
|
|
61
|
-
# 4. Realpath sandbox check. Advisory-tier: exit 0 silently on
|
|
62
|
-
# sandbox failure (with a single-line breadcrumb to stderr).
|
|
63
|
-
if ! command -v node >/dev/null 2>&1; then
|
|
64
|
-
exit 0
|
|
65
|
-
fi
|
|
66
|
-
|
|
67
|
-
sandbox_check=$(node -e '
|
|
68
|
-
const fs = require("fs");
|
|
69
|
-
const path = require("path");
|
|
70
|
-
const cli = process.argv[1];
|
|
71
|
-
const projDir = process.argv[2];
|
|
72
|
-
let real, realProj;
|
|
73
|
-
try { real = fs.realpathSync(cli); } catch (e) {
|
|
74
|
-
process.stdout.write("bad:realpath"); process.exit(1);
|
|
75
|
-
}
|
|
76
|
-
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
77
|
-
process.stdout.write("bad:realpath-proj"); process.exit(1);
|
|
78
|
-
}
|
|
79
|
-
const sep = path.sep;
|
|
80
|
-
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
81
|
-
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
82
|
-
process.stdout.write("bad:cli-escapes-project"); process.exit(1);
|
|
83
|
-
}
|
|
84
|
-
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
85
|
-
let found = false;
|
|
86
|
-
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
87
|
-
const pj = path.join(cur, "package.json");
|
|
88
|
-
if (fs.existsSync(pj)) {
|
|
89
|
-
try {
|
|
90
|
-
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
91
|
-
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
92
|
-
} catch (e) { /* keep walking */ }
|
|
93
|
-
}
|
|
94
|
-
cur = path.dirname(cur);
|
|
95
|
-
}
|
|
96
|
-
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
97
|
-
process.stdout.write("ok");
|
|
98
|
-
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
99
|
-
|
|
100
|
-
if [ "$sandbox_check" != "ok" ]; then
|
|
101
|
-
printf 'rea: architecture-review-gate skipped (sandbox check: %s)\n' "$sandbox_check" >&2
|
|
102
|
-
exit 0
|
|
103
|
-
fi
|
|
104
|
-
|
|
105
|
-
# 5. Version-probe. Advisory-tier: exit 0 on probe failure.
|
|
106
|
-
probe_out=$("${REA_ARGV[@]}" hook architecture-review-gate --help 2>&1)
|
|
107
|
-
probe_status=$?
|
|
108
|
-
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'architecture-review-gate'; then
|
|
109
|
-
printf 'rea: this shim requires the `rea hook architecture-review-gate` subcommand (introduced in 0.33.0).\n' >&2
|
|
110
|
-
printf 'Run `pnpm install` (or `npm install`) to sync the CLI; falling through silently.\n' >&2
|
|
111
|
-
exit 0
|
|
112
|
-
fi
|
|
17
|
+
SHIM_NAME="architecture-review-gate"
|
|
18
|
+
SHIM_INTRODUCED_IN="0.33.0"
|
|
19
|
+
SHIM_FAIL_OPEN=1
|
|
20
|
+
SHIM_REFUSAL_NOUN="the architecture-review advisory"
|
|
113
21
|
|
|
114
|
-
#
|
|
115
|
-
|
|
116
|
-
|
|
22
|
+
# shellcheck source=_lib/shim-runtime.sh
|
|
23
|
+
source "$(dirname "$0")/_lib/shim-runtime.sh"
|
|
24
|
+
shim_run
|