@bookedsolid/rea 0.10.2 → 0.11.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 +22 -167
- package/agents/codex-adversarial.md +5 -3
- package/commands/codex-review.md +3 -5
- package/dist/audit/append.d.ts +7 -32
- package/dist/audit/append.js +7 -35
- package/dist/cli/audit.d.ts +0 -31
- package/dist/cli/audit.js +5 -74
- package/dist/cli/doctor.js +6 -16
- package/dist/cli/hook.d.ts +48 -0
- package/dist/cli/hook.js +127 -0
- package/dist/cli/index.js +5 -80
- package/dist/cli/init.js +1 -1
- package/dist/cli/install/gitignore.d.ts +2 -2
- package/dist/cli/install/gitignore.js +3 -3
- package/dist/cli/install/pre-push.d.ts +146 -271
- package/dist/cli/install/pre-push.js +471 -2633
- package/dist/cli/install/settings-merge.d.ts +17 -0
- package/dist/cli/install/settings-merge.js +48 -1
- package/dist/cli/upgrade.js +131 -3
- package/dist/config/tier-map.js +18 -25
- package/dist/hooks/push-gate/base.d.ts +57 -0
- package/dist/hooks/push-gate/base.js +77 -0
- package/dist/hooks/push-gate/codex-runner.d.ts +126 -0
- package/dist/hooks/push-gate/codex-runner.js +223 -0
- package/dist/hooks/push-gate/findings.d.ts +68 -0
- package/dist/hooks/push-gate/findings.js +142 -0
- package/dist/hooks/push-gate/halt.d.ts +28 -0
- package/dist/hooks/push-gate/halt.js +49 -0
- package/dist/hooks/push-gate/index.d.ts +90 -0
- package/dist/hooks/push-gate/index.js +351 -0
- package/dist/hooks/push-gate/policy.d.ts +41 -0
- package/dist/hooks/push-gate/policy.js +55 -0
- package/dist/hooks/push-gate/report.d.ts +89 -0
- package/dist/hooks/push-gate/report.js +140 -0
- package/dist/policy/loader.d.ts +10 -10
- package/dist/policy/loader.js +7 -6
- package/dist/policy/types.d.ts +31 -22
- package/package.json +1 -1
- package/dist/cache/review-cache.d.ts +0 -115
- package/dist/cache/review-cache.js +0 -200
- package/dist/cli/cache.d.ts +0 -84
- package/dist/cli/cache.js +0 -150
- package/dist/hooks/review-gate/args.d.ts +0 -126
- package/dist/hooks/review-gate/args.js +0 -315
- package/dist/hooks/review-gate/banner.d.ts +0 -97
- package/dist/hooks/review-gate/banner.js +0 -172
- package/dist/hooks/review-gate/cache-key.d.ts +0 -55
- package/dist/hooks/review-gate/cache-key.js +0 -41
- package/dist/hooks/review-gate/constants.d.ts +0 -26
- package/dist/hooks/review-gate/constants.js +0 -34
- package/dist/hooks/review-gate/errors.d.ts +0 -72
- package/dist/hooks/review-gate/errors.js +0 -100
- package/dist/hooks/review-gate/hash.d.ts +0 -43
- package/dist/hooks/review-gate/hash.js +0 -46
- package/dist/hooks/review-gate/index.d.ts +0 -21
- package/dist/hooks/review-gate/index.js +0 -21
- package/dist/hooks/review-gate/metadata.d.ts +0 -98
- package/dist/hooks/review-gate/metadata.js +0 -158
- package/dist/hooks/review-gate/policy.d.ts +0 -55
- package/dist/hooks/review-gate/policy.js +0 -71
- package/dist/hooks/review-gate/protected-paths.d.ts +0 -46
- package/dist/hooks/review-gate/protected-paths.js +0 -76
- package/hooks/_lib/push-review-core.sh +0 -1250
- package/hooks/commit-review-gate.sh +0 -330
- package/hooks/push-review-gate-git.sh +0 -94
- package/hooks/push-review-gate.sh +0 -92
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OS / agent / repo identity collection for audit records.
|
|
3
|
-
*
|
|
4
|
-
* Closes defect M (0.9.4): the bash core's `jq --arg os_pid "$PID"` wrote
|
|
5
|
-
* the pid as a JSON string; the audit consumer expected an integer, and the
|
|
6
|
-
* schema had to tolerate both. The TS port emits pid/ppid as numbers from
|
|
7
|
-
* day one (the JS runtime's `process.pid` is already a `number`), removing
|
|
8
|
-
* the whole class of jq `--arg` vs `--argjson` confusion.
|
|
9
|
-
*
|
|
10
|
-
* ## Why capture this at all
|
|
11
|
-
*
|
|
12
|
-
* A skip-audit record (`REA_SKIP_PUSH_REVIEW` / `REA_SKIP_CODEX_REVIEW`) is
|
|
13
|
-
* the one place the gate is voluntarily weakened. The actor field (from
|
|
14
|
-
* `git config user.email`) is mutable — a process with write access to
|
|
15
|
-
* `.git/config` can stamp any email it likes. Supplementing that with
|
|
16
|
-
* non-forgeable host-level identity (uid, hostname, pid, ppid, tty, CI
|
|
17
|
-
* flag) gives a forensic investigator something to cross-reference when
|
|
18
|
-
* a skip record turns out to have been unauthorized.
|
|
19
|
-
*
|
|
20
|
-
* ## Determinism + testability
|
|
21
|
-
*
|
|
22
|
-
* Every collector is a plain function over `process` / `os` / `node:child_process`
|
|
23
|
-
* so tests can stub the collector's inputs cleanly. The public API takes no
|
|
24
|
-
* arguments (production use) and the helpers are exported for tests.
|
|
25
|
-
*/
|
|
26
|
-
export interface OsIdentity {
|
|
27
|
-
/** POSIX uid as a string (empty when not available — Windows). */
|
|
28
|
-
uid: string;
|
|
29
|
-
/** `whoami` output — username or empty string. */
|
|
30
|
-
whoami: string;
|
|
31
|
-
/** `hostname` output. */
|
|
32
|
-
hostname: string;
|
|
33
|
-
/** This process's pid — number, not string (defect M). */
|
|
34
|
-
pid: number;
|
|
35
|
-
/** Parent process's pid — number, not string (defect M). */
|
|
36
|
-
ppid: number;
|
|
37
|
-
/** `ps -o command= -p $PPID` output, capped at 512 bytes. */
|
|
38
|
-
ppid_cmd: string;
|
|
39
|
-
/** `tty` output or `"not-a-tty"`. */
|
|
40
|
-
tty: string;
|
|
41
|
-
/** `CI` env var value or empty string. */
|
|
42
|
-
ci: string;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Collect current-process OS identity. Every individual collector degrades
|
|
46
|
-
* to an empty-string fallback on any exception — matching the bash core's
|
|
47
|
-
* `id -u || echo ""`, `whoami || echo ""`, `hostname || echo ""` pattern
|
|
48
|
-
* (push-review-core.sh §420-422, 426). Codex pass-1 on phase 1 flagged
|
|
49
|
-
* the earlier implementation that called `userInfo()` and `hostname()`
|
|
50
|
-
* outside any try/catch and could therefore throw on hosts with broken
|
|
51
|
-
* NSS/passwd lookups, which would silently block the skip-audit path.
|
|
52
|
-
*/
|
|
53
|
-
export declare function collectOsIdentity(): OsIdentity;
|
|
54
|
-
/**
|
|
55
|
-
* Read POSIX uid + whoami. Each field is collected INDEPENDENTLY so a
|
|
56
|
-
* partial-lookup-failure (e.g. LDAP/NSS returns the uid but no passwd
|
|
57
|
-
* entry) still yields one field rather than dropping both. Bash-core
|
|
58
|
-
* parity (push-review-core.sh §420-421): `id -u` and `whoami` are two
|
|
59
|
-
* separate invocations, each with its own `|| echo ""` fallback.
|
|
60
|
-
*
|
|
61
|
-
* Codex pass-2 on phase 1 flagged the prior single-`userInfo()` version:
|
|
62
|
-
* a broken NSS lookup zeroed out both fields at once, weakening forensic
|
|
63
|
-
* metadata on shared hosts. We now prefer the POSIX primitives
|
|
64
|
-
* (`os.userInfo()` internally reads the passwd entry) but isolate the
|
|
65
|
-
* failures so uid and whoami cannot both disappear together when only
|
|
66
|
-
* one of them is actually unavailable.
|
|
67
|
-
*/
|
|
68
|
-
export declare function readUidAndWhoami(): {
|
|
69
|
-
uid: string;
|
|
70
|
-
whoami: string;
|
|
71
|
-
};
|
|
72
|
-
/**
|
|
73
|
-
* Read `hostname` via `os.hostname`. Returns an empty string on any error.
|
|
74
|
-
* Exported for unit tests.
|
|
75
|
-
*/
|
|
76
|
-
export declare function readHostname(): string;
|
|
77
|
-
/**
|
|
78
|
-
* Read `ps -o command= -p <ppid>` safely. Returns the (truncated) command
|
|
79
|
-
* or an empty string on any failure.
|
|
80
|
-
*
|
|
81
|
-
* Security: args are passed as an array, never interpolated into a shell
|
|
82
|
-
* string. `ps` is the only executable spawned.
|
|
83
|
-
*/
|
|
84
|
-
export declare function readPpidCommand(ppid: number): string;
|
|
85
|
-
/**
|
|
86
|
-
* Return the actual controlling tty path (e.g. `/dev/ttys001`) when one
|
|
87
|
-
* exists, or the literal string `not-a-tty` otherwise. Bash-core parity
|
|
88
|
-
* (push-review-core.sh §426): `tty 2>/dev/null || echo "not-a-tty"`.
|
|
89
|
-
*
|
|
90
|
-
* Codex pass-1 on phase 1 flagged the earlier `/dev/tty` literal as a
|
|
91
|
-
* parity regression — the audit consumer expects the real device path so
|
|
92
|
-
* forensic tooling can distinguish tty1 from tty2 on the same host.
|
|
93
|
-
*
|
|
94
|
-
* Implementation: we shell out to `tty(1)` exactly as bash does. On
|
|
95
|
-
* systems without `tty` (distroless, minimal Alpine without coreutils-
|
|
96
|
-
* full) we degrade to `not-a-tty`.
|
|
97
|
-
*/
|
|
98
|
-
export declare function readTty(): string;
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OS / agent / repo identity collection for audit records.
|
|
3
|
-
*
|
|
4
|
-
* Closes defect M (0.9.4): the bash core's `jq --arg os_pid "$PID"` wrote
|
|
5
|
-
* the pid as a JSON string; the audit consumer expected an integer, and the
|
|
6
|
-
* schema had to tolerate both. The TS port emits pid/ppid as numbers from
|
|
7
|
-
* day one (the JS runtime's `process.pid` is already a `number`), removing
|
|
8
|
-
* the whole class of jq `--arg` vs `--argjson` confusion.
|
|
9
|
-
*
|
|
10
|
-
* ## Why capture this at all
|
|
11
|
-
*
|
|
12
|
-
* A skip-audit record (`REA_SKIP_PUSH_REVIEW` / `REA_SKIP_CODEX_REVIEW`) is
|
|
13
|
-
* the one place the gate is voluntarily weakened. The actor field (from
|
|
14
|
-
* `git config user.email`) is mutable — a process with write access to
|
|
15
|
-
* `.git/config` can stamp any email it likes. Supplementing that with
|
|
16
|
-
* non-forgeable host-level identity (uid, hostname, pid, ppid, tty, CI
|
|
17
|
-
* flag) gives a forensic investigator something to cross-reference when
|
|
18
|
-
* a skip record turns out to have been unauthorized.
|
|
19
|
-
*
|
|
20
|
-
* ## Determinism + testability
|
|
21
|
-
*
|
|
22
|
-
* Every collector is a plain function over `process` / `os` / `node:child_process`
|
|
23
|
-
* so tests can stub the collector's inputs cleanly. The public API takes no
|
|
24
|
-
* arguments (production use) and the helpers are exported for tests.
|
|
25
|
-
*/
|
|
26
|
-
import { hostname, userInfo } from 'node:os';
|
|
27
|
-
import { spawnSync } from 'node:child_process';
|
|
28
|
-
/**
|
|
29
|
-
* Collect current-process OS identity. Every individual collector degrades
|
|
30
|
-
* to an empty-string fallback on any exception — matching the bash core's
|
|
31
|
-
* `id -u || echo ""`, `whoami || echo ""`, `hostname || echo ""` pattern
|
|
32
|
-
* (push-review-core.sh §420-422, 426). Codex pass-1 on phase 1 flagged
|
|
33
|
-
* the earlier implementation that called `userInfo()` and `hostname()`
|
|
34
|
-
* outside any try/catch and could therefore throw on hosts with broken
|
|
35
|
-
* NSS/passwd lookups, which would silently block the skip-audit path.
|
|
36
|
-
*/
|
|
37
|
-
export function collectOsIdentity() {
|
|
38
|
-
const { uid, whoami } = readUidAndWhoami();
|
|
39
|
-
const host = readHostname();
|
|
40
|
-
const pid = process.pid;
|
|
41
|
-
const ppid = process.ppid;
|
|
42
|
-
const ppid_cmd = readPpidCommand(ppid);
|
|
43
|
-
const tty = readTty();
|
|
44
|
-
const ci = process.env['CI'] ?? '';
|
|
45
|
-
return { uid, whoami, hostname: host, pid, ppid, ppid_cmd, tty, ci };
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Read POSIX uid + whoami. Each field is collected INDEPENDENTLY so a
|
|
49
|
-
* partial-lookup-failure (e.g. LDAP/NSS returns the uid but no passwd
|
|
50
|
-
* entry) still yields one field rather than dropping both. Bash-core
|
|
51
|
-
* parity (push-review-core.sh §420-421): `id -u` and `whoami` are two
|
|
52
|
-
* separate invocations, each with its own `|| echo ""` fallback.
|
|
53
|
-
*
|
|
54
|
-
* Codex pass-2 on phase 1 flagged the prior single-`userInfo()` version:
|
|
55
|
-
* a broken NSS lookup zeroed out both fields at once, weakening forensic
|
|
56
|
-
* metadata on shared hosts. We now prefer the POSIX primitives
|
|
57
|
-
* (`os.userInfo()` internally reads the passwd entry) but isolate the
|
|
58
|
-
* failures so uid and whoami cannot both disappear together when only
|
|
59
|
-
* one of them is actually unavailable.
|
|
60
|
-
*/
|
|
61
|
-
export function readUidAndWhoami() {
|
|
62
|
-
let uid = '';
|
|
63
|
-
let whoami = '';
|
|
64
|
-
try {
|
|
65
|
-
// Node exposes the raw numeric uid via process.getuid() on POSIX —
|
|
66
|
-
// this goes through the kernel, not through passwd. If it throws
|
|
67
|
-
// (Windows), the fallback is '' and we still try whoami below.
|
|
68
|
-
const getuid = process.getuid;
|
|
69
|
-
if (typeof getuid === 'function') {
|
|
70
|
-
const raw = getuid.call(process);
|
|
71
|
-
if (typeof raw === 'number' && raw >= 0)
|
|
72
|
-
uid = String(raw);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
// swallow — uid stays ''
|
|
77
|
-
}
|
|
78
|
-
try {
|
|
79
|
-
const info = userInfo({ encoding: 'utf8' });
|
|
80
|
-
whoami = info.username ?? '';
|
|
81
|
-
// If the kernel-uid probe above failed but userInfo() succeeded, use
|
|
82
|
-
// its uid as a secondary source.
|
|
83
|
-
if (uid.length === 0 && typeof info.uid === 'number' && info.uid >= 0) {
|
|
84
|
-
uid = String(info.uid);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
// swallow — whoami stays ''
|
|
89
|
-
}
|
|
90
|
-
return { uid, whoami };
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Read `hostname` via `os.hostname`. Returns an empty string on any error.
|
|
94
|
-
* Exported for unit tests.
|
|
95
|
-
*/
|
|
96
|
-
export function readHostname() {
|
|
97
|
-
try {
|
|
98
|
-
return hostname();
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
return '';
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Read `ps -o command= -p <ppid>` safely. Returns the (truncated) command
|
|
106
|
-
* or an empty string on any failure.
|
|
107
|
-
*
|
|
108
|
-
* Security: args are passed as an array, never interpolated into a shell
|
|
109
|
-
* string. `ps` is the only executable spawned.
|
|
110
|
-
*/
|
|
111
|
-
export function readPpidCommand(ppid) {
|
|
112
|
-
if (!Number.isFinite(ppid) || ppid <= 0)
|
|
113
|
-
return '';
|
|
114
|
-
try {
|
|
115
|
-
const result = spawnSync('ps', ['-o', 'command=', '-p', String(ppid)], {
|
|
116
|
-
encoding: 'utf8',
|
|
117
|
-
timeout: 2_000,
|
|
118
|
-
});
|
|
119
|
-
if (result.status !== 0)
|
|
120
|
-
return '';
|
|
121
|
-
const out = (result.stdout ?? '').replace(/\n+$/, '');
|
|
122
|
-
return out.slice(0, 512);
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
return '';
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Return the actual controlling tty path (e.g. `/dev/ttys001`) when one
|
|
130
|
-
* exists, or the literal string `not-a-tty` otherwise. Bash-core parity
|
|
131
|
-
* (push-review-core.sh §426): `tty 2>/dev/null || echo "not-a-tty"`.
|
|
132
|
-
*
|
|
133
|
-
* Codex pass-1 on phase 1 flagged the earlier `/dev/tty` literal as a
|
|
134
|
-
* parity regression — the audit consumer expects the real device path so
|
|
135
|
-
* forensic tooling can distinguish tty1 from tty2 on the same host.
|
|
136
|
-
*
|
|
137
|
-
* Implementation: we shell out to `tty(1)` exactly as bash does. On
|
|
138
|
-
* systems without `tty` (distroless, minimal Alpine without coreutils-
|
|
139
|
-
* full) we degrade to `not-a-tty`.
|
|
140
|
-
*/
|
|
141
|
-
export function readTty() {
|
|
142
|
-
try {
|
|
143
|
-
const result = spawnSync('tty', [], {
|
|
144
|
-
encoding: 'utf8',
|
|
145
|
-
timeout: 2_000,
|
|
146
|
-
stdio: ['inherit', 'pipe', 'pipe'],
|
|
147
|
-
});
|
|
148
|
-
if (result.status === 0) {
|
|
149
|
-
const out = (result.stdout ?? '').replace(/\n+$/, '');
|
|
150
|
-
if (out.length > 0)
|
|
151
|
-
return out;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
catch {
|
|
155
|
-
// fall through to the not-a-tty fallback
|
|
156
|
-
}
|
|
157
|
-
return 'not-a-tty';
|
|
158
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Policy accessors for the review-gate modules.
|
|
3
|
-
*
|
|
4
|
-
* The bash core resolves `review.codex_required` via `node dist/scripts/read-
|
|
5
|
-
* policy-field.js` and evaluates `REA_SKIP_PUSH_REVIEW` / `REA_SKIP_CODEX_REVIEW`
|
|
6
|
-
* directly from env vars. The TS port composes the same behavior over the
|
|
7
|
-
* already-TS `loadPolicy` helper, removing the fork/exec hop and the
|
|
8
|
-
* exit-code-parsing that came with it.
|
|
9
|
-
*
|
|
10
|
-
* Fail-closed: when the policy file is malformed or unreadable, the
|
|
11
|
-
* returned `codex_required` is `true`. This matches the bash core's
|
|
12
|
-
* "treating as true" path (design §2, security carry-forward).
|
|
13
|
-
*/
|
|
14
|
-
import type { Policy } from '../../policy/types.js';
|
|
15
|
-
export interface ResolvedPolicy {
|
|
16
|
-
/** Resolved `review.codex_required`; true when malformed/absent. */
|
|
17
|
-
codex_required: boolean;
|
|
18
|
-
/** Resolved `review.allow_skip_in_ci`; false when absent. */
|
|
19
|
-
allow_skip_in_ci: boolean;
|
|
20
|
-
/** Full policy (undefined when load failed — caller emits a WARN). */
|
|
21
|
-
policy: Policy | null;
|
|
22
|
-
/** Warning text from the loader, if any — surfaced to stderr by the caller. */
|
|
23
|
-
warning: string | null;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Resolve review-related policy fields. Never throws — any error path
|
|
27
|
-
* returns `codex_required: true` with a `warning` populated so the caller
|
|
28
|
-
* can decide whether to echo it.
|
|
29
|
-
*/
|
|
30
|
-
export declare function resolveReviewPolicy(baseDir: string): ResolvedPolicy;
|
|
31
|
-
/**
|
|
32
|
-
* Skip-env evaluation. The bash core reads `REA_SKIP_PUSH_REVIEW` +
|
|
33
|
-
* `REA_SKIP_CODEX_REVIEW` with simple non-empty semantics. We preserve
|
|
34
|
-
* that exactly: any non-empty value triggers the skip path, and the
|
|
35
|
-
* VALUE is used as the skip reason. Empty or unset = no skip.
|
|
36
|
-
*/
|
|
37
|
-
export interface SkipEnv {
|
|
38
|
-
/** `REA_SKIP_PUSH_REVIEW` value or null. */
|
|
39
|
-
push_review_reason: string | null;
|
|
40
|
-
/** `REA_SKIP_CODEX_REVIEW` value or null. */
|
|
41
|
-
codex_review_reason: string | null;
|
|
42
|
-
}
|
|
43
|
-
export declare function readSkipEnv(env?: NodeJS.ProcessEnv): SkipEnv;
|
|
44
|
-
/**
|
|
45
|
-
* True iff `CI` env is set to a non-empty value. The bash core checks
|
|
46
|
-
* `[[ -n "${CI:-}" ]]` — we match that.
|
|
47
|
-
*/
|
|
48
|
-
export declare function isCiContext(env?: NodeJS.ProcessEnv): boolean;
|
|
49
|
-
/**
|
|
50
|
-
* Legacy-bash kill switch (design §11.2). When `REA_LEGACY_PUSH_REVIEW=1`,
|
|
51
|
-
* the CLI entry point delegates to the preserved bash core for one
|
|
52
|
-
* release window. Advertised in `rea doctor`; sunset after 90 days of
|
|
53
|
-
* clean 0.11.x running on canaries.
|
|
54
|
-
*/
|
|
55
|
-
export declare function isLegacyBashKillSwitchOn(env?: NodeJS.ProcessEnv): boolean;
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Policy accessors for the review-gate modules.
|
|
3
|
-
*
|
|
4
|
-
* The bash core resolves `review.codex_required` via `node dist/scripts/read-
|
|
5
|
-
* policy-field.js` and evaluates `REA_SKIP_PUSH_REVIEW` / `REA_SKIP_CODEX_REVIEW`
|
|
6
|
-
* directly from env vars. The TS port composes the same behavior over the
|
|
7
|
-
* already-TS `loadPolicy` helper, removing the fork/exec hop and the
|
|
8
|
-
* exit-code-parsing that came with it.
|
|
9
|
-
*
|
|
10
|
-
* Fail-closed: when the policy file is malformed or unreadable, the
|
|
11
|
-
* returned `codex_required` is `true`. This matches the bash core's
|
|
12
|
-
* "treating as true" path (design §2, security carry-forward).
|
|
13
|
-
*/
|
|
14
|
-
import { loadPolicy } from '../../policy/loader.js';
|
|
15
|
-
/**
|
|
16
|
-
* Resolve review-related policy fields. Never throws — any error path
|
|
17
|
-
* returns `codex_required: true` with a `warning` populated so the caller
|
|
18
|
-
* can decide whether to echo it.
|
|
19
|
-
*/
|
|
20
|
-
export function resolveReviewPolicy(baseDir) {
|
|
21
|
-
try {
|
|
22
|
-
const policy = loadPolicy(baseDir);
|
|
23
|
-
const review = policy.review;
|
|
24
|
-
const codex_required = review?.codex_required === false ? false : true;
|
|
25
|
-
const allow_skip_in_ci = review?.allow_skip_in_ci === true;
|
|
26
|
-
return {
|
|
27
|
-
codex_required,
|
|
28
|
-
allow_skip_in_ci,
|
|
29
|
-
policy,
|
|
30
|
-
warning: null,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
catch (err) {
|
|
34
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
35
|
-
// Policy-file-not-found is a first-run condition for consumers running
|
|
36
|
-
// the gate before `rea init`; treat codex_required as true and surface
|
|
37
|
-
// the warning so the operator knows what state the gate is in.
|
|
38
|
-
return {
|
|
39
|
-
codex_required: true,
|
|
40
|
-
allow_skip_in_ci: false,
|
|
41
|
-
policy: null,
|
|
42
|
-
warning: `review-gate: could not load .rea/policy.yaml — ${msg}. Treating codex_required=true (fail-closed).`,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
export function readSkipEnv(env = process.env) {
|
|
47
|
-
const pr = env['REA_SKIP_PUSH_REVIEW'];
|
|
48
|
-
const cr = env['REA_SKIP_CODEX_REVIEW'];
|
|
49
|
-
return {
|
|
50
|
-
push_review_reason: typeof pr === 'string' && pr.length > 0 ? pr : null,
|
|
51
|
-
codex_review_reason: typeof cr === 'string' && cr.length > 0 ? cr : null,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* True iff `CI` env is set to a non-empty value. The bash core checks
|
|
56
|
-
* `[[ -n "${CI:-}" ]]` — we match that.
|
|
57
|
-
*/
|
|
58
|
-
export function isCiContext(env = process.env) {
|
|
59
|
-
const v = env['CI'];
|
|
60
|
-
return typeof v === 'string' && v.length > 0;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Legacy-bash kill switch (design §11.2). When `REA_LEGACY_PUSH_REVIEW=1`,
|
|
64
|
-
* the CLI entry point delegates to the preserved bash core for one
|
|
65
|
-
* release window. Advertised in `rea doctor`; sunset after 90 days of
|
|
66
|
-
* clean 0.11.x running on canaries.
|
|
67
|
-
*/
|
|
68
|
-
export function isLegacyBashKillSwitchOn(env = process.env) {
|
|
69
|
-
const v = env['REA_LEGACY_PUSH_REVIEW'];
|
|
70
|
-
return typeof v === 'string' && v.length > 0 && v !== '0';
|
|
71
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Protected-path detection. Given a `git diff --name-status` output blob,
|
|
3
|
-
* return true iff any change touches one of the prefixes in
|
|
4
|
-
* `PROTECTED_PATH_PREFIXES`.
|
|
5
|
-
*
|
|
6
|
-
* ## Why this is a dedicated module
|
|
7
|
-
*
|
|
8
|
-
* The bash core uses `awk -v re='^(src/gateway/...)' '{...}'` inline in
|
|
9
|
-
* the main gate loop (push-review-core.sh:904-923). That regex is
|
|
10
|
-
* duplicated in `.husky/pre-push` (the native-git shim) and in at least
|
|
11
|
-
* two places in THREAT_MODEL.md. A single TS helper with a grep-able
|
|
12
|
-
* constant in `constants.ts` removes the drift risk.
|
|
13
|
-
*
|
|
14
|
-
* ## Input shape
|
|
15
|
-
*
|
|
16
|
-
* `git diff --name-status <merge_base>..<local_sha>` output. Each line is:
|
|
17
|
-
* <STATUS>\t<path1>[\t<path2>]
|
|
18
|
-
* STATUS is one letter, possibly followed by a similarity score for
|
|
19
|
-
* rename/copy (`R100`, `C95`). STATUS letters we care about: A, C, D, M,
|
|
20
|
-
* R, T, U — the bash core's `status !~ /^[ACDMRTU]/` filter. We match
|
|
21
|
-
* that exactly.
|
|
22
|
-
*/
|
|
23
|
-
/**
|
|
24
|
-
* Parse a single `git diff --name-status` line and extract the paths that
|
|
25
|
-
* matter for protected-path detection. Rename (`R`) and copy (`C`) lines
|
|
26
|
-
* carry two paths separated by tabs; both are checked against the
|
|
27
|
-
* protected-path set.
|
|
28
|
-
*
|
|
29
|
-
* Returns an empty array for irrelevant status letters or malformed lines.
|
|
30
|
-
*/
|
|
31
|
-
export declare function extractPathsFromStatusLine(line: string): string[];
|
|
32
|
-
/**
|
|
33
|
-
* True iff `path` starts with one of the protected-path prefixes. Exported
|
|
34
|
-
* for unit tests; callers should usually use `diffTouchesProtectedPaths`.
|
|
35
|
-
*/
|
|
36
|
-
export declare function isProtectedPath(filePath: string): boolean;
|
|
37
|
-
/**
|
|
38
|
-
* True iff the given `git diff --name-status` output contains at least
|
|
39
|
-
* one protected-path hit. Returns the set of hit paths (deduped) for
|
|
40
|
-
* audit-record metadata.
|
|
41
|
-
*/
|
|
42
|
-
export interface ProtectedPathScanResult {
|
|
43
|
-
hit: boolean;
|
|
44
|
-
paths: string[];
|
|
45
|
-
}
|
|
46
|
-
export declare function scanNameStatusForProtectedPaths(nameStatusOutput: string): ProtectedPathScanResult;
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Protected-path detection. Given a `git diff --name-status` output blob,
|
|
3
|
-
* return true iff any change touches one of the prefixes in
|
|
4
|
-
* `PROTECTED_PATH_PREFIXES`.
|
|
5
|
-
*
|
|
6
|
-
* ## Why this is a dedicated module
|
|
7
|
-
*
|
|
8
|
-
* The bash core uses `awk -v re='^(src/gateway/...)' '{...}'` inline in
|
|
9
|
-
* the main gate loop (push-review-core.sh:904-923). That regex is
|
|
10
|
-
* duplicated in `.husky/pre-push` (the native-git shim) and in at least
|
|
11
|
-
* two places in THREAT_MODEL.md. A single TS helper with a grep-able
|
|
12
|
-
* constant in `constants.ts` removes the drift risk.
|
|
13
|
-
*
|
|
14
|
-
* ## Input shape
|
|
15
|
-
*
|
|
16
|
-
* `git diff --name-status <merge_base>..<local_sha>` output. Each line is:
|
|
17
|
-
* <STATUS>\t<path1>[\t<path2>]
|
|
18
|
-
* STATUS is one letter, possibly followed by a similarity score for
|
|
19
|
-
* rename/copy (`R100`, `C95`). STATUS letters we care about: A, C, D, M,
|
|
20
|
-
* R, T, U — the bash core's `status !~ /^[ACDMRTU]/` filter. We match
|
|
21
|
-
* that exactly.
|
|
22
|
-
*/
|
|
23
|
-
import { PROTECTED_PATH_PREFIXES } from './constants.js';
|
|
24
|
-
/** Set of single-letter status codes the gate cares about. */
|
|
25
|
-
const RELEVANT_STATUS = new Set(['A', 'C', 'D', 'M', 'R', 'T', 'U']);
|
|
26
|
-
/**
|
|
27
|
-
* Parse a single `git diff --name-status` line and extract the paths that
|
|
28
|
-
* matter for protected-path detection. Rename (`R`) and copy (`C`) lines
|
|
29
|
-
* carry two paths separated by tabs; both are checked against the
|
|
30
|
-
* protected-path set.
|
|
31
|
-
*
|
|
32
|
-
* Returns an empty array for irrelevant status letters or malformed lines.
|
|
33
|
-
*/
|
|
34
|
-
export function extractPathsFromStatusLine(line) {
|
|
35
|
-
if (line.length === 0)
|
|
36
|
-
return [];
|
|
37
|
-
const parts = line.split('\t');
|
|
38
|
-
if (parts.length < 2)
|
|
39
|
-
return [];
|
|
40
|
-
const status = parts[0] ?? '';
|
|
41
|
-
if (status.length === 0)
|
|
42
|
-
return [];
|
|
43
|
-
const statusLetter = status[0];
|
|
44
|
-
if (statusLetter === undefined || !RELEVANT_STATUS.has(statusLetter)) {
|
|
45
|
-
return [];
|
|
46
|
-
}
|
|
47
|
-
return parts.slice(1).filter((p) => p.length > 0);
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* True iff `path` starts with one of the protected-path prefixes. Exported
|
|
51
|
-
* for unit tests; callers should usually use `diffTouchesProtectedPaths`.
|
|
52
|
-
*/
|
|
53
|
-
export function isProtectedPath(filePath) {
|
|
54
|
-
for (const prefix of PROTECTED_PATH_PREFIXES) {
|
|
55
|
-
if (filePath.startsWith(prefix))
|
|
56
|
-
return true;
|
|
57
|
-
// A bare `.rea` or `hooks` path (no trailing slash) is a directory
|
|
58
|
-
// boundary match — `.rea/audit.jsonl` passes, `my-rea.config` does
|
|
59
|
-
// not. startsWith on the prefix-with-slash enforces that naturally.
|
|
60
|
-
}
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
export function scanNameStatusForProtectedPaths(nameStatusOutput) {
|
|
64
|
-
if (nameStatusOutput.length === 0) {
|
|
65
|
-
return { hit: false, paths: [] };
|
|
66
|
-
}
|
|
67
|
-
const hits = new Set();
|
|
68
|
-
for (const line of nameStatusOutput.split('\n')) {
|
|
69
|
-
const paths = extractPathsFromStatusLine(line);
|
|
70
|
-
for (const p of paths) {
|
|
71
|
-
if (isProtectedPath(p))
|
|
72
|
-
hits.add(p);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return { hit: hits.size > 0, paths: Array.from(hits).sort() };
|
|
76
|
-
}
|