@bookedsolid/rea 0.48.0 → 0.49.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/THREAT_MODEL.md +70 -0
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.js +241 -0
- package/dist/cli/init.d.ts +12 -0
- package/dist/cli/init.js +161 -0
- package/dist/cli/install/self-pin.d.ts +440 -0
- package/dist/cli/install/self-pin.js +853 -0
- package/dist/cli/upgrade.js +134 -0
- package/dist/policy/loader.d.ts +13 -0
- package/dist/policy/loader.js +36 -0
- package/dist/policy/profiles.d.ts +13 -0
- package/dist/policy/profiles.js +12 -0
- package/dist/policy/types.d.ts +38 -0
- package/hooks/_lib/bootstrap-allowlist.sh +1075 -0
- package/hooks/_lib/shim-cache.sh +96 -8
- package/hooks/_lib/shim-runtime.sh +88 -53
- package/hooks/blocked-paths-bash-gate.sh +35 -12
- package/hooks/protected-paths-bash-gate.sh +30 -12
- package/package.json +3 -1
- package/profiles/bst-internal-no-codex.yaml +4 -0
- package/profiles/bst-internal.yaml +28 -0
- package/profiles/client-engagement.yaml +9 -0
- package/profiles/lit-wc.yaml +6 -0
- package/profiles/minimal.yaml +11 -0
- package/profiles/open-source-no-codex.yaml +4 -0
- package/profiles/open-source.yaml +11 -0
- package/templates/_lib_shim-cache.dogfood-staged.sh +96 -8
- package/templates/_lib_shim-runtime.dogfood-staged.sh +88 -53
package/dist/cli/upgrade.js
CHANGED
|
@@ -48,6 +48,7 @@ import { canonicalSettingsSubsetHash, defaultDesiredHooks, mergeSettings, pruneH
|
|
|
48
48
|
import { validateSettings } from '../config/settings-schema.js';
|
|
49
49
|
import { ensureReaGitignore } from './install/gitignore.js';
|
|
50
50
|
import { installPrepareCommitMsgHook } from './install/prepare-commit-msg.js';
|
|
51
|
+
import { checkUpgradeBlockingPin, selfPinRea } from './install/self-pin.js';
|
|
51
52
|
import { manifestExists, readManifest, writeManifestAtomic } from './install/manifest-io.js';
|
|
52
53
|
import { sha256OfBuffer, sha256OfFile } from './install/sha.js';
|
|
53
54
|
import { err, getPkgVersion, log, warn } from './utils.js';
|
|
@@ -499,6 +500,65 @@ export async function runUpgrade(options = {}) {
|
|
|
499
500
|
`unchanged. zod errors: ${validation.errors.join('; ')}`);
|
|
500
501
|
}
|
|
501
502
|
}
|
|
503
|
+
// R9-P1 (codex round 9 / 0.49.0): blocking-pin pre-flight.
|
|
504
|
+
//
|
|
505
|
+
// When package.json pins @bookedsolid/rea to a version that does
|
|
506
|
+
// NOT admit the installed CLI version (workspace:*, file:.., git
|
|
507
|
+
// URLs, dist-tags, exact older pins, cross-major caret), writing
|
|
508
|
+
// 0.49 hooks + policy artifacts on top creates a hook/CLI skew:
|
|
509
|
+
// the bash gates resolve the older CLI from node_modules and that
|
|
510
|
+
// CLI's strict policy loader rejects the new `bootstrap_allowlist:`
|
|
511
|
+
// top-level key, breaking every Bash payload until the operator
|
|
512
|
+
// reconciles the pin.
|
|
513
|
+
//
|
|
514
|
+
// The check is READ-ONLY (single package.json read) and runs
|
|
515
|
+
// BEFORE every artifact-writing step:
|
|
516
|
+
// - migrateReviewPolicyFor0110 (rewrites .rea/policy.yaml)
|
|
517
|
+
// - the canonical file-write loop (hooks, agents, commands)
|
|
518
|
+
// - the dedicated selfPinRea call further below
|
|
519
|
+
// - .gitignore + prepare-commit-msg + manifest writes
|
|
520
|
+
//
|
|
521
|
+
// In dry-run we DESCRIBE the abort condition without exiting
|
|
522
|
+
// non-zero — operators using `rea upgrade --check` get to see the
|
|
523
|
+
// would-block diagnostic, then the dry-run completes normally.
|
|
524
|
+
// In live mode we abort with exit 1 and the operator-facing
|
|
525
|
+
// reconciliation steps.
|
|
526
|
+
{
|
|
527
|
+
const pinCheck = await checkUpgradeBlockingPin({
|
|
528
|
+
cwd: resolvedRoot,
|
|
529
|
+
cliVersion: getPkgVersion(),
|
|
530
|
+
mode: 'upgrade',
|
|
531
|
+
});
|
|
532
|
+
if (pinCheck.kind === 'block' || pinCheck.kind === 'block-symlink') {
|
|
533
|
+
// R10-P2 (codex round 10): block-symlink is the new variant —
|
|
534
|
+
// package.json is a symlink and writing through it would
|
|
535
|
+
// mutate a file outside the requested project tree. Handle
|
|
536
|
+
// it identically to the R9-P1 block case (throw with the
|
|
537
|
+
// operator-actionable reason; dry-run describes without
|
|
538
|
+
// throwing).
|
|
539
|
+
const dryRunHeadline = pinCheck.kind === 'block'
|
|
540
|
+
? `dry-run: rea upgrade WOULD refuse — pin ${JSON.stringify(pinCheck.existingRange)} ` +
|
|
541
|
+
`in ${path.relative(resolvedRoot, pinCheck.packageJsonPath)} does not admit ` +
|
|
542
|
+
`CLI ${pinCheck.newCliVersion}.`
|
|
543
|
+
: `dry-run: rea upgrade WOULD refuse — ${path.relative(resolvedRoot, pinCheck.packageJsonPath)} ` +
|
|
544
|
+
`is a symlink and rea must not mutate files outside the requested project tree.`;
|
|
545
|
+
if (dryRun) {
|
|
546
|
+
console.log('');
|
|
547
|
+
warn(dryRunHeadline);
|
|
548
|
+
for (const line of pinCheck.reason.split('\n')) {
|
|
549
|
+
console.log(` ${line}`);
|
|
550
|
+
}
|
|
551
|
+
console.log('');
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
// R9-P1: throw (don't process.exit) so the CLI wrapper can
|
|
555
|
+
// surface the message via its standard error path AND tests
|
|
556
|
+
// can assert on the thrown reason. Matches the settings
|
|
557
|
+
// pre-flight (line ~588) which also throws.
|
|
558
|
+
throw new Error(pinCheck.reason);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
502
562
|
// 0.11.0 migration — strip removed review.* fields and backfill the new
|
|
503
563
|
// concerns_blocks default. Runs before canonical file reconciliation so a
|
|
504
564
|
// policy that fails strict schema load (which happens on upgrade from
|
|
@@ -681,6 +741,80 @@ export async function runUpgrade(options = {}) {
|
|
|
681
741
|
for (const w of pcmResult.warnings)
|
|
682
742
|
warn(w);
|
|
683
743
|
}
|
|
744
|
+
// 0.49.0 — self-heal legacy installs that pre-date `rea init`'s
|
|
745
|
+
// self-pin step. Bricks-on-fresh-clone is a 0.48.x-and-earlier
|
|
746
|
+
// problem; `rea upgrade` rolls forward to a healthy state by
|
|
747
|
+
// re-running the same self-pin module.
|
|
748
|
+
//
|
|
749
|
+
// P1-1 (codex round 2): `mode: 'upgrade'` opts the upgrade path
|
|
750
|
+
// into auto-bumping a managed-caret pin when the existing range
|
|
751
|
+
// does not admit the new CLI minor. `^0.49.0` does NOT admit
|
|
752
|
+
// `0.50.0` (npm pre-1.0 caret = tilde semantics) — without auto-
|
|
753
|
+
// bump, `rea upgrade 0.50.0` would copy newer hooks against the
|
|
754
|
+
// older CLI pin and recreate the hook/CLI skew this feature
|
|
755
|
+
// exists to prevent. Auto-bump fires ONLY when the existing
|
|
756
|
+
// range is a strict managed-caret shape (something we wrote);
|
|
757
|
+
// workspace:*, file:.., git URLs, tags, exact pins, tildes, and
|
|
758
|
+
// cross-major bumps all hands-off and warn+skip. See
|
|
759
|
+
// src/cli/install/self-pin.ts::shouldBumpManagedCaret for the
|
|
760
|
+
// gate.
|
|
761
|
+
//
|
|
762
|
+
// R3-P2 (codex round 3): dry-run must surface the SAME action the
|
|
763
|
+
// live run would take — operators ran `rea upgrade --dry-run`,
|
|
764
|
+
// saw zero pin-related output, and were surprised when the live
|
|
765
|
+
// run mutated `package.json`. We now invoke `selfPinRea` in BOTH
|
|
766
|
+
// dry-run and live mode; the helper's `dryRun: true` opt computes
|
|
767
|
+
// the action discriminant without writing. Dry-run console output
|
|
768
|
+
// uses "would" verbs so the planned vs done distinction is
|
|
769
|
+
// unambiguous.
|
|
770
|
+
{
|
|
771
|
+
const selfPinResult = await selfPinRea({
|
|
772
|
+
cwd: resolvedRoot,
|
|
773
|
+
cliVersion: getPkgVersion(),
|
|
774
|
+
mode: 'upgrade',
|
|
775
|
+
dryRun,
|
|
776
|
+
});
|
|
777
|
+
const rel = selfPinResult.packageJsonPath !== null
|
|
778
|
+
? path.relative(resolvedRoot, selfPinResult.packageJsonPath)
|
|
779
|
+
: null;
|
|
780
|
+
if (selfPinResult.action === 'wrote' && rel !== null) {
|
|
781
|
+
console.log(dryRun
|
|
782
|
+
? ` ~ ${rel} (would self-pin: write devDependencies["@bookedsolid/rea"] = ${selfPinResult.pinnedRange})`
|
|
783
|
+
: ` ~ ${rel} (self-pin: @bookedsolid/rea@${selfPinResult.pinnedRange})`);
|
|
784
|
+
}
|
|
785
|
+
else if (selfPinResult.action === 'bumped' && rel !== null) {
|
|
786
|
+
// P1-1: surface the bump explicitly so the upgrade log shows
|
|
787
|
+
// exactly what changed about the pin (vs the silent same-bytes
|
|
788
|
+
// case which uses the dot marker).
|
|
789
|
+
console.log(dryRun
|
|
790
|
+
? ` ~ ${rel} (would self-pin: bump @bookedsolid/rea from ${selfPinResult.existingRange ?? '?'} to ${selfPinResult.pinnedRange})`
|
|
791
|
+
: ` ✓ ${rel} (self-pin: bumped @bookedsolid/rea from ${selfPinResult.existingRange ?? '?'} to ${selfPinResult.pinnedRange})`);
|
|
792
|
+
}
|
|
793
|
+
else if (selfPinResult.action === 'skipped-same' && rel !== null) {
|
|
794
|
+
console.log(` · ${rel} (self-pin: already declared)`);
|
|
795
|
+
}
|
|
796
|
+
else if (selfPinResult.action === 'skipped-different') {
|
|
797
|
+
// Same warn-and-skip path for both dry-run and live — the
|
|
798
|
+
// operator-owned-pin posture doesn't change between modes.
|
|
799
|
+
// The message itself describes the existing pin and the
|
|
800
|
+
// hands-off rationale.
|
|
801
|
+
warn(selfPinResult.message);
|
|
802
|
+
}
|
|
803
|
+
else if (selfPinResult.action === 'skipped-no-package-json') {
|
|
804
|
+
warn('self-pin skipped — no package.json found upward from target; bash gates may refuse without it');
|
|
805
|
+
}
|
|
806
|
+
else if (selfPinResult.action === 'skipped-malformed-package-json') {
|
|
807
|
+
warn(selfPinResult.message);
|
|
808
|
+
}
|
|
809
|
+
else if (selfPinResult.action === 'skipped-symlink-package-json') {
|
|
810
|
+
// R10-P2 (codex round 10): dry-run path only. Live mode throws
|
|
811
|
+
// at the symlink check; reaching this branch means the
|
|
812
|
+
// pre-flight already described the block-symlink condition to
|
|
813
|
+
// the operator earlier in this `rea upgrade --dry-run`
|
|
814
|
+
// invocation. No additional output needed beyond the
|
|
815
|
+
// already-printed pre-flight diagnostic.
|
|
816
|
+
}
|
|
817
|
+
}
|
|
684
818
|
// BUG-010 — ensure `.gitignore` carries every runtime artifact entry. This
|
|
685
819
|
// backfills older installs that predate the scaffolding in `rea init`. A
|
|
686
820
|
// consumer who upgraded from 0.3.x/0.4.0 was previously seeing
|
package/dist/policy/loader.d.ts
CHANGED
|
@@ -308,6 +308,13 @@ declare const PolicySchema: z.ZodObject<{
|
|
|
308
308
|
}, {
|
|
309
309
|
enabled?: boolean | undefined;
|
|
310
310
|
}>>;
|
|
311
|
+
bootstrap_allowlist: z.ZodOptional<z.ZodObject<{
|
|
312
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
313
|
+
}, "strict", z.ZodTypeAny, {
|
|
314
|
+
enabled: boolean;
|
|
315
|
+
}, {
|
|
316
|
+
enabled?: boolean | undefined;
|
|
317
|
+
}>>;
|
|
311
318
|
}, "strict", z.ZodTypeAny, {
|
|
312
319
|
version: string;
|
|
313
320
|
profile: string;
|
|
@@ -389,6 +396,9 @@ declare const PolicySchema: z.ZodObject<{
|
|
|
389
396
|
shim_cache?: {
|
|
390
397
|
enabled: boolean;
|
|
391
398
|
} | undefined;
|
|
399
|
+
bootstrap_allowlist?: {
|
|
400
|
+
enabled: boolean;
|
|
401
|
+
} | undefined;
|
|
392
402
|
}, {
|
|
393
403
|
version: string;
|
|
394
404
|
profile: string;
|
|
@@ -470,6 +480,9 @@ declare const PolicySchema: z.ZodObject<{
|
|
|
470
480
|
shim_cache?: {
|
|
471
481
|
enabled?: boolean | undefined;
|
|
472
482
|
} | undefined;
|
|
483
|
+
bootstrap_allowlist?: {
|
|
484
|
+
enabled?: boolean | undefined;
|
|
485
|
+
} | undefined;
|
|
473
486
|
}>;
|
|
474
487
|
/**
|
|
475
488
|
* Async policy loader with TTL cache and mtime-based invalidation.
|
package/dist/policy/loader.js
CHANGED
|
@@ -354,6 +354,32 @@ const ShimCachePolicySchema = z
|
|
|
354
354
|
enabled: z.boolean().default(true),
|
|
355
355
|
})
|
|
356
356
|
.strict();
|
|
357
|
+
/**
|
|
358
|
+
* 0.49.0 — bootstrap allowlist policy. Drives the narrow CLI-missing
|
|
359
|
+
* pass-through in `hooks/_lib/bootstrap-allowlist.sh` that the
|
|
360
|
+
* `blocked-paths-bash-gate.sh` and `protected-paths-bash-gate.sh`
|
|
361
|
+
* shims consult when the rea CLI is unreachable. See the helper
|
|
362
|
+
* header for the full security contract.
|
|
363
|
+
*
|
|
364
|
+
* The block is optional — vanilla installs with no
|
|
365
|
+
* `bootstrap_allowlist:` block get the default behavior (enabled).
|
|
366
|
+
* To disable: `bootstrap_allowlist: { enabled: false }`.
|
|
367
|
+
*
|
|
368
|
+
* Strict mode rejects unknown keys so a typo (`enabld`, `enable`)
|
|
369
|
+
* fails loudly at policy load.
|
|
370
|
+
*
|
|
371
|
+
* The bash-tier helper does a narrow YAML grep for the field via
|
|
372
|
+
* inline node (engines.node >=22 guarantees availability) BEFORE the
|
|
373
|
+
* canonical 4-tier policy reader is available (the allowlist runs
|
|
374
|
+
* precisely BECAUSE the CLI is missing). This zod schema validates
|
|
375
|
+
* the field at CLI load time so wrong types / typos are caught at the
|
|
376
|
+
* load boundary.
|
|
377
|
+
*/
|
|
378
|
+
const BootstrapAllowlistPolicySchema = z
|
|
379
|
+
.object({
|
|
380
|
+
enabled: z.boolean().default(true),
|
|
381
|
+
})
|
|
382
|
+
.strict();
|
|
357
383
|
const PolicySchema = z
|
|
358
384
|
.object({
|
|
359
385
|
version: z.string(),
|
|
@@ -423,6 +449,16 @@ const PolicySchema = z
|
|
|
423
449
|
// in env overrides this to `false` for the current invocation
|
|
424
450
|
// regardless of policy.
|
|
425
451
|
shim_cache: ShimCachePolicySchema.optional(),
|
|
452
|
+
// 0.49.0 — bootstrap allowlist. Narrow CLI-missing pass-through
|
|
453
|
+
// for the `pnpm install` / `npm ci` / `yarn` / `corepack enable`
|
|
454
|
+
// class of recovery commands. ALWAYS-ON by default; opt-out via
|
|
455
|
+
// `bootstrap_allowlist: { enabled: false }`. The bash-tier
|
|
456
|
+
// gate consults the field via inline node BEFORE the canonical
|
|
457
|
+
// 4-tier policy reader is reachable, since the whole reason it
|
|
458
|
+
// runs is that the CLI is unbuilt. See
|
|
459
|
+
// `hooks/_lib/bootstrap-allowlist.sh` and
|
|
460
|
+
// `THREAT_MODEL.md §5.X`.
|
|
461
|
+
bootstrap_allowlist: BootstrapAllowlistPolicySchema.optional(),
|
|
426
462
|
})
|
|
427
463
|
.strict();
|
|
428
464
|
const DEFAULT_CACHE_TTL_MS = 30_000;
|
|
@@ -121,6 +121,13 @@ export declare const ProfileSchema: z.ZodObject<{
|
|
|
121
121
|
threshold?: number | undefined;
|
|
122
122
|
exempt_subagents?: string[] | undefined;
|
|
123
123
|
}>>;
|
|
124
|
+
bootstrap_allowlist: z.ZodOptional<z.ZodObject<{
|
|
125
|
+
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
126
|
+
}, "strict", z.ZodTypeAny, {
|
|
127
|
+
enabled?: boolean | undefined;
|
|
128
|
+
}, {
|
|
129
|
+
enabled?: boolean | undefined;
|
|
130
|
+
}>>;
|
|
124
131
|
}, "strict", z.ZodTypeAny, {
|
|
125
132
|
autonomy_level?: AutonomyLevel | undefined;
|
|
126
133
|
max_autonomy_level?: AutonomyLevel | undefined;
|
|
@@ -160,6 +167,9 @@ export declare const ProfileSchema: z.ZodObject<{
|
|
|
160
167
|
threshold?: number | undefined;
|
|
161
168
|
exempt_subagents?: string[] | undefined;
|
|
162
169
|
} | undefined;
|
|
170
|
+
bootstrap_allowlist?: {
|
|
171
|
+
enabled?: boolean | undefined;
|
|
172
|
+
} | undefined;
|
|
163
173
|
}, {
|
|
164
174
|
autonomy_level?: AutonomyLevel | undefined;
|
|
165
175
|
max_autonomy_level?: AutonomyLevel | undefined;
|
|
@@ -199,6 +209,9 @@ export declare const ProfileSchema: z.ZodObject<{
|
|
|
199
209
|
threshold?: number | undefined;
|
|
200
210
|
exempt_subagents?: string[] | undefined;
|
|
201
211
|
} | undefined;
|
|
212
|
+
bootstrap_allowlist?: {
|
|
213
|
+
enabled?: boolean | undefined;
|
|
214
|
+
} | undefined;
|
|
202
215
|
}>;
|
|
203
216
|
export type Profile = z.infer<typeof ProfileSchema>;
|
|
204
217
|
/** Hard defaults applied before any profile or wizard answer. */
|
package/dist/policy/profiles.js
CHANGED
|
@@ -116,6 +116,18 @@ export const ProfileSchema = z
|
|
|
116
116
|
})
|
|
117
117
|
.strict()
|
|
118
118
|
.optional(),
|
|
119
|
+
// 0.49.0+ bootstrap allowlist (P3-1) — narrow CLI-missing pass-through
|
|
120
|
+
// in `hooks/_lib/bootstrap-allowlist.sh`. The `bst-internal` profile
|
|
121
|
+
// pins `enabled: true` for parity with `.rea/policy.yaml`; every other
|
|
122
|
+
// shipped profile inherits the schema default (also `true`). The
|
|
123
|
+
// profile-layer schema mirrors the policy-loader's
|
|
124
|
+
// `BootstrapAllowlistPolicySchema`; strict mode catches typos at init.
|
|
125
|
+
bootstrap_allowlist: z
|
|
126
|
+
.object({
|
|
127
|
+
enabled: z.boolean().optional(),
|
|
128
|
+
})
|
|
129
|
+
.strict()
|
|
130
|
+
.optional(),
|
|
119
131
|
})
|
|
120
132
|
.strict();
|
|
121
133
|
/** Hard defaults applied before any profile or wizard answer. */
|
package/dist/policy/types.d.ts
CHANGED
|
@@ -557,6 +557,44 @@ export interface Policy {
|
|
|
557
557
|
* methodology note.
|
|
558
558
|
*/
|
|
559
559
|
shim_cache?: ShimCachePolicy;
|
|
560
|
+
/**
|
|
561
|
+
* Bootstrap allowlist (0.49.0+).
|
|
562
|
+
*
|
|
563
|
+
* Drives the narrow CLI-missing pass-through in
|
|
564
|
+
* `hooks/_lib/bootstrap-allowlist.sh`. When the bash-tier
|
|
565
|
+
* `blocked-paths-bash-gate.sh` / `protected-paths-bash-gate.sh`
|
|
566
|
+
* shims would refuse a Bash payload because the rea CLI is
|
|
567
|
+
* unreachable, the allowlist consults this block + the
|
|
568
|
+
* consumer's `package.json` to decide whether the payload is a
|
|
569
|
+
* legitimate recovery command (pnpm install / npm ci / yarn /
|
|
570
|
+
* corepack enable / etc.) that should pass through.
|
|
571
|
+
*
|
|
572
|
+
* Always-on by default. Set `enabled: false` to disable.
|
|
573
|
+
* Opt-out is the only knob — the allowlist itself does not
|
|
574
|
+
* accept env-var participation in the decision (security
|
|
575
|
+
* architect locked).
|
|
576
|
+
*/
|
|
577
|
+
bootstrap_allowlist?: BootstrapAllowlistPolicy;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Bootstrap allowlist policy (0.49.0+).
|
|
581
|
+
*
|
|
582
|
+
* See `hooks/_lib/bootstrap-allowlist.sh` for the full contract and
|
|
583
|
+
* `THREAT_MODEL.md §5.X` for the threat-model analysis. The single
|
|
584
|
+
* knob is `enabled` — there is no list of allowed shapes in policy;
|
|
585
|
+
* the shape set is hardcoded in the helper because the threat model
|
|
586
|
+
* depends on it being fixed (a consumer-mutable shape list would let
|
|
587
|
+
* an attacker who can edit `policy.yaml` widen the pass-through).
|
|
588
|
+
*
|
|
589
|
+
* `policy.yaml` is itself a `blocked_paths` entry in the
|
|
590
|
+
* `bst-internal` profile (so a consumer who is dogfooding rea's
|
|
591
|
+
* own profile cannot turn the allowlist off via a Bash payload
|
|
592
|
+
* without first earning a separate Write-tier audit event), but
|
|
593
|
+
* the field is intentionally simple so external profiles can drop
|
|
594
|
+
* it back to consumer-editable.
|
|
595
|
+
*/
|
|
596
|
+
export interface BootstrapAllowlistPolicy {
|
|
597
|
+
enabled?: boolean;
|
|
560
598
|
}
|
|
561
599
|
/**
|
|
562
600
|
* Per-session shim cache policy (0.48.0+).
|