@bookedsolid/rea 0.6.2 → 0.7.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/pre-push +59 -4
- package/THREAT_MODEL.md +14 -0
- package/dist/cli/install/pre-push.js +3 -0
- package/dist/gateway/downstream.d.ts +11 -14
- package/dist/gateway/downstream.js +50 -18
- package/hooks/_lib/push-review-core.sh +1013 -0
- package/hooks/push-review-gate-git.sh +92 -0
- package/hooks/push-review-gate.sh +47 -987
- package/package.json +1 -1
- package/scripts/dist-regression-gate.sh +220 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bookedsolid/rea",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Agentic governance layer for Claude Code — policy enforcement, hook-based safety gates, audit logging, and Codex-integrated adversarial review for AI-assisted projects",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Booked Solid Technology <oss@bookedsolid.tech> (https://bookedsolid.tech)",
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# dist-regression-gate.sh — class-level guard against "src/ changed, dist/ didn't"
|
|
3
|
+
#
|
|
4
|
+
# Generalizes BUG-013's trust-repair from a `[security]`-changeset-keyed gate
|
|
5
|
+
# (scripts/tarball-smoke.sh) to a gate that fires on ANY change set. Catches
|
|
6
|
+
# the 0.6.0 → 0.6.1 regression class: a release that ships dist/ byte-identical
|
|
7
|
+
# to the previous release despite src/ edits (i.e. dist/ was not rebuilt from
|
|
8
|
+
# the shipping commit).
|
|
9
|
+
#
|
|
10
|
+
# Bypass-resistant by construction:
|
|
11
|
+
# - Does not depend on changeset labels. A changeset-free PR that touches
|
|
12
|
+
# src/ without rebuilding dist/ still fails.
|
|
13
|
+
# - Does not depend on the release.yml rebuild step. That step is
|
|
14
|
+
# defense-in-depth at publish time; this gate fires on every PR and every
|
|
15
|
+
# push:main, so the regression is caught BEFORE it reaches the release
|
|
16
|
+
# branch.
|
|
17
|
+
#
|
|
18
|
+
# ## Algorithm
|
|
19
|
+
#
|
|
20
|
+
# 1. Read last-published version from `npm view @bookedsolid/rea version`.
|
|
21
|
+
# 2. Resolve the matching `v<version>` git tag. (Tag scheme verified across
|
|
22
|
+
# v0.1.0 … v0.6.2.)
|
|
23
|
+
# 3. Diff `src/` between HEAD and the tag. If unchanged, exit 0 — nothing
|
|
24
|
+
# to verify.
|
|
25
|
+
# 4. `npm pack @bookedsolid/rea@<version>` in a tempdir, hash the dist/
|
|
26
|
+
# tree in the extracted tarball.
|
|
27
|
+
# 5. Hash the local `dist/` tree the same way (assumes `pnpm build` has
|
|
28
|
+
# already run — CI enforces this, local runs need to pre-build).
|
|
29
|
+
# 6. If hashes are equal AND src/ changed → FAIL.
|
|
30
|
+
#
|
|
31
|
+
# ## Hash scheme
|
|
32
|
+
#
|
|
33
|
+
# `find dist -type f -print0 | sort -z | xargs -0 shasum -a 256 | shasum -a 256`
|
|
34
|
+
# matches release.yml's own hash recipe (lines 82, 130) so this gate and the
|
|
35
|
+
# post-publish verify step use the same digest. Per-file content sort, no
|
|
36
|
+
# mtime/permission bits — matches the logical-equality question we're asking.
|
|
37
|
+
#
|
|
38
|
+
# ## Exit codes
|
|
39
|
+
#
|
|
40
|
+
# 0 — pass, or skip (see "Skip surface" below)
|
|
41
|
+
# 1 — preflight failure: required tool missing from PATH, or dist/ directory
|
|
42
|
+
# absent at script start (run `pnpm build` first)
|
|
43
|
+
# 2 — REGRESSION — src/ changed vs last release but dist/ hash identical
|
|
44
|
+
#
|
|
45
|
+
# ## Skip surface (exit 0 without running the full check)
|
|
46
|
+
#
|
|
47
|
+
# The gate degrades to a clean skip on infrastructure failures so a transient
|
|
48
|
+
# registry outage or a malformed prior release does not pin every PR and
|
|
49
|
+
# every push:main run into red. All skip branches log a specific reason:
|
|
50
|
+
#
|
|
51
|
+
# - npm view found no published version → first-ever release, or registry
|
|
52
|
+
# outage at lookup time
|
|
53
|
+
# - matching git tag `v<version>` is not reachable and cannot be fetched
|
|
54
|
+
# from origin
|
|
55
|
+
# - src/ is byte-identical between HEAD and the tag → nothing to verify
|
|
56
|
+
# - npm pack against the previous version fails → registry outage, auth
|
|
57
|
+
# trouble, or the tarball was unpublished
|
|
58
|
+
# - npm pack succeeds but produces no `.tgz` → malformed pack output
|
|
59
|
+
# (rare; guarded separately from the pack-failure branch so the log
|
|
60
|
+
# reason stays specific)
|
|
61
|
+
# - the fetched tarball has no `package/dist/` → the baseline itself is
|
|
62
|
+
# broken; holding the next PR hostage to a bad prior release hurts more
|
|
63
|
+
# than it helps
|
|
64
|
+
#
|
|
65
|
+
# The release.yml rebuild-from-HEAD + post-publish tarball hash verification
|
|
66
|
+
# steps (see .github/workflows/release.yml lines 78-138) are the publish-time
|
|
67
|
+
# catching net that covers any case this gate skips. That layered defense is
|
|
68
|
+
# the point: this gate closes the common BUG-013 class on PR + push:main;
|
|
69
|
+
# the release workflow closes the rest at the moment it matters most.
|
|
70
|
+
#
|
|
71
|
+
# ## False-positive surface (known, documented)
|
|
72
|
+
#
|
|
73
|
+
# A whitespace-only edit in src/ that tsc compiles to byte-identical output
|
|
74
|
+
# WILL fail this gate. The failure message tells the committer to either:
|
|
75
|
+
# (a) include a meaningful src change whose dist artifact differs, or
|
|
76
|
+
# (b) `rm -rf dist && pnpm build && git add dist` to refresh timestamps
|
|
77
|
+
# inside dist — except the gate hashes content, not mtime, so (b) alone
|
|
78
|
+
# won't lift the failure.
|
|
79
|
+
#
|
|
80
|
+
# Adding an `[allow-noop-dist]` bypass marker was considered and rejected —
|
|
81
|
+
# it would re-open the BUG-013 attack surface for anyone who can open a PR.
|
|
82
|
+
# If you hit a legitimate noop-dist case, solve it by not committing the
|
|
83
|
+
# whitespace-only src change in isolation, or by combining it with a dist-
|
|
84
|
+
# affecting change in the same PR.
|
|
85
|
+
#
|
|
86
|
+
# ## Local usage
|
|
87
|
+
#
|
|
88
|
+
# pnpm build && scripts/dist-regression-gate.sh
|
|
89
|
+
#
|
|
90
|
+
# ## CI wiring
|
|
91
|
+
#
|
|
92
|
+
# Runs as its own ci.yml job after build. See .github/workflows/ci.yml
|
|
93
|
+
# `dist-regression` job for wiring.
|
|
94
|
+
|
|
95
|
+
set -euo pipefail
|
|
96
|
+
|
|
97
|
+
REPO_ROOT="$(cd -- "$(dirname -- "$0")/.." && pwd -P)"
|
|
98
|
+
cd "$REPO_ROOT"
|
|
99
|
+
|
|
100
|
+
log() { printf '[dist-regression] %s\n' "$*"; }
|
|
101
|
+
err() { printf '[dist-regression] %s\n' "$*" >&2; }
|
|
102
|
+
|
|
103
|
+
# Preflight — need npm, jq, git, shasum, tar.
|
|
104
|
+
for tool in npm jq git shasum tar; do
|
|
105
|
+
if ! command -v "$tool" >/dev/null 2>&1; then
|
|
106
|
+
err "FAIL — required tool not on PATH: $tool"
|
|
107
|
+
exit 1
|
|
108
|
+
fi
|
|
109
|
+
done
|
|
110
|
+
|
|
111
|
+
if [ ! -d "dist" ]; then
|
|
112
|
+
err "FAIL — dist/ not found. Run 'pnpm build' first."
|
|
113
|
+
exit 1
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# Resolve last published version from npm. Use --silent to suppress npm's
|
|
117
|
+
# own progress chatter; swallow stderr because we want a clean skip on a
|
|
118
|
+
# network error rather than a hard failure blocking every CI run.
|
|
119
|
+
PKG_NAME="$(jq -r '.name' package.json)"
|
|
120
|
+
PREV_VERSION="$(npm view "$PKG_NAME" version 2>/dev/null || true)"
|
|
121
|
+
if [ -z "$PREV_VERSION" ]; then
|
|
122
|
+
log "skip — no previous published version found for $PKG_NAME (network issue or first release)"
|
|
123
|
+
exit 0
|
|
124
|
+
fi
|
|
125
|
+
log "last published: $PKG_NAME@$PREV_VERSION"
|
|
126
|
+
|
|
127
|
+
# Resolve the matching git tag. In CI, actions/checkout fetches tags only
|
|
128
|
+
# when fetch-depth: 0 (see ci.yml). Locally, the tag should exist. If the
|
|
129
|
+
# tag isn't reachable, fetch it from origin; if the fetch fails (offline,
|
|
130
|
+
# first-ever release), skip — this is the same safety valve as the
|
|
131
|
+
# "no previous published version" branch.
|
|
132
|
+
PREV_TAG="v$PREV_VERSION"
|
|
133
|
+
if ! git rev-parse --verify --quiet "$PREV_TAG" >/dev/null 2>&1; then
|
|
134
|
+
# Try a shallow fetch of just that tag. Redirect stderr because a missing
|
|
135
|
+
# tag on origin is expected behaviour for a brand-new package; we degrade
|
|
136
|
+
# to "skip" rather than "fail".
|
|
137
|
+
if ! git fetch --quiet --depth=1 origin "refs/tags/${PREV_TAG}:refs/tags/${PREV_TAG}" 2>/dev/null; then
|
|
138
|
+
log "skip — tag $PREV_TAG not reachable (offline CI or tag pruned)"
|
|
139
|
+
exit 0
|
|
140
|
+
fi
|
|
141
|
+
fi
|
|
142
|
+
log "anchor tag: $PREV_TAG ($(git rev-parse --short "$PREV_TAG"))"
|
|
143
|
+
|
|
144
|
+
# Compare src/ trees. We use `git diff --name-only` so renames and mode-
|
|
145
|
+
# only changes count. If src/ is unchanged vs the tag, the dist/ gate has
|
|
146
|
+
# nothing to verify — a PR that touches only docs/hooks/CI should not fail
|
|
147
|
+
# this check.
|
|
148
|
+
SRC_CHANGED_COUNT="$(git diff --name-only "$PREV_TAG" HEAD -- src/ | wc -l | awk '{print $1}')"
|
|
149
|
+
if [ "$SRC_CHANGED_COUNT" -eq 0 ]; then
|
|
150
|
+
log "skip — src/ unchanged since $PREV_TAG"
|
|
151
|
+
exit 0
|
|
152
|
+
fi
|
|
153
|
+
log "src/ changes vs $PREV_TAG: $SRC_CHANGED_COUNT file(s)"
|
|
154
|
+
|
|
155
|
+
# Fetch the published tarball and compute its dist/ hash. Using `npm pack`
|
|
156
|
+
# against the published name is more stable than scraping the registry URL
|
|
157
|
+
# because npm handles auth + CDN redirects. --silent keeps the only stdout
|
|
158
|
+
# noise to the tarball filename.
|
|
159
|
+
WORK="$(mktemp -d -t rea-dist-regression-XXXXXX)"
|
|
160
|
+
trap 'rm -rf -- "$WORK"' EXIT HUP INT TERM
|
|
161
|
+
|
|
162
|
+
# Degrade-to-skip on infrastructure failures. `npm view` above already
|
|
163
|
+
# skips on network errors; `npm pack` and tarball-shape checks need to
|
|
164
|
+
# match, otherwise a registry outage or a broken prior release (e.g. if
|
|
165
|
+
# the shipping 0.6.1 tarball itself had been malformed rather than merely
|
|
166
|
+
# stale) would pin every PR run into red until the registry recovered or
|
|
167
|
+
# a new tarball was published. The release.yml rebuild+verify step
|
|
168
|
+
# remains the catching net at publish time, so skipping here does not
|
|
169
|
+
# re-open the BUG-013 attack surface for the merge-to-main path.
|
|
170
|
+
if ! ( cd "$WORK" && npm pack "${PKG_NAME}@${PREV_VERSION}" --silent >/dev/null 2>&1 ); then
|
|
171
|
+
log "skip — npm pack ${PKG_NAME}@${PREV_VERSION} failed (network issue or registry outage)"
|
|
172
|
+
exit 0
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
TARBALL="$(find "$WORK" -maxdepth 1 -type f -name '*.tgz' | head -1)"
|
|
176
|
+
if [ -z "$TARBALL" ] || [ ! -f "$TARBALL" ]; then
|
|
177
|
+
log "skip — npm pack produced no tarball for ${PKG_NAME}@${PREV_VERSION} in $WORK"
|
|
178
|
+
exit 0
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
mkdir -p "$WORK/extract"
|
|
182
|
+
tar -xzf "$TARBALL" -C "$WORK/extract"
|
|
183
|
+
|
|
184
|
+
if [ ! -d "$WORK/extract/package/dist" ]; then
|
|
185
|
+
log "skip — published tarball for $PREV_VERSION has no dist/ at package/dist (broken baseline)"
|
|
186
|
+
exit 0
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
hash_tree() {
|
|
190
|
+
# $1 — directory holding `dist/`
|
|
191
|
+
# Match the recipe from .github/workflows/release.yml:82,130 exactly so
|
|
192
|
+
# this gate and the release-time verify step speak the same digest.
|
|
193
|
+
( cd "$1" && find dist -type f -print0 | sort -z | xargs -0 shasum -a 256 | shasum -a 256 | awk '{print $1}' )
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
PUBLISHED_HASH="$(hash_tree "$WORK/extract/package")"
|
|
197
|
+
CURRENT_HASH="$(hash_tree "$REPO_ROOT")"
|
|
198
|
+
|
|
199
|
+
log "published ($PREV_VERSION) dist/ hash: $PUBLISHED_HASH"
|
|
200
|
+
log "current dist/ hash: $CURRENT_HASH"
|
|
201
|
+
|
|
202
|
+
if [ "$PUBLISHED_HASH" = "$CURRENT_HASH" ]; then
|
|
203
|
+
err ""
|
|
204
|
+
err "FAIL — REGRESSION: src/ has $SRC_CHANGED_COUNT file change(s) vs $PREV_TAG"
|
|
205
|
+
err " but current dist/ hash is byte-identical to the published tarball."
|
|
206
|
+
err ""
|
|
207
|
+
err " This is the 0.6.0 → 0.6.1 regression class (BUG-013): dist/ was"
|
|
208
|
+
err " not rebuilt from HEAD. Running a fresh build should refresh the"
|
|
209
|
+
err " compiled output; if it does not, one or more src/ changes are"
|
|
210
|
+
err " whitespace-only and produce no dist/ delta — in that case,"
|
|
211
|
+
err " batch them with a change that DOES affect compiled output."
|
|
212
|
+
err ""
|
|
213
|
+
err " To diagnose locally:"
|
|
214
|
+
err " rm -rf dist && pnpm build"
|
|
215
|
+
err " scripts/dist-regression-gate.sh"
|
|
216
|
+
err ""
|
|
217
|
+
exit 2
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
log "PASS — dist/ differs from $PREV_TAG baseline, as expected."
|