@bookedsolid/rea 0.22.0 → 0.23.1
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/README.md +15 -0
- package/THREAT_MODEL.md +753 -0
- package/dist/audit/append.js +1 -1
- package/dist/cli/doctor.js +11 -12
- package/dist/cli/hook.d.ts +37 -3
- package/dist/cli/hook.js +167 -5
- package/dist/cli/init.js +14 -26
- package/dist/cli/install/canonical.js +18 -3
- package/dist/cli/install/commit-msg.js +1 -2
- package/dist/cli/install/copy.js +4 -13
- package/dist/cli/install/fs-safe.js +5 -16
- package/dist/cli/install/gitignore.js +1 -5
- package/dist/cli/install/pre-push.js +3 -8
- package/dist/cli/install/settings-merge.js +79 -16
- package/dist/cli/upgrade.js +14 -10
- package/dist/gateway/downstream.js +1 -2
- package/dist/gateway/live-state.js +3 -1
- package/dist/gateway/log.js +1 -3
- package/dist/gateway/middleware/audit.js +1 -1
- package/dist/gateway/middleware/injection.js +3 -9
- package/dist/gateway/middleware/policy.js +3 -1
- package/dist/gateway/middleware/redact.js +1 -1
- package/dist/gateway/observability/codex-telemetry.js +1 -2
- package/dist/gateway/reviewers/claude-self.js +10 -6
- package/dist/hooks/bash-scanner/blocked-scan.d.ts +26 -0
- package/dist/hooks/bash-scanner/blocked-scan.js +467 -0
- package/dist/hooks/bash-scanner/index.d.ts +41 -0
- package/dist/hooks/bash-scanner/index.js +62 -0
- package/dist/hooks/bash-scanner/parse-fail-closed.d.ts +31 -0
- package/dist/hooks/bash-scanner/parse-fail-closed.js +27 -0
- package/dist/hooks/bash-scanner/parser.d.ts +42 -0
- package/dist/hooks/bash-scanner/parser.js +92 -0
- package/dist/hooks/bash-scanner/protected-scan.d.ts +76 -0
- package/dist/hooks/bash-scanner/protected-scan.js +868 -0
- package/dist/hooks/bash-scanner/verdict.d.ts +80 -0
- package/dist/hooks/bash-scanner/verdict.js +49 -0
- package/dist/hooks/bash-scanner/walker.d.ts +165 -0
- package/dist/hooks/bash-scanner/walker.js +9087 -0
- package/dist/hooks/push-gate/base.js +2 -6
- package/dist/hooks/push-gate/codex-runner.js +3 -1
- package/dist/hooks/push-gate/index.js +9 -10
- package/dist/policy/loader.js +4 -1
- package/dist/registry/tofu-gate.js +2 -2
- package/hooks/blocked-paths-bash-gate.sh +142 -272
- package/hooks/protected-paths-bash-gate.sh +227 -511
- package/package.json +3 -2
- package/profiles/bst-internal-no-codex.yaml +1 -1
- package/profiles/bst-internal.yaml +1 -1
- package/profiles/client-engagement.yaml +1 -1
- package/profiles/lit-wc.yaml +1 -1
- package/profiles/minimal.yaml +1 -1
- package/profiles/open-source-no-codex.yaml +1 -1
- package/profiles/open-source.yaml +1 -1
- package/scripts/postinstall.mjs +1 -2
- package/scripts/run-vitest.mjs +117 -0
|
@@ -138,9 +138,7 @@ export function mergeSettings(existing, desired) {
|
|
|
138
138
|
type: wantHook.type,
|
|
139
139
|
command: wantHook.command,
|
|
140
140
|
...(wantHook.timeout !== undefined ? { timeout: wantHook.timeout } : {}),
|
|
141
|
-
...(wantHook.statusMessage !== undefined
|
|
142
|
-
? { statusMessage: wantHook.statusMessage }
|
|
143
|
-
: {}),
|
|
141
|
+
...(wantHook.statusMessage !== undefined ? { statusMessage: wantHook.statusMessage } : {}),
|
|
144
142
|
});
|
|
145
143
|
seen.add(k);
|
|
146
144
|
addedCount += 1;
|
|
@@ -257,31 +255,96 @@ export function defaultDesiredHooks() {
|
|
|
257
255
|
event: 'PreToolUse',
|
|
258
256
|
matcher: 'Bash',
|
|
259
257
|
hooks: [
|
|
260
|
-
{
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
{
|
|
267
|
-
|
|
258
|
+
{
|
|
259
|
+
type: 'command',
|
|
260
|
+
command: `${base}/dangerous-bash-interceptor.sh`,
|
|
261
|
+
timeout: 10000,
|
|
262
|
+
statusMessage: 'Checking command safety...',
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
type: 'command',
|
|
266
|
+
command: `${base}/env-file-protection.sh`,
|
|
267
|
+
timeout: 5000,
|
|
268
|
+
statusMessage: 'Checking for .env file reads...',
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
type: 'command',
|
|
272
|
+
command: `${base}/protected-paths-bash-gate.sh`,
|
|
273
|
+
timeout: 5000,
|
|
274
|
+
statusMessage: 'Checking for shell-redirect to protected paths...',
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
type: 'command',
|
|
278
|
+
command: `${base}/blocked-paths-bash-gate.sh`,
|
|
279
|
+
timeout: 5000,
|
|
280
|
+
statusMessage: 'Checking for shell-redirect to policy-blocked paths...',
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
type: 'command',
|
|
284
|
+
command: `${base}/dependency-audit-gate.sh`,
|
|
285
|
+
timeout: 15000,
|
|
286
|
+
statusMessage: 'Verifying package exists...',
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
type: 'command',
|
|
290
|
+
command: `${base}/security-disclosure-gate.sh`,
|
|
291
|
+
timeout: 5000,
|
|
292
|
+
statusMessage: 'Checking disclosure policy...',
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
type: 'command',
|
|
296
|
+
command: `${base}/pr-issue-link-gate.sh`,
|
|
297
|
+
timeout: 5000,
|
|
298
|
+
statusMessage: 'Checking PR for issue reference...',
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
type: 'command',
|
|
302
|
+
command: `${base}/attribution-advisory.sh`,
|
|
303
|
+
timeout: 5000,
|
|
304
|
+
statusMessage: 'Checking for AI attribution...',
|
|
305
|
+
},
|
|
268
306
|
],
|
|
269
307
|
},
|
|
270
308
|
{
|
|
271
309
|
event: 'PreToolUse',
|
|
272
310
|
matcher: 'Write|Edit|MultiEdit|NotebookEdit',
|
|
273
311
|
hooks: [
|
|
274
|
-
{
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
312
|
+
{
|
|
313
|
+
type: 'command',
|
|
314
|
+
command: `${base}/secret-scanner.sh`,
|
|
315
|
+
timeout: 15000,
|
|
316
|
+
statusMessage: 'Scanning for credentials...',
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
type: 'command',
|
|
320
|
+
command: `${base}/settings-protection.sh`,
|
|
321
|
+
timeout: 5000,
|
|
322
|
+
statusMessage: 'Checking settings protection...',
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
type: 'command',
|
|
326
|
+
command: `${base}/blocked-paths-enforcer.sh`,
|
|
327
|
+
timeout: 5000,
|
|
328
|
+
statusMessage: 'Checking blocked paths...',
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
type: 'command',
|
|
332
|
+
command: `${base}/changeset-security-gate.sh`,
|
|
333
|
+
timeout: 5000,
|
|
334
|
+
statusMessage: 'Checking changeset for security leaks...',
|
|
335
|
+
},
|
|
278
336
|
],
|
|
279
337
|
},
|
|
280
338
|
{
|
|
281
339
|
event: 'PostToolUse',
|
|
282
340
|
matcher: 'Write|Edit|MultiEdit|NotebookEdit',
|
|
283
341
|
hooks: [
|
|
284
|
-
{
|
|
342
|
+
{
|
|
343
|
+
type: 'command',
|
|
344
|
+
command: `${base}/architecture-review-gate.sh`,
|
|
345
|
+
timeout: 10000,
|
|
346
|
+
statusMessage: 'Checking architecture impact...',
|
|
347
|
+
},
|
|
285
348
|
],
|
|
286
349
|
},
|
|
287
350
|
];
|
package/dist/cli/upgrade.js
CHANGED
|
@@ -42,11 +42,11 @@ import path from 'node:path';
|
|
|
42
42
|
import * as p from '@clack/prompts';
|
|
43
43
|
import { loadPolicy } from '../policy/loader.js';
|
|
44
44
|
import { CLAUDE_MD_MANIFEST_PATH, SETTINGS_MANIFEST_PATH, enumerateCanonicalFiles, } from './install/canonical.js';
|
|
45
|
-
import { buildFragment, extractFragment
|
|
45
|
+
import { buildFragment, extractFragment } from './install/claude-md.js';
|
|
46
46
|
import { atomicReplaceFile, safeDeleteFile, safeInstallFile, safeReadFile, } from './install/fs-safe.js';
|
|
47
47
|
import { canonicalSettingsSubsetHash, defaultDesiredHooks, mergeSettings, pruneHookCommands, readSettings, writeSettingsAtomic, } from './install/settings-merge.js';
|
|
48
48
|
import { ensureReaGitignore } from './install/gitignore.js';
|
|
49
|
-
import { manifestExists, readManifest, writeManifestAtomic
|
|
49
|
+
import { manifestExists, readManifest, writeManifestAtomic } from './install/manifest-io.js';
|
|
50
50
|
import { sha256OfBuffer, sha256OfFile } from './install/sha.js';
|
|
51
51
|
import { err, getPkgVersion, log, warn } from './utils.js';
|
|
52
52
|
// ---------------------------------------------------------------------------
|
|
@@ -219,7 +219,11 @@ async function promptDriftDecision(resolvedRoot, canonical, opts) {
|
|
|
219
219
|
initialValue: 'keep',
|
|
220
220
|
options: [
|
|
221
221
|
{ value: 'keep', label: 'keep', hint: 'leave your version untouched (default)' },
|
|
222
|
-
{
|
|
222
|
+
{
|
|
223
|
+
value: 'overwrite',
|
|
224
|
+
label: 'overwrite',
|
|
225
|
+
hint: `replace with canonical (rea v${getPkgVersion()})`,
|
|
226
|
+
},
|
|
223
227
|
{ value: 'diff', label: 'diff', hint: 'show diff, then re-prompt' },
|
|
224
228
|
],
|
|
225
229
|
});
|
|
@@ -302,8 +306,7 @@ async function classifyFiles(resolvedRoot, canonicalFiles, manifest) {
|
|
|
302
306
|
// Removed-upstream: in manifest but not in canonical set.
|
|
303
307
|
if (manifest !== null) {
|
|
304
308
|
for (const entry of manifest.files) {
|
|
305
|
-
if (entry.path === CLAUDE_MD_MANIFEST_PATH ||
|
|
306
|
-
entry.path === SETTINGS_MANIFEST_PATH) {
|
|
309
|
+
if (entry.path === CLAUDE_MD_MANIFEST_PATH || entry.path === SETTINGS_MANIFEST_PATH) {
|
|
307
310
|
continue; // synthetic entries handled separately
|
|
308
311
|
}
|
|
309
312
|
if (!canonicalByPath.has(entry.path)) {
|
|
@@ -555,9 +558,7 @@ export async function runUpgrade(options = {}) {
|
|
|
555
558
|
}
|
|
556
559
|
}
|
|
557
560
|
else if (c.kind === 'removed-upstream') {
|
|
558
|
-
const decision = dryRun
|
|
559
|
-
? 'skip'
|
|
560
|
-
: await promptRemovedDecision(c.entry.path, options);
|
|
561
|
+
const decision = dryRun ? 'skip' : await promptRemovedDecision(c.entry.path, options);
|
|
561
562
|
if (decision === 'delete') {
|
|
562
563
|
console.log(` - ${c.entry.path} (deleted)`);
|
|
563
564
|
if (!dryRun) {
|
|
@@ -626,9 +627,12 @@ export async function runUpgrade(options = {}) {
|
|
|
626
627
|
if (dryRun) {
|
|
627
628
|
console.log('');
|
|
628
629
|
log('dry run — no changes written.');
|
|
629
|
-
const planned = counts.new_ +
|
|
630
|
+
const planned = counts.new_ +
|
|
631
|
+
counts.drifted +
|
|
632
|
+
counts.removedUpstream +
|
|
630
633
|
(classifications.some((c) => c.kind === 'unmodified' && c.canonicalSha !== c.localSha)
|
|
631
|
-
? classifications.filter((c) => c.kind === 'unmodified' && c.canonicalSha !== c.localSha)
|
|
634
|
+
? classifications.filter((c) => c.kind === 'unmodified' && c.canonicalSha !== c.localSha)
|
|
635
|
+
.length
|
|
632
636
|
: 0);
|
|
633
637
|
console.log(` ${planned} file action(s) planned.`);
|
|
634
638
|
return;
|
|
@@ -214,8 +214,7 @@ export class DownstreamConnection {
|
|
|
214
214
|
// loud instead.
|
|
215
215
|
throw new TypeError(`DownstreamConnection#lastErrorMessage: expected string | null, got ${typeof msg}`);
|
|
216
216
|
}
|
|
217
|
-
this.#lastErrorBacking =
|
|
218
|
-
msg === null ? null : boundedDiagnosticString(msg);
|
|
217
|
+
this.#lastErrorBacking = msg === null ? null : boundedDiagnosticString(msg);
|
|
219
218
|
}
|
|
220
219
|
constructor(config,
|
|
221
220
|
/**
|
|
@@ -374,7 +374,9 @@ export class LiveStatePublisher {
|
|
|
374
374
|
if (parsed.session_id === this.opts.sessionId)
|
|
375
375
|
return true;
|
|
376
376
|
// Foreign session_id. Use owner_pid to decide whether to yield or steal.
|
|
377
|
-
if (typeof parsed.owner_pid !== 'number' ||
|
|
377
|
+
if (typeof parsed.owner_pid !== 'number' ||
|
|
378
|
+
!Number.isFinite(parsed.owner_pid) ||
|
|
379
|
+
parsed.owner_pid <= 0) {
|
|
378
380
|
// Pre-0.9.0 file (no owner_pid recorded) or malformed value. We
|
|
379
381
|
// cannot prove the writer is alive, and refusing to write forever
|
|
380
382
|
// is the bigger hazard — claim the file. This is the same
|
package/dist/gateway/log.js
CHANGED
|
@@ -317,9 +317,7 @@ export function buildRegexRedactor(patterns) {
|
|
|
317
317
|
// the SafeRegex worker in the redact middleware) can leave non-zero,
|
|
318
318
|
// causing String.replace to start mid-string and silently skip leading
|
|
319
319
|
// secrets. The `y` (sticky) flag carries the same hazard as `g`.
|
|
320
|
-
const re =
|
|
321
|
-
? new RegExp(pattern.source, pattern.flags)
|
|
322
|
-
: pattern;
|
|
320
|
+
const re = pattern.global || pattern.sticky ? new RegExp(pattern.source, pattern.flags) : pattern;
|
|
323
321
|
out = out.replace(re, '[REDACTED]');
|
|
324
322
|
}
|
|
325
323
|
return out;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { Tier, InvocationStatus } from '../../policy/types.js';
|
|
4
|
-
import { computeHash, fsyncFile, readLastRecord, withAuditLock
|
|
4
|
+
import { computeHash, fsyncFile, readLastRecord, withAuditLock } from '../../audit/fs.js';
|
|
5
5
|
import { maybeRotate } from '../audit/rotator.js';
|
|
6
6
|
/**
|
|
7
7
|
* Post-execution middleware: appends a hash-chained JSONL audit record.
|
|
@@ -260,15 +260,11 @@ export function compileInjectionPatterns(timeoutMs, onTimeout) {
|
|
|
260
260
|
return {
|
|
261
261
|
base64Token: wrapRegex(INJECTION_BASE64_PATTERN, {
|
|
262
262
|
timeoutMs,
|
|
263
|
-
...(onTimeout
|
|
264
|
-
? { onTimeout: (_p, i) => onTimeout('INJECTION_BASE64_PATTERN', i) }
|
|
265
|
-
: {}),
|
|
263
|
+
...(onTimeout ? { onTimeout: (_p, i) => onTimeout('INJECTION_BASE64_PATTERN', i) } : {}),
|
|
266
264
|
}),
|
|
267
265
|
base64Shape: wrapRegex(INJECTION_BASE64_SHAPE, {
|
|
268
266
|
timeoutMs,
|
|
269
|
-
...(onTimeout
|
|
270
|
-
? { onTimeout: (_p, i) => onTimeout('INJECTION_BASE64_SHAPE', i) }
|
|
271
|
-
: {}),
|
|
267
|
+
...(onTimeout ? { onTimeout: (_p, i) => onTimeout('INJECTION_BASE64_SHAPE', i) } : {}),
|
|
272
268
|
}),
|
|
273
269
|
};
|
|
274
270
|
}
|
|
@@ -364,9 +360,7 @@ export function classifyInjection(scan, tier) {
|
|
|
364
360
|
// Dedupe: a phrase that appears both literally AND in a base64-decoded
|
|
365
361
|
// payload in the same input counts once in `matched_patterns`. Union via
|
|
366
362
|
// Set before sorting.
|
|
367
|
-
const matched = [
|
|
368
|
-
...new Set([...scan.literalMatches, ...scan.base64DecodedMatches]),
|
|
369
|
-
].sort();
|
|
363
|
+
const matched = [...new Set([...scan.literalMatches, ...scan.base64DecodedMatches])].sort();
|
|
370
364
|
// Rule 2 — base64 always escalates, regardless of count or tier.
|
|
371
365
|
if (base64Count > 0) {
|
|
372
366
|
return {
|
|
@@ -12,7 +12,9 @@ function extractReaSubcommand(command) {
|
|
|
12
12
|
if (first === undefined)
|
|
13
13
|
return null;
|
|
14
14
|
let idx = 0;
|
|
15
|
-
if (first === 'npx' &&
|
|
15
|
+
if (first === 'npx' &&
|
|
16
|
+
tokens.length >= 2 &&
|
|
17
|
+
(tokens[1] === 'rea' || tokens[1] === '@bookedsolid/rea')) {
|
|
16
18
|
idx = 2;
|
|
17
19
|
}
|
|
18
20
|
else if (first === 'rea' || first.endsWith('/rea')) {
|
|
@@ -202,8 +202,7 @@ export async function summarizeTelemetry(baseDir, windowDays = 7) {
|
|
|
202
202
|
if (!countsByKey.has(key))
|
|
203
203
|
continue; // outside window
|
|
204
204
|
countsByKey.set(key, (countsByKey.get(key) ?? 0) + 1);
|
|
205
|
-
totalTokens +=
|
|
206
|
-
(r.estimated_input_tokens ?? 0) + (r.estimated_output_tokens ?? 0);
|
|
205
|
+
totalTokens += (r.estimated_input_tokens ?? 0) + (r.estimated_output_tokens ?? 0);
|
|
207
206
|
if (r.rate_limited)
|
|
208
207
|
rateLimitedCount += 1;
|
|
209
208
|
durationSum += r.duration_ms ?? 0;
|
|
@@ -140,10 +140,12 @@ function toReviewFinding(input) {
|
|
|
140
140
|
'performance',
|
|
141
141
|
];
|
|
142
142
|
const validSeverities = ['high', 'medium', 'low'];
|
|
143
|
-
if (typeof o['category'] !== 'string' ||
|
|
143
|
+
if (typeof o['category'] !== 'string' ||
|
|
144
|
+
!validCategories.includes(o['category'])) {
|
|
144
145
|
return undefined;
|
|
145
146
|
}
|
|
146
|
-
if (typeof o['severity'] !== 'string' ||
|
|
147
|
+
if (typeof o['severity'] !== 'string' ||
|
|
148
|
+
!validSeverities.includes(o['severity'])) {
|
|
147
149
|
return undefined;
|
|
148
150
|
}
|
|
149
151
|
if (typeof o['file'] !== 'string')
|
|
@@ -210,9 +212,7 @@ export class ClaudeSelfReviewer {
|
|
|
210
212
|
}
|
|
211
213
|
const diffBytes = Buffer.byteLength(req.diff, 'utf8');
|
|
212
214
|
const truncated = diffBytes > DIFF_TRUNCATE_BYTES;
|
|
213
|
-
const effectiveDiff = truncated
|
|
214
|
-
? req.diff.slice(0, DIFF_TRUNCATE_BYTES)
|
|
215
|
-
: req.diff;
|
|
215
|
+
const effectiveDiff = truncated ? req.diff.slice(0, DIFF_TRUNCATE_BYTES) : req.diff;
|
|
216
216
|
const userMessage = buildUserMessage({ ...req, diff: effectiveDiff }, truncated);
|
|
217
217
|
// G11.5 — measure the SDK call. The telemetry write is best-effort and
|
|
218
218
|
// fire-and-forget; it MUST NOT block or fail the review. We call
|
|
@@ -237,7 +237,11 @@ export class ClaudeSelfReviewer {
|
|
|
237
237
|
// Rate-limits, 5xx, network errors all land here. Surface the raw
|
|
238
238
|
// message so operators can act on it; the caller decides whether
|
|
239
239
|
// to retry or abort.
|
|
240
|
-
const message = err instanceof APIError
|
|
240
|
+
const message = err instanceof APIError
|
|
241
|
+
? `API ${err.status ?? '?'}: ${err.message}`
|
|
242
|
+
: err instanceof Error
|
|
243
|
+
? err.message
|
|
244
|
+
: String(err);
|
|
241
245
|
this.emitTelemetry({
|
|
242
246
|
invocation_type: 'adversarial-review',
|
|
243
247
|
input_text: userMessage,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blocked-paths policy composition. Mirrors `blocked-paths-bash-gate.sh`
|
|
3
|
+
* + the `_match_blocked` helper byte-for-byte:
|
|
4
|
+
*
|
|
5
|
+
* - directory entry (ends with `/`): prefix match OR exact match
|
|
6
|
+
* against the bare-dir form (entry without trailing slash)
|
|
7
|
+
* - glob entry (contains `*`): convert to ERE (escape `.`, `*` → `.*`),
|
|
8
|
+
* anchored, case-insensitive
|
|
9
|
+
* - exact (case-insensitive) otherwise
|
|
10
|
+
*
|
|
11
|
+
* Path normalization is identical to protected-scan.ts: URL-decode,
|
|
12
|
+
* backslash → slash, leading-./ strip, `..` walk-up + outside-root
|
|
13
|
+
* sentinel, optional symlink-resolved form.
|
|
14
|
+
*
|
|
15
|
+
* Out-of-scope-of-blocked: paths outside REA_ROOT. blocked_paths is a
|
|
16
|
+
* project-relative concept; an outside-root write can't match a
|
|
17
|
+
* blocked_paths entry. The PROTECTED-paths gate handles outside-root
|
|
18
|
+
* rejection on the protected list itself.
|
|
19
|
+
*/
|
|
20
|
+
import { type Verdict } from './verdict.js';
|
|
21
|
+
import type { DetectedWrite } from './walker.js';
|
|
22
|
+
export interface BlockedScanContext {
|
|
23
|
+
reaRoot: string;
|
|
24
|
+
blockedPaths: readonly string[];
|
|
25
|
+
}
|
|
26
|
+
export declare function scanForBlockedViolations(ctx: BlockedScanContext, detections: readonly DetectedWrite[]): Verdict;
|