@bookedsolid/rea 0.37.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/shim-runtime.sh +405 -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 +405 -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
|
@@ -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
|
|
@@ -1,158 +1,52 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# PreToolUse hook: delegation-capture.sh
|
|
3
3
|
# 0.29.0+ — delegation-telemetry MVP.
|
|
4
|
+
# 0.38.0+ — migrated to `_lib/shim-runtime.sh` (shared runtime).
|
|
4
5
|
#
|
|
5
|
-
# Fires BEFORE every `Agent` or `Skill` tool call.
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# Fires BEFORE every `Agent` or `Skill` tool call. Pipes the payload to
|
|
7
|
+
# `rea hook delegation-signal --detach`, BACKGROUNDED so the hook
|
|
8
|
+
# returns instantly even when the CLI's startup takes a few ms. The
|
|
9
|
+
# signal is OBSERVATIONAL — never gates tool dispatch.
|
|
8
10
|
#
|
|
9
|
-
#
|
|
10
|
-
# latency budget is ~50ms even when the audit chain is under
|
|
11
|
-
# cross-process contention, because the audit append runs in the
|
|
12
|
-
# background (via `&`) and the CLI subcommand itself only validates
|
|
13
|
-
# the payload before forking the writer.
|
|
11
|
+
# Matcher: `Agent|Skill` (NOT `Task|Skill`).
|
|
14
12
|
#
|
|
15
|
-
#
|
|
16
|
-
# are the unrelated todo-list tools and MUST NOT match).
|
|
13
|
+
# # CLI subcommand differs from SHIM_NAME
|
|
17
14
|
#
|
|
18
|
-
#
|
|
15
|
+
# The forward target is `rea hook delegation-signal`, not `rea hook
|
|
16
|
+
# delegation-capture` — the hook name and the CLI subcommand differ.
|
|
17
|
+
# We use shim_forward to invoke the correct subcommand.
|
|
19
18
|
#
|
|
20
|
-
#
|
|
21
|
-
# binary via `$REA_ROOT/node_modules/.bin/rea` then PATH-walked
|
|
22
|
-
# `command -v rea`. Either path was attacker-influenced in a consumer
|
|
23
|
-
# repo with a forged `node_modules/.bin/rea` symlink or a
|
|
24
|
-
# PATH-prepended fake `rea` binary — giving attacker-controlled code
|
|
25
|
-
# execution on every Agent/Skill dispatch.
|
|
19
|
+
# # No version probe
|
|
26
20
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
# published artifact)
|
|
31
|
-
# 2. dist/cli/index.js under CLAUDE_PROJECT_DIR (the rea repo's own
|
|
32
|
-
# dogfood install)
|
|
33
|
-
#
|
|
34
|
-
# A realpath sandbox check ensures the resolved CLI lives INSIDE
|
|
35
|
-
# realpath(CLAUDE_PROJECT_DIR) — catches symlink-out attacks.
|
|
36
|
-
#
|
|
37
|
-
# Exit codes:
|
|
38
|
-
# 0 — always (under normal operation). Failure to write the audit
|
|
39
|
-
# signal must NEVER block Claude Code's tool dispatch. Stderr
|
|
40
|
-
# breadcrumbs surface diagnostic info to the operator. HALT
|
|
41
|
-
# still exits 2 because the kill-switch contract must hold.
|
|
42
|
-
# 2 — HALT active.
|
|
21
|
+
# SHIM_SKIP_VERSION_PROBE=1: the pre-port body had no probe; a stale
|
|
22
|
+
# CLI drops the signal silently rather than emit a probe-skew banner
|
|
23
|
+
# on every Agent/Skill dispatch.
|
|
43
24
|
|
|
44
25
|
set -uo pipefail
|
|
45
26
|
|
|
46
|
-
# 1. HALT check. Even though this hook is observational, refusing to
|
|
47
|
-
# emit signals while frozen matches the rest of the hook tree and
|
|
48
|
-
# keeps the kill-switch contract uniform.
|
|
49
27
|
# shellcheck source=_lib/halt-check.sh
|
|
50
28
|
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
51
29
|
check_halt
|
|
52
30
|
REA_ROOT=$(rea_root)
|
|
53
31
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
70
|
-
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
71
|
-
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
72
|
-
# rea repo dogfood: the project IS @bookedsolid/rea.
|
|
73
|
-
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
74
|
-
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
75
|
-
fi
|
|
76
|
-
|
|
77
|
-
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
78
|
-
# No rea CLI in scope — drop the signal silently. This is the
|
|
79
|
-
# expected state during bootstrap (consumer ran `rea init` but
|
|
80
|
-
# hasn't installed the npm package yet) or in non-rea repos. A
|
|
81
|
-
# noisy stderr warning here would fire on every Agent/Skill
|
|
82
|
-
# dispatch and drown legitimate signals.
|
|
83
|
-
exit 0
|
|
84
|
-
fi
|
|
85
|
-
|
|
86
|
-
# 3. Realpath sandbox check — mirrors protected-paths-bash-gate.sh §6.
|
|
87
|
-
# The resolved CLI MUST live inside realpath(CLAUDE_PROJECT_DIR)
|
|
88
|
-
# AND have an ancestor package.json declaring `@bookedsolid/rea`
|
|
89
|
-
# as its `name`. Catches symlink-out attacks where an attacker
|
|
90
|
-
# writes node_modules/@bookedsolid/rea → /tmp/forged-tree.
|
|
91
|
-
if ! command -v node >/dev/null 2>&1; then
|
|
92
|
-
# Node not on PATH — we can't verify the CLI shape. Fail safe by
|
|
93
|
-
# dropping the signal (observability is not a security claim; the
|
|
94
|
-
# rest of the Bash gate suite refuses on this path).
|
|
32
|
+
SHIM_NAME="delegation-capture"
|
|
33
|
+
SHIM_INTRODUCED_IN="0.29.0"
|
|
34
|
+
SHIM_FAIL_OPEN=1
|
|
35
|
+
SHIM_SKIP_VERSION_PROBE=1
|
|
36
|
+
SHIM_REFUSAL_NOUN="the delegation telemetry signal"
|
|
37
|
+
|
|
38
|
+
shim_forward() {
|
|
39
|
+
# Pipe to `rea hook delegation-signal --detach`. `--detach` tells the
|
|
40
|
+
# CLI to suppress stderr; the whole pipeline is backgrounded with
|
|
41
|
+
# `& disown` so the shell hook returns instantly.
|
|
42
|
+
{
|
|
43
|
+
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook delegation-signal --detach \
|
|
44
|
+
>/dev/null 2>&1 &
|
|
45
|
+
disown 2>/dev/null || true
|
|
46
|
+
} 2>/dev/null
|
|
95
47
|
exit 0
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
sandbox_check=$(node -e '
|
|
99
|
-
const fs = require("fs");
|
|
100
|
-
const path = require("path");
|
|
101
|
-
const cli = process.argv[1];
|
|
102
|
-
const projDir = process.argv[2];
|
|
103
|
-
let real, realProj;
|
|
104
|
-
try { real = fs.realpathSync(cli); } catch (e) {
|
|
105
|
-
process.stdout.write("bad:realpath");
|
|
106
|
-
process.exit(1);
|
|
107
|
-
}
|
|
108
|
-
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
109
|
-
process.stdout.write("bad:realpath-proj");
|
|
110
|
-
process.exit(1);
|
|
111
|
-
}
|
|
112
|
-
const sep = path.sep;
|
|
113
|
-
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
114
|
-
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
115
|
-
process.stdout.write("bad:cli-escapes-project");
|
|
116
|
-
process.exit(1);
|
|
117
|
-
}
|
|
118
|
-
// Walk up looking for package.json with the protected name.
|
|
119
|
-
let cur = path.dirname(path.dirname(path.dirname(real))); // pkg root
|
|
120
|
-
let found = false;
|
|
121
|
-
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
122
|
-
const pj = path.join(cur, "package.json");
|
|
123
|
-
if (fs.existsSync(pj)) {
|
|
124
|
-
try {
|
|
125
|
-
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
126
|
-
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
127
|
-
} catch (e) { /* keep walking */ }
|
|
128
|
-
}
|
|
129
|
-
cur = path.dirname(cur);
|
|
130
|
-
}
|
|
131
|
-
if (!found) {
|
|
132
|
-
process.stdout.write("bad:no-rea-pkg-json");
|
|
133
|
-
process.exit(1);
|
|
134
|
-
}
|
|
135
|
-
process.stdout.write("ok");
|
|
136
|
-
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
137
|
-
|
|
138
|
-
if [ "$sandbox_check" != "ok" ]; then
|
|
139
|
-
# CLI failed the sandbox check — silent drop. The forensic
|
|
140
|
-
# breadcrumb in stderr is intentional but trimmed so this doesn't
|
|
141
|
-
# become spammy on every dispatch.
|
|
142
|
-
printf 'rea: delegation-capture skipped (sandbox check: %s)\n' "$sandbox_check" >&2
|
|
143
|
-
exit 0
|
|
144
|
-
fi
|
|
145
|
-
|
|
146
|
-
# 4. Read stdin and pipe to the CLI. `--detach` tells the CLI to
|
|
147
|
-
# suppress stderr output (no parent shell is listening); we ALSO
|
|
148
|
-
# background the whole pipeline with `&` and `disown` so the
|
|
149
|
-
# shell hook returns instantly even if the CLI's own startup
|
|
150
|
-
# takes a few ms.
|
|
151
|
-
INPUT=$(cat)
|
|
152
|
-
{
|
|
153
|
-
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook delegation-signal --detach \
|
|
154
|
-
>/dev/null 2>&1 &
|
|
155
|
-
disown 2>/dev/null || true
|
|
156
|
-
} 2>/dev/null
|
|
48
|
+
}
|
|
157
49
|
|
|
158
|
-
|
|
50
|
+
# shellcheck source=_lib/shim-runtime.sh
|
|
51
|
+
source "$(dirname "$0")/_lib/shim-runtime.sh"
|
|
52
|
+
shim_run
|
|
@@ -1,138 +1,46 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# PreToolUse hook: dependency-audit-gate.sh
|
|
3
3
|
# 0.33.0+ — Node-binary shim for `rea hook dependency-audit-gate`.
|
|
4
|
+
# 0.38.0+ — migrated to `_lib/shim-runtime.sh` (shared runtime).
|
|
4
5
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# `npm view` probe
|
|
8
|
-
#
|
|
6
|
+
# Blocking-tier: refuses install commands when any requested package
|
|
7
|
+
# isn't published on the registry (pre-port behavior). The full
|
|
8
|
+
# segment splitter + per-package `npm view` probe is in
|
|
9
|
+
# `src/hooks/dependency-audit-gate/index.ts`.
|
|
9
10
|
#
|
|
10
|
-
#
|
|
11
|
-
# pass-through / all-packages-verified, exit 2 on HALT / any package
|
|
12
|
-
# missing / malformed payload.
|
|
11
|
+
# # Relevance pre-gate
|
|
13
12
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# # Fail-closed posture
|
|
20
|
-
#
|
|
21
|
-
# dependency-audit-gate is BLOCKING-tier — the pre-0.33.0 bash body
|
|
22
|
-
# refused on missing packages. Early-exit branches (CLI missing,
|
|
23
|
-
# node missing, sandbox failed, version skew) fail closed AFTER the
|
|
24
|
-
# relevance pre-gate passes.
|
|
13
|
+
# 2026-05-15 codex round-2 P2 fix: scan `tool_input.command` ONLY,
|
|
14
|
+
# not the raw JSON payload. Pre-fix `git commit -m "docs: run pnpm
|
|
15
|
+
# install foo"` triggered fail-closed on fresh checkout (the regex hit
|
|
16
|
+
# the substring inside the commit-message ARG). The jq-less fallback
|
|
17
|
+
# preserves the pre-0.33.0 over-trigger shape.
|
|
25
18
|
|
|
26
19
|
set -uo pipefail
|
|
27
20
|
|
|
28
|
-
# 1. HALT check.
|
|
29
21
|
# shellcheck source=_lib/halt-check.sh
|
|
30
22
|
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
31
23
|
check_halt
|
|
32
24
|
REA_ROOT=$(rea_root)
|
|
33
25
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
# must match that posture.
|
|
46
|
-
#
|
|
47
|
-
# `jq`-less fallback preserves the pre-0.33.0 over-trigger shape.
|
|
48
|
-
INPUT=$(cat)
|
|
49
|
-
RELEVANT=0
|
|
50
|
-
PROBE=""
|
|
51
|
-
if command -v jq >/dev/null 2>&1; then
|
|
52
|
-
PROBE=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null || true)
|
|
53
|
-
if printf '%s' "$PROBE" | grep -qE '(npm[[:space:]]+(install|i|add)|pnpm[[:space:]]+(add|install|i)|yarn[[:space:]]+add)[[:space:]]'; then
|
|
54
|
-
RELEVANT=1
|
|
26
|
+
SHIM_NAME="dependency-audit-gate"
|
|
27
|
+
SHIM_INTRODUCED_IN="0.33.0"
|
|
28
|
+
SHIM_FAIL_OPEN=0
|
|
29
|
+
SHIM_REFUSAL_NOUN="dependency-audit refusal"
|
|
30
|
+
|
|
31
|
+
shim_is_relevant() {
|
|
32
|
+
local probe
|
|
33
|
+
if command -v jq >/dev/null 2>&1; then
|
|
34
|
+
probe=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null || true)
|
|
35
|
+
else
|
|
36
|
+
probe="$INPUT"
|
|
55
37
|
fi
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
RELEVANT=1
|
|
38
|
+
if printf '%s' "$probe" | grep -qE '(npm[[:space:]]+(install|i|add)|pnpm[[:space:]]+(add|install|i)|yarn[[:space:]]+add)[[:space:]]'; then
|
|
39
|
+
return 0
|
|
59
40
|
fi
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
exit 0
|
|
63
|
-
fi
|
|
64
|
-
|
|
65
|
-
# 3. Resolve the rea CLI.
|
|
66
|
-
REA_ARGV=()
|
|
67
|
-
RESOLVED_CLI_PATH=""
|
|
68
|
-
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
69
|
-
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
70
|
-
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
71
|
-
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
72
|
-
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
73
|
-
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
74
|
-
fi
|
|
75
|
-
|
|
76
|
-
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
77
|
-
printf 'rea: dependency-audit-gate cannot run — the rea CLI is not built.\n' >&2
|
|
78
|
-
printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
|
|
79
|
-
exit 2
|
|
80
|
-
fi
|
|
81
|
-
|
|
82
|
-
# 4. Realpath sandbox check.
|
|
83
|
-
if ! command -v node >/dev/null 2>&1; then
|
|
84
|
-
printf 'rea: dependency-audit-gate cannot run — `node` is not on PATH.\n' >&2
|
|
85
|
-
exit 2
|
|
86
|
-
fi
|
|
87
|
-
|
|
88
|
-
sandbox_check=$(node -e '
|
|
89
|
-
const fs = require("fs");
|
|
90
|
-
const path = require("path");
|
|
91
|
-
const cli = process.argv[1];
|
|
92
|
-
const projDir = process.argv[2];
|
|
93
|
-
let real, realProj;
|
|
94
|
-
try { real = fs.realpathSync(cli); } catch (e) {
|
|
95
|
-
process.stdout.write("bad:realpath"); process.exit(1);
|
|
96
|
-
}
|
|
97
|
-
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
98
|
-
process.stdout.write("bad:realpath-proj"); process.exit(1);
|
|
99
|
-
}
|
|
100
|
-
const sep = path.sep;
|
|
101
|
-
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
102
|
-
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
103
|
-
process.stdout.write("bad:cli-escapes-project"); process.exit(1);
|
|
104
|
-
}
|
|
105
|
-
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
106
|
-
let found = false;
|
|
107
|
-
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
108
|
-
const pj = path.join(cur, "package.json");
|
|
109
|
-
if (fs.existsSync(pj)) {
|
|
110
|
-
try {
|
|
111
|
-
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
112
|
-
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
113
|
-
} catch (e) { /* keep walking */ }
|
|
114
|
-
}
|
|
115
|
-
cur = path.dirname(cur);
|
|
116
|
-
}
|
|
117
|
-
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
118
|
-
process.stdout.write("ok");
|
|
119
|
-
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
120
|
-
|
|
121
|
-
if [ "$sandbox_check" != "ok" ]; then
|
|
122
|
-
printf 'rea: dependency-audit-gate FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
|
|
123
|
-
exit 2
|
|
124
|
-
fi
|
|
125
|
-
|
|
126
|
-
# 5. Version-probe.
|
|
127
|
-
probe_out=$("${REA_ARGV[@]}" hook dependency-audit-gate --help 2>&1)
|
|
128
|
-
probe_status=$?
|
|
129
|
-
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'dependency-audit-gate'; then
|
|
130
|
-
printf 'rea: this shim requires the `rea hook dependency-audit-gate` subcommand (introduced in 0.33.0).\n' >&2
|
|
131
|
-
printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
|
|
132
|
-
printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
|
|
133
|
-
exit 2
|
|
134
|
-
fi
|
|
41
|
+
return 1
|
|
42
|
+
}
|
|
135
43
|
|
|
136
|
-
#
|
|
137
|
-
|
|
138
|
-
|
|
44
|
+
# shellcheck source=_lib/shim-runtime.sh
|
|
45
|
+
source "$(dirname "$0")/_lib/shim-runtime.sh"
|
|
46
|
+
shim_run
|