@bookedsolid/rea 0.31.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.husky/prepare-commit-msg +80 -6
- package/MIGRATING.md +24 -15
- package/dist/cli/hook.js +60 -22
- package/dist/hooks/_lib/halt-check.d.ts +78 -0
- package/dist/hooks/_lib/halt-check.js +106 -0
- package/dist/hooks/_lib/payload.d.ts +124 -0
- package/dist/hooks/_lib/payload.js +245 -0
- package/dist/hooks/_lib/segments.d.ts +125 -0
- package/dist/hooks/_lib/segments.js +766 -0
- package/dist/hooks/architecture-review-gate/index.d.ts +58 -0
- package/dist/hooks/architecture-review-gate/index.js +250 -0
- package/dist/hooks/attribution-advisory/index.d.ts +72 -0
- package/dist/hooks/attribution-advisory/index.js +233 -0
- package/dist/hooks/bash-scanner/protected-scan.js +14 -2
- package/dist/hooks/changeset-security-gate/index.d.ts +71 -0
- package/dist/hooks/changeset-security-gate/index.js +330 -0
- package/dist/hooks/dependency-audit-gate/index.d.ts +91 -0
- package/dist/hooks/dependency-audit-gate/index.js +294 -0
- package/dist/hooks/env-file-protection/index.d.ts +55 -0
- package/dist/hooks/env-file-protection/index.js +159 -0
- package/dist/hooks/pr-issue-link-gate/index.d.ts +91 -0
- package/dist/hooks/pr-issue-link-gate/index.js +127 -0
- package/dist/hooks/security-disclosure-gate/index.d.ts +91 -0
- package/dist/hooks/security-disclosure-gate/index.js +502 -0
- package/hooks/_lib/protected-paths.sh +10 -3
- package/hooks/architecture-review-gate.sh +92 -77
- package/hooks/attribution-advisory.sh +139 -131
- package/hooks/changeset-security-gate.sh +114 -149
- package/hooks/dependency-audit-gate.sh +115 -156
- package/hooks/env-file-protection.sh +130 -97
- package/hooks/pr-issue-link-gate.sh +114 -45
- package/hooks/security-disclosure-gate.sh +148 -316
- package/hooks/settings-protection.sh +13 -9
- package/package.json +1 -1
- package/templates/architecture-review-gate.dogfood-staged.sh +116 -0
- package/templates/attribution-advisory.dogfood-staged.sh +170 -0
- package/templates/changeset-security-gate.dogfood-staged.sh +137 -0
- package/templates/dependency-audit-gate.dogfood-staged.sh +138 -0
- package/templates/env-file-protection.dogfood-staged.sh +157 -0
- package/templates/pr-issue-link-gate.dogfood-staged.sh +134 -0
- package/templates/prepare-commit-msg.husky.sh +80 -6
- package/templates/security-disclosure-gate.dogfood-staged.sh +171 -0
- package/templates/settings-protection.dogfood.patch +58 -0
|
@@ -1,172 +1,137 @@
|
|
|
1
|
-
#!/
|
|
2
|
-
# changeset-security-gate.sh
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: changeset-security-gate.sh
|
|
3
|
+
# 0.33.0+ — Node-binary shim for `rea hook changeset-security-gate`.
|
|
3
4
|
#
|
|
4
|
-
#
|
|
5
|
+
# Pre-0.33.0 the gate's full body lived here as bash (172 LOC, frontmatter
|
|
6
|
+
# validation + GHSA/CVE scan + MultiEdit-aware tool handling). The
|
|
7
|
+
# migration to the parser-backed Node binary moves all of that into
|
|
8
|
+
# `src/hooks/changeset-security-gate/index.ts`.
|
|
5
9
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# creates public pre-disclosure in git history.
|
|
10
|
+
# Behavioral contract is preserved byte-for-byte: exit 0 on
|
|
11
|
+
# pass-through / non-changeset / valid frontmatter, exit 2 on HALT /
|
|
12
|
+
# disclosure leak / malformed frontmatter / malformed payload.
|
|
10
13
|
#
|
|
11
|
-
#
|
|
12
|
-
# are silently ignored by the changesets tool, wasting the release entry.
|
|
14
|
+
# # CLI-resolution trust boundary
|
|
13
15
|
#
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
# Realpath sandbox check + version probe. Same shape as the 0.32.0
|
|
17
|
+
# pilots.
|
|
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.
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
source "$(dirname "$0")/_lib/common.sh"
|
|
25
|
+
set -uo pipefail
|
|
20
26
|
|
|
27
|
+
# 1. HALT check.
|
|
28
|
+
# shellcheck source=_lib/halt-check.sh
|
|
29
|
+
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
21
30
|
check_halt
|
|
31
|
+
REA_ROOT=$(rea_root)
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
25
|
-
|
|
26
|
-
# 0.15.0 fix: MultiEdit was not in the allowed tool_name set, so the gate
|
|
27
|
-
# silently exited 0 on every MultiEdit call against `.changeset/*.md` —
|
|
28
|
-
# letting GHSA / CVE pre-disclosure through and skipping frontmatter
|
|
29
|
-
# validation. 0.16.0: NotebookEdit added too (changesets are .md files
|
|
30
|
-
# but a malicious agent could in principle route a .md write through
|
|
31
|
-
# NotebookEdit's new_source path; cheap to allow, free to test).
|
|
32
|
-
if [[ "$TOOL_NAME" != "Write" && "$TOOL_NAME" != "Edit" && "$TOOL_NAME" != "MultiEdit" && "$TOOL_NAME" != "NotebookEdit" ]]; then
|
|
33
|
-
exit 0
|
|
34
|
-
fi
|
|
35
|
-
|
|
36
|
-
require_jq
|
|
37
|
-
|
|
38
|
-
# 0.16.0: payload extraction migrated to `_lib/payload-read.sh`. Shared
|
|
39
|
-
# helpers handle every write-tier tool with the same defensive
|
|
40
|
-
# coercion. Adding the next write-tier tool is a one-line edit there.
|
|
41
|
-
# shellcheck source=_lib/payload-read.sh
|
|
42
|
-
source "$(dirname "$0")/_lib/payload-read.sh"
|
|
33
|
+
proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
|
|
43
34
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
#
|
|
47
|
-
if ! echo "$FILE_PATH" | grep -qE '\.changeset/[^/]+\.md$' || echo "$FILE_PATH" | grep -qE '\.changeset/README\.md$'; then
|
|
48
|
-
exit 0
|
|
49
|
-
fi
|
|
50
|
-
|
|
51
|
-
CONTENT=$(extract_write_content "$INPUT")
|
|
52
|
-
|
|
53
|
-
# ─── 1. SECURITY DISCLOSURE CHECK ───────────────────────────────────────────
|
|
35
|
+
# 2. Relevance pre-gate. This is a PreToolUse Write/Edit/MultiEdit/
|
|
36
|
+
# NotebookEdit matcher, so the payload always has a `tool_input.
|
|
37
|
+
# file_path` (or `notebook_path`).
|
|
54
38
|
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
39
|
+
# 2026-05-15 codex round-2 P2 fix: scan `tool_input.file_path` /
|
|
40
|
+
# `tool_input.notebook_path` ONLY, NOT the raw JSON payload. Pre-fix
|
|
41
|
+
# a Write to `README.md` whose body merely mentions `.changeset/`
|
|
42
|
+
# (e.g. "See .changeset/example.md") tripped the fail-closed branch
|
|
43
|
+
# when the CLI was unbuilt — the substring lived in the
|
|
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
|
|
54
|
+
fi
|
|
55
|
+
else
|
|
56
|
+
if printf '%s' "$INPUT" | grep -qE '\.changeset/'; then
|
|
57
|
+
RELEVANT=1
|
|
69
58
|
fi
|
|
70
|
-
done
|
|
71
|
-
|
|
72
|
-
if [[ -n "$MATCHED_PATTERN" ]]; then
|
|
73
|
-
json_output "block" \
|
|
74
|
-
"CHANGESET SECURITY GATE: This changeset contains a security advisory identifier (matched: '${MATCHED_PATTERN}').
|
|
75
|
-
|
|
76
|
-
Do NOT reference GHSA IDs or CVE numbers in changeset files before the advisory is published.
|
|
77
|
-
Changeset files are committed to git — this creates pre-disclosure in public history and CHANGELOG.
|
|
78
|
-
|
|
79
|
-
CORRECT approach for security fix changesets:
|
|
80
|
-
Use vague language only — no identifiers, no vulnerability details.
|
|
81
|
-
|
|
82
|
-
WRONG: 'fix(hooks): patch GHSA-3w3m-7gg4-f82g — symlink-guard now covers Edit tool'
|
|
83
|
-
RIGHT: 'security: extend symlink protection to cover all write-capable tools'
|
|
84
|
-
|
|
85
|
-
WRONG: 'security: fix CVE-2026-1234 prompt injection via tool descriptions'
|
|
86
|
-
RIGHT: 'security: harden middleware chain against indirect instruction attacks'
|
|
87
|
-
|
|
88
|
-
After the release ships:
|
|
89
|
-
1. Publish the GitHub Security Advisory (Security tab → Advisories → Publish)
|
|
90
|
-
2. The GHSA becomes the detailed public disclosure document
|
|
91
|
-
3. Optionally update CHANGELOG.md post-publish to add the GHSA reference"
|
|
92
59
|
fi
|
|
93
|
-
|
|
94
|
-
# ─── 2. FRONTMATTER VALIDATION ───────────────────────────────────────────────
|
|
95
|
-
#
|
|
96
|
-
# A changeset without valid frontmatter is silently ignored by the changesets
|
|
97
|
-
# tool — the package bump and CHANGELOG entry never appear in the release.
|
|
98
|
-
#
|
|
99
|
-
# 0.15.0 fix: skip frontmatter validation for MultiEdit. MultiEdit's
|
|
100
|
-
# `tool_input.edits[].new_string` payload is a list of partial string
|
|
101
|
-
# replacements, not the full file body — running the frontmatter
|
|
102
|
-
# validator against the concatenation of new_strings would reject every
|
|
103
|
-
# legitimate MultiEdit on an existing changeset (none of the edit
|
|
104
|
-
# fragments individually contains a frontmatter block, even though the
|
|
105
|
-
# resulting file does). The disclosure scan above still runs on
|
|
106
|
-
# MultiEdit content because GHSA/CVE patterns match per-fragment without
|
|
107
|
-
# any structural assumption.
|
|
108
|
-
if [[ "$TOOL_NAME" == "MultiEdit" ]]; then
|
|
60
|
+
if [ "$RELEVANT" -eq 0 ]; then
|
|
109
61
|
exit 0
|
|
110
62
|
fi
|
|
111
63
|
|
|
112
|
-
#
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
---
|
|
122
|
-
|
|
123
|
-
Brief description of what changed and why (close #N if applicable).
|
|
124
|
-
|
|
125
|
-
Bump types: patch (bug fix/security), minor (new feature), major (breaking change)"
|
|
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"
|
|
126
73
|
fi
|
|
127
74
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
# changeset with `"@scope/name": patch` was rejected as malformed even
|
|
133
|
-
# though the Changesets tool itself accepts every form.
|
|
134
|
-
#
|
|
135
|
-
# Codex round-1 P2-1 fix: explicit-alternation form (no backref) so
|
|
136
|
-
# the unquoted variant matches on BSD grep too. The earlier
|
|
137
|
-
# `^([\"']?)[^\"']+\1: ...` shape relied on backref-with-empty-capture
|
|
138
|
-
# semantics that BSD's grep rejects when the capture group's `?` made
|
|
139
|
-
# it absent — quoted forms matched on macOS but unquoted did not.
|
|
140
|
-
FRONTMATTER=$(echo "$CONTENT" | awk '/^---/{count++; if(count==2){exit} next} count==1{print}')
|
|
141
|
-
if ! echo "$FRONTMATTER" | grep -qE "^(\"[^\"]+\"|'[^']+'|[^\"'[:space:]]+): (patch|minor|major)"; then
|
|
142
|
-
json_output "block" \
|
|
143
|
-
"CHANGESET FORMAT GATE: Frontmatter does not contain a valid package bump entry.
|
|
144
|
-
|
|
145
|
-
The frontmatter must include at least one package/bump pair:
|
|
146
|
-
|
|
147
|
-
---
|
|
148
|
-
'@bookedsolid/rea': patch
|
|
149
|
-
---
|
|
150
|
-
|
|
151
|
-
Valid bump types: patch | minor | major"
|
|
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
|
|
152
79
|
fi
|
|
153
80
|
|
|
154
|
-
#
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
Add a meaningful description explaining what changed and why:
|
|
161
|
-
|
|
162
|
-
---
|
|
163
|
-
'@bookedsolid/rea': patch
|
|
164
|
-
---
|
|
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
|
|
165
86
|
|
|
166
|
-
|
|
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
|
|
167
124
|
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
170
133
|
fi
|
|
171
134
|
|
|
172
|
-
|
|
135
|
+
# 6. Forward stdin.
|
|
136
|
+
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook changeset-security-gate
|
|
137
|
+
exit $?
|
|
@@ -1,179 +1,138 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# PreToolUse hook: dependency-audit-gate.sh
|
|
3
|
-
#
|
|
4
|
-
# Detects package install commands (npm install, pnpm add, yarn add) and
|
|
5
|
-
# verifies the package exists on the registry before allowing the install.
|
|
3
|
+
# 0.33.0+ — Node-binary shim for `rea hook dependency-audit-gate`.
|
|
6
4
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
5
|
+
# Pre-0.33.0 the gate's full body lived here as bash (179 LOC, the
|
|
6
|
+
# segment splitter + install-pattern detection + per-package
|
|
7
|
+
# `npm view` probe). The migration to the parser-backed Node binary
|
|
8
|
+
# moves all of that into `src/hooks/dependency-audit-gate/index.ts`.
|
|
9
|
+
#
|
|
10
|
+
# Behavioral contract is preserved byte-for-byte: exit 0 on
|
|
11
|
+
# pass-through / all-packages-verified, exit 2 on HALT / any package
|
|
12
|
+
# missing / malformed payload.
|
|
13
|
+
#
|
|
14
|
+
# # CLI-resolution trust boundary
|
|
15
|
+
#
|
|
16
|
+
# Realpath sandbox check + version probe. Same shape as the 0.32.0
|
|
17
|
+
# pilots and the env-file-protection shim above.
|
|
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.
|
|
10
25
|
|
|
11
26
|
set -uo pipefail
|
|
12
27
|
|
|
13
|
-
#
|
|
14
|
-
INPUT=$(cat)
|
|
15
|
-
|
|
16
|
-
# ── 2. Dependency check ──────────────────────────────────────────────────────
|
|
17
|
-
if ! command -v jq >/dev/null 2>&1; then
|
|
18
|
-
printf 'REA ERROR: jq is required but not installed.\n' >&2
|
|
19
|
-
printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
|
|
20
|
-
exit 2
|
|
21
|
-
fi
|
|
22
|
-
|
|
23
|
-
# ── 3. HALT check ────────────────────────────────────────────────────────────
|
|
24
|
-
# 0.16.0: HALT check sourced from shared _lib/halt-check.sh.
|
|
28
|
+
# 1. HALT check.
|
|
25
29
|
# shellcheck source=_lib/halt-check.sh
|
|
26
30
|
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
27
31
|
check_halt
|
|
28
32
|
REA_ROOT=$(rea_root)
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
32
|
-
|
|
33
|
-
if [[ -z "$CMD" ]]; then
|
|
34
|
-
exit 0
|
|
35
|
-
fi
|
|
36
|
-
|
|
37
|
-
# ── 5. Detect package install commands ────────────────────────────────────────
|
|
38
|
-
# Match: npm install <pkg>, npm i <pkg>, pnpm add <pkg>, yarn add <pkg>
|
|
39
|
-
# Skip: npm install (no args), npm ci, npm install --save-dev (without new pkg)
|
|
34
|
+
proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
|
|
40
35
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# 0.17.0 helix-017 #3: unwrap nested-shell wrappers (`bash -c 'PAYLOAD'`,
|
|
62
|
-
# `sh -lc "PAYLOAD"`, etc.) before splitting so the inner install
|
|
63
|
-
# command becomes a segment that anchors against the install-pattern
|
|
64
|
-
# check below. Pre-fix `bash -lc 'npm install pkg'` produced a single
|
|
65
|
-
# segment whose first token was `bash` — install-detection skipped.
|
|
66
|
-
# 0.17.0 helix-019 #3: delegate splitting to the shared
|
|
67
|
-
# `_rea_split_segments` so this gate inherits the full separator set
|
|
68
|
-
# (including bare `&` background-process operator added in 0.16.1)
|
|
69
|
-
# and the quote-mask that prevents over-fire from in-quote separators.
|
|
70
|
-
# Pre-fix the local segmenter splat on `|||&&|;|` only, missing bare
|
|
71
|
-
# `&` — `echo warmup & pnpm add lodash` stayed merged into one segment
|
|
72
|
-
# and the install-pattern leading-token check skipped it entirely.
|
|
73
|
-
local segments
|
|
74
|
-
if [ -f "$(dirname "$0")/_lib/cmd-segments.sh" ]; then
|
|
75
|
-
# shellcheck source=_lib/cmd-segments.sh
|
|
76
|
-
source "$(dirname "$0")/_lib/cmd-segments.sh"
|
|
77
|
-
segments=$(_rea_split_segments "$cmd")
|
|
78
|
-
else
|
|
79
|
-
# Fallback (lib unavailable): legacy local splitter preserved.
|
|
80
|
-
segments=$(printf '%s\n' "$cmd" | sed -E 's/(\|\||\&\&|;|\||\&)/\n/g')
|
|
36
|
+
# 2. Relevance pre-gate. Look for any install-pattern keyword.
|
|
37
|
+
#
|
|
38
|
+
# 2026-05-15 codex round-2 P2 fix: scan `tool_input.command` ONLY,
|
|
39
|
+
# not the raw JSON payload. Pre-fix `git commit -m "docs: run pnpm
|
|
40
|
+
# install foo before start"` triggered the fail-closed branch on a
|
|
41
|
+
# fresh checkout (the install-pattern regex hit the substring
|
|
42
|
+
# inside the commit-message ARG of the git command, not a real
|
|
43
|
+
# install invocation). The Node body's segment-anchored matcher
|
|
44
|
+
# correctly distinguishes between the two — the shim's pre-gate
|
|
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
|
|
81
55
|
fi
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
# `time` / etc.
|
|
89
|
-
#
|
|
90
|
-
# 0.16.1 helix-016 P2 fix: also strip leading KEY=VALUE env-var
|
|
91
|
-
# assignments. Pre-fix the prefix allow-list only permitted
|
|
92
|
-
# sudo/exec/time, so `CI=1 pnpm add foo` and
|
|
93
|
-
# `NODE_ENV=development npm install bar` bypassed the audit
|
|
94
|
-
# entirely. POSIX shell allows any number of leading KEY=VALUE
|
|
95
|
-
# assignments before the command word; we strip them the same
|
|
96
|
-
# way the shell does.
|
|
97
|
-
local stripped_segment
|
|
98
|
-
stripped_segment=$(printf '%s' "$segment" | sed -E 's/^([[:space:]]*[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]+[[:space:]]+)+//')
|
|
99
|
-
|
|
100
|
-
if printf '%s' "$stripped_segment" | grep -qiE '^(sudo[[:space:]]+|exec[[:space:]]+|time[[:space:]]+)*(npm[[:space:]]+(install|i|add)|pnpm[[:space:]]+(add|install|i)|yarn[[:space:]]+add)[[:space:]]+'; then
|
|
101
|
-
# Strip the leading prefix wrappers + install command, leaving args.
|
|
102
|
-
local after_cmd
|
|
103
|
-
after_cmd=$(printf '%s' "$stripped_segment" | sed -E 's/^(sudo[[:space:]]+|exec[[:space:]]+|time[[:space:]]+)*(npm[[:space:]]+(install|i|add)|pnpm[[:space:]]+(add|install|i)|yarn[[:space:]]+add)[[:space:]]+//')
|
|
104
|
-
|
|
105
|
-
for token in $after_cmd; do
|
|
106
|
-
if [[ "$token" == -* ]]; then continue; fi
|
|
107
|
-
if [[ "$token" == ./* || "$token" == /* || "$token" == ../* ]]; then continue; fi
|
|
108
|
-
if [[ -z "$token" ]]; then continue; fi
|
|
109
|
-
# 0.16.1: tighten token classification (helix-016 sibling concern).
|
|
110
|
-
# A "package name" is something that doesn't contain shell
|
|
111
|
-
# metacharacters — `2>&1`, `$VAR`, etc. are never valid npm
|
|
112
|
-
# package names. Skip any token containing `=`, `>`, `<`, `&`,
|
|
113
|
-
# `|`, `;`, `$`, backtick, or quotes.
|
|
114
|
-
if [[ "$token" == *=* || "$token" == *">"* || "$token" == *"<"* ||
|
|
115
|
-
"$token" == *"&"* || "$token" == *"|"* || "$token" == *";"* ||
|
|
116
|
-
"$token" == *'$'* || "$token" == *'`'* ||
|
|
117
|
-
"$token" == *'"'* || "$token" == *"'"* ]]; then continue; fi
|
|
118
|
-
# `npm view` can't validate `@workspace:*` / `link:` / `file:`
|
|
119
|
-
# prefixes (workspace protocols). Skip them — they're never npm
|
|
120
|
-
# registry packages.
|
|
121
|
-
if [[ "$token" == workspace:* || "$token" == link:* || "$token" == file:* || "$token" == git+* ]]; then continue; fi
|
|
122
|
-
local pkg_name
|
|
123
|
-
pkg_name=$(printf '%s' "$token" | sed -E 's/@[^@/]+$//')
|
|
124
|
-
if [[ -z "$pkg_name" ]]; then
|
|
125
|
-
pkg_name="$token"
|
|
126
|
-
fi
|
|
127
|
-
printf '%s\n' "$pkg_name"
|
|
128
|
-
done
|
|
129
|
-
fi
|
|
130
|
-
done <<< "$segments"
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
PACKAGES=$(extract_packages "$CMD")
|
|
134
|
-
|
|
135
|
-
if [[ -z "$PACKAGES" ]]; then
|
|
56
|
+
else
|
|
57
|
+
if printf '%s' "$INPUT" | grep -qE '(npm[[:space:]]+(install|i|add)|pnpm[[:space:]]+(add|install|i)|yarn[[:space:]]+add)[[:space:]]'; then
|
|
58
|
+
RELEVANT=1
|
|
59
|
+
fi
|
|
60
|
+
fi
|
|
61
|
+
if [ "$RELEVANT" -eq 0 ]; then
|
|
136
62
|
exit 0
|
|
137
63
|
fi
|
|
138
64
|
|
|
139
|
-
#
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
142
75
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
146
81
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
151
87
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
165
125
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
printf ' Rule: All packages must exist on the npm registry before installation.\n'
|
|
174
|
-
printf ' Check: Is the package name spelled correctly? Does it exist on npmjs.com?\n'
|
|
175
|
-
} >&2
|
|
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
|
|
176
133
|
exit 2
|
|
177
134
|
fi
|
|
178
135
|
|
|
179
|
-
|
|
136
|
+
# 6. Forward stdin.
|
|
137
|
+
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook dependency-audit-gate
|
|
138
|
+
exit $?
|