@bookedsolid/rea 0.4.0 → 0.6.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.
@@ -56,8 +56,31 @@ fi
56
56
  # ── 4. Parse command ──────────────────────────────────────────────────────────
57
57
  CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
58
58
 
59
+ # ── 4a. BUG-008: self-detect git's native pre-push contract ───────────────────
60
+ # When the hook is wired into `.husky/pre-push`, git invokes it with
61
+ # `$1 = remote name`, `$2 = remote url`
62
+ # and delivers one line per refspec on stdin:
63
+ # `<local_ref> <local_sha> <remote_ref> <remote_sha>`
64
+ # The Claude Code PreToolUse wrapper instead delivers JSON on stdin, which is
65
+ # what the jq parse above targets. When jq returns empty, the stdin may in
66
+ # fact be git's pre-push ref-list — sniff the first non-blank line, and if it
67
+ # matches the `<ref> <40-hex> <ref> <40-hex>` shape, synthesize CMD as
68
+ # `git push <remote>` (from argv $1) so the remainder of the gate runs
69
+ # through the pre-push parser in step 6 rather than the argv fallback.
70
+ #
71
+ # Any other stdin shape (empty, random JSON, a non-push tool call) still
72
+ # exits 0 here — the gate is a no-op for non-push Bash calls by design.
73
+ FIRST_STDIN_LINE=$(printf '%s' "$INPUT" | awk 'NF { print; exit }')
59
74
  if [[ -z "$CMD" ]]; then
60
- exit 0
75
+ if [[ -n "$FIRST_STDIN_LINE" ]] \
76
+ && printf '%s' "$FIRST_STDIN_LINE" \
77
+ | grep -qE '^[^[:space:]]+[[:space:]]+[0-9a-f]{40}[[:space:]]+[^[:space:]]+[[:space:]]+[0-9a-f]{40}[[:space:]]*$'; then
78
+ # Git native pre-push path. Remote comes from argv $1 — falls back to
79
+ # `origin` for safety if the hook was invoked without arguments.
80
+ CMD="git push ${1:-origin}"
81
+ else
82
+ exit 0
83
+ fi
61
84
  fi
62
85
 
63
86
  # Only trigger on git push commands
@@ -73,6 +96,167 @@ if [[ -f "$POLICY_FILE" ]]; then
73
96
  fi
74
97
  fi
75
98
 
99
+ # ── 5a. REA_SKIP_PUSH_REVIEW — whole-gate escape hatch ───────────────────────
100
+ # An opt-in bypass for the ENTIRE push-review gate (not just the Codex branch).
101
+ # Exists to unblock consumers when rea itself is broken (as in BUG-009 pre-0.5.0)
102
+ # or a corrupt policy/audit file would otherwise deadlock a push. Requires an
103
+ # explicit non-empty reason; the value of REA_SKIP_PUSH_REVIEW is recorded
104
+ # verbatim in the audit record as the reason.
105
+ #
106
+ # Fail-closed contract matches REA_SKIP_CODEX_REVIEW:
107
+ # - missing dist/audit/append.js → exit 2
108
+ # - missing git identity → exit 2
109
+ # - Node failure → exit 2
110
+ #
111
+ # Audit tool_name is `push.review.skipped`. This is intentionally NOT
112
+ # `codex.review` or `codex.review.skipped` — a skip of the whole gate is a
113
+ # separately-audited event and does not satisfy the Codex-review jq predicate.
114
+ if [[ -n "${REA_SKIP_PUSH_REVIEW:-}" ]]; then
115
+ SKIP_REASON="$REA_SKIP_PUSH_REVIEW"
116
+ AUDIT_APPEND_JS="${REA_ROOT}/dist/audit/append.js"
117
+
118
+ if [[ ! -f "$AUDIT_APPEND_JS" ]]; then
119
+ {
120
+ printf 'PUSH BLOCKED: REA_SKIP_PUSH_REVIEW requires rea to be built.\n'
121
+ printf '\n'
122
+ printf ' REA_SKIP_PUSH_REVIEW is set but %s is missing.\n' "$AUDIT_APPEND_JS"
123
+ printf ' Run: pnpm build\n'
124
+ printf '\n'
125
+ } >&2
126
+ exit 2
127
+ fi
128
+
129
+ # Codex F2: CI-aware refusal. The skip hatch is ambient — any process that
130
+ # can set env vars can flip the gate off with a forged git identity (git
131
+ # config is mutable repo config). In a CI context, refuse by default; only
132
+ # allow if the policy explicitly opted in via review.allow_skip_in_ci=true.
133
+ if [[ -n "${CI:-}" ]]; then
134
+ ALLOW_CI_SKIP=""
135
+ READ_FIELD_JS="${REA_ROOT}/dist/scripts/read-policy-field.js"
136
+ if [[ -f "$READ_FIELD_JS" ]]; then
137
+ ALLOW_CI_SKIP=$(REA_ROOT="$REA_ROOT" node "$READ_FIELD_JS" review.allow_skip_in_ci 2>/dev/null || echo "")
138
+ fi
139
+ if [[ "$ALLOW_CI_SKIP" != "true" ]]; then
140
+ {
141
+ printf 'PUSH BLOCKED: REA_SKIP_PUSH_REVIEW refused in CI context.\n'
142
+ printf '\n'
143
+ printf ' CI env var is set. An unauthenticated env-var bypass in a shared\n'
144
+ printf ' build agent is not trusted. To enable, set\n'
145
+ printf ' review:\n'
146
+ printf ' allow_skip_in_ci: true\n'
147
+ printf ' in .rea/policy.yaml — explicitly authorizing env-var skips in CI.\n'
148
+ printf '\n'
149
+ } >&2
150
+ exit 2
151
+ fi
152
+ fi
153
+
154
+ SKIP_ACTOR=$(cd "$REA_ROOT" && git config user.email 2>/dev/null || echo "")
155
+ if [[ -z "$SKIP_ACTOR" ]]; then
156
+ SKIP_ACTOR=$(cd "$REA_ROOT" && git config user.name 2>/dev/null || echo "")
157
+ fi
158
+ if [[ -z "$SKIP_ACTOR" ]]; then
159
+ {
160
+ printf 'PUSH BLOCKED: REA_SKIP_PUSH_REVIEW requires a git identity.\n'
161
+ printf '\n'
162
+ # shellcheck disable=SC2016 # backticks are literal markdown in user-facing message
163
+ printf ' Neither `git config user.email` nor `git config user.name`\n'
164
+ printf ' is set. The skip audit record would have no actor; refusing\n'
165
+ printf ' to bypass without one.\n'
166
+ printf '\n'
167
+ } >&2
168
+ exit 2
169
+ fi
170
+
171
+ SKIP_BRANCH=$(cd "$REA_ROOT" && git branch --show-current 2>/dev/null || echo "")
172
+ SKIP_HEAD=$(cd "$REA_ROOT" && git rev-parse HEAD 2>/dev/null || echo "")
173
+
174
+ # Codex F2: record OS identity alongside the (mutable, git-sourced) actor so
175
+ # downstream auditors can reconstruct who REALLY invoked the bypass on a
176
+ # shared host. None of these are forgeable from inside the push process alone.
177
+ SKIP_OS_UID=$(id -u 2>/dev/null || echo "")
178
+ SKIP_OS_WHOAMI=$(whoami 2>/dev/null || echo "")
179
+ SKIP_OS_HOST=$(hostname 2>/dev/null || echo "")
180
+ SKIP_OS_PID=$$
181
+ SKIP_OS_PPID=$PPID
182
+ SKIP_OS_PPID_CMD=$(ps -o command= -p "$PPID" 2>/dev/null | head -c 512 || echo "")
183
+ SKIP_OS_TTY=$(tty 2>/dev/null || echo "not-a-tty")
184
+ SKIP_OS_CI="${CI:-}"
185
+
186
+ SKIP_METADATA=$(jq -n \
187
+ --arg head_sha "$SKIP_HEAD" \
188
+ --arg branch "$SKIP_BRANCH" \
189
+ --arg reason "$SKIP_REASON" \
190
+ --arg actor "$SKIP_ACTOR" \
191
+ --arg os_uid "$SKIP_OS_UID" \
192
+ --arg os_whoami "$SKIP_OS_WHOAMI" \
193
+ --arg os_hostname "$SKIP_OS_HOST" \
194
+ --arg os_pid "$SKIP_OS_PID" \
195
+ --arg os_ppid "$SKIP_OS_PPID" \
196
+ --arg os_ppid_cmd "$SKIP_OS_PPID_CMD" \
197
+ --arg os_tty "$SKIP_OS_TTY" \
198
+ --arg os_ci "$SKIP_OS_CI" \
199
+ '{
200
+ head_sha: $head_sha,
201
+ branch: $branch,
202
+ reason: $reason,
203
+ actor: $actor,
204
+ verdict: "skipped",
205
+ os_identity: {
206
+ uid: $os_uid,
207
+ whoami: $os_whoami,
208
+ hostname: $os_hostname,
209
+ pid: $os_pid,
210
+ ppid: $os_ppid,
211
+ ppid_cmd: $os_ppid_cmd,
212
+ tty: $os_tty,
213
+ ci: $os_ci
214
+ }
215
+ }' 2>/dev/null)
216
+
217
+ if [[ -z "$SKIP_METADATA" ]]; then
218
+ {
219
+ printf 'PUSH BLOCKED: REA_SKIP_PUSH_REVIEW could not serialize audit metadata.\n' >&2
220
+ } >&2
221
+ exit 2
222
+ fi
223
+
224
+ REA_ROOT="$REA_ROOT" REA_SKIP_METADATA="$SKIP_METADATA" \
225
+ node --input-type=module -e "
226
+ const mod = await import(process.env.REA_ROOT + '/dist/audit/append.js');
227
+ const metadata = JSON.parse(process.env.REA_SKIP_METADATA);
228
+ await mod.appendAuditRecord(process.env.REA_ROOT, {
229
+ tool_name: 'push.review.skipped',
230
+ server_name: 'rea.escape_hatch',
231
+ status: mod.InvocationStatus.Allowed,
232
+ tier: mod.Tier.Read,
233
+ metadata,
234
+ });
235
+ " 2>/dev/null
236
+ NODE_STATUS=$?
237
+ if [[ "$NODE_STATUS" -ne 0 ]]; then
238
+ {
239
+ printf 'PUSH BLOCKED: REA_SKIP_PUSH_REVIEW audit-append failed (node exit %s).\n' "$NODE_STATUS"
240
+ printf ' Refusing to bypass the push gate without a receipt.\n'
241
+ } >&2
242
+ exit 2
243
+ fi
244
+
245
+ {
246
+ printf '\n'
247
+ printf '== PUSH REVIEW GATE SKIPPED via REA_SKIP_PUSH_REVIEW\n'
248
+ printf ' Reason: %s\n' "$SKIP_REASON"
249
+ printf ' Actor: %s\n' "$SKIP_ACTOR"
250
+ printf ' Branch: %s\n' "${SKIP_BRANCH:-<detached>}"
251
+ printf ' Head: %s\n' "${SKIP_HEAD:-<unknown>}"
252
+ printf ' Audited: .rea/audit.jsonl (tool_name=push.review.skipped)\n'
253
+ printf '\n'
254
+ printf ' This is a gate weakening. Every invocation is permanently audited.\n'
255
+ printf '\n'
256
+ } >&2
257
+ exit 0
258
+ fi
259
+
76
260
  # ── 6. Determine source/target commits for each refspec ──────────────────────
77
261
  # The authoritative source for which commits are being pushed is the pre-push
78
262
  # hook stdin contract: one line per refspec, with fields
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bookedsolid/rea",
3
- "version": "0.4.0",
3
+ "version": "0.6.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)",