@bookedsolid/rea 0.33.0 → 0.35.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/dist/cli/hook.js +49 -0
- package/dist/hooks/_lib/path-normalize.d.ts +81 -0
- package/dist/hooks/_lib/path-normalize.js +171 -0
- package/dist/hooks/_lib/payload.js +1 -1
- package/dist/hooks/_lib/protected-paths.d.ts +0 -0
- package/dist/hooks/_lib/protected-paths.js +232 -0
- package/dist/hooks/_lib/segments.d.ts +102 -0
- package/dist/hooks/_lib/segments.js +290 -0
- package/dist/hooks/blocked-paths-bash-gate/index.d.ts +55 -0
- package/dist/hooks/blocked-paths-bash-gate/index.js +175 -0
- package/dist/hooks/blocked-paths-enforcer/index.d.ts +51 -0
- package/dist/hooks/blocked-paths-enforcer/index.js +287 -0
- package/dist/hooks/dangerous-bash-interceptor/index.d.ts +103 -0
- package/dist/hooks/dangerous-bash-interceptor/index.js +669 -0
- package/dist/hooks/local-review-gate/index.d.ts +145 -0
- package/dist/hooks/local-review-gate/index.js +374 -0
- package/dist/hooks/protected-paths-bash-gate/index.d.ts +47 -0
- package/dist/hooks/protected-paths-bash-gate/index.js +168 -0
- package/dist/hooks/secret-scanner/index.d.ts +143 -0
- package/dist/hooks/secret-scanner/index.js +404 -0
- package/dist/hooks/settings-protection/index.d.ts +74 -0
- package/dist/hooks/settings-protection/index.js +485 -0
- package/hooks/blocked-paths-bash-gate.sh +118 -116
- package/hooks/blocked-paths-enforcer.sh +152 -256
- package/hooks/dangerous-bash-interceptor.sh +168 -386
- package/hooks/local-review-gate.sh +523 -410
- package/hooks/protected-paths-bash-gate.sh +123 -210
- package/hooks/secret-scanner.sh +210 -200
- package/hooks/settings-protection.sh +171 -549
- package/package.json +1 -1
- package/templates/blocked-paths-bash-gate.dogfood-staged.sh +177 -0
- package/templates/blocked-paths-enforcer.dogfood-staged.sh +180 -0
- package/templates/dangerous-bash-interceptor.dogfood-staged.sh +196 -0
- package/templates/local-review-gate.dogfood-staged.sh +573 -0
- package/templates/protected-paths-bash-gate.dogfood-staged.sh +186 -0
- package/templates/secret-scanner.dogfood-staged.sh +240 -0
- package/templates/settings-protection.dogfood-staged.sh +204 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-binary port of `hooks/blocked-paths-enforcer.sh`.
|
|
3
|
+
*
|
|
4
|
+
* 0.35.0 Phase 4 port (paired Write/Edit tier). Enforces
|
|
5
|
+
* `policy.blocked_paths` against Write/Edit/MultiEdit/NotebookEdit
|
|
6
|
+
* tool calls. Sibling of `blocked-paths-bash-gate` (Bash-tier) — same
|
|
7
|
+
* policy data, different surface.
|
|
8
|
+
*
|
|
9
|
+
* Behavioral contract — preserves the bash hook byte-for-byte:
|
|
10
|
+
*
|
|
11
|
+
* 1. HALT check → exit 2 with shared banner.
|
|
12
|
+
* 2. Read stdin, extract `tool_input.file_path` (or `notebook_path`).
|
|
13
|
+
* Missing/empty → exit 0.
|
|
14
|
+
* 3. Load policy permissively (a partial / migrating policy.yaml
|
|
15
|
+
* must NOT collapse the blocked_paths list).
|
|
16
|
+
* 4. Empty `blocked_paths` → exit 0.
|
|
17
|
+
* 5. §5a path-traversal rejection. Refuses any path with a `..`
|
|
18
|
+
* segment in EITHER the raw form OR the normalized form. Also
|
|
19
|
+
* catches URL-encoded traversal (`%2E%2E/`, `..%2F`, etc.)
|
|
20
|
+
* against the raw input.
|
|
21
|
+
* 6. §5a-bis interior `/./` segment rejection (0.29.0 helix-/./-class).
|
|
22
|
+
* NORMALIZED form only — `normalize_path` already strips leading
|
|
23
|
+
* `./` segments, so anything remaining is interior by construction.
|
|
24
|
+
* 7. Agent-writable allow-list short-circuit (`.rea/tasks.jsonl`,
|
|
25
|
+
* `.rea/audit/`) — even if blocked_paths includes `.rea/` as a
|
|
26
|
+
* prefix block, these are PM-data writeables.
|
|
27
|
+
* 8. Match the normalized path against each blocked entry:
|
|
28
|
+
* - directory prefix (entry ends with `/`)
|
|
29
|
+
* - glob (entry contains `*`)
|
|
30
|
+
* - exact (lower-case, case-INSENSITIVE)
|
|
31
|
+
* Match → exit 2 with reason.
|
|
32
|
+
* 9. §H.2 intermediate-symlink resolution. If the parent dir exists,
|
|
33
|
+
* resolve its realpath. If the resolved target falls inside a
|
|
34
|
+
* blocked entry, refuse.
|
|
35
|
+
*
|
|
36
|
+
* Audit-log parity: emits a `rea.hook.blocked-paths-enforcer` entry.
|
|
37
|
+
*/
|
|
38
|
+
import path from 'node:path';
|
|
39
|
+
import fs from 'node:fs';
|
|
40
|
+
import { parse as parseYaml } from 'yaml';
|
|
41
|
+
import { checkHalt, formatHaltBanner } from '../_lib/halt-check.js';
|
|
42
|
+
import { parseWriteHookPayload, MalformedPayloadError, TypePayloadError, readStdinWithTimeout, } from '../_lib/payload.js';
|
|
43
|
+
import { normalizePath, hasTraversalSegment, hasInteriorDotSegment, resolveCanonRoot, resolveParentRealpath, } from '../_lib/path-normalize.js';
|
|
44
|
+
import { appendAuditRecord, InvocationStatus, Tier } from '../../audit/append.js';
|
|
45
|
+
const AGENT_WRITABLE = ['.rea/tasks.jsonl', '.rea/audit/'];
|
|
46
|
+
/** Match `pathLc` against a single blocked entry. Returns true on hit. */
|
|
47
|
+
function matchBlockedEntry(pathLc, blockedEntry) {
|
|
48
|
+
const entryLc = blockedEntry.toLowerCase();
|
|
49
|
+
// Directory prefix.
|
|
50
|
+
if (entryLc.endsWith('/')) {
|
|
51
|
+
if (pathLc.startsWith(entryLc))
|
|
52
|
+
return true;
|
|
53
|
+
if (pathLc === entryLc.slice(0, -1))
|
|
54
|
+
return true;
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
// Glob (contains *).
|
|
58
|
+
if (entryLc.includes('*')) {
|
|
59
|
+
// Convert glob to regex: . → \., * → .*; anchor to whole string.
|
|
60
|
+
const escaped = entryLc.replace(/[.+^${}()|[\]\\]/g, (m) => `\\${m}`);
|
|
61
|
+
const re = '^' + escaped.replace(/\*/g, '.*') + '$';
|
|
62
|
+
try {
|
|
63
|
+
return new RegExp(re).test(pathLc);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Exact.
|
|
70
|
+
return pathLc === entryLc;
|
|
71
|
+
}
|
|
72
|
+
function loadBlockedPathsPermissive(reaRoot) {
|
|
73
|
+
const policyPath = path.join(reaRoot, '.rea', 'policy.yaml');
|
|
74
|
+
if (!fs.existsSync(policyPath))
|
|
75
|
+
return [];
|
|
76
|
+
let raw;
|
|
77
|
+
try {
|
|
78
|
+
raw = fs.readFileSync(policyPath, 'utf8');
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = parseYaml(raw);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
const obj = parsed;
|
|
94
|
+
const bp = obj['blocked_paths'];
|
|
95
|
+
if (!Array.isArray(bp))
|
|
96
|
+
return [];
|
|
97
|
+
const out = [];
|
|
98
|
+
for (const entry of bp) {
|
|
99
|
+
if (typeof entry === 'string' && entry.length > 0)
|
|
100
|
+
out.push(entry);
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
export async function runBlockedPathsEnforcer(options = {}) {
|
|
105
|
+
const reaRoot = options.reaRoot ?? process.env['CLAUDE_PROJECT_DIR'] ?? process.cwd();
|
|
106
|
+
let stderr = '';
|
|
107
|
+
const writeStderr = (s) => {
|
|
108
|
+
stderr += s;
|
|
109
|
+
if (options.stderrWrite)
|
|
110
|
+
options.stderrWrite(s);
|
|
111
|
+
};
|
|
112
|
+
// 1. HALT check.
|
|
113
|
+
const halt = checkHalt(reaRoot);
|
|
114
|
+
if (halt.halted) {
|
|
115
|
+
writeStderr(formatHaltBanner(halt.reason));
|
|
116
|
+
return { exitCode: 2, stderr, matched: null };
|
|
117
|
+
}
|
|
118
|
+
// 2. Read + parse stdin.
|
|
119
|
+
const stdinRaw = options.stdinOverride !== undefined
|
|
120
|
+
? options.stdinOverride
|
|
121
|
+
: await readStdinWithTimeout(5_000);
|
|
122
|
+
let filePath = '';
|
|
123
|
+
try {
|
|
124
|
+
const payload = parseWriteHookPayload(stdinRaw);
|
|
125
|
+
filePath = payload.filePath;
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
if (err instanceof MalformedPayloadError || err instanceof TypePayloadError) {
|
|
129
|
+
writeStderr(`blocked-paths-enforcer: ${err.message} — refusing on uncertainty.\n`);
|
|
130
|
+
return { exitCode: 2, stderr, matched: null };
|
|
131
|
+
}
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
if (filePath.length === 0) {
|
|
135
|
+
return { exitCode: 0, stderr, matched: null };
|
|
136
|
+
}
|
|
137
|
+
// 3. Load policy permissively.
|
|
138
|
+
const blockedPaths = loadBlockedPathsPermissive(reaRoot);
|
|
139
|
+
if (blockedPaths.length === 0) {
|
|
140
|
+
return { exitCode: 0, stderr, matched: null };
|
|
141
|
+
}
|
|
142
|
+
// 4. Normalize.
|
|
143
|
+
const normalized = normalizePath(filePath, reaRoot);
|
|
144
|
+
const lowerNorm = normalized.toLowerCase();
|
|
145
|
+
// 5. §5a path-traversal rejection. Both raw + normalized.
|
|
146
|
+
const rawTraversal = hasTraversalSegment(filePath.replace(/\\/g, '/'));
|
|
147
|
+
const normTraversal = hasTraversalSegment(normalized);
|
|
148
|
+
// URL-encoded traversal check on raw input.
|
|
149
|
+
const urlEncodedTraversal = /%2[Ee]%2[Ee]|%2[Ee]\.|\.%2[Ee]/.test(filePath);
|
|
150
|
+
if (rawTraversal || normTraversal || urlEncodedTraversal) {
|
|
151
|
+
writeStderr('BLOCKED PATH: path traversal rejected\n');
|
|
152
|
+
writeStderr('\n');
|
|
153
|
+
writeStderr(` File: ${filePath}\n`);
|
|
154
|
+
writeStderr(" Rule: path contains a '..' segment; rewrite to a canonical\n");
|
|
155
|
+
writeStderr(' project-relative path without traversal.\n');
|
|
156
|
+
return { exitCode: 2, stderr, matched: null };
|
|
157
|
+
}
|
|
158
|
+
// 6. §5a-bis interior `/./` segment rejection.
|
|
159
|
+
if (hasInteriorDotSegment(normalized)) {
|
|
160
|
+
writeStderr('BLOCKED PATH: interior dot-segment rejected\n');
|
|
161
|
+
writeStderr('\n');
|
|
162
|
+
writeStderr(` File: ${filePath}\n`);
|
|
163
|
+
writeStderr(" Rule: path contains an interior '/./' segment; rewrite to a\n");
|
|
164
|
+
writeStderr(' canonical project-relative path without dot segments.\n');
|
|
165
|
+
return { exitCode: 2, stderr, matched: null };
|
|
166
|
+
}
|
|
167
|
+
// 7. Agent-writable allow-list.
|
|
168
|
+
for (const writable of AGENT_WRITABLE) {
|
|
169
|
+
if (normalized === writable)
|
|
170
|
+
return { exitCode: 0, stderr, matched: null };
|
|
171
|
+
if (writable.endsWith('/') && normalized.startsWith(writable)) {
|
|
172
|
+
return { exitCode: 0, stderr, matched: null };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// 8. Match against blocked_paths.
|
|
176
|
+
let matched = null;
|
|
177
|
+
for (const blocked of blockedPaths) {
|
|
178
|
+
if (matchBlockedEntry(lowerNorm, blocked)) {
|
|
179
|
+
matched = blocked;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (matched !== null) {
|
|
184
|
+
const isGlob = matched.includes('*');
|
|
185
|
+
writeStderr('BLOCKED PATH: Write denied by policy\n');
|
|
186
|
+
writeStderr('\n');
|
|
187
|
+
writeStderr(` File: ${filePath}\n`);
|
|
188
|
+
writeStderr(` Blocked by: ${matched}${isGlob ? ' (glob pattern)' : ''}\n`);
|
|
189
|
+
writeStderr(' Source: .rea/policy.yaml → blocked_paths\n');
|
|
190
|
+
if (matched.endsWith('/')) {
|
|
191
|
+
writeStderr('\n');
|
|
192
|
+
writeStderr(' This path is protected by policy. To modify it, a human must\n');
|
|
193
|
+
writeStderr(' either update blocked_paths in policy.yaml or edit the file directly.\n');
|
|
194
|
+
}
|
|
195
|
+
await maybeAudit(reaRoot, 'denied', matched, filePath);
|
|
196
|
+
return { exitCode: 2, stderr, matched };
|
|
197
|
+
}
|
|
198
|
+
// 9. §H.2 intermediate-symlink resolution.
|
|
199
|
+
const symMatched = checkSymlinkResolution(filePath, blockedPaths, reaRoot);
|
|
200
|
+
if (symMatched !== null) {
|
|
201
|
+
writeStderr('BLOCKED PATH: intermediate-symlink resolution blocked\n');
|
|
202
|
+
writeStderr('\n');
|
|
203
|
+
writeStderr(` Logical: ${filePath}\n`);
|
|
204
|
+
writeStderr(` Resolved: ${symMatched.resolvedTarget}\n`);
|
|
205
|
+
writeStderr(` Blocked by: ${symMatched.entry}\n`);
|
|
206
|
+
writeStderr(' Source: .rea/policy.yaml → blocked_paths\n');
|
|
207
|
+
writeStderr('\n');
|
|
208
|
+
writeStderr(' Rule: an intermediate directory of the path is a symlink\n');
|
|
209
|
+
writeStderr(' whose target falls inside a blocked policy entry.\n');
|
|
210
|
+
await maybeAudit(reaRoot, 'denied', symMatched.entry, filePath);
|
|
211
|
+
return { exitCode: 2, stderr, matched: symMatched.entry };
|
|
212
|
+
}
|
|
213
|
+
await maybeAudit(reaRoot, 'allowed', null, filePath);
|
|
214
|
+
return { exitCode: 0, stderr, matched: null };
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Symlink-resolution check — mirrors `hooks/blocked-paths-enforcer.sh`
|
|
218
|
+
* §H.2. Returns the matched entry + resolved target form, or null.
|
|
219
|
+
*/
|
|
220
|
+
function checkSymlinkResolution(filePath, blockedPaths, reaRoot) {
|
|
221
|
+
// Only attempt resolution if the target exists or its parent dir
|
|
222
|
+
// exists — matches the bash `if [[ -e "$FILE_PATH" || -d ... ]]`.
|
|
223
|
+
let targetExists = false;
|
|
224
|
+
try {
|
|
225
|
+
targetExists = fs.existsSync(filePath);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
/* fall through */
|
|
229
|
+
}
|
|
230
|
+
const parentDir = path.dirname(filePath);
|
|
231
|
+
let parentExists = false;
|
|
232
|
+
try {
|
|
233
|
+
parentExists = fs.statSync(parentDir).isDirectory();
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
/* falls through */
|
|
237
|
+
}
|
|
238
|
+
if (!targetExists && !parentExists)
|
|
239
|
+
return null;
|
|
240
|
+
if (!parentExists)
|
|
241
|
+
return null;
|
|
242
|
+
const resolvedParent = resolveParentRealpath(filePath);
|
|
243
|
+
if (resolvedParent.length === 0)
|
|
244
|
+
return null;
|
|
245
|
+
const canonRoot = resolveCanonRoot(reaRoot);
|
|
246
|
+
// Resolved parent must be inside REA_ROOT for the check to be
|
|
247
|
+
// meaningful — external paths are out of scope (the logical-path
|
|
248
|
+
// matchers handle them).
|
|
249
|
+
if (resolvedParent !== canonRoot && !resolvedParent.startsWith(canonRoot + '/')) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
const relativeResolved = resolvedParent === canonRoot ? '' : resolvedParent.slice(canonRoot.length + 1);
|
|
253
|
+
const resolvedTarget = relativeResolved.length > 0
|
|
254
|
+
? `${relativeResolved}/${path.basename(filePath)}`
|
|
255
|
+
: path.basename(filePath);
|
|
256
|
+
const resolvedTargetLc = resolvedTarget.toLowerCase();
|
|
257
|
+
for (const blocked of blockedPaths) {
|
|
258
|
+
if (matchBlockedEntry(resolvedTargetLc, blocked)) {
|
|
259
|
+
return { entry: blocked, resolvedTarget };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
async function maybeAudit(reaRoot, status, matched, filePath) {
|
|
265
|
+
try {
|
|
266
|
+
await appendAuditRecord(reaRoot, {
|
|
267
|
+
tool_name: 'rea.hook.blocked-paths-enforcer',
|
|
268
|
+
server_name: 'rea',
|
|
269
|
+
tier: Tier.Write,
|
|
270
|
+
status: status === 'allowed' ? InvocationStatus.Allowed : InvocationStatus.Denied,
|
|
271
|
+
metadata: {
|
|
272
|
+
...(matched !== null ? { matched } : {}),
|
|
273
|
+
file_path_preview: filePath.slice(0, 256),
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
/* best-effort */
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
export async function runHookBlockedPathsEnforcer(options = {}) {
|
|
282
|
+
const result = await runBlockedPathsEnforcer({
|
|
283
|
+
...options,
|
|
284
|
+
stderrWrite: (s) => process.stderr.write(s),
|
|
285
|
+
});
|
|
286
|
+
process.exit(result.exitCode);
|
|
287
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-binary port of `hooks/dangerous-bash-interceptor.sh`.
|
|
3
|
+
*
|
|
4
|
+
* 0.34.0 Phase 2 port #1 (tier-2 medium-complexity hooks with enforcer
|
|
5
|
+
* logic). This is the agent-runaway gate — it refuses destructive Bash
|
|
6
|
+
* commands before Claude Code dispatches them. Every refusal class in
|
|
7
|
+
* the 414-LOC bash body must be preserved byte-for-byte; the bypass
|
|
8
|
+
* corpus pinned across 0.13–0.27 demands it.
|
|
9
|
+
*
|
|
10
|
+
* Behavioral contract — preserves the bash hook byte-for-byte:
|
|
11
|
+
*
|
|
12
|
+
* 1. HALT check → exit 2 with the shared banner.
|
|
13
|
+
* 2. Read stdin, extract `tool_input.command`. Non-Bash payloads or
|
|
14
|
+
* empty command → exit 0.
|
|
15
|
+
* 3. Compute smart exclusion flags:
|
|
16
|
+
* - `CMD_IS_REBASE_SAFE` → segments that begin with
|
|
17
|
+
* `git rebase --abort|--continue` skip the H2 rebase advisory.
|
|
18
|
+
* - `CMD_IS_CLEAN_DRY` → segments that begin with
|
|
19
|
+
* `git clean -n|--dry-run` skip the H5 destructive-clean check.
|
|
20
|
+
* 4. Run every HIGH check (H1–H17, M1) against the command. Each
|
|
21
|
+
* check returns 0..N matches; matches are accumulated into the
|
|
22
|
+
* violations table. The accumulator preserves the original bash
|
|
23
|
+
* hook's first-match-wins-per-check semantics — H1 fires once
|
|
24
|
+
* per command even if multiple push segments are unsafe.
|
|
25
|
+
* 5. If any HIGH match → emit "BASH INTERCEPTED" banner + exit 2.
|
|
26
|
+
* Else if MEDIUM-only → emit "BASH ADVISORY" banner + exit 0.
|
|
27
|
+
* Else exit 0 silently.
|
|
28
|
+
*
|
|
29
|
+
* The pattern catalog is in `RULES` below. Each rule is a self-
|
|
30
|
+
* contained closure with a stable identifier (`H1`, `H2`, …) so a
|
|
31
|
+
* future rule addition lands as a one-line array push, not a rewrite.
|
|
32
|
+
* Identifiers match the bash hook's `add_high "H<N>: …"` shape so
|
|
33
|
+
* audit/log consumers grepping for `H12` continue to work.
|
|
34
|
+
*
|
|
35
|
+
* Key parity choices:
|
|
36
|
+
*
|
|
37
|
+
* - Segment-anchored detection via `anySegmentStartsWith` (and
|
|
38
|
+
* `forEachSegment` for per-segment work). The bash 0.15.0 fix
|
|
39
|
+
* (segment-aware instead of full-command grep) is reproduced here.
|
|
40
|
+
* - Env-var-prefix shapes (H10 `HUSKY=0 git`, H15 `REA_BYPASS=…`,
|
|
41
|
+
* H16 alias/function defs) use `anySegmentRawMatches` since the
|
|
42
|
+
* prefix IS the signal — `stripSegmentPrefix` would eat it.
|
|
43
|
+
* - H12 (`curl|sh` pipe-RCE) scans the whole command via
|
|
44
|
+
* `quoteMaskedCmd` because pipe-RCE is a multi-segment property
|
|
45
|
+
* (`|` is the separator that joins curl to sh). The bash hook's
|
|
46
|
+
* `_rea_unwrap_nested_shells` is mirrored via `unwrapNestedShells`
|
|
47
|
+
* so inner payloads of `bash -c "curl … | sh"` are also scanned.
|
|
48
|
+
* - H17 (context-protection) reads
|
|
49
|
+
* `policy.context_protection.delegate_to_subagent` via the canonical
|
|
50
|
+
* YAML loader (matches the bash hook's 0.16.0 fix J.2).
|
|
51
|
+
*/
|
|
52
|
+
import type { Buffer } from 'node:buffer';
|
|
53
|
+
export interface DangerousBashOptions {
|
|
54
|
+
reaRoot?: string;
|
|
55
|
+
stdinOverride?: string | Buffer;
|
|
56
|
+
stderrWrite?: (s: string) => void;
|
|
57
|
+
}
|
|
58
|
+
export interface DangerousBashResult {
|
|
59
|
+
exitCode: number;
|
|
60
|
+
stderr: string;
|
|
61
|
+
/** Test seam — violations the run accumulated, in catalog order. */
|
|
62
|
+
violations: Violation[];
|
|
63
|
+
}
|
|
64
|
+
export interface Violation {
|
|
65
|
+
severity: 'HIGH' | 'MEDIUM';
|
|
66
|
+
/** Stable identifier (`H1`, `H10`, `M1`, …) — matches bash labels. */
|
|
67
|
+
id: string;
|
|
68
|
+
/** Banner headline. */
|
|
69
|
+
label: string;
|
|
70
|
+
/** Banner explanation paragraph. */
|
|
71
|
+
detail: string;
|
|
72
|
+
/** Suggested alternatives. */
|
|
73
|
+
alternatives: string[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Rule descriptor + execution closure. The closure receives the raw
|
|
77
|
+
* command + the active exclusion flags and returns 0..N violations
|
|
78
|
+
* for that rule.
|
|
79
|
+
*/
|
|
80
|
+
interface RuleContext {
|
|
81
|
+
cmd: string;
|
|
82
|
+
cmdIsRebaseSafe: boolean;
|
|
83
|
+
cmdIsCleanDry: boolean;
|
|
84
|
+
delegatePatterns: string[];
|
|
85
|
+
}
|
|
86
|
+
interface Rule {
|
|
87
|
+
id: string;
|
|
88
|
+
severity: 'HIGH' | 'MEDIUM';
|
|
89
|
+
run: (ctx: RuleContext) => Violation[];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Pure executor. Returns `{ exitCode, stderr, violations }`; the CLI
|
|
93
|
+
* wrapper translates them into `process.stderr.write` + `process.exit`.
|
|
94
|
+
*/
|
|
95
|
+
export declare function runDangerousBashInterceptor(options?: DangerousBashOptions): Promise<DangerousBashResult>;
|
|
96
|
+
/**
|
|
97
|
+
* CLI entry point — `rea hook dangerous-bash-interceptor`.
|
|
98
|
+
*/
|
|
99
|
+
export declare function runHookDangerousBashInterceptor(options?: DangerousBashOptions): Promise<void>;
|
|
100
|
+
export declare const __INTERNAL_FOR_TESTS: {
|
|
101
|
+
RULES: readonly Rule[];
|
|
102
|
+
};
|
|
103
|
+
export {};
|