@bookedsolid/rea 0.10.3 → 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.
Files changed (74) hide show
  1. package/.husky/pre-push +22 -167
  2. package/agents/codex-adversarial.md +5 -3
  3. package/commands/codex-review.md +3 -5
  4. package/dist/audit/append.d.ts +7 -32
  5. package/dist/audit/append.js +7 -35
  6. package/dist/cli/audit.d.ts +0 -31
  7. package/dist/cli/audit.js +5 -74
  8. package/dist/cli/doctor.js +6 -16
  9. package/dist/cli/hook.d.ts +48 -0
  10. package/dist/cli/hook.js +127 -0
  11. package/dist/cli/index.js +5 -80
  12. package/dist/cli/init.js +1 -1
  13. package/dist/cli/install/gitignore.d.ts +2 -2
  14. package/dist/cli/install/gitignore.js +3 -3
  15. package/dist/cli/install/pre-push.d.ts +146 -271
  16. package/dist/cli/install/pre-push.js +471 -2633
  17. package/dist/cli/install/settings-merge.d.ts +17 -0
  18. package/dist/cli/install/settings-merge.js +48 -1
  19. package/dist/cli/upgrade.js +131 -3
  20. package/dist/config/tier-map.js +18 -25
  21. package/dist/hooks/push-gate/base.d.ts +57 -0
  22. package/dist/hooks/push-gate/base.js +77 -0
  23. package/dist/hooks/push-gate/codex-runner.d.ts +126 -0
  24. package/dist/hooks/push-gate/codex-runner.js +223 -0
  25. package/dist/hooks/push-gate/findings.d.ts +68 -0
  26. package/dist/hooks/push-gate/findings.js +142 -0
  27. package/dist/hooks/push-gate/halt.d.ts +28 -0
  28. package/dist/hooks/push-gate/halt.js +49 -0
  29. package/dist/hooks/push-gate/index.d.ts +90 -0
  30. package/dist/hooks/push-gate/index.js +351 -0
  31. package/dist/hooks/push-gate/policy.d.ts +41 -0
  32. package/dist/hooks/push-gate/policy.js +55 -0
  33. package/dist/hooks/push-gate/report.d.ts +89 -0
  34. package/dist/hooks/push-gate/report.js +140 -0
  35. package/dist/policy/loader.d.ts +10 -10
  36. package/dist/policy/loader.js +7 -6
  37. package/dist/policy/types.d.ts +31 -22
  38. package/package.json +1 -1
  39. package/dist/cache/review-cache.d.ts +0 -115
  40. package/dist/cache/review-cache.js +0 -200
  41. package/dist/cli/cache.d.ts +0 -84
  42. package/dist/cli/cache.js +0 -150
  43. package/dist/hooks/review-gate/args.d.ts +0 -126
  44. package/dist/hooks/review-gate/args.js +0 -315
  45. package/dist/hooks/review-gate/audit.d.ts +0 -131
  46. package/dist/hooks/review-gate/audit.js +0 -181
  47. package/dist/hooks/review-gate/banner.d.ts +0 -97
  48. package/dist/hooks/review-gate/banner.js +0 -172
  49. package/dist/hooks/review-gate/base-resolve.d.ts +0 -155
  50. package/dist/hooks/review-gate/base-resolve.js +0 -247
  51. package/dist/hooks/review-gate/cache-key.d.ts +0 -55
  52. package/dist/hooks/review-gate/cache-key.js +0 -41
  53. package/dist/hooks/review-gate/cache.d.ts +0 -108
  54. package/dist/hooks/review-gate/cache.js +0 -120
  55. package/dist/hooks/review-gate/constants.d.ts +0 -26
  56. package/dist/hooks/review-gate/constants.js +0 -34
  57. package/dist/hooks/review-gate/diff.d.ts +0 -181
  58. package/dist/hooks/review-gate/diff.js +0 -232
  59. package/dist/hooks/review-gate/errors.d.ts +0 -72
  60. package/dist/hooks/review-gate/errors.js +0 -100
  61. package/dist/hooks/review-gate/hash.d.ts +0 -43
  62. package/dist/hooks/review-gate/hash.js +0 -46
  63. package/dist/hooks/review-gate/index.d.ts +0 -31
  64. package/dist/hooks/review-gate/index.js +0 -35
  65. package/dist/hooks/review-gate/metadata.d.ts +0 -98
  66. package/dist/hooks/review-gate/metadata.js +0 -158
  67. package/dist/hooks/review-gate/policy.d.ts +0 -55
  68. package/dist/hooks/review-gate/policy.js +0 -71
  69. package/dist/hooks/review-gate/protected-paths.d.ts +0 -46
  70. package/dist/hooks/review-gate/protected-paths.js +0 -76
  71. package/hooks/_lib/push-review-core.sh +0 -1250
  72. package/hooks/commit-review-gate.sh +0 -330
  73. package/hooks/push-review-gate-git.sh +0 -94
  74. package/hooks/push-review-gate.sh +0 -92
@@ -1,100 +0,0 @@
1
- /**
2
- * Typed error set for the review-gate modules (G — push-review/commit-review
3
- * TypeScript port).
4
- *
5
- * The bash core signals outcomes via exit codes + stderr banners. The TS port
6
- * expresses each outcome as a typed error so callers can branch on class
7
- * instead of parsing banner text, and so the eventual CLI entry point can
8
- * translate a thrown error into the same exit code + banner the bash core
9
- * emitted (preserving external contract per design §2, non-goals).
10
- *
11
- * Every error subclass carries:
12
- * - a stable `code` (for programmatic dispatch in tests + the CLI shim)
13
- * - an `exitCode` (matches the bash core's `exit N` semantics)
14
- * - the operator-facing `message` composed by `banner.ts`
15
- *
16
- * This module is intentionally dependency-free so unit tests can import it
17
- * without dragging in fs/child_process. The CLI entry point is the single
18
- * place that translates these to `process.exit(N)`.
19
- */
20
- /**
21
- * Base class. All review-gate errors derive from this so a CLI dispatch layer
22
- * can `catch (e) { if (e instanceof ReviewGateError) ... }`.
23
- */
24
- export class ReviewGateError extends Error {
25
- code;
26
- exitCode;
27
- details;
28
- constructor(code, message, exitCode, details = {}) {
29
- super(message);
30
- this.name = 'ReviewGateError';
31
- this.code = code;
32
- this.exitCode = exitCode;
33
- this.details = details;
34
- // Node gives us a stable prototype chain via Error; no need to hack it.
35
- }
36
- }
37
- /**
38
- * Blocked-push errors (exit 2). The bash core uses exit 2 for every blocked
39
- * condition; we preserve that invariant so the shim's exit code is unchanged.
40
- */
41
- export class BlockedError extends ReviewGateError {
42
- constructor(code, message, details = {}) {
43
- super(code, message, 2, details);
44
- this.name = 'BlockedError';
45
- }
46
- }
47
- /**
48
- * Deletion detected anywhere in the push (defect J). Must fail-closed even
49
- * when a sibling refspec would have been reviewable.
50
- */
51
- export class DeletionBlockedError extends BlockedError {
52
- constructor() {
53
- super('PUSH_BLOCKED_DELETE', 'refspec is a branch deletion.\n' +
54
- '\n' +
55
- ' Branch deletions are sensitive operations and require explicit\n' +
56
- ' human action outside the agent. Perform the deletion manually.\n');
57
- this.name = 'DeletionBlockedError';
58
- }
59
- }
60
- /**
61
- * A refspec with `HEAD` as destination is operator error (design §3.1,
62
- * `pr_resolve_argv_refspecs`). Carry the refspec in details for banner render.
63
- */
64
- export class HeadRefspecBlockedError extends BlockedError {
65
- constructor(spec) {
66
- super('PUSH_BLOCKED_HEAD_REFSPEC', `refspec resolves to HEAD (from ${JSON.stringify(spec)})\n` +
67
- '\n' +
68
- ' `git push <remote> HEAD:<branch>` or similar is almost always\n' +
69
- ' operator error in this context. Name the destination branch\n' +
70
- ' explicitly so the review gate can diff against it.\n', { spec });
71
- this.name = 'HeadRefspecBlockedError';
72
- }
73
- }
74
- /**
75
- * Invalid `--delete` refspec (empty destination or HEAD destination).
76
- * Distinct from the general HEAD-refspec error because bash emits a
77
- * different operator banner for the delete-mode case — the remediation
78
- * text is "name the branch you meant to delete" rather than the HEAD
79
- * destination error. See push-review-core.sh §161-168.
80
- */
81
- export class InvalidDeleteRefspecError extends BlockedError {
82
- constructor(spec) {
83
- super('PUSH_BLOCKED_HEAD_REFSPEC', `--delete refspec resolves to HEAD or empty (from ${JSON.stringify(spec)})\n`, { spec, mode: 'delete' });
84
- this.name = 'InvalidDeleteRefspecError';
85
- }
86
- }
87
- /**
88
- * Defect N completion (landed in phase 4, type reserved in phase 1 so
89
- * `base-resolve.ts` can throw it later without schema churn).
90
- */
91
- export class NoBaseResolvableError extends BlockedError {
92
- constructor(source) {
93
- super('PUSH_BLOCKED_NO_BASE_RESOLVABLE', `cannot resolve base branch for ${source}; run ` +
94
- '`git branch --set-upstream-to=origin/<target>` or ' +
95
- '`git config branch.' +
96
- source +
97
- '.base <ref>`', { source });
98
- this.name = 'NoBaseResolvableError';
99
- }
100
- }
@@ -1,43 +0,0 @@
1
- /**
2
- * Portable SHA-256 over arbitrary strings. Replaces the bash core's
3
- * sha256sum → shasum → openssl fallback chain (defect L) with a single
4
- * Node-stdlib call, removing the Alpine/distroless "no hasher on PATH"
5
- * regression class entirely.
6
- *
7
- * ## Why a dedicated module
8
- *
9
- * The cache-key contract (design §8) requires byte-exact parity with the
10
- * 0.10.1 bash implementation. The bash implementation computes
11
- * `sha256sum( <full git diff> )` and uses the hex digest as the cache key.
12
- * `crypto.createHash('sha256').update(s).digest('hex')` is bit-identical to
13
- * GNU `sha256sum < <(printf '%s' s)` output (neither includes the
14
- * filename-suffix padding). Regression-tested against the fixture in
15
- * `__fixtures__/cache-keys.json`.
16
- *
17
- * ## Hex-64 validation
18
- *
19
- * The bash core validates the hasher output is `^[0-9a-f]{64}$` before using
20
- * it as a cache key; a partial read or broken pipe would otherwise cache
21
- * garbage. Node's `createHash` is synchronous and crypto-backed, so the
22
- * digest is always a valid hex-64. We preserve the validation helper for
23
- * any future path where user-supplied strings might be treated as SHAs (the
24
- * bash core does this for push_sha env-pass, which the TS port rejects at
25
- * the type level instead).
26
- */
27
- /** A hex-lowercased SHA-256 digest (64 chars). */
28
- export type HexSha256 = string;
29
- /**
30
- * Compute a SHA-256 over the UTF-8 bytes of `input`. Returns the lowercase
31
- * hex digest.
32
- *
33
- * This is the one function the cache-key contract depends on — never change
34
- * the encoding or the digest format without bumping the cache-key version
35
- * (which none of phases 1–4 are permitted to do, per design §8).
36
- */
37
- export declare function sha256Hex(input: string): HexSha256;
38
- /**
39
- * True iff the input looks like a canonical lowercase SHA-256 hex digest.
40
- * Used by tests and by defensive callers that accept a string and want to
41
- * reject malformed input before writing it into the cache.
42
- */
43
- export declare function isValidSha256Hex(value: string): value is HexSha256;
@@ -1,46 +0,0 @@
1
- /**
2
- * Portable SHA-256 over arbitrary strings. Replaces the bash core's
3
- * sha256sum → shasum → openssl fallback chain (defect L) with a single
4
- * Node-stdlib call, removing the Alpine/distroless "no hasher on PATH"
5
- * regression class entirely.
6
- *
7
- * ## Why a dedicated module
8
- *
9
- * The cache-key contract (design §8) requires byte-exact parity with the
10
- * 0.10.1 bash implementation. The bash implementation computes
11
- * `sha256sum( <full git diff> )` and uses the hex digest as the cache key.
12
- * `crypto.createHash('sha256').update(s).digest('hex')` is bit-identical to
13
- * GNU `sha256sum < <(printf '%s' s)` output (neither includes the
14
- * filename-suffix padding). Regression-tested against the fixture in
15
- * `__fixtures__/cache-keys.json`.
16
- *
17
- * ## Hex-64 validation
18
- *
19
- * The bash core validates the hasher output is `^[0-9a-f]{64}$` before using
20
- * it as a cache key; a partial read or broken pipe would otherwise cache
21
- * garbage. Node's `createHash` is synchronous and crypto-backed, so the
22
- * digest is always a valid hex-64. We preserve the validation helper for
23
- * any future path where user-supplied strings might be treated as SHAs (the
24
- * bash core does this for push_sha env-pass, which the TS port rejects at
25
- * the type level instead).
26
- */
27
- import { createHash } from 'node:crypto';
28
- /**
29
- * Compute a SHA-256 over the UTF-8 bytes of `input`. Returns the lowercase
30
- * hex digest.
31
- *
32
- * This is the one function the cache-key contract depends on — never change
33
- * the encoding or the digest format without bumping the cache-key version
34
- * (which none of phases 1–4 are permitted to do, per design §8).
35
- */
36
- export function sha256Hex(input) {
37
- return createHash('sha256').update(input, 'utf8').digest('hex');
38
- }
39
- /**
40
- * True iff the input looks like a canonical lowercase SHA-256 hex digest.
41
- * Used by tests and by defensive callers that accept a string and want to
42
- * reject malformed input before writing it into the cache.
43
- */
44
- export function isValidSha256Hex(value) {
45
- return /^[0-9a-f]{64}$/.test(value);
46
- }
@@ -1,31 +0,0 @@
1
- /**
2
- * Public entry point for the review-gate TypeScript port (G).
3
- *
4
- * ## Scope after Phase 2a (0.10.3)
5
- *
6
- * - Phase 1 primitives (args, banner, cache-key, constants, errors,
7
- * hash, metadata, policy, protected-paths) — pure, dependency-free.
8
- * - Phase 2a supporting modules (base-resolve, diff, audit, cache) —
9
- * wrap git subprocesses, wrap audit append/scan, wrap the review-
10
- * cache lookup. No top-level gate yet; composition lands in Phase 2b.
11
- *
12
- * The bash core in `hooks/_lib/push-review-core.sh` continues to run in
13
- * production until phase 4. These exports are library-level primitives
14
- * that tests and Phase 2b compose; no behavioral surface is registered
15
- * for external callers here.
16
- *
17
- * See `docs/design/push-review-ts-port.md` for the full plan.
18
- */
19
- export * from './args.js';
20
- export * from './banner.js';
21
- export * from './cache-key.js';
22
- export * from './constants.js';
23
- export * from './errors.js';
24
- export * from './hash.js';
25
- export * from './metadata.js';
26
- export * from './policy.js';
27
- export * from './protected-paths.js';
28
- export { currentBranch, diffNameStatus, fullDiff, gitCommonDir, hasCommitLocally, mergeBase, readGitActor, readGitConfig, refExists, resolveHead, resolveRefToSha, resolveRemoteDefaultRef, resolveUpstream, revListCount, spawnGit, type DiffResult, type GitRunResult, type GitRunner, type NameStatusResult, } from './diff.js';
29
- export { computeInitialTargetLabel, resolveBaseForRefspec, resolveNewBranchBase, stripRefsHeadsOnly, type ResolveBaseDeps, type ResolvedBase, } from './base-resolve.js';
30
- export { CODEX_REVIEW_SKIPPED_TOOL, ESCAPE_HATCH_SERVER, PUSH_REVIEW_CACHE_ERROR_TOOL, PUSH_REVIEW_CACHE_HIT_TOOL, PUSH_REVIEW_SERVER, PUSH_REVIEW_SKIPPED_TOOL, emitCodexReviewSkipped, emitPushReviewSkipped, hasValidCodexReview, isQualifyingCodexReview, type SkipCodexReviewAuditInput, type SkipPushReviewAuditInput, } from './audit.js';
31
- export { checkReviewCache, type CacheOutcome, type CheckReviewCacheInput, } from './cache.js';
@@ -1,35 +0,0 @@
1
- /**
2
- * Public entry point for the review-gate TypeScript port (G).
3
- *
4
- * ## Scope after Phase 2a (0.10.3)
5
- *
6
- * - Phase 1 primitives (args, banner, cache-key, constants, errors,
7
- * hash, metadata, policy, protected-paths) — pure, dependency-free.
8
- * - Phase 2a supporting modules (base-resolve, diff, audit, cache) —
9
- * wrap git subprocesses, wrap audit append/scan, wrap the review-
10
- * cache lookup. No top-level gate yet; composition lands in Phase 2b.
11
- *
12
- * The bash core in `hooks/_lib/push-review-core.sh` continues to run in
13
- * production until phase 4. These exports are library-level primitives
14
- * that tests and Phase 2b compose; no behavioral surface is registered
15
- * for external callers here.
16
- *
17
- * See `docs/design/push-review-ts-port.md` for the full plan.
18
- */
19
- // Phase 1 primitives
20
- export * from './args.js';
21
- export * from './banner.js';
22
- export * from './cache-key.js';
23
- export * from './constants.js';
24
- export * from './errors.js';
25
- export * from './hash.js';
26
- export * from './metadata.js';
27
- export * from './policy.js';
28
- export * from './protected-paths.js';
29
- // Phase 2a supporting modules — re-export explicit names to avoid
30
- // double-exporting `computeCacheKey` (which lives in both cache-key.ts
31
- // and cache.ts; cache.ts's is a strict re-export of Phase 1's).
32
- export { currentBranch, diffNameStatus, fullDiff, gitCommonDir, hasCommitLocally, mergeBase, readGitActor, readGitConfig, refExists, resolveHead, resolveRefToSha, resolveRemoteDefaultRef, resolveUpstream, revListCount, spawnGit, } from './diff.js';
33
- export { computeInitialTargetLabel, resolveBaseForRefspec, resolveNewBranchBase, stripRefsHeadsOnly, } from './base-resolve.js';
34
- export { CODEX_REVIEW_SKIPPED_TOOL, ESCAPE_HATCH_SERVER, PUSH_REVIEW_CACHE_ERROR_TOOL, PUSH_REVIEW_CACHE_HIT_TOOL, PUSH_REVIEW_SERVER, PUSH_REVIEW_SKIPPED_TOOL, emitCodexReviewSkipped, emitPushReviewSkipped, hasValidCodexReview, isQualifyingCodexReview, } from './audit.js';
35
- export { checkReviewCache, } from './cache.js';
@@ -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
- }