@bookedsolid/rea 0.2.1 → 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/THREAT_MODEL.md +100 -29
- package/dist/audit/append.d.ts +21 -8
- package/dist/audit/append.js +48 -83
- package/dist/audit/fs.d.ts +68 -0
- package/dist/audit/fs.js +171 -0
- package/dist/cli/audit.d.ts +40 -0
- package/dist/cli/audit.js +205 -0
- package/dist/cli/doctor.d.ts +19 -4
- package/dist/cli/doctor.js +172 -5
- package/dist/cli/index.js +26 -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/audit/rotator.d.ts +116 -0
- package/dist/gateway/audit/rotator.js +289 -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 +24 -1
- package/dist/gateway/middleware/audit.js +103 -58
- 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 +47 -0
- package/dist/policy/loader.js +47 -0
- package/dist/policy/profiles.d.ts +13 -0
- package/dist/policy/profiles.js +12 -0
- package/dist/policy/types.d.ts +52 -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 +3 -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/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
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
+
import { runAuditRotate, runAuditVerify } from './audit.js';
|
|
3
4
|
import { runCheck } from './check.js';
|
|
4
5
|
import { runDoctor } from './doctor.js';
|
|
5
6
|
import { runFreeze, runUnfreeze } from './freeze.js';
|
|
6
7
|
import { runInit } from './init.js';
|
|
7
8
|
import { runServe } from './serve.js';
|
|
9
|
+
import { runStatus } from './status.js';
|
|
8
10
|
import { runUpgrade } from './upgrade.js';
|
|
9
11
|
import { err, getPkgVersion } from './utils.js';
|
|
10
12
|
async function main() {
|
|
@@ -52,7 +54,7 @@ async function main() {
|
|
|
52
54
|
});
|
|
53
55
|
program
|
|
54
56
|
.command('serve')
|
|
55
|
-
.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.')
|
|
56
58
|
.action(async () => {
|
|
57
59
|
await runServe();
|
|
58
60
|
});
|
|
@@ -76,6 +78,29 @@ async function main() {
|
|
|
76
78
|
.action(() => {
|
|
77
79
|
runCheck();
|
|
78
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
|
+
});
|
|
88
|
+
const audit = program
|
|
89
|
+
.command('audit')
|
|
90
|
+
.description('Audit log operations — rotate and verify .rea/audit.jsonl (G1).');
|
|
91
|
+
audit
|
|
92
|
+
.command('rotate')
|
|
93
|
+
.description('Force-rotate .rea/audit.jsonl now. Preserves hash-chain via a marker record.')
|
|
94
|
+
.action(async () => {
|
|
95
|
+
await runAuditRotate({});
|
|
96
|
+
});
|
|
97
|
+
audit
|
|
98
|
+
.command('verify')
|
|
99
|
+
.description('Re-hash the audit chain; exit 0 on clean, 1 on the first tampered record.')
|
|
100
|
+
.option('--since <file>', 'verify starting at a rotated file (e.g. audit-YYYYMMDD-HHMMSS.jsonl), walking forward through the chain')
|
|
101
|
+
.action(async (opts) => {
|
|
102
|
+
await runAuditVerify({ ...(opts.since !== undefined ? { since: opts.since } : {}) });
|
|
103
|
+
});
|
|
79
104
|
program
|
|
80
105
|
.command('doctor')
|
|
81
106
|
.description('Validate the install: policy parses, .rea/ layout, hooks, Codex plugin.')
|
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.');
|