@bookedsolid/rea 0.49.0 → 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 +105 -0
- package/README.md +62 -0
- package/THREAT_MODEL.md +65 -0
- package/dist/cli/doctor.d.ts +50 -0
- package/dist/cli/doctor.js +379 -0
- package/dist/cli/global-cli.d.ts +274 -0
- package/dist/cli/global-cli.js +709 -0
- package/dist/cli/hook.js +26 -0
- package/dist/cli/index.js +10 -0
- package/dist/cli/install/global.d.ts +93 -0
- package/dist/cli/install/global.js +397 -0
- package/dist/cli/trust.d.ts +70 -0
- package/dist/cli/trust.js +222 -0
- package/dist/hooks/bash-scanner/protected-scan.d.ts +24 -0
- package/dist/hooks/bash-scanner/protected-scan.js +152 -7
- package/dist/hooks/protected-paths-bash-gate/index.js +24 -1
- package/dist/policy/loader.d.ts +13 -0
- package/dist/policy/loader.js +52 -0
- package/dist/policy/types.d.ts +40 -0
- package/hooks/_lib/shim-cache.sh +42 -8
- package/hooks/_lib/shim-runtime.sh +494 -29
- package/hooks/local-review-gate.sh +51 -5
- package/package.json +1 -1
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
|
|
package/dist/cli/doctor.d.ts
CHANGED
|
@@ -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). */
|