@bookedsolid/rea 0.3.0 → 0.4.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 +15 -18
- package/README.md +41 -1
- package/dist/cli/doctor.d.ts +19 -4
- package/dist/cli/doctor.js +172 -5
- package/dist/cli/index.js +9 -1
- package/dist/cli/init.js +93 -7
- package/dist/cli/install/pre-push.d.ts +335 -0
- package/dist/cli/install/pre-push.js +2818 -0
- package/dist/cli/serve.d.ts +64 -0
- package/dist/cli/serve.js +270 -2
- package/dist/cli/status.d.ts +90 -0
- package/dist/cli/status.js +399 -0
- package/dist/cli/utils.d.ts +4 -0
- package/dist/cli/utils.js +4 -0
- package/dist/gateway/circuit-breaker.d.ts +17 -0
- package/dist/gateway/circuit-breaker.js +32 -3
- package/dist/gateway/downstream-pool.d.ts +2 -1
- package/dist/gateway/downstream-pool.js +2 -2
- package/dist/gateway/downstream.d.ts +39 -3
- package/dist/gateway/downstream.js +73 -14
- package/dist/gateway/log.d.ts +122 -0
- package/dist/gateway/log.js +334 -0
- package/dist/gateway/middleware/audit.d.ts +10 -1
- package/dist/gateway/middleware/audit.js +26 -1
- package/dist/gateway/middleware/blocked-paths.d.ts +0 -9
- package/dist/gateway/middleware/blocked-paths.js +439 -67
- package/dist/gateway/middleware/injection.d.ts +218 -13
- package/dist/gateway/middleware/injection.js +433 -51
- package/dist/gateway/middleware/kill-switch.d.ts +10 -1
- package/dist/gateway/middleware/kill-switch.js +20 -1
- package/dist/gateway/observability/metrics.d.ts +125 -0
- package/dist/gateway/observability/metrics.js +321 -0
- package/dist/gateway/server.d.ts +19 -0
- package/dist/gateway/server.js +99 -15
- package/dist/policy/loader.d.ts +13 -0
- package/dist/policy/loader.js +28 -0
- package/dist/policy/profiles.d.ts +13 -0
- package/dist/policy/profiles.js +12 -0
- package/dist/policy/types.d.ts +28 -0
- package/dist/registry/fingerprint.d.ts +73 -0
- package/dist/registry/fingerprint.js +81 -0
- package/dist/registry/fingerprints-store.d.ts +62 -0
- package/dist/registry/fingerprints-store.js +111 -0
- package/dist/registry/interpolate.d.ts +58 -0
- package/dist/registry/interpolate.js +121 -0
- package/dist/registry/loader.d.ts +2 -2
- package/dist/registry/loader.js +22 -1
- package/dist/registry/tofu-gate.d.ts +41 -0
- package/dist/registry/tofu-gate.js +189 -0
- package/dist/registry/tofu.d.ts +111 -0
- package/dist/registry/tofu.js +173 -0
- package/dist/registry/types.d.ts +9 -1
- package/package.json +1 -1
- package/profiles/bst-internal-no-codex.yaml +5 -0
- package/profiles/bst-internal.yaml +7 -0
- package/scripts/tarball-smoke.sh +197 -0
package/.husky/pre-push
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
+
# rea:husky-pre-push-gate v1
|
|
3
|
+
# rea:gate-body-v1
|
|
2
4
|
# .husky/pre-push — rea governance gate for terminal-initiated pushes.
|
|
3
5
|
#
|
|
4
6
|
# Mirrors the logic of `.claude/hooks/push-review-gate.sh` but consumes the
|
|
@@ -20,8 +22,10 @@
|
|
|
20
22
|
# which ran the loop in a subshell — `exit 1` inside the loop aborted the
|
|
21
23
|
# subshell only, and the script then ran `exit 0` and allowed the push. We
|
|
22
24
|
# now feed the loop with a here-doc so it runs in the main shell, and we
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
+
# abort immediately (`exit 1`) on the first blocking refspec. The accumulator
|
|
26
|
+
# pattern (`block_push=1; continue`) was dropped so the text-level detector
|
|
27
|
+
# in `src/cli/install/pre-push.ts` can verify the miss-path is truly blocking
|
|
28
|
+
# without modeling loop-carried flags and post-loop exit blocks.
|
|
25
29
|
|
|
26
30
|
set -eu
|
|
27
31
|
|
|
@@ -63,13 +67,11 @@ if [ -f "$READ_FIELD_JS" ]; then
|
|
|
63
67
|
fi
|
|
64
68
|
fi
|
|
65
69
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
# inside it would only abort that subshell — NOT the push — which was a
|
|
72
|
-
# real governance defect in the pre-review version of this file.
|
|
70
|
+
# Here-doc feeds the loop without creating a subshell, so an `exit 1`
|
|
71
|
+
# inside the loop terminates the hook and blocks the push. A pipeline
|
|
72
|
+
# would run the loop in a subshell and `exit 1` inside it would only
|
|
73
|
+
# abort that subshell — NOT the push — which was a real governance
|
|
74
|
+
# defect in the pre-review version of this file.
|
|
73
75
|
while IFS=' ' read -r local_ref local_sha remote_ref remote_sha; do
|
|
74
76
|
[ -z "${local_sha:-}" ] && continue
|
|
75
77
|
# Branch deletion: local_sha is 40 zeros. Skip protected-path check.
|
|
@@ -103,26 +105,21 @@ while IFS=' ' read -r local_ref local_sha remote_ref remote_sha; do
|
|
|
103
105
|
if [ ! -f "$AUDIT_LOG" ]; then
|
|
104
106
|
printf 'PUSH BLOCKED: protected paths changed but no audit log found at %s\n' "$AUDIT_LOG" >&2
|
|
105
107
|
printf ' Run /codex-review on HEAD %s before pushing.\n' "$local_sha" >&2
|
|
106
|
-
|
|
107
|
-
continue
|
|
108
|
+
exit 1
|
|
108
109
|
fi
|
|
109
110
|
# Require both (a) a `codex.review` tool_name and (b) the exact head_sha
|
|
110
111
|
# on the same JSONL line. The `codex.review` pattern ends with a closing
|
|
111
|
-
# quote, so `codex.review.skipped` never satisfies the gate.
|
|
112
|
+
# quote, so `codex.review.skipped` never satisfies the gate. The first
|
|
113
|
+
# refspec that fails this check aborts the hook — no accumulator needed.
|
|
112
114
|
if ! grep -E '"tool_name":"codex\.review"' "$AUDIT_LOG" 2>/dev/null | \
|
|
113
115
|
grep -qF "\"head_sha\":\"$local_sha\""; then
|
|
114
116
|
printf 'PUSH BLOCKED: protected paths changed — /codex-review required for HEAD %s\n' "$local_sha" >&2
|
|
115
117
|
printf ' Run /codex-review, or set REA_SKIP_CODEX_REVIEW=<reason> to bypass.\n' >&2
|
|
116
|
-
|
|
117
|
-
continue
|
|
118
|
+
exit 1
|
|
118
119
|
fi
|
|
119
120
|
fi
|
|
120
121
|
done <<HOOK_INPUT_EOF
|
|
121
122
|
$INPUT
|
|
122
123
|
HOOK_INPUT_EOF
|
|
123
124
|
|
|
124
|
-
if [ "$block_push" -ne 0 ]; then
|
|
125
|
-
exit 1
|
|
126
|
-
fi
|
|
127
|
-
|
|
128
125
|
exit 0
|
package/README.md
CHANGED
|
@@ -66,7 +66,10 @@ to build a separate package that composes with REA.
|
|
|
66
66
|
`policy.yaml` is the maximum surface area — one outbound POST, opt-in.
|
|
67
67
|
- **Not a daemon supervisor.** `rea serve` is started by Claude Code via
|
|
68
68
|
`.mcp.json`. Claude Code owns the lifecycle. There is no `rea start`,
|
|
69
|
-
no `rea stop`, no
|
|
69
|
+
no `rea stop`, no systemd unit. A short-lived `.rea/serve.pid`
|
|
70
|
+
breadcrumb is written at startup so `rea status` can detect a live
|
|
71
|
+
gateway — it is removed on graceful shutdown and never used for
|
|
72
|
+
locking or lifecycle management.
|
|
70
73
|
- **Not a hosted service.** There is no REA Cloud, no SaaS tier, no
|
|
71
74
|
multi-token workstreams, no workload isolation platform.
|
|
72
75
|
- **Not a 70-agent roster.** 10 curated agents ship in the package. Four
|
|
@@ -132,6 +135,43 @@ install, `.mcp.json` gateway wiring, Codex plugin availability, and the
|
|
|
132
135
|
integrity of the audit hash chain. It returns a pass/fail summary with
|
|
133
136
|
specific remediation hints.
|
|
134
137
|
|
|
138
|
+
### 4. Watch the running gateway
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
rea status # human-readable summary
|
|
142
|
+
rea status --json # JSON — pipe to jq
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`rea status` is the live-process view. It reads the pidfile written by
|
|
146
|
+
`rea serve`, verifies the pid is alive, and surfaces the session id,
|
|
147
|
+
policy summary (profile, autonomy, HALT state), and audit stats (lines,
|
|
148
|
+
last timestamp, whether the tail record's hash looks well-formed). Use
|
|
149
|
+
`rea check` when you want the pure on-disk view without probing for a
|
|
150
|
+
live process.
|
|
151
|
+
|
|
152
|
+
### 5. Optional Prometheus `/metrics` endpoint
|
|
153
|
+
|
|
154
|
+
`rea serve` can expose a loopback-only Prometheus endpoint when the
|
|
155
|
+
`REA_METRICS_PORT` environment variable is set:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
REA_METRICS_PORT=9464 rea serve
|
|
159
|
+
# in another shell
|
|
160
|
+
curl http://127.0.0.1:9464/metrics
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Metrics exposed: per-downstream call and error counters, in-flight
|
|
164
|
+
gauge, audit-lines-appended counter, circuit-breaker state gauge, and a
|
|
165
|
+
seconds-since-last-HALT-check gauge. The listener binds to `127.0.0.1`
|
|
166
|
+
only, serves only `GET /metrics` (everything else is a fixed-body 404),
|
|
167
|
+
and never binds by default — "no silent listeners" is a design rule.
|
|
168
|
+
There is no TLS; scrape through SSH/a reverse proxy if you need
|
|
169
|
+
cross-host access.
|
|
170
|
+
|
|
171
|
+
Set `REA_LOG_LEVEL=debug` for verbose gateway logs; the default is
|
|
172
|
+
`info`. Records are JSON lines on a non-TTY stderr and pretty-printed
|
|
173
|
+
on an interactive terminal.
|
|
174
|
+
|
|
135
175
|
## Architecture
|
|
136
176
|
|
|
137
177
|
### Middleware chain
|
package/dist/cli/doctor.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type CodexProbeState } from '../gateway/observability/codex-probe.js';
|
|
2
|
+
import { type PrePushDoctorState } from './install/pre-push.js';
|
|
2
3
|
export interface CheckResult {
|
|
3
4
|
label: string;
|
|
4
5
|
/**
|
|
@@ -9,6 +10,15 @@ export interface CheckResult {
|
|
|
9
10
|
status: 'pass' | 'fail' | 'warn' | 'info';
|
|
10
11
|
detail?: string;
|
|
11
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* G7: report the TOFU fingerprint-store state. Pass = every enabled server
|
|
15
|
+
* in the registry has a matching stored fingerprint. Warn = at least one
|
|
16
|
+
* server would be first-seen or drifted at next `rea serve`. Info = no
|
|
17
|
+
* enabled servers (nothing to fingerprint). Fail only for unreadable store.
|
|
18
|
+
*
|
|
19
|
+
* Exported so tests can drive this without spinning up the full `runDoctor`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function checkFingerprintStore(baseDir: string): Promise<CheckResult>;
|
|
12
22
|
/**
|
|
13
23
|
* Translate a `CodexProbeState` into two doctor CheckResults: one for
|
|
14
24
|
* responsiveness (pass/warn) and one informational line about the last
|
|
@@ -22,11 +32,16 @@ export declare function checksFromProbeState(state: CodexProbeState): CheckResul
|
|
|
22
32
|
* `runDoctor`.
|
|
23
33
|
*
|
|
24
34
|
* `codexProbeState` is consulted ONLY when Codex is required by policy.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
35
|
+
* `prePushState` is the pre-computed G6 pre-push inspection; when omitted
|
|
36
|
+
* the pre-push check is skipped entirely (older call sites that don't yet
|
|
37
|
+
* thread the state through keep working without behavioural change).
|
|
38
|
+
* Callers that already have fresh state (e.g. `runDoctor`) should pass
|
|
39
|
+
* both; callers that don't (e.g. unit tests of the existing doctor
|
|
40
|
+
* surface) can omit them and those checks are skipped.
|
|
41
|
+
*
|
|
42
|
+
* `activeForeign` always yields `fail` — a foreign hook bypassing the gate is a hard governance gap.
|
|
28
43
|
*/
|
|
29
|
-
export declare function collectChecks(baseDir: string, codexProbeState?: CodexProbeState): CheckResult[];
|
|
44
|
+
export declare function collectChecks(baseDir: string, codexProbeState?: CodexProbeState, prePushState?: PrePushDoctorState): CheckResult[];
|
|
30
45
|
export interface RunDoctorOptions {
|
|
31
46
|
/** When true, print a 7-day telemetry summary after the checks (G11.5). */
|
|
32
47
|
metrics?: boolean;
|
package/dist/cli/doctor.js
CHANGED
|
@@ -2,7 +2,10 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { loadPolicy } from '../policy/loader.js';
|
|
4
4
|
import { loadRegistry } from '../registry/loader.js';
|
|
5
|
+
import { loadFingerprintStore } from '../registry/fingerprints-store.js';
|
|
6
|
+
import { fingerprintServer } from '../registry/fingerprint.js';
|
|
5
7
|
import { CodexProbe, } from '../gateway/observability/codex-probe.js';
|
|
8
|
+
import { inspectPrePushState, } from './install/pre-push.js';
|
|
6
9
|
import { summarizeTelemetry } from '../gateway/observability/codex-telemetry.js';
|
|
7
10
|
import { CLAUDE_MD_MANIFEST_PATH, SETTINGS_MANIFEST_PATH, enumerateCanonicalFiles, } from './install/canonical.js';
|
|
8
11
|
import { buildFragment } from './install/claude-md.js';
|
|
@@ -40,6 +43,69 @@ function checkPolicyParses(baseDir, policyPath) {
|
|
|
40
43
|
};
|
|
41
44
|
}
|
|
42
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* G7: report the TOFU fingerprint-store state. Pass = every enabled server
|
|
48
|
+
* in the registry has a matching stored fingerprint. Warn = at least one
|
|
49
|
+
* server would be first-seen or drifted at next `rea serve`. Info = no
|
|
50
|
+
* enabled servers (nothing to fingerprint). Fail only for unreadable store.
|
|
51
|
+
*
|
|
52
|
+
* Exported so tests can drive this without spinning up the full `runDoctor`.
|
|
53
|
+
*/
|
|
54
|
+
export async function checkFingerprintStore(baseDir) {
|
|
55
|
+
const label = 'fingerprint store';
|
|
56
|
+
let registry;
|
|
57
|
+
try {
|
|
58
|
+
registry = loadRegistry(baseDir);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return {
|
|
62
|
+
label,
|
|
63
|
+
status: 'info',
|
|
64
|
+
detail: 'registry missing — no fingerprints to compare',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const enabled = registry.servers.filter((s) => s.enabled);
|
|
68
|
+
if (enabled.length === 0) {
|
|
69
|
+
return { label, status: 'info', detail: 'no enabled servers to fingerprint' };
|
|
70
|
+
}
|
|
71
|
+
let store;
|
|
72
|
+
try {
|
|
73
|
+
store = await loadFingerprintStore(baseDir);
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
return {
|
|
77
|
+
label,
|
|
78
|
+
status: 'fail',
|
|
79
|
+
detail: e instanceof Error ? e.message : String(e),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
let firstSeen = 0;
|
|
83
|
+
let drifted = 0;
|
|
84
|
+
for (const s of enabled) {
|
|
85
|
+
const stored = store.servers[s.name];
|
|
86
|
+
if (stored === undefined)
|
|
87
|
+
firstSeen += 1;
|
|
88
|
+
else if (stored !== fingerprintServer(s))
|
|
89
|
+
drifted += 1;
|
|
90
|
+
}
|
|
91
|
+
if (firstSeen === 0 && drifted === 0) {
|
|
92
|
+
return {
|
|
93
|
+
label,
|
|
94
|
+
status: 'pass',
|
|
95
|
+
detail: `${enabled.length} server(s) trusted`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const parts = [];
|
|
99
|
+
if (firstSeen > 0)
|
|
100
|
+
parts.push(`${firstSeen} first-seen`);
|
|
101
|
+
if (drifted > 0)
|
|
102
|
+
parts.push(`${drifted} drifted`);
|
|
103
|
+
return {
|
|
104
|
+
label,
|
|
105
|
+
status: 'warn',
|
|
106
|
+
detail: `${parts.join(', ')} — next \`rea serve\` will block drift (set REA_ACCEPT_DRIFT=<name> to accept)`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
43
109
|
function checkRegistryParses(baseDir, registryPath) {
|
|
44
110
|
if (!fs.existsSync(registryPath)) {
|
|
45
111
|
return {
|
|
@@ -198,6 +264,86 @@ function checkCommitMsgHook(baseDir) {
|
|
|
198
264
|
};
|
|
199
265
|
}
|
|
200
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* G6 — Verify at least one pre-push hook is installed and executable AND
|
|
269
|
+
* actually wires the protected-path review gate.
|
|
270
|
+
*
|
|
271
|
+
* Three install shapes are acceptable:
|
|
272
|
+
* 1. `.git/hooks/pre-push` — vanilla git (no hooksPath). Must carry the
|
|
273
|
+
* rea fallback marker or delegate to `push-review-gate.sh`.
|
|
274
|
+
* 2. `${core.hooksPath}/pre-push` — husky 9 or custom hooksPath. Same
|
|
275
|
+
* governance rule.
|
|
276
|
+
* 3. `.husky/pre-push` is present on disk but only counts if husky has
|
|
277
|
+
* configured `core.hooksPath=.husky`. A `.husky/pre-push` with an
|
|
278
|
+
* unconfigured hooksPath is dead weight; we do NOT treat it as
|
|
279
|
+
* sufficient.
|
|
280
|
+
*
|
|
281
|
+
* Two possible outcomes:
|
|
282
|
+
* - `pass`: active hook exists, is executable, and governance-carrying
|
|
283
|
+
* (rea-managed marker or direct gate delegation).
|
|
284
|
+
* - `fail`: no active hook, active file is non-executable, OR the active
|
|
285
|
+
* hook does not reference `.claude/hooks/push-review-gate.sh`. The last
|
|
286
|
+
* case is the "silent bypass" state — a lint-only husky hook or a
|
|
287
|
+
* pre-existing repo hook that bypasses the Codex audit gate entirely.
|
|
288
|
+
* Always a hard fail; `rea init` can install the fallback if the user
|
|
289
|
+
* removes or updates the existing hook.
|
|
290
|
+
*
|
|
291
|
+
* "Executable" is defined by any user/group/other exec bit, matching
|
|
292
|
+
* `checkHooksInstalled`.
|
|
293
|
+
*/
|
|
294
|
+
function checkPrePushHook(state) {
|
|
295
|
+
if (state.ok) {
|
|
296
|
+
const active = state.candidates.find((c) => c.path === state.activePath);
|
|
297
|
+
const kind = active?.reaManaged === true
|
|
298
|
+
? 'rea-managed'
|
|
299
|
+
: active?.delegatesToGate === true
|
|
300
|
+
? 'external (delegates to push-review-gate.sh)'
|
|
301
|
+
: 'external';
|
|
302
|
+
const detail = active !== undefined ? `${kind} at ${active.path}` : undefined;
|
|
303
|
+
return detail !== undefined
|
|
304
|
+
? { label: 'pre-push hook installed', status: 'pass', detail }
|
|
305
|
+
: { label: 'pre-push hook installed', status: 'pass' };
|
|
306
|
+
}
|
|
307
|
+
if (state.activeForeign) {
|
|
308
|
+
// Executable file exists at the active path but does not carry
|
|
309
|
+
// governance — the parser could not confirm the review gate is
|
|
310
|
+
// invoked unconditionally. Always a hard fail.
|
|
311
|
+
//
|
|
312
|
+
// R13 F3: previously, a substring match of the gate path in the hook
|
|
313
|
+
// downgraded this to WARN. That was unsafe — any comment, echo, or
|
|
314
|
+
// dead string mentioning the path would mask a silent-bypass hook.
|
|
315
|
+
// The classifier now fails closed: either the structural parser
|
|
316
|
+
// (`referencesReviewGate` in `pre-push.ts`) recognizes a real
|
|
317
|
+
// invocation, or doctor reports fail.
|
|
318
|
+
return {
|
|
319
|
+
label: 'pre-push hook installed',
|
|
320
|
+
status: 'fail',
|
|
321
|
+
detail: `active pre-push at ${state.activePath} is present and executable but does NOT ` +
|
|
322
|
+
`reference \`.claude/hooks/push-review-gate.sh\` — the protected-path ` +
|
|
323
|
+
`Codex audit gate is silently bypassed. Either add ` +
|
|
324
|
+
'`exec .claude/hooks/push-review-gate.sh "$@"` to the existing hook, or ' +
|
|
325
|
+
'remove it and re-run `rea init` to install the fallback.',
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
const present = state.candidates
|
|
329
|
+
.filter((c) => c.exists)
|
|
330
|
+
.map((c) => `${c.path}${c.executable ? '' : ' (not executable)'}`);
|
|
331
|
+
if (present.length > 0) {
|
|
332
|
+
return {
|
|
333
|
+
label: 'pre-push hook installed',
|
|
334
|
+
status: 'fail',
|
|
335
|
+
detail: `no active pre-push hook. Files on disk: ${present.join(', ')}. ` +
|
|
336
|
+
'Run `rea init` to install the fallback, or configure `core.hooksPath=.husky` ' +
|
|
337
|
+
'if you are using husky.',
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
label: 'pre-push hook installed',
|
|
342
|
+
status: 'fail',
|
|
343
|
+
detail: 'no pre-push hook found in `.git/hooks/`, configured `core.hooksPath`, or `.husky/`. ' +
|
|
344
|
+
'Run `rea init` to install the fallback.',
|
|
345
|
+
};
|
|
346
|
+
}
|
|
201
347
|
function checkCodexAgent(baseDir) {
|
|
202
348
|
const agentPath = path.join(baseDir, '.claude', 'agents', 'codex-adversarial.md');
|
|
203
349
|
if (fs.existsSync(agentPath))
|
|
@@ -277,11 +423,16 @@ function codexRequiredFromPolicy(baseDir) {
|
|
|
277
423
|
* `runDoctor`.
|
|
278
424
|
*
|
|
279
425
|
* `codexProbeState` is consulted ONLY when Codex is required by policy.
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
426
|
+
* `prePushState` is the pre-computed G6 pre-push inspection; when omitted
|
|
427
|
+
* the pre-push check is skipped entirely (older call sites that don't yet
|
|
428
|
+
* thread the state through keep working without behavioural change).
|
|
429
|
+
* Callers that already have fresh state (e.g. `runDoctor`) should pass
|
|
430
|
+
* both; callers that don't (e.g. unit tests of the existing doctor
|
|
431
|
+
* surface) can omit them and those checks are skipped.
|
|
432
|
+
*
|
|
433
|
+
* `activeForeign` always yields `fail` — a foreign hook bypassing the gate is a hard governance gap.
|
|
283
434
|
*/
|
|
284
|
-
export function collectChecks(baseDir, codexProbeState) {
|
|
435
|
+
export function collectChecks(baseDir, codexProbeState, prePushState) {
|
|
285
436
|
const policyPath = reaPath(baseDir, POLICY_FILE);
|
|
286
437
|
const registryPath = reaPath(baseDir, REGISTRY_FILE);
|
|
287
438
|
const reaDirPath = path.join(baseDir, REA_DIR);
|
|
@@ -294,6 +445,9 @@ export function collectChecks(baseDir, codexProbeState) {
|
|
|
294
445
|
checkSettingsJson(baseDir),
|
|
295
446
|
checkCommitMsgHook(baseDir),
|
|
296
447
|
];
|
|
448
|
+
if (prePushState !== undefined) {
|
|
449
|
+
checks.push(checkPrePushHook(prePushState));
|
|
450
|
+
}
|
|
297
451
|
if (codexRequiredFromPolicy(baseDir)) {
|
|
298
452
|
checks.push(checkCodexAgent(baseDir), checkCodexCommand(baseDir));
|
|
299
453
|
if (codexProbeState !== undefined) {
|
|
@@ -486,7 +640,20 @@ export async function runDoctor(opts = {}) {
|
|
|
486
640
|
probeState = undefined;
|
|
487
641
|
}
|
|
488
642
|
}
|
|
489
|
-
|
|
643
|
+
// G6 — inspect pre-push state. Never throws; unreadable files downgrade
|
|
644
|
+
// individual candidates but never break the whole check.
|
|
645
|
+
let prePushState;
|
|
646
|
+
try {
|
|
647
|
+
prePushState = await inspectPrePushState(baseDir);
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
prePushState = undefined;
|
|
651
|
+
}
|
|
652
|
+
const checks = collectChecks(baseDir, probeState, prePushState);
|
|
653
|
+
// G7: async fingerprint-store check. Kept out of `collectChecks` so the
|
|
654
|
+
// existing sync contract stays intact for downstream consumers; appended
|
|
655
|
+
// here so runDoctor surfaces it inline.
|
|
656
|
+
checks.push(await checkFingerprintStore(baseDir));
|
|
490
657
|
console.log('');
|
|
491
658
|
log(`Doctor — ${baseDir}`);
|
|
492
659
|
console.log('');
|
package/dist/cli/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { runDoctor } from './doctor.js';
|
|
|
6
6
|
import { runFreeze, runUnfreeze } from './freeze.js';
|
|
7
7
|
import { runInit } from './init.js';
|
|
8
8
|
import { runServe } from './serve.js';
|
|
9
|
+
import { runStatus } from './status.js';
|
|
9
10
|
import { runUpgrade } from './upgrade.js';
|
|
10
11
|
import { err, getPkgVersion } from './utils.js';
|
|
11
12
|
async function main() {
|
|
@@ -53,7 +54,7 @@ async function main() {
|
|
|
53
54
|
});
|
|
54
55
|
program
|
|
55
56
|
.command('serve')
|
|
56
|
-
.description('Start the MCP gateway
|
|
57
|
+
.description('Start the MCP gateway — stdio server that proxies downstream MCPs declared in .rea/registry.yaml through the middleware chain.')
|
|
57
58
|
.action(async () => {
|
|
58
59
|
await runServe();
|
|
59
60
|
});
|
|
@@ -77,6 +78,13 @@ async function main() {
|
|
|
77
78
|
.action(() => {
|
|
78
79
|
runCheck();
|
|
79
80
|
});
|
|
81
|
+
program
|
|
82
|
+
.command('status')
|
|
83
|
+
.description('Running-process view — is `rea serve` live for this project? Session id, policy summary, audit stats. Use `rea check` for the on-disk view.')
|
|
84
|
+
.option('--json', 'emit JSON instead of the pretty table (composes with jq)')
|
|
85
|
+
.action((opts) => {
|
|
86
|
+
runStatus({ json: opts.json });
|
|
87
|
+
});
|
|
80
88
|
const audit = program
|
|
81
89
|
.command('audit')
|
|
82
90
|
.description('Audit log operations — rotate and verify .rea/audit.jsonl (G1).');
|
package/dist/cli/init.js
CHANGED
|
@@ -7,6 +7,8 @@ import { HARD_DEFAULTS, loadProfile, mergeProfiles } from '../policy/profiles.js
|
|
|
7
7
|
import { copyArtifacts } from './install/copy.js';
|
|
8
8
|
import { canonicalSettingsSubsetHash, defaultDesiredHooks, mergeSettings, readSettings, writeSettingsAtomic, } from './install/settings-merge.js';
|
|
9
9
|
import { installCommitMsgHook } from './install/commit-msg.js';
|
|
10
|
+
import { installPrePushFallback } from './install/pre-push.js';
|
|
11
|
+
import { CodexProbe } from '../gateway/observability/codex-probe.js';
|
|
10
12
|
import { buildFragment, writeClaudeMdFragment } from './install/claude-md.js';
|
|
11
13
|
import { CLAUDE_MD_MANIFEST_PATH, SETTINGS_MANIFEST_PATH, enumerateCanonicalFiles, } from './install/canonical.js';
|
|
12
14
|
import { writeManifestAtomic } from './install/manifest-io.js';
|
|
@@ -188,6 +190,48 @@ async function runWizard(options, targetDir, reagentPolicyPath, layeredBase) {
|
|
|
188
190
|
reagentNotices: [],
|
|
189
191
|
};
|
|
190
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* G6 — Codex install-assist probe.
|
|
195
|
+
*
|
|
196
|
+
* Runs a single {@link CodexProbe} attempt and prints a guidance block when
|
|
197
|
+
* the CLI is NOT responsive. Behavior:
|
|
198
|
+
*
|
|
199
|
+
* - `cli_responsive === true` → print a single-line "Codex CLI detected"
|
|
200
|
+
* acknowledgement (informational, not verbose).
|
|
201
|
+
* - `cli_responsive === false` → print a 4-line install guidance block
|
|
202
|
+
* naming the Claude Code helper that installs Codex.
|
|
203
|
+
*
|
|
204
|
+
* Failure of the probe itself is never fatal — a hung CLI must not stall
|
|
205
|
+
* `rea init`. The probe class already caps each subcommand at 2s/5s. Any
|
|
206
|
+
* throw bubbling out here is caught and treated as "not responsive".
|
|
207
|
+
*
|
|
208
|
+
* We deliberately reference the user-visible helper path (`/codex:setup`)
|
|
209
|
+
* rather than shelling out to install Codex ourselves. `rea init` does not
|
|
210
|
+
* auto-install third-party tooling; the operator signs off.
|
|
211
|
+
*/
|
|
212
|
+
async function printCodexInstallAssist() {
|
|
213
|
+
let responsive = false;
|
|
214
|
+
let versionLine;
|
|
215
|
+
try {
|
|
216
|
+
const state = await new CodexProbe().probe();
|
|
217
|
+
responsive = state.cli_responsive;
|
|
218
|
+
versionLine = state.version;
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// probe() is documented as never-throws, but belt-and-suspenders.
|
|
222
|
+
responsive = false;
|
|
223
|
+
}
|
|
224
|
+
console.log('');
|
|
225
|
+
if (responsive) {
|
|
226
|
+
const suffix = versionLine !== undefined ? ` (${versionLine})` : '';
|
|
227
|
+
console.log(`Codex CLI detected${suffix}.`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
console.log('Codex CLI not detected on PATH.');
|
|
231
|
+
console.log(' Adversarial review via `/codex-review` requires the Codex plugin.');
|
|
232
|
+
console.log(' Install via the Claude Code Codex plugin helper: `/codex:setup`,');
|
|
233
|
+
console.log(' or set `review.codex_required: false` in .rea/policy.yaml to opt out.');
|
|
234
|
+
}
|
|
191
235
|
function writePolicyYaml(targetDir, config, layered) {
|
|
192
236
|
const policyPath = path.join(targetDir, REA_DIR, POLICY_FILE);
|
|
193
237
|
const installedBy = process.env.USER ?? os.userInfo().username ?? 'unknown';
|
|
@@ -213,6 +257,14 @@ function writePolicyYaml(targetDir, config, layered) {
|
|
|
213
257
|
if (layered.injection_detection !== undefined) {
|
|
214
258
|
lines.push(`injection_detection: ${layered.injection_detection}`);
|
|
215
259
|
}
|
|
260
|
+
// G9: preserve `injection.suspicious_blocks_writes` when the layered profile
|
|
261
|
+
// pinned it (bst-internal/bst-internal-no-codex pin `true`). External profiles
|
|
262
|
+
// leave this unset so the policy loader's schema default (`false`) applies,
|
|
263
|
+
// which keeps 0.2.x consumers from being silently tightened on upgrade.
|
|
264
|
+
if (layered.injection?.suspicious_blocks_writes !== undefined) {
|
|
265
|
+
lines.push(`injection:`);
|
|
266
|
+
lines.push(` suspicious_blocks_writes: ${layered.injection.suspicious_blocks_writes ? 'true' : 'false'}`);
|
|
267
|
+
}
|
|
216
268
|
if (layered.context_protection !== undefined) {
|
|
217
269
|
lines.push(`context_protection:`);
|
|
218
270
|
const cp = layered.context_protection;
|
|
@@ -243,6 +295,21 @@ function writeRegistryYaml(targetDir) {
|
|
|
243
295
|
const content = [
|
|
244
296
|
`# .rea/registry.yaml — downstream MCP servers proxied through rea serve.`,
|
|
245
297
|
`# Every entry below is subject to the same middleware chain as native tool calls.`,
|
|
298
|
+
`#`,
|
|
299
|
+
`# env: values support \${VAR} interpolation against rea-serve's own process.env.`,
|
|
300
|
+
`# If a referenced var is unset at startup, the affected server fails to start`,
|
|
301
|
+
`# (the rest of the gateway still comes up). Only the curly-brace form is`,
|
|
302
|
+
`# supported — no $VAR, no defaults, no command substitution.`,
|
|
303
|
+
`#`,
|
|
304
|
+
`# Example (uncomment and export the vars in your shell before running \`rea serve\`):`,
|
|
305
|
+
`#`,
|
|
306
|
+
`# - name: discord-ops`,
|
|
307
|
+
`# command: npx`,
|
|
308
|
+
`# args: ['-y', 'discord-ops@latest']`,
|
|
309
|
+
`# env:`,
|
|
310
|
+
`# BOOKED_DISCORD_BOT_TOKEN: '\${BOOKED_DISCORD_BOT_TOKEN}'`,
|
|
311
|
+
`# CLARITY_DISCORD_BOT_TOKEN: '\${CLARITY_DISCORD_BOT_TOKEN}'`,
|
|
312
|
+
`# enabled: false # flip to true after exporting the tokens`,
|
|
246
313
|
`version: "1"`,
|
|
247
314
|
`servers: []`,
|
|
248
315
|
``,
|
|
@@ -387,6 +454,7 @@ export async function runInit(options) {
|
|
|
387
454
|
const mergeResult = mergeSettings(settings, desired);
|
|
388
455
|
await writeSettingsAtomic(settingsPath, mergeResult.merged);
|
|
389
456
|
const commitMsgResult = await installCommitMsgHook(targetDir);
|
|
457
|
+
const prePushResult = await installPrePushFallback(targetDir);
|
|
390
458
|
const fragmentInput = {
|
|
391
459
|
policyPath: `.${path.sep}rea${path.sep}policy.yaml`.replace(/\\/g, '/'),
|
|
392
460
|
profile: config.profile,
|
|
@@ -410,6 +478,14 @@ export async function runInit(options) {
|
|
|
410
478
|
console.log(` + ${path.relative(targetDir, commitMsgResult.gitHook)}`);
|
|
411
479
|
if (commitMsgResult.huskyHook)
|
|
412
480
|
console.log(` + ${path.relative(targetDir, commitMsgResult.huskyHook)}`);
|
|
481
|
+
if (prePushResult.written !== undefined) {
|
|
482
|
+
const verb = prePushResult.decision.action === 'refresh' ? '~' : '+';
|
|
483
|
+
console.log(` ${verb} ${path.relative(targetDir, prePushResult.written)} (pre-push fallback)`);
|
|
484
|
+
}
|
|
485
|
+
else if (prePushResult.decision.action === 'skip' &&
|
|
486
|
+
prePushResult.decision.reason === 'active-pre-push-present') {
|
|
487
|
+
console.log(` = ${path.relative(targetDir, prePushResult.decision.hookPath)} (active pre-push already present — skipped fallback)`);
|
|
488
|
+
}
|
|
413
489
|
console.log(` ${mdResult.replaced ? '~' : '+'} ${path.relative(targetDir, mdResult.path)} (fragment ${mdResult.replaced ? 'replaced' : 'written'})`);
|
|
414
490
|
console.log(` + ${path.relative(targetDir, manifestPath)}`);
|
|
415
491
|
if (mergeResult.warnings.length > 0) {
|
|
@@ -419,15 +495,25 @@ export async function runInit(options) {
|
|
|
419
495
|
}
|
|
420
496
|
for (const w of commitMsgResult.warnings)
|
|
421
497
|
warn(w);
|
|
498
|
+
for (const w of prePushResult.warnings)
|
|
499
|
+
warn(w);
|
|
422
500
|
for (const n of config.reagentNotices)
|
|
423
501
|
warn(n);
|
|
424
|
-
// G11.4:
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
|
|
502
|
+
// G6 + G11.4: Codex install-assist.
|
|
503
|
+
//
|
|
504
|
+
// Split by codex_required:
|
|
505
|
+
// - codex_required=true → probe the CLI; if it is not responsive, print
|
|
506
|
+
// a clear "install Codex" guidance block so the
|
|
507
|
+
// operator knows why /codex-review will fail.
|
|
508
|
+
// - codex_required=false → skip the probe entirely and print the
|
|
509
|
+
// existing "Codex review disabled" notice.
|
|
510
|
+
// Probing here is pointless (wasted 2s) and
|
|
511
|
+
// actively confusing — no-codex mode is a
|
|
512
|
+
// supported first-class configuration.
|
|
513
|
+
if (config.codexRequired) {
|
|
514
|
+
await printCodexInstallAssist();
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
431
517
|
console.log('');
|
|
432
518
|
console.log('Codex review disabled. ClaudeSelfReviewer will be used.');
|
|
433
519
|
console.log(' Set review.codex_required: true in .rea/policy.yaml to re-enable.');
|