@bookedsolid/rea 0.49.1 → 0.50.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/MIGRATING.md CHANGED
@@ -10,6 +10,43 @@ If you hit something this doc doesn't cover, file an issue at
10
10
  https://github.com/bookedsolidtech/rea/issues with the offending hook
11
11
  body and the prior tool name.
12
12
 
13
+ ## Coming from `@bookedsolid/reagent`?
14
+
15
+ `@bookedsolid/rea` is the successor to the now-**deprecated**
16
+ `@bookedsolid/reagent`. Same core idea (every AI tool call governed by
17
+ your `policy.yaml`), rebuilt with a cleaner hook model and a
18
+ codex-integrated push-gate. reagent receives no further releases.
19
+
20
+ The package/CLI/config names changed:
21
+
22
+ | | reagent (old) | rea (new) |
23
+ |---|---|---|
24
+ | npm package | `@bookedsolid/reagent` | `@bookedsolid/rea` |
25
+ | CLI binary | `reagent` | `rea` |
26
+ | Config dir | `.reagent/` | `.rea/` |
27
+ | Policy file | `.reagent/policy.yaml` | `.rea/policy.yaml` |
28
+ | Install | `reagent init` | `rea init` |
29
+
30
+ Steps:
31
+
32
+ 1. `npm rm -D @bookedsolid/reagent` (or pnpm/yarn equivalent)
33
+ 2. `npm i -D @bookedsolid/rea`
34
+ 3. `npx rea init` — installs the current hook set and writes a fresh
35
+ `.rea/policy.yaml`
36
+ 4. Port your settings from `.reagent/policy.yaml` into the new
37
+ `.rea/policy.yaml` (review the generated file rather than copying
38
+ verbatim — autonomy levels, blocked paths, audit logging, and
39
+ `block_ai_attribution` carry over, but the shape evolved)
40
+ 5. `rea doctor` — confirm the install is green
41
+ 6. Delete the old `.reagent/` directory and any reagent-installed hooks
42
+ rea has replaced; update any `reagent` CLI calls in CI to `rea`
43
+
44
+ The rest of this document covers the harder case: integrating rea with
45
+ **other** prior tooling already in your repo (commitlint, lint-staged,
46
+ gitleaks, branch-policy linters, husky layouts). If you're a fresh
47
+ reagent → rea swap, the six steps above plus `rea doctor` are usually
48
+ all you need.
49
+
13
50
  ## Prerequisite — husky must be installed and `core.hooksPath` configured
14
51
 
15
52
  The `.husky/{commit-msg,pre-push}.d/` extension surface is sourced from
@@ -750,3 +787,71 @@ the dominant cost ceiling that motivated 0.45.0's profiling work.
750
787
  See `docs/shim-session-cache-design.md` for the security contract
751
788
  and `docs/hook-perf-baseline.md` §"Per-session shim cache and the
752
789
  baseline" for the perf methodology note.
790
+
791
+ ## Governing a shared repo without touching shared files (added in 0.50.0)
792
+
793
+ The driving case: you work in a shared repo. You want rea's hooks to
794
+ govern **your** clone on **your** machine, without other developers
795
+ using rea and without touching the shared `package.json` or lockfile.
796
+ The 0.50.0 opt-in global rea CLI tier is built for exactly this — it is
797
+ OFF by default and changes nothing until you install it and bless a
798
+ checkout.
799
+
800
+ The shape is a two-step, human-only setup:
801
+
802
+ 1. **Install the CLI once, per-user, from OUTSIDE the repo.** The
803
+ installer refuses to place the CLI inside a git checkout (it would
804
+ become a committable artifact):
805
+
806
+ ```bash
807
+ cd ~ # anywhere outside the repo
808
+ rea install --global # real `npm install --prefix ~/.rea/cli`
809
+ ```
810
+
811
+ 2. **Trust the checkout, in a plain shell.** `rea trust` /
812
+ `rea install --global` refuse to run under a governed agent session
813
+ (`CLAUDE_PROJECT_DIR` set) — establishing trust is a human action, not
814
+ something the agent can grant itself:
815
+
816
+ ```bash
817
+ cd /path/to/shared-repo
818
+ rea trust # or: rea install --global --trust . in step 1
819
+ ```
820
+
821
+ Nothing about the shared repo changes: no dependency is added to
822
+ `package.json`, no lockfile entry, no committed `.claude/`. Keep your
823
+ personal `.claude/` gitignored as usual. The global tier supplies a CLI
824
+ binary; the policy enforced is always the checkout's own
825
+ `.rea/policy.yaml`, so blessing a repo means vouching for its policy —
826
+ it can never waive a gate.
827
+
828
+ ### Verification
829
+
830
+ Run `rea doctor` from inside the checkout and read three rows:
831
+
832
+ ```text
833
+ [pass] global rea CLI installed v0.50.0 at /Users/you/.rea/cli/node_modules/@bookedsolid/rea/dist/cli/index.js
834
+ [pass] global rea CLI active tier global — this checkout is trusted (in the global-CLI trust registry /Users/you/.rea/trusted-projects); hooks run <realpath>
835
+ [info] global rea CLI residual risk integrity relies on filesystem ownership of /Users/you/.rea; prefer in-project install for shared or CI checkouts
836
+ ```
837
+
838
+ If the checkout is not trusted yet, the active-tier row is a `warn`
839
+ telling you the hooks fail closed here until you `rea trust` it. If an
840
+ in-project rea resolves, doctor reports the active tier as `in-project`
841
+ (global present, unused) — the in-project tier always wins. `rea trust
842
+ --list` prints the current registry; `rea untrust` removes a checkout.
843
+
844
+ ### Notes
845
+
846
+ - **Human actions only.** `rea install --global`, `rea trust`, and
847
+ `rea untrust` refuse under a governed agent session. Run them from a
848
+ plain shell.
849
+ - **POSIX-only in v1** — the tier is disabled on Windows.
850
+ - **`rea init` / `rea upgrade` are unchanged** — they never touch
851
+ `~/.rea/`, never prompt about the global tier, and never write
852
+ `runtime.allow_global_cli`. A repo that wants to forbid the tier for
853
+ everyone can set `runtime.allow_global_cli: false` in its
854
+ `.rea/policy.yaml`.
855
+ - The trust registry `~/.rea/trusted-projects` (per-user global-CLI
856
+ allow-list) is a different file from `.rea/registry.yaml` (the
857
+ per-project MCP-server TOFU fingerprint store).
package/README.md CHANGED
@@ -51,6 +51,7 @@ the [migration section](#migration-from-010x) below.
51
51
  - [Slash commands](#slash-commands)
52
52
  - [Curated agents](#curated-agents)
53
53
  - [CLI reference](#cli-reference)
54
+ - [Global install (govern a checkout without a shared dependency)](#global-install-govern-a-checkout-without-a-shared-dependency)
54
55
  - [Migration from 0.10.x](#migration-from-010x)
55
56
  - [Contributor quality gates](#contributor-quality-gates)
56
57
  - [Threat model and security](#threat-model-and-security)
@@ -1066,6 +1067,67 @@ rea hook push-gate --base origin/main
1066
1067
 
1067
1068
  ---
1068
1069
 
1070
+ ## Global install (govern a checkout without a shared dependency)
1071
+
1072
+ Opt-in, off by default (added in 0.50.0). It lets rea's hooks govern a
1073
+ checkout **without** adding `@bookedsolid/rea` to that checkout's shared
1074
+ `package.json` — useful for a shared repo where you want rea for yourself
1075
+ while other developers, and the shared manifest, stay rea-free.
1076
+
1077
+ Install the CLI once, per-user and out of any project, then bless the
1078
+ checkouts you want governed:
1079
+
1080
+ ```bash
1081
+ # From OUTSIDE any git checkout (it refuses to install inside one):
1082
+ rea install --global # → <home>/.rea/cli (real npm install)
1083
+ rea install --global --trust . # install and trust the cwd in one step
1084
+
1085
+ # From a plain shell (NOT under a governed agent session):
1086
+ rea trust # bless the current checkout
1087
+ rea trust /path/to/repo # bless another checkout
1088
+ rea trust --list # print the trust registry
1089
+ rea untrust # remove the current checkout
1090
+ ```
1091
+
1092
+ `rea install --global` and `rea trust` / `rea untrust` are **human
1093
+ actions**: the CLI refuses to mutate your per-user trust root when a
1094
+ governed agent session is detected (`CLAUDE_PROJECT_DIR` is set). The
1095
+ per-user root is derived from the password database, never from `$HOME`
1096
+ or `$XDG_*`.
1097
+
1098
+ How resolution behaves:
1099
+
1100
+ - **In-project resolution always wins.** If `node_modules/@bookedsolid/rea`
1101
+ (or a `dist/cli/index.js`) resolves in the project, the global tier is
1102
+ never consulted and behavior is byte-identical to a build without the
1103
+ feature.
1104
+ - **Off / un-blessed is invisible.** A checkout that is not in the trust
1105
+ registry resolves exactly as it would without the feature installed.
1106
+ - **The registry only ever enables — never disables — enforcement.** The
1107
+ global tier supplies a CLI binary; the policy enforced is always the
1108
+ checkout's own `.rea/policy.yaml`. A repo can further-restrict the tier
1109
+ with `runtime.allow_global_cli: false`; nothing in the registry can
1110
+ waive a gate.
1111
+
1112
+ Check which tier a checkout actually runs through:
1113
+
1114
+ ```bash
1115
+ rea doctor # shows: global rea CLI installed / active tier / trust anchor
1116
+ ```
1117
+
1118
+ `rea doctor` names the resolved realpath and reports the active tier
1119
+ (`in-project` when the global tier is present but unused, `global` +
1120
+ trusted, a `warn` when the checkout is not trusted, or the
1121
+ `runtime.allow_global_cli: false` veto). The trust registry
1122
+ (`<home>/.rea/trusted-projects`) is distinct from `.rea/registry.yaml`
1123
+ (the per-project MCP TOFU store).
1124
+
1125
+ POSIX-only in v1 (the tier is disabled on Windows). `rea init` and
1126
+ `rea upgrade` never touch `<home>/.rea/` and never write
1127
+ `runtime.allow_global_cli` — the option is per-developer, per-machine.
1128
+
1129
+ ---
1130
+
1069
1131
  ## Migration from 0.10.x
1070
1132
 
1071
1133
  `rea upgrade` handles the policy and on-disk pieces. You run it once:
package/THREAT_MODEL.md CHANGED
@@ -565,6 +565,68 @@ Ref: `src/cli/install/self-pin.ts`, `src/cli/init.ts`, `src/cli/upgrade.ts`, `sr
565
565
 
566
566
  ---
567
567
 
568
+ ### 5.24 Global rea CLI resolution tier (opt-in)
569
+
570
+ **Threat class.** 0.50.0 adds an OPT-IN, off-by-default resolution tier. A developer runs `rea install --global` (a real `npm install --prefix <pw_dir>/.rea/cli @bookedsolid/rea`) and `rea trust <checkout>`, and the shim hooks then govern that checkout by resolving a per-user rea CLI from `<pw_dir>/.rea/cli` — **without** adding `@bookedsolid/rea` to the checkout's shared `package.json`. The driving case is a shared repo (well-lit) with a gitignored personal `.claude/`, where the shared manifest must stay rea-free and other developers stay rea-free. The moment a per-user CLI can serve *any* checkout, three surfaces open that the in-project tier does not have: cross-project ambient authority, cross-user plant, and env/symlink redirect of the root. This section states the trust-boundary delta, the closures, and — plainly — the residuals.
571
+
572
+ The honest bar is **"no weaker than the in-project tier, plus close the three new surfaces a shared global location adds."** It is NOT "sound against a same-uid agent" — the in-project tier is not either (a same-uid process can overwrite `node_modules/@bookedsolid/rea/dist/cli/index.js` in place today; see §8.3). Everything below is measured against that bar.
573
+
574
+ #### 5.24.1 Trust-boundary delta
575
+
576
+ The load-bearing framing: **the global tier supplies a CLI *binary*, never a *policy*.** Enforcement always runs against the in-project `.rea/policy.yaml`. Blessing a checkout into the trust registry means "run the trusted per-user CLI against *this repo's own policy*" — it is an act of vouching for that repo's policy, not a grant of new authority. Concretely, the tier adds:
577
+
578
+ - A per-user CLI tree at `<pw_dir>/.rea/cli/` (out of any project).
579
+ - A per-user allow-list `<pw_dir>/.rea/trusted-projects` (mode `0600`, owner==euid). **Naming:** this is the GLOBAL-CLI trust registry; it is NOT `.rea/registry.yaml` (the per-project MCP-server TOFU fingerprint store of §5.20). The two are unrelated files with unrelated schemas.
580
+ - An optional in-project veto key `runtime.allow_global_cli?: boolean` (§7 zod, `.strict()`, `.optional()` so absent ≠ explicit false).
581
+
582
+ In-project resolution **always wins**: the shim probes `$proj/node_modules/@bookedsolid/rea/dist/cli/index.js` then `$proj/dist/cli/index.js` first, and the trust registry is NEVER consulted when either resolves. An un-blessed checkout is **byte-identical** to a rea build without the feature — any registry-unusable condition in the hot path degrades to a silent `unavailable` (no banner), so shared per-user state cannot perturb a CLI-less checkout (codex design-gate P1-3). Loud diagnosis lives only in `rea doctor` / `rea trust` / `rea install --global`.
583
+
584
+ #### 5.24.2 The passwd-derived anchor
585
+
586
+ The per-user root is `os.userInfo().homedir` — libuv `getpwuid_r(geteuid())→pw_dir`, which reads the password database, **not** the environment. `$HOME` and `$XDG_CONFIG_HOME` are never consulted on either the bash or the TypeScript side; `homedir !== $HOME` uses the passwd value and raises a `rea doctor` advisory. A throw (squashed/absent passwd entry, e.g. NFS root_squash) makes the tier **unavailable** — the code never falls back to `$HOME`. POSIX-only: the tier is disabled on Windows in v1 (no `getuid`; shipping a half-enforced ownership walk would be worse than not shipping).
587
+
588
+ This is a deliberate divergence from the sibling openrouter-key-source design, stated verbatim so the reason is not lost:
589
+
590
+ > openrouter-key-source derives its root from the env bag (`XDG_CONFIG_HOME`→`HOME`) because its actor is a careless operator needing overridability of a credential lookup; the global-CLI tier derives its root from the password database via `os.userInfo().homedir` because its actor is the agent itself, which can set `$HOME`/`$XDG_CONFIG_HOME` in-process — any env-derived root is agent-redirectable, exactly the N3 surface we close. A trust root an agent can move is not a trust root.
591
+
592
+ #### 5.24.3 New-surface closures (N1 / N2 / N3)
593
+
594
+ - **N1 — cross-project ambient authority.** One per-user CLI could otherwise serve *every* checkout on the machine, including a freshly-cloned hostile repo. Closure: a **default-deny registry**. The global tier fires only for a checkout whose `realpath(CLAUDE_PROJECT_DIR)` is an exact whole-line match (`grep -Fxq`) in `<pw_dir>/.rea/trusted-projects`. No entry → no global tier. The registry is a CONSENT gate, not a containment gate: `CLAUDE_PROJECT_DIR` is agent-controllable, but spoofing it to a *blessed* path only causes the **trusted** CLI to run against a repo the human already vouched for — never a bypass.
595
+ - **N2 — cross-user plant.** Another local user (or a group/other-writable directory) could plant a CLI tree. Closure: a **per-component `lstat` walk** (A2) from the resolved candidate up to and including `<pw_dir>/.rea`, requiring for every component `uid === geteuid()`, no group/other write (`mode & 0o022 === 0`), and no device-number change vs `g_root` (`st_dev`, defeats a mount/bind/automount aliased subtree — codex P2-6). The candidate `index.js` and the ancestor `package.json` must each be `nlink === 1` (a pre-existing hardlink is a same-uid mutation primitive — codex P2-7). The registry file itself is held to the strict `0600` mask (`mode & 0o077 === 0`) plus `nlink === 1`.
596
+ - **N3 — env/symlink redirect of the root.** An agent that can set `$HOME`/`$XDG_*` in-process could otherwise redirect the whole root. Closure is three-part: **passwd-derivation** (§5.24.2, the root is not env-derived), **realpath containment** (A1: `realpath(candidate)` must equal or sit under `realpath(g_root)`), and **reject-ANY-symlink** in the A2 walk — not only outward-pointing links. An inside-pointing symlink is still a same-uid repoint primitive, so every symlink component is fatal (codex `sec(f)`).
597
+
598
+ #### 5.24.4 Accept predicate (A1–A5)
599
+
600
+ Evaluated cheapest/most-decisive first. A5 is the silent consent gate; A1–A4 are fatal-to-tier checks that fire only for a blessed-but-hostile tree. Reasons are the shim's `bad:global-*` strings (`hooks/_lib/shim-runtime.sh`, `shim_sandbox_check_global`).
601
+
602
+ | Step | Check | Outcome on failure |
603
+ | --- | --- | --- |
604
+ | **A5** | registry entry-gate: passwd-derive `pw_dir`; `<pw_dir>/.rea` dir-safe; registry file-safe (`0600`, owner, `nlink===1`); `realpath(proj)` is a `grep -Fxq` member | **silent `unavailable`** → today's no-CLI path (NO banner; parity with feature-absent) |
605
+ | **A2** | per-component `lstat` walk (candidate → `<pw_dir>/.rea`): any symlink; foreign uid or group/other-write; `st_dev` change; candidate `nlink!==1` | `bad:global-symlink` / `bad:global-perm` / `bad:global-hardlink` (FATAL to tier) |
606
+ | **A1** | `realpath(cand)` contained in `realpath(g_root)` | `bad:global-escapes-root` (FATAL); `realpath` throw → silent `unavailable` |
607
+ | **A4** | `realpath` ends `…/dist/cli/index.js` (ALWAYS-on for global, independent of `SHIM_ENFORCE_CLI_SHAPE`) | `bad:global-shape` (FATAL) |
608
+ | **A3** | ancestor `package.json#name === "@bookedsolid/rea"` (walk ≤20) and that `package.json` is `nlink===1` | `bad:global-no-rea-pkg` / `bad:global-hardlink` (FATAL) |
609
+
610
+ **Fatal vs. short-circuit.** An A5 miss short-circuits **silently** — REA_ARGV stays empty and the existing relevance-gated no-CLI terminal runs (harmless payload → exit 0 silent; a relevant payload on a blocking shim → cli-missing banner + exit 2), byte-identical to feature-absent. An A1–A4 failure (blessed, but the global tree is hostile or malformed) is **fatal to the tier**: REA_ARGV is cleared, a single one-line `bad:global-*` advisory is emitted to stderr, and the no-CLI terminal runs. Advisory hooks are NOT escalated to a hard block on a global plant — A2 already proves only euid wrote the tree, so escalation would only add a same-uid DoS. **Registry problems are never surfaced as a `bad:global-registry-*` banner in the hot path** (codex P1-3); they collapse into the silent `unavailable` path so shared per-user state cannot perturb a CLI-less checkout.
611
+
612
+ **The veto is POST-resolution, and the asymmetry is load-bearing.** The pre-CLI A5 gate reads registry membership ONLY — there is no inline `policy.yaml` read in the no-CLI window (codex P1-1/P1-2: a narrow YAML grep is a split-brain magnet, and `policy.yaml` is not scanner-protected when no CLI is resolved). The `runtime.allow_global_cli` veto instead runs *after* A1–A4 pass, read through the sandbox-validated CLI (`rea hook policy-get runtime.allow_global_cli`) at the same trust level `shim_policy_short_circuit` already relies on: `false`, or any `policy-get` error/unparseable policy → **refuse-forward** (clear REA_ARGV, revert `TRUST_TIER` to `project`, silent — a veto is a project choice, not a security signal). It is kept out of the cache key so a mid-session `allow_global_cli: false` is honored on the very next fire. This is safe because the **asymmetry holds: the registry (primary, protected, human-established) can only ever ENABLE the tier; a project's own policy can only ever further-RESTRICT it, never the reverse.** An agent that strips a `false` veto from an in-project `policy.yaml` only affects a repo *already* in the registry (the human already vouched); an agent that adds a veto only restricts. Policy mutation in the no-CLI window therefore cannot grant authority beyond the registry.
613
+
614
+ #### 5.24.5 Registry asymmetry — ENABLE-only, never DISABLE
615
+
616
+ Restated because it is the property the whole design turns on: **the trust registry can only ENABLE enforcement, never DISABLE a gate.** Adding a checkout means "govern this repo with the trusted CLI against its own policy." It cannot lower an autonomy level, remove a blocked path, or waive a hook — those live in the in-project `.rea/policy.yaml`, which the registry does not touch. The worst an attacker who forges registry membership (or spoofs `CLAUDE_PROJECT_DIR` to a blessed path) achieves is *"the trusted rea CLI runs against a policy the human already vouched for"* — strictly more governance, never less. Conversely, a checkout's own policy can veto the tier down to in-project-required, but can never use the registry to escape its own gates.
617
+
618
+ #### 5.24.6 Residual risks
619
+
620
+ Stated plainly; the tier does not claim to close these.
621
+
622
+ - **(a) Same-uid in-place file swap — ACCEPTED.** A2 proves only euid wrote the global tree, but a same-uid process can still overwrite the resolved `dist/cli/index.js` *in place* after validation. This is the **same residual the in-project package tier carries** (§8.3 "`@bookedsolid/rea` package-tier supply-chain compromise"; §8.6 out-of-scope) — at that privilege level the attacker can forge any dependency the agent uses, so hook-tier defense is already past and the trust boundary is the `npm install` provenance check. The TOCTOU realpath-exec precursor (Phase 1a, retrofitted to the in-project tier too) shrinks the resolve→execute window to exactly this in-place-swap residual and no wider. The global tier is therefore **no weaker** than the in-project tier on this class.
623
+ - **(b) `cp -l` / `cp --link` / `link` hardlink-alias creation — PRE-EXISTING walker gap, equal on both tiers.** The `nlink === 1` checks reject a *pre-existing* hardlinked registry / `index.js` / `package.json`. They do NOT stop an agent from *creating* a new hardlink alias to a protected file at Bash-write time (`cp -l …`, `link …`) — the bash-tier scanner (§8) does not yet resolve a create-a-hardlink-to-a-protected-target write. This is a pre-existing gap that applies **equally to the in-project kill-switch**: `cp -l .rea/policy.yaml ./alias` passes the scanner today, exactly as `cp -l <pw_dir>/.rea/trusted-projects ./alias` would. The global tier introduces **no new weakness** here relative to the in-project tier. Tracked as a walker follow-up (inode-aware hardlink-alias detection across both tiers).
624
+ - **(c) POSIX ACL cross-user write — degraded-env class.** The A2 mode-bit checks (`mode & 0o022`, `mode & 0o077`) inspect the classic permission bits, not POSIX ACLs. An attacker who already holds a second uid on the box AND has pre-set an ACL granting that uid write to a component of `<pw_dir>/.rea/cli` could evade the mode-bit gate. This requires a second uid plus a pre-existing ACL — the same degraded-environment class as NFS `root_squash` uid-mismatch — and is documented, not defended, in v1. Operators on ACL-bearing filesystems should prefer the in-project install for shared checkouts.
625
+
626
+ Ref: `hooks/_lib/shim-runtime.sh` (`shim_global_entry_gate`, `shim_sandbox_check_global`, `shim_resolve_cli_global`, 4-global-veto), `src/cli/global-cli.ts`, `src/cli/trust.ts`, `src/cli/install/global.ts`, `src/cli/doctor.ts` (`resolveGlobalCliTier`, `checkGlobalCli`), `src/policy/loader.ts` (`RuntimePolicySchema`), `__tests__/hooks/global-cli/`.
627
+
628
+ ---
629
+
568
630
  ## 6. Residual Risks and Open Issues
569
631
 
570
632
  | Risk | Severity | Status / Tracking |
@@ -582,6 +644,9 @@ Ref: `src/cli/install/self-pin.ts`, `src/cli/init.ts`, `src/cli/upgrade.ts`, `sr
582
644
  | Escape-hatch abuse signal not surfaced in `rea doctor` | Low | Tracked (threshold: ≥3 / 7d) |
583
645
  | Local user can escalate policy.yaml outside gateway | Low | By design (trusted actor) |
584
646
  | Registry pin mismatch → hard fail (no rollback) on TOFU | Low | By design — operator clears `.rea/fingerprints.json` to re-pin |
647
+ | Global-CLI tier: same-uid in-place swap of resolved `dist/cli/index.js` | Low | Accepted — same residual as the in-project package tier (§5.24.6a / §8.3); realpath-exec precursor shrinks the TOCTOU window |
648
+ | Global-CLI tier: `cp -l` / `link` hardlink-alias to a protected target | Low | Pre-existing scanner gap, equal on the in-project kill-switch (§5.24.6b) — walker follow-up (inode-aware alias detection) |
649
+ | Global-CLI tier: POSIX ACL cross-user write evades the mode-bit walk | Low | Degraded-env class (needs 2nd uid + pre-set ACL), alongside NFS root_squash (§5.24.6c) — prefer in-project install on ACL filesystems |
585
650
 
586
651
  ---
587
652
 
@@ -1,5 +1,6 @@
1
1
  import { type CodexProbeState } from '../gateway/observability/codex-probe.js';
2
2
  import { type PrePushDoctorState } from './install/pre-push.js';
3
+ import { type GlobalCandidateFail, type SafetyFail } from './global-cli.js';
3
4
  export interface CheckResult {
4
5
  label: string;
5
6
  /**
@@ -426,6 +427,54 @@ export declare function checkDelegationAdvisoryHookRegistered(baseDir: string):
426
427
  * after install / upgrade to confirm the pipeline is wired end-to-end.
427
428
  */
428
429
  export declare function checkDelegationRoundTrip(baseDir: string): Promise<CheckResult>;
430
+ /**
431
+ * The tier a checkout resolves to under the global-CLI feature, plus the
432
+ * reason (which drives the doctor rows) and the resolved realpaths. Mirrors
433
+ * `shim_run`'s tier resolution in `hooks/_lib/shim-runtime.sh` (steps 4 /
434
+ * 4-global / 4-global-veto) exactly, so a parity test can bind the two.
435
+ */
436
+ export interface GlobalCliTier {
437
+ /** The tier the bash shim would resolve to for this checkout. */
438
+ tier: 'in-project' | 'global';
439
+ reason: 'global-unavailable-platform' | 'global-root-absent' | 'global-root-unsafe' | 'global-registry-unsafe' | 'global-unresolvable' | 'in-project-wins' | 'untrusted' | 'global-candidate-unsafe' | 'global-cli-incapable' | 'policy-veto' | 'trusted';
440
+ /** realpath of the CLI the shim would actually run, or null when no CLI resolves. */
441
+ cliRealpath: string | null;
442
+ /** realpath of the global candidate (existence probe) when the root resolved one. */
443
+ globalCliRealpath: string | null;
444
+ /** Populated for `global-root-unsafe` (the .rea dir) or `global-registry-unsafe` (the registry file). */
445
+ safety?: SafetyFail;
446
+ /** Populated only for `global-candidate-unsafe`. */
447
+ candidateSafety?: GlobalCandidateFail;
448
+ }
449
+ /**
450
+ * THE single predicate (F1). Resolve which tier a checkout runs through and
451
+ * why, consuming `global-cli.ts`. Follows the authoritative Phase-3b decision
452
+ * tree top-to-bottom; each branch corresponds to a `shim_run` outcome so the
453
+ * parity test can assert byte-agreement on `tier` + `cliRealpath`.
454
+ *
455
+ * `home` defaults to the passwd-derived home (env-immune by design); tests
456
+ * inject a temp home to stay hermetic. `probeCapability` defaults to the shared
457
+ * `probeGlobalCliCapability` (spawns the real CLI); tests inject a fake to stay
458
+ * hermetic. NO CLI flag exposes either parameter.
459
+ */
460
+ export declare function resolveGlobalCliTier(baseDir: string, home?: string, probeCapability?: (cliPath: string) => {
461
+ ok: boolean;
462
+ }): GlobalCliTier;
463
+ /**
464
+ * Render the global rea CLI section as flat `CheckResult` rows. Off-by-default
465
+ * quiet: when the global root does NOT exist, emits exactly ONE `info` row.
466
+ * When it exists, walks the authoritative decision tree via
467
+ * `resolveGlobalCliTier` and always names the RESOLVED REALPATH (helix-013.1
468
+ * lesson). A `global-root-unsafe` / `global-unresolvable` verdict yields a
469
+ * `fail` row so `rea doctor` exits 1.
470
+ *
471
+ * The registry named in these rows is the per-user GLOBAL-CLI TRUST REGISTRY
472
+ * (`<home>/.rea/trusted-projects`), NOT `.rea/registry.yaml` (the MCP
473
+ * fingerprint store).
474
+ */
475
+ export declare function checkGlobalCli(baseDir: string, homeArg?: string, probeCapability?: (cliPath: string) => {
476
+ ok: boolean;
477
+ }): CheckResult[];
429
478
  /**
430
479
  * Assemble the full checklist for a given baseDir. Exported so tests can
431
480
  * exercise the conditional branching without capturing stdout from
@@ -443,6 +492,7 @@ export declare function checkDelegationRoundTrip(baseDir: string): Promise<Check
443
492
  */
444
493
  export declare function collectChecks(baseDir: string, codexProbeState?: CodexProbeState, prePushState?: PrePushDoctorState, options?: {
445
494
  strict?: boolean;
495
+ globalHome?: string;
446
496
  }): CheckResult[];
447
497
  export interface RunDoctorOptions {
448
498
  /** When true, print a 7-day telemetry summary after the checks (G11.5). */