@bookedsolid/rea 0.28.2 → 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.
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bookedsolid/rea",
3
- "version": "0.28.2",
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)",