@bookedsolid/rea 0.36.0 → 0.38.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/hooks/_lib/policy-reader.sh +948 -0
- package/hooks/_lib/shim-runtime.sh +405 -0
- package/hooks/architecture-review-gate.sh +11 -103
- package/hooks/attribution-advisory.sh +43 -155
- package/hooks/blocked-paths-bash-gate.sh +35 -149
- package/hooks/blocked-paths-enforcer.sh +35 -140
- 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 +191 -396
- package/hooks/pr-issue-link-gate.sh +16 -118
- package/hooks/protected-paths-bash-gate.sh +57 -160
- package/hooks/secret-scanner.sh +90 -213
- package/hooks/security-disclosure-gate.sh +32 -155
- package/hooks/settings-protection.sh +56 -179
- package/package.json +1 -1
- package/templates/_lib_policy-reader.dogfood-staged.sh +948 -0
- package/templates/_lib_shim-runtime.dogfood-staged.sh +405 -0
- package/templates/architecture-review-gate.dogfood-staged.sh +11 -103
- package/templates/attribution-advisory.dogfood-staged.sh +43 -155
- package/templates/blocked-paths-bash-gate.dogfood-staged.sh +35 -149
- package/templates/blocked-paths-enforcer.dogfood-staged.sh +35 -140
- 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 +191 -396
- package/templates/pr-issue-link-gate.dogfood-staged.sh +16 -118
- package/templates/protected-paths-bash-gate.dogfood-staged.sh +57 -160
- 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 -179
|
@@ -1,137 +1,44 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# PreToolUse hook: changeset-security-gate.sh
|
|
3
3
|
# 0.33.0+ — Node-binary shim for `rea hook changeset-security-gate`.
|
|
4
|
+
# 0.38.0+ — migrated to `_lib/shim-runtime.sh` (shared runtime).
|
|
4
5
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# migration to the parser-backed Node binary moves all of that into
|
|
6
|
+
# Blocking-tier: frontmatter validation + GHSA/CVE scan over
|
|
7
|
+
# .changeset/ writes. Full logic in
|
|
8
8
|
# `src/hooks/changeset-security-gate/index.ts`.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
# pass-through / non-changeset / valid frontmatter, exit 2 on HALT /
|
|
12
|
-
# disclosure leak / malformed frontmatter / malformed payload.
|
|
10
|
+
# # Relevance pre-gate
|
|
13
11
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# # Fail-closed posture
|
|
20
|
-
#
|
|
21
|
-
# changeset-security-gate is BLOCKING-tier — the pre-0.33.0 bash body
|
|
22
|
-
# refused on GHSA/CVE patterns and on malformed frontmatter. Early-exit
|
|
23
|
-
# branches fail closed AFTER the relevance pre-gate passes.
|
|
12
|
+
# 2026-05-15 codex round-2 P2 fix: scan `tool_input.file_path` /
|
|
13
|
+
# `tool_input.notebook_path` ONLY, not the raw JSON payload, so a
|
|
14
|
+
# Write to README.md mentioning `.changeset/` in its content body
|
|
15
|
+
# doesn't trip the fail-closed branch.
|
|
24
16
|
|
|
25
17
|
set -uo pipefail
|
|
26
18
|
|
|
27
|
-
# 1. HALT check.
|
|
28
19
|
# shellcheck source=_lib/halt-check.sh
|
|
29
20
|
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
30
21
|
check_halt
|
|
31
22
|
REA_ROOT=$(rea_root)
|
|
32
23
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
# tool_input.content blob, not in the target path. The Node body
|
|
45
|
-
# correctly filters by file_path; the shim's pre-gate must match
|
|
46
|
-
# that posture.
|
|
47
|
-
INPUT=$(cat)
|
|
48
|
-
RELEVANT=0
|
|
49
|
-
PROBE=""
|
|
50
|
-
if command -v jq >/dev/null 2>&1; then
|
|
51
|
-
PROBE=$(printf '%s' "$INPUT" | jq -r '(.tool_input.file_path // .tool_input.notebook_path // "")' 2>/dev/null || true)
|
|
52
|
-
if printf '%s' "$PROBE" | grep -qE '\.changeset/'; then
|
|
53
|
-
RELEVANT=1
|
|
24
|
+
SHIM_NAME="changeset-security-gate"
|
|
25
|
+
SHIM_INTRODUCED_IN="0.33.0"
|
|
26
|
+
SHIM_FAIL_OPEN=0
|
|
27
|
+
SHIM_REFUSAL_NOUN="changeset disclosure refusal"
|
|
28
|
+
|
|
29
|
+
shim_is_relevant() {
|
|
30
|
+
local probe
|
|
31
|
+
if command -v jq >/dev/null 2>&1; then
|
|
32
|
+
probe=$(printf '%s' "$INPUT" | jq -r '(.tool_input.file_path // .tool_input.notebook_path // "")' 2>/dev/null || true)
|
|
33
|
+
else
|
|
34
|
+
probe="$INPUT"
|
|
54
35
|
fi
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
RELEVANT=1
|
|
36
|
+
if printf '%s' "$probe" | grep -qE '\.changeset/'; then
|
|
37
|
+
return 0
|
|
58
38
|
fi
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
exit 0
|
|
62
|
-
fi
|
|
63
|
-
|
|
64
|
-
# 3. Resolve the rea CLI.
|
|
65
|
-
REA_ARGV=()
|
|
66
|
-
RESOLVED_CLI_PATH=""
|
|
67
|
-
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
68
|
-
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
69
|
-
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
70
|
-
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
71
|
-
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
72
|
-
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
73
|
-
fi
|
|
74
|
-
|
|
75
|
-
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
76
|
-
printf 'rea: changeset-security-gate cannot run — the rea CLI is not built.\n' >&2
|
|
77
|
-
printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
|
|
78
|
-
exit 2
|
|
79
|
-
fi
|
|
80
|
-
|
|
81
|
-
# 4. Realpath sandbox check.
|
|
82
|
-
if ! command -v node >/dev/null 2>&1; then
|
|
83
|
-
printf 'rea: changeset-security-gate cannot run — `node` is not on PATH.\n' >&2
|
|
84
|
-
exit 2
|
|
85
|
-
fi
|
|
86
|
-
|
|
87
|
-
sandbox_check=$(node -e '
|
|
88
|
-
const fs = require("fs");
|
|
89
|
-
const path = require("path");
|
|
90
|
-
const cli = process.argv[1];
|
|
91
|
-
const projDir = process.argv[2];
|
|
92
|
-
let real, realProj;
|
|
93
|
-
try { real = fs.realpathSync(cli); } catch (e) {
|
|
94
|
-
process.stdout.write("bad:realpath"); process.exit(1);
|
|
95
|
-
}
|
|
96
|
-
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
97
|
-
process.stdout.write("bad:realpath-proj"); process.exit(1);
|
|
98
|
-
}
|
|
99
|
-
const sep = path.sep;
|
|
100
|
-
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
101
|
-
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
102
|
-
process.stdout.write("bad:cli-escapes-project"); process.exit(1);
|
|
103
|
-
}
|
|
104
|
-
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
105
|
-
let found = false;
|
|
106
|
-
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
107
|
-
const pj = path.join(cur, "package.json");
|
|
108
|
-
if (fs.existsSync(pj)) {
|
|
109
|
-
try {
|
|
110
|
-
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
111
|
-
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
112
|
-
} catch (e) { /* keep walking */ }
|
|
113
|
-
}
|
|
114
|
-
cur = path.dirname(cur);
|
|
115
|
-
}
|
|
116
|
-
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
117
|
-
process.stdout.write("ok");
|
|
118
|
-
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
119
|
-
|
|
120
|
-
if [ "$sandbox_check" != "ok" ]; then
|
|
121
|
-
printf 'rea: changeset-security-gate FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
|
|
122
|
-
exit 2
|
|
123
|
-
fi
|
|
124
|
-
|
|
125
|
-
# 5. Version-probe.
|
|
126
|
-
probe_out=$("${REA_ARGV[@]}" hook changeset-security-gate --help 2>&1)
|
|
127
|
-
probe_status=$?
|
|
128
|
-
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'changeset-security-gate'; then
|
|
129
|
-
printf 'rea: this shim requires the `rea hook changeset-security-gate` subcommand (introduced in 0.33.0).\n' >&2
|
|
130
|
-
printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
|
|
131
|
-
printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
|
|
132
|
-
exit 2
|
|
133
|
-
fi
|
|
39
|
+
return 1
|
|
40
|
+
}
|
|
134
41
|
|
|
135
|
-
#
|
|
136
|
-
|
|
137
|
-
|
|
42
|
+
# shellcheck source=_lib/shim-runtime.sh
|
|
43
|
+
source "$(dirname "$0")/_lib/shim-runtime.sh"
|
|
44
|
+
shim_run
|
|
@@ -1,196 +1,72 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# PreToolUse hook: dangerous-bash-interceptor.sh
|
|
3
3
|
# 0.34.0+ — Node-binary shim for `rea hook dangerous-bash-interceptor`.
|
|
4
|
+
# 0.38.0+ — migrated to `_lib/shim-runtime.sh` (shared runtime).
|
|
4
5
|
#
|
|
5
|
-
# Pre-0.34.0 the gate's full body lived here as bash (414 LOC
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# the CLI and exits with whatever the CLI returns.
|
|
6
|
+
# Pre-0.34.0 the gate's full body lived here as bash (414 LOC: refusal
|
|
7
|
+
# classes H1-H17 + M1 plus their bypass-corpus regressions). Migration
|
|
8
|
+
# in `src/hooks/dangerous-bash-interceptor/index.ts`. Behavioral
|
|
9
|
+
# contract preserved byte-for-byte: exit 0 on pass-through / MEDIUM-only
|
|
10
|
+
# advisory, exit 2 on HALT / HIGH match / malformed payload.
|
|
11
11
|
#
|
|
12
|
-
#
|
|
13
|
-
# pass-through / MEDIUM-only advisory, exit 2 on HALT / HIGH rule
|
|
14
|
-
# match / malformed payload (fail-closed).
|
|
12
|
+
# # Relevance pre-gate (CLI-missing only)
|
|
15
13
|
#
|
|
16
|
-
#
|
|
14
|
+
# 0.34.0 round-7 P1 fix: substring scan over the EXTRACTED command for
|
|
15
|
+
# destructive-catalog keywords. When CLI is missing AND no keyword
|
|
16
|
+
# matches, exit 0 (the pre-port bash body would have done the same —
|
|
17
|
+
# no rule to match). When CLI is missing AND a keyword DOES match,
|
|
18
|
+
# fail closed.
|
|
17
19
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# whose `name` is `@bookedsolid/rea`. Defends against symlink-out and
|
|
22
|
-
# tarball-replacement attacks on the resolved CLI.
|
|
23
|
-
#
|
|
24
|
-
# # Fail-closed posture
|
|
25
|
-
#
|
|
26
|
-
# dangerous-bash-interceptor is the agent-runaway gate — the pre-0.34.0
|
|
27
|
-
# bash body refused destructive commands without any compiled CLI. The
|
|
28
|
-
# early-exit branches (CLI missing, node missing, sandbox failed,
|
|
29
|
-
# version skew) fail closed AFTER the relevance pre-gate passes.
|
|
30
|
-
# Irrelevant Bash calls exit 0 regardless of CLI state.
|
|
31
|
-
#
|
|
32
|
-
# # Relevance pre-gate
|
|
33
|
-
#
|
|
34
|
-
# 0.34.0 round-7 P1 fix: the pre-0.34.0 bash body refused destructive
|
|
35
|
-
# commands without any compiled CLI. The round-0 shim preserved that
|
|
36
|
-
# fail-closed-on-CLI-missing posture for ALL Bash, but that's stricter
|
|
37
|
-
# than the pre-0.34.0 body which only refused commands matching the
|
|
38
|
-
# destructive catalog. On a fresh / unbuilt install (`npx rea init`,
|
|
39
|
-
# pre-`pnpm build` checkout) the shim blocked benign Bash like `ls`,
|
|
40
|
-
# `mkdir`, `pnpm install` — defeating the install path itself.
|
|
41
|
-
#
|
|
42
|
-
# Fix: substring pre-gate over the EXTRACTED command (not raw payload —
|
|
43
|
-
# the local-review-gate round-2 lesson). When CLI is missing AND no
|
|
44
|
-
# destructive-keyword appears in the extracted command, exit 0 (the
|
|
45
|
-
# pre-0.34.0 bash body would have done the same — there's no rule to
|
|
46
|
-
# match). When CLI is missing AND a destructive-keyword DOES appear,
|
|
47
|
-
# preserve the original fail-closed posture (we'd rather refuse than
|
|
48
|
-
# silently allow a destructive command).
|
|
49
|
-
#
|
|
50
|
-
# The keyword list is coarse — it over-triggers (e.g. `git status` hits
|
|
51
|
-
# `git` substring) but that's fine: the CLI does the real evaluation
|
|
52
|
-
# and lets benign forms through. Over-trigger costs one node-spawn;
|
|
53
|
-
# under-trigger is the bypass we MUST avoid. Same posture as the
|
|
54
|
-
# 0.32.0 secret-scanner `gh issue create` substring fix.
|
|
20
|
+
# Keywords cover every rule head H1-H17 + M1. Coarse by design — the
|
|
21
|
+
# CLI does the real per-rule evaluation when reachable; over-trigger
|
|
22
|
+
# costs one node-spawn, under-trigger is the bypass we MUST avoid.
|
|
55
23
|
|
|
56
24
|
set -uo pipefail
|
|
57
25
|
|
|
58
|
-
# 1. HALT check.
|
|
59
26
|
# shellcheck source=_lib/halt-check.sh
|
|
60
27
|
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
61
28
|
check_halt
|
|
62
29
|
REA_ROOT=$(rea_root)
|
|
63
30
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
31
|
+
SHIM_NAME="dangerous-bash-interceptor"
|
|
32
|
+
SHIM_INTRODUCED_IN="0.34.0"
|
|
33
|
+
SHIM_FAIL_OPEN=0
|
|
34
|
+
SHIM_REFUSAL_NOUN="destructive-command refusal"
|
|
68
35
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
RESOLVED_CLI_PATH=""
|
|
72
|
-
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
73
|
-
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
74
|
-
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
75
|
-
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
76
|
-
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
77
|
-
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
78
|
-
fi
|
|
79
|
-
|
|
80
|
-
# 3b. Relevance pre-gate (round-7 P1). Only used when the CLI is
|
|
81
|
-
# missing — when present, every Bash call goes through the CLI.
|
|
82
|
-
# Extract the command string from the payload, then substring-scan
|
|
83
|
-
# it for destructive-catalog keywords. Mirrors the H1-H17 + M1
|
|
84
|
-
# rule heads.
|
|
85
|
-
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
86
|
-
CLI_MISSING_CMD=""
|
|
36
|
+
shim_cli_missing_relevant() {
|
|
37
|
+
local cli_missing_cmd=""
|
|
87
38
|
if command -v jq >/dev/null 2>&1; then
|
|
88
|
-
|
|
89
|
-
# a non-string value (object/number) doesn't blow up jq.
|
|
90
|
-
CLI_MISSING_CMD=$(printf '%s' "$INPUT" | jq -r '
|
|
39
|
+
cli_missing_cmd=$(printf '%s' "$INPUT" | jq -r '
|
|
91
40
|
(.tool_input.command // "") | tostring
|
|
92
41
|
' 2>/dev/null || true)
|
|
93
42
|
else
|
|
94
43
|
# jq missing — fall back to scanning the raw payload. Over-trigger
|
|
95
44
|
# by design (the CLI is the source of truth; this is fail-closed
|
|
96
|
-
# only when keywords match).
|
|
97
|
-
|
|
98
|
-
CLI_MISSING_CMD="$INPUT"
|
|
45
|
+
# only when keywords match).
|
|
46
|
+
cli_missing_cmd="$INPUT"
|
|
99
47
|
fi
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
# Empty command (or non-Bash payload). The pre-0.34.0 bash body
|
|
104
|
-
# would have exited 0 here — no command, no rule match.
|
|
105
|
-
exit 0
|
|
48
|
+
if [ -z "$cli_missing_cmd" ]; then
|
|
49
|
+
# Empty/non-Bash payload → pre-port body would have exit 0'd.
|
|
50
|
+
return 1
|
|
106
51
|
fi
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
*"
|
|
112
|
-
*"
|
|
113
|
-
*"
|
|
114
|
-
*"
|
|
115
|
-
*"
|
|
116
|
-
*"
|
|
117
|
-
*"
|
|
118
|
-
*"
|
|
119
|
-
*"
|
|
120
|
-
*"
|
|
121
|
-
*"core.hooksPath"*|*"core.hookspath"*) CLI_MISSING_RELEVANT=1 ;;
|
|
122
|
-
*"npm "*|*"pnpm "*|*"yarn "*) CLI_MISSING_RELEVANT=1 ;;
|
|
123
|
-
*"--no-verify"*|*"--force"*) CLI_MISSING_RELEVANT=1 ;;
|
|
52
|
+
case "$cli_missing_cmd" in
|
|
53
|
+
*"git "*) return 0 ;;
|
|
54
|
+
*"git "*) return 0 ;;
|
|
55
|
+
*"rm "*|*"rm "*) return 0 ;;
|
|
56
|
+
*"psql"*|*"pgcli"*) return 0 ;;
|
|
57
|
+
*"DROP "*|*"DROP "*) return 0 ;;
|
|
58
|
+
*"kill "*|*"kill "*|*"killall"*) return 0 ;;
|
|
59
|
+
*"HUSKY="*) return 0 ;;
|
|
60
|
+
*"curl"*|*"wget"*) return 0 ;;
|
|
61
|
+
*"REA_BYPASS"*) return 0 ;;
|
|
62
|
+
*"alias "*|*"function "*) return 0 ;;
|
|
63
|
+
*"core.hooksPath"*|*"core.hookspath"*) return 0 ;;
|
|
64
|
+
*"npm "*|*"pnpm "*|*"yarn "*) return 0 ;;
|
|
65
|
+
*"--no-verify"*|*"--force"*) return 0 ;;
|
|
124
66
|
esac
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
# bash body would have allowed this — exit 0 to preserve install-
|
|
128
|
-
# path / unbuilt-checkout workflows.
|
|
129
|
-
exit 0
|
|
130
|
-
fi
|
|
131
|
-
# Keyword matched. Preserve fail-closed posture — the pre-0.34.0
|
|
132
|
-
# bash body would have evaluated this command and potentially refused.
|
|
133
|
-
printf 'rea: dangerous-bash-interceptor cannot run — the rea CLI is not built.\n' >&2
|
|
134
|
-
printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
|
|
135
|
-
printf 'This shim fails closed because the pre-0.34.0 bash body enforced destructive-command refusal without a CLI.\n' >&2
|
|
136
|
-
exit 2
|
|
137
|
-
fi
|
|
138
|
-
|
|
139
|
-
# 4. Realpath sandbox check.
|
|
140
|
-
if ! command -v node >/dev/null 2>&1; then
|
|
141
|
-
printf 'rea: dangerous-bash-interceptor cannot run — `node` is not on PATH.\n' >&2
|
|
142
|
-
printf 'Install Node 22+ (engines.node) to restore destructive-command refusal.\n' >&2
|
|
143
|
-
exit 2
|
|
144
|
-
fi
|
|
145
|
-
|
|
146
|
-
sandbox_check=$(node -e '
|
|
147
|
-
const fs = require("fs");
|
|
148
|
-
const path = require("path");
|
|
149
|
-
const cli = process.argv[1];
|
|
150
|
-
const projDir = process.argv[2];
|
|
151
|
-
let real, realProj;
|
|
152
|
-
try { real = fs.realpathSync(cli); } catch (e) {
|
|
153
|
-
process.stdout.write("bad:realpath"); process.exit(1);
|
|
154
|
-
}
|
|
155
|
-
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
156
|
-
process.stdout.write("bad:realpath-proj"); process.exit(1);
|
|
157
|
-
}
|
|
158
|
-
const sep = path.sep;
|
|
159
|
-
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
160
|
-
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
161
|
-
process.stdout.write("bad:cli-escapes-project"); process.exit(1);
|
|
162
|
-
}
|
|
163
|
-
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
164
|
-
let found = false;
|
|
165
|
-
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
166
|
-
const pj = path.join(cur, "package.json");
|
|
167
|
-
if (fs.existsSync(pj)) {
|
|
168
|
-
try {
|
|
169
|
-
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
170
|
-
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
171
|
-
} catch (e) { /* keep walking */ }
|
|
172
|
-
}
|
|
173
|
-
cur = path.dirname(cur);
|
|
174
|
-
}
|
|
175
|
-
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
176
|
-
process.stdout.write("ok");
|
|
177
|
-
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
178
|
-
|
|
179
|
-
if [ "$sandbox_check" != "ok" ]; then
|
|
180
|
-
printf 'rea: dangerous-bash-interceptor FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
|
|
181
|
-
exit 2
|
|
182
|
-
fi
|
|
183
|
-
|
|
184
|
-
# 5. Version-probe.
|
|
185
|
-
probe_out=$("${REA_ARGV[@]}" hook dangerous-bash-interceptor --help 2>&1)
|
|
186
|
-
probe_status=$?
|
|
187
|
-
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'dangerous-bash-interceptor'; then
|
|
188
|
-
printf 'rea: this shim requires the `rea hook dangerous-bash-interceptor` subcommand (introduced in 0.34.0).\n' >&2
|
|
189
|
-
printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
|
|
190
|
-
printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
|
|
191
|
-
exit 2
|
|
192
|
-
fi
|
|
67
|
+
return 1
|
|
68
|
+
}
|
|
193
69
|
|
|
194
|
-
#
|
|
195
|
-
|
|
196
|
-
|
|
70
|
+
# shellcheck source=_lib/shim-runtime.sh
|
|
71
|
+
source "$(dirname "$0")/_lib/shim-runtime.sh"
|
|
72
|
+
shim_run
|
|
@@ -1,162 +1,44 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# PostToolUse hook: delegation-advisory.sh
|
|
3
3
|
# 0.31.0+ — delegation-telemetry completion (the *nudge*).
|
|
4
|
+
# 0.38.0+ — migrated to `_lib/shim-runtime.sh` (shared runtime).
|
|
4
5
|
#
|
|
5
|
-
# Fires AFTER every write-class tool call.
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# 0.29.0 shipped the delegation-telemetry *observability* layer
|
|
11
|
-
# (`delegation-capture.sh` + `rea audit specialists`). 0.31.0 closes the
|
|
12
|
-
# loop with the *nudge*: `rea hook delegation-advisory` maintains a
|
|
13
|
-
# per-session write-class counter and, the FIRST time that counter
|
|
14
|
-
# crosses `policy.delegation_advisory.threshold` while the session has
|
|
15
|
-
# recorded zero real delegation signals, prints a one-time stderr
|
|
16
|
-
# advisory ("this session has done a lot of work without delegating to
|
|
17
|
-
# a specialist").
|
|
18
|
-
#
|
|
19
|
-
# # Advisory, never gating
|
|
20
|
-
#
|
|
21
|
-
# This hook ALWAYS exits 0 (under normal operation). The advisory is a
|
|
22
|
-
# nudge — it never blocks a tool call. The ONLY non-zero exit is 2
|
|
23
|
-
# under HALT, to keep the kill-switch contract uniform with the rest of
|
|
24
|
-
# the hook tree.
|
|
6
|
+
# Fires AFTER every write-class tool call. ALWAYS exits 0 except under
|
|
7
|
+
# HALT. The CLI maintains a per-session write-class counter; first
|
|
8
|
+
# crossing of `policy.delegation_advisory.threshold` with zero recorded
|
|
9
|
+
# delegation signals prints a one-time stderr advisory.
|
|
25
10
|
#
|
|
26
11
|
# # Synchronous, NOT detached
|
|
27
12
|
#
|
|
28
|
-
# Unlike
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
# advisory text must reach the operator's stderr before the hook
|
|
32
|
-
# returns — backgrounding it would race the hook's own exit and the
|
|
33
|
-
# message could be lost or interleaved with the next tool call's
|
|
34
|
-
# output. The CLI is cheap on the hot path: below the threshold it
|
|
35
|
-
# only bumps an integer counter file and exits, no audit scan, no
|
|
36
|
-
# roster discovery.
|
|
13
|
+
# Unlike delegation-capture.sh, this hook runs the CLI synchronously
|
|
14
|
+
# so the advisory text reaches stderr BEFORE the hook returns. The
|
|
15
|
+
# default `shim_default_forward` already does this — no override needed.
|
|
37
16
|
#
|
|
38
|
-
# #
|
|
17
|
+
# # No version probe (codex round-1 P2)
|
|
39
18
|
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
# INSIDE realpath(CLAUDE_PROJECT_DIR) with an ancestor package.json
|
|
50
|
-
# declaring `@bookedsolid/rea`.
|
|
51
|
-
#
|
|
52
|
-
# Exit codes:
|
|
53
|
-
# 0 — always (under normal operation). Disabled-by-policy,
|
|
54
|
-
# below-threshold, already-fired, just-fired — all exit 0.
|
|
55
|
-
# 2 — HALT active.
|
|
19
|
+
# SHIM_SKIP_VERSION_PROBE=1: this hook runs on EVERY write-class
|
|
20
|
+
# PostToolUse (matcher `Bash|Edit|Write|MultiEdit|NotebookEdit`), so
|
|
21
|
+
# the hot path is hot. The pre-port body had NO version probe — it
|
|
22
|
+
# went straight from sandbox check to forward. Adding a probe doubles
|
|
23
|
+
# Node startups on every tool call (`--help` invocation + the real
|
|
24
|
+
# forward), which noticeably regresses interactive latency during
|
|
25
|
+
# long sessions. Skip the probe; a stale CLI without the subcommand
|
|
26
|
+
# will still fail at forward time, which is fine for an advisory-tier
|
|
27
|
+
# nudge (the operator will run `pnpm install` to fix it).
|
|
56
28
|
|
|
57
29
|
set -uo pipefail
|
|
58
30
|
|
|
59
|
-
# 1. HALT check. Even though this hook is advisory, refusing to run
|
|
60
|
-
# while frozen matches the rest of the hook tree and keeps the
|
|
61
|
-
# kill-switch contract uniform.
|
|
62
31
|
# shellcheck source=_lib/halt-check.sh
|
|
63
32
|
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
64
33
|
check_halt
|
|
65
34
|
REA_ROOT=$(rea_root)
|
|
66
35
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
# posture; the nudge is a convenience, not a security claim.
|
|
73
|
-
REA_ARGV=()
|
|
74
|
-
RESOLVED_CLI_PATH=""
|
|
75
|
-
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
76
|
-
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
77
|
-
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
78
|
-
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
79
|
-
# rea repo dogfood: the project IS @bookedsolid/rea.
|
|
80
|
-
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
81
|
-
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
82
|
-
fi
|
|
83
|
-
|
|
84
|
-
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
85
|
-
# No rea CLI in scope — drop the advisory silently. This is the
|
|
86
|
-
# expected state during bootstrap (consumer ran `rea init` but
|
|
87
|
-
# hasn't installed the npm package yet) or in non-rea repos. A
|
|
88
|
-
# noisy stderr warning here would fire on every write-class tool
|
|
89
|
-
# call and drown legitimate output.
|
|
90
|
-
exit 0
|
|
91
|
-
fi
|
|
92
|
-
|
|
93
|
-
# 3. Realpath sandbox check — mirrors delegation-capture.sh §3 and
|
|
94
|
-
# protected-paths-bash-gate.sh §6. The resolved CLI MUST live inside
|
|
95
|
-
# realpath(CLAUDE_PROJECT_DIR) AND have an ancestor package.json
|
|
96
|
-
# declaring `@bookedsolid/rea` as its `name`. Catches symlink-out
|
|
97
|
-
# attacks where an attacker writes
|
|
98
|
-
# node_modules/@bookedsolid/rea → /tmp/forged-tree.
|
|
99
|
-
if ! command -v node >/dev/null 2>&1; then
|
|
100
|
-
# Node not on PATH — we can't verify the CLI shape. Fail safe by
|
|
101
|
-
# dropping the advisory (it is not a security claim; the rest of
|
|
102
|
-
# the Bash gate suite refuses on this path).
|
|
103
|
-
exit 0
|
|
104
|
-
fi
|
|
105
|
-
|
|
106
|
-
sandbox_check=$(node -e '
|
|
107
|
-
const fs = require("fs");
|
|
108
|
-
const path = require("path");
|
|
109
|
-
const cli = process.argv[1];
|
|
110
|
-
const projDir = process.argv[2];
|
|
111
|
-
let real, realProj;
|
|
112
|
-
try { real = fs.realpathSync(cli); } catch (e) {
|
|
113
|
-
process.stdout.write("bad:realpath");
|
|
114
|
-
process.exit(1);
|
|
115
|
-
}
|
|
116
|
-
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
117
|
-
process.stdout.write("bad:realpath-proj");
|
|
118
|
-
process.exit(1);
|
|
119
|
-
}
|
|
120
|
-
const sep = path.sep;
|
|
121
|
-
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
122
|
-
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
123
|
-
process.stdout.write("bad:cli-escapes-project");
|
|
124
|
-
process.exit(1);
|
|
125
|
-
}
|
|
126
|
-
// Walk up looking for package.json with the protected name.
|
|
127
|
-
let cur = path.dirname(path.dirname(path.dirname(real))); // pkg root
|
|
128
|
-
let found = false;
|
|
129
|
-
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
130
|
-
const pj = path.join(cur, "package.json");
|
|
131
|
-
if (fs.existsSync(pj)) {
|
|
132
|
-
try {
|
|
133
|
-
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
134
|
-
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
135
|
-
} catch (e) { /* keep walking */ }
|
|
136
|
-
}
|
|
137
|
-
cur = path.dirname(cur);
|
|
138
|
-
}
|
|
139
|
-
if (!found) {
|
|
140
|
-
process.stdout.write("bad:no-rea-pkg-json");
|
|
141
|
-
process.exit(1);
|
|
142
|
-
}
|
|
143
|
-
process.stdout.write("ok");
|
|
144
|
-
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
145
|
-
|
|
146
|
-
if [ "$sandbox_check" != "ok" ]; then
|
|
147
|
-
# CLI failed the sandbox check — silent drop. The forensic
|
|
148
|
-
# breadcrumb in stderr is intentional but trimmed so this doesn't
|
|
149
|
-
# become spammy on every tool call.
|
|
150
|
-
printf 'rea: delegation-advisory skipped (sandbox check: %s)\n' "$sandbox_check" >&2
|
|
151
|
-
exit 0
|
|
152
|
-
fi
|
|
36
|
+
SHIM_NAME="delegation-advisory"
|
|
37
|
+
SHIM_INTRODUCED_IN="0.31.0"
|
|
38
|
+
SHIM_FAIL_OPEN=1
|
|
39
|
+
SHIM_SKIP_VERSION_PROBE=1
|
|
40
|
+
SHIM_REFUSAL_NOUN="the delegation-advisory nudge"
|
|
153
41
|
|
|
154
|
-
#
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
# CLI resolves the same REA_ROOT this shim did. The CLI's own exit
|
|
158
|
-
# code is the hook's exit code: 0 normally, 2 under HALT (the CLI
|
|
159
|
-
# re-checks HALT itself for defense-in-depth).
|
|
160
|
-
INPUT=$(cat)
|
|
161
|
-
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook delegation-advisory
|
|
162
|
-
exit $?
|
|
42
|
+
# shellcheck source=_lib/shim-runtime.sh
|
|
43
|
+
source "$(dirname "$0")/_lib/shim-runtime.sh"
|
|
44
|
+
shim_run
|