@bookedsolid/rea 0.28.1 → 0.29.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/agents/rea-orchestrator.md +18 -0
- package/dist/audit/append.d.ts +1 -0
- package/dist/audit/append.js +1 -0
- package/dist/audit/delegation-event.d.ts +215 -0
- package/dist/audit/delegation-event.js +113 -0
- package/dist/cli/audit-specialists.d.ts +113 -0
- package/dist/cli/audit-specialists.js +220 -0
- package/dist/cli/doctor.d.ts +65 -0
- package/dist/cli/doctor.js +258 -0
- package/dist/cli/hook.d.ts +40 -8
- package/dist/cli/hook.js +305 -8
- package/dist/cli/index.js +7 -0
- package/dist/cli/install/manifest-schema.d.ts +6 -6
- package/dist/cli/install/settings-merge.js +20 -0
- package/dist/config/tier-map.js +22 -1
- package/dist/registry/loader.d.ts +6 -6
- package/hooks/blocked-paths-enforcer.sh +39 -0
- package/hooks/delegation-capture.sh +158 -0
- package/hooks/settings-protection.sh +39 -0
- package/package.json +1 -1
- package/scripts/dist-regression-gate.sh +46 -2
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: delegation-capture.sh
|
|
3
|
+
# 0.29.0+ — delegation-telemetry MVP.
|
|
4
|
+
#
|
|
5
|
+
# Fires BEFORE every `Agent` or `Skill` tool call. Reads the Claude
|
|
6
|
+
# Code hook payload from stdin, pipes it to
|
|
7
|
+
# `rea hook delegation-signal --detach`, and exits 0 immediately.
|
|
8
|
+
#
|
|
9
|
+
# The signal is OBSERVATIONAL — never gates tool dispatch. Worst-case
|
|
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.
|
|
14
|
+
#
|
|
15
|
+
# Matcher: `Agent|Skill` (NOT `Task|Skill` — `TaskCreate`/`TaskList`
|
|
16
|
+
# are the unrelated todo-list tools and MUST NOT match).
|
|
17
|
+
#
|
|
18
|
+
# # CLI-resolution trust boundary
|
|
19
|
+
#
|
|
20
|
+
# Codex round 3 P1 (2026-05-12): pre-fix this hook resolved the rea
|
|
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.
|
|
26
|
+
#
|
|
27
|
+
# Fix: this hook now uses the same 2-tier sandboxed resolution that
|
|
28
|
+
# protected-paths-bash-gate.sh + blocked-paths-bash-gate.sh use:
|
|
29
|
+
# 1. node_modules/@bookedsolid/rea/dist/cli/index.js (consumer-side
|
|
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.
|
|
43
|
+
|
|
44
|
+
set -uo pipefail
|
|
45
|
+
|
|
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
|
+
# shellcheck source=_lib/halt-check.sh
|
|
50
|
+
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
51
|
+
check_halt
|
|
52
|
+
REA_ROOT=$(rea_root)
|
|
53
|
+
|
|
54
|
+
proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
|
|
55
|
+
|
|
56
|
+
# 2. Resolve the rea CLI through the same fixed 2-tier sandboxed order
|
|
57
|
+
# the protected-paths / blocked-paths bash gates use. PATH lookup
|
|
58
|
+
# is INTENTIONALLY OMITTED — agent-controlled $PATH would let a
|
|
59
|
+
# forged `rea` binary on a consumer machine intercept the
|
|
60
|
+
# delegation signal on every Agent/Skill dispatch. The trade-off:
|
|
61
|
+
# consumers MUST have `@bookedsolid/rea` installed under
|
|
62
|
+
# `node_modules` (the common case after `pnpm i`) OR be running
|
|
63
|
+
# against the rea repo's own dogfood (where dist/cli/index.js
|
|
64
|
+
# holds the canonical CLI). Other install shapes silently drop the
|
|
65
|
+
# signal — matching the bash-gate posture.
|
|
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 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).
|
|
95
|
+
exit 0
|
|
96
|
+
fi
|
|
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
|
|
157
|
+
|
|
158
|
+
exit 0
|
|
@@ -128,6 +128,45 @@ if [[ "$raw_has_traversal" -eq 1 ]] || [[ "$norm_has_traversal" -eq 1 ]]; then
|
|
|
128
128
|
exit 2
|
|
129
129
|
fi
|
|
130
130
|
|
|
131
|
+
# ── 5a-bis. Reject interior single-dot segments (0.29.0 helix-/./-class) ─────
|
|
132
|
+
# Companion to the `..` guard above. The `normalize_path` helper deliberately
|
|
133
|
+
# does NOT collapse interior `./` segments because doing so would corrupt
|
|
134
|
+
# `..` traversals — but that leaves a parallel bypass class. A path like
|
|
135
|
+
# `.husky/./pre-push` resolves on disk to `.husky/pre-push`, yet the literal/
|
|
136
|
+
# prefix matchers in §6 compare against the un-collapsed `.husky/./pre-push`
|
|
137
|
+
# string and miss the match.
|
|
138
|
+
#
|
|
139
|
+
# Conservative reading (per Jake 2026-05-12): treat any interior `./`
|
|
140
|
+
# segment exactly like a `..` segment — refuse outright, force the caller
|
|
141
|
+
# to send a canonical path. The corpus design pairs shell-scripting-specialist
|
|
142
|
+
# with adversarial-test-specialist; the canonical attack shapes are:
|
|
143
|
+
#
|
|
144
|
+
# .husky/./pre-push — single segment
|
|
145
|
+
# .husky/././pre-push — repeated segments
|
|
146
|
+
# .husky/.//pre-push — `./` immediately followed by another `/`
|
|
147
|
+
# .claude/hooks/./_lib/halt-check.sh — inside a protected directory
|
|
148
|
+
# %2E%2F — percent-encoded `./`, caught after URL-decode
|
|
149
|
+
# .\.\pre-push — backslash variant, normalize_path → `./`
|
|
150
|
+
#
|
|
151
|
+
# Only the NORMALIZED form is checked (not the raw form) because raw `./foo`
|
|
152
|
+
# at start-of-string is a legitimate relative path; `normalize_path` already
|
|
153
|
+
# strips leading `./` segments, so anything that survives into the normalized
|
|
154
|
+
# form's `/./` shape is INTERIOR by construction.
|
|
155
|
+
norm_has_dot_segment=0
|
|
156
|
+
case "/$NORMALIZED/" in
|
|
157
|
+
*/./*) norm_has_dot_segment=1 ;;
|
|
158
|
+
esac
|
|
159
|
+
if [[ "$norm_has_dot_segment" -eq 1 ]]; then
|
|
160
|
+
{
|
|
161
|
+
printf 'SETTINGS PROTECTION: interior dot-segment rejected\n'
|
|
162
|
+
printf '\n'
|
|
163
|
+
printf ' File: %s\n' "$SAFE_FILE_PATH"
|
|
164
|
+
printf " Rule: path contains an interior '/./' segment; rewrite to a\n"
|
|
165
|
+
printf ' canonical project-relative path without dot segments.\n'
|
|
166
|
+
} >&2
|
|
167
|
+
exit 2
|
|
168
|
+
fi
|
|
169
|
+
|
|
131
170
|
# Compute lower-cased path early so the §5b allow-list (and §6/§6b matchers
|
|
132
171
|
# below) all reference a single normalized variable.
|
|
133
172
|
LOWER_NORM=$(printf '%s' "$NORMALIZED" | tr '[:upper:]' '[:lower:]')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bookedsolid/rea",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.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)",
|
|
@@ -167,8 +167,52 @@ trap 'rm -rf -- "$WORK"' EXIT HUP INT TERM
|
|
|
167
167
|
# a new tarball was published. The release.yml rebuild+verify step
|
|
168
168
|
# remains the catching net at publish time, so skipping here does not
|
|
169
169
|
# re-open the BUG-013 attack surface for the merge-to-main path.
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
#
|
|
171
|
+
# 0.29.0: bounded retry loop for npm CDN propagation lag. The memory
|
|
172
|
+
# entries for 0.9.0, 0.12.0, 0.13.0, 0.28.0, and 0.28.1 all note
|
|
173
|
+
# "release verify flaked on npm CDN lag" — `npm view` returns the
|
|
174
|
+
# version metadata but `npm pack` against the same version times out
|
|
175
|
+
# or 404s because the tarball blob has not propagated to all CDN edges
|
|
176
|
+
# yet. The CI-side workflow already has a 12×10s retry (release.yml
|
|
177
|
+
# phase 2); this script runs locally / in PR CI where the failure
|
|
178
|
+
# window is shorter but still occurs.
|
|
179
|
+
#
|
|
180
|
+
# Shape: initial attempt + three retries with sleeps 2s / 8s / 30s.
|
|
181
|
+
# Total worst-case wait = 2 + 8 + 30 = 40s, all on the failure path.
|
|
182
|
+
# That covers the empirically observed CDN propagation window (cf.
|
|
183
|
+
# release.yml phase 2 retry loops) while bounding the local-/ PR-side
|
|
184
|
+
# blocking time to under a minute on a genuine outage.
|
|
185
|
+
NPM_PACK_OK=0
|
|
186
|
+
NPM_PACK_DELAYS=(2 8 30)
|
|
187
|
+
NPM_PACK_ATTEMPTS=$((${#NPM_PACK_DELAYS[@]} + 1))
|
|
188
|
+
# Codex round 1 P2-2: use bash arithmetic for-loop instead of `$(seq 1 N)`.
|
|
189
|
+
# `seq` is not in the preflight tool list (line 104: npm jq git shasum tar)
|
|
190
|
+
# and `set -e` at the top of the script would exit 127 inside the loop body
|
|
191
|
+
# on minimal images that lack it (Alpine, some BusyBox shells). Bash's
|
|
192
|
+
# arithmetic for-loop is a builtin and works on every supported version.
|
|
193
|
+
for ((attempt = 1; attempt <= NPM_PACK_ATTEMPTS; attempt++)); do
|
|
194
|
+
if ( cd "$WORK" && npm pack "${PKG_NAME}@${PREV_VERSION}" --silent >/dev/null 2>&1 ); then
|
|
195
|
+
if [ "$attempt" -gt 1 ]; then
|
|
196
|
+
log "npm pack succeeded after ${attempt} attempt(s) (CDN propagation lag)"
|
|
197
|
+
fi
|
|
198
|
+
NPM_PACK_OK=1
|
|
199
|
+
break
|
|
200
|
+
fi
|
|
201
|
+
# Clean up any partial artifact npm pack may have left in $WORK
|
|
202
|
+
# before retrying so the next attempt has a clean slate.
|
|
203
|
+
find "$WORK" -maxdepth 1 -type f -name '*.tgz' -delete 2>/dev/null || true
|
|
204
|
+
if [ "$attempt" -lt "$NPM_PACK_ATTEMPTS" ]; then
|
|
205
|
+
# Bash array is 0-indexed; $attempt is 1-indexed; index into
|
|
206
|
+
# NPM_PACK_DELAYS at $attempt-1 to read the delay AFTER this
|
|
207
|
+
# failed attempt (before the next try).
|
|
208
|
+
idx=$((attempt - 1))
|
|
209
|
+
delay="${NPM_PACK_DELAYS[$idx]}"
|
|
210
|
+
log "npm pack attempt ${attempt}/${NPM_PACK_ATTEMPTS} failed; sleeping ${delay}s for CDN propagation"
|
|
211
|
+
sleep "$delay"
|
|
212
|
+
fi
|
|
213
|
+
done
|
|
214
|
+
if [ "$NPM_PACK_OK" -ne 1 ]; then
|
|
215
|
+
log "skip — npm pack ${PKG_NAME}@${PREV_VERSION} failed after ${NPM_PACK_ATTEMPTS} attempts (network issue, registry outage, or persistent CDN lag)"
|
|
172
216
|
exit 0
|
|
173
217
|
fi
|
|
174
218
|
|