@bookedsolid/rea 0.2.1 → 0.4.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/.husky/pre-push +15 -18
- package/README.md +41 -1
- package/THREAT_MODEL.md +100 -29
- package/dist/audit/append.d.ts +21 -8
- package/dist/audit/append.js +48 -83
- package/dist/audit/fs.d.ts +68 -0
- package/dist/audit/fs.js +171 -0
- package/dist/cli/audit.d.ts +40 -0
- package/dist/cli/audit.js +205 -0
- package/dist/cli/doctor.d.ts +19 -4
- package/dist/cli/doctor.js +172 -5
- package/dist/cli/index.js +26 -1
- package/dist/cli/init.js +93 -7
- package/dist/cli/install/pre-push.d.ts +335 -0
- package/dist/cli/install/pre-push.js +2818 -0
- package/dist/cli/serve.d.ts +64 -0
- package/dist/cli/serve.js +270 -2
- package/dist/cli/status.d.ts +90 -0
- package/dist/cli/status.js +399 -0
- package/dist/cli/utils.d.ts +4 -0
- package/dist/cli/utils.js +4 -0
- package/dist/gateway/audit/rotator.d.ts +116 -0
- package/dist/gateway/audit/rotator.js +289 -0
- package/dist/gateway/circuit-breaker.d.ts +17 -0
- package/dist/gateway/circuit-breaker.js +32 -3
- package/dist/gateway/downstream-pool.d.ts +2 -1
- package/dist/gateway/downstream-pool.js +2 -2
- package/dist/gateway/downstream.d.ts +39 -3
- package/dist/gateway/downstream.js +73 -14
- package/dist/gateway/log.d.ts +122 -0
- package/dist/gateway/log.js +334 -0
- package/dist/gateway/middleware/audit.d.ts +24 -1
- package/dist/gateway/middleware/audit.js +103 -58
- package/dist/gateway/middleware/blocked-paths.d.ts +0 -9
- package/dist/gateway/middleware/blocked-paths.js +439 -67
- package/dist/gateway/middleware/injection.d.ts +218 -13
- package/dist/gateway/middleware/injection.js +433 -51
- package/dist/gateway/middleware/kill-switch.d.ts +10 -1
- package/dist/gateway/middleware/kill-switch.js +20 -1
- package/dist/gateway/observability/metrics.d.ts +125 -0
- package/dist/gateway/observability/metrics.js +321 -0
- package/dist/gateway/server.d.ts +19 -0
- package/dist/gateway/server.js +99 -15
- package/dist/policy/loader.d.ts +47 -0
- package/dist/policy/loader.js +47 -0
- package/dist/policy/profiles.d.ts +13 -0
- package/dist/policy/profiles.js +12 -0
- package/dist/policy/types.d.ts +52 -0
- package/dist/registry/fingerprint.d.ts +73 -0
- package/dist/registry/fingerprint.js +81 -0
- package/dist/registry/fingerprints-store.d.ts +62 -0
- package/dist/registry/fingerprints-store.js +111 -0
- package/dist/registry/interpolate.d.ts +58 -0
- package/dist/registry/interpolate.js +121 -0
- package/dist/registry/loader.d.ts +2 -2
- package/dist/registry/loader.js +22 -1
- package/dist/registry/tofu-gate.d.ts +41 -0
- package/dist/registry/tofu-gate.js +189 -0
- package/dist/registry/tofu.d.ts +111 -0
- package/dist/registry/tofu.js +173 -0
- package/dist/registry/types.d.ts +9 -1
- package/package.json +3 -1
- package/profiles/bst-internal-no-codex.yaml +5 -0
- package/profiles/bst-internal.yaml +7 -0
- package/scripts/tarball-smoke.sh +197 -0
|
@@ -1,11 +1,43 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import type { Middleware } from './chain.js';
|
|
3
|
+
import { Tier } from '../../policy/types.js';
|
|
2
4
|
import { type SafeRegex } from '../redact-safe/match-timeout.js';
|
|
3
5
|
/**
|
|
4
6
|
* Known prompt injection phrases (lowercase for case-insensitive matching).
|
|
5
7
|
* These patterns are commonly used to override system instructions in tool
|
|
6
8
|
* descriptions or resource content returned by downstream MCP servers.
|
|
9
|
+
*
|
|
10
|
+
* SECURITY (G9 follow-up): inputs are NFKC-normalized, whitespace-collapsed,
|
|
11
|
+
* and zero-width-stripped before matching (see `normalizeForMatch`). That
|
|
12
|
+
* means the phrases below can safely be written with plain ASCII spaces —
|
|
13
|
+
* the normalizer will fold NBSP, en-space, em-space, zero-width joiners,
|
|
14
|
+
* etc. into the same form so crafted Unicode variants cannot bypass.
|
|
15
|
+
*
|
|
16
|
+
* The pattern library is intentionally terse. Extending it is follow-up
|
|
17
|
+
* work (G9.1): pattern-set extensibility via policy is out of scope for
|
|
18
|
+
* this patch. Phrases added here must be short, lowercase, and tolerate
|
|
19
|
+
* the normalization pipeline (no Unicode, no non-ASCII punctuation).
|
|
7
20
|
*/
|
|
8
21
|
export declare const INJECTION_PHRASES: readonly string[];
|
|
22
|
+
/**
|
|
23
|
+
* G9 follow-up — normalize an input string to a canonical form for literal
|
|
24
|
+
* phrase matching.
|
|
25
|
+
*
|
|
26
|
+
* 1. NFKC Unicode normalization — folds compatibility forms (fullwidth
|
|
27
|
+
* letters, mathematical alphanumerics) into ASCII equivalents.
|
|
28
|
+
* 2. Strip all Default_Ignorable_Code_Point characters — invisible codepoints
|
|
29
|
+
* that have no rendering and are used only to visually split or obscure
|
|
30
|
+
* injection keywords (soft hyphen, zero-width joiners/non-joiners/spaces,
|
|
31
|
+
* BIDI isolation controls, variation selectors, BOM, etc.).
|
|
32
|
+
* 3. Collapse any run of Unicode whitespace (including NBSP, en/em space)
|
|
33
|
+
* to a single ASCII space.
|
|
34
|
+
* 4. Lowercase — matches the case-insensitive contract of INJECTION_PHRASES.
|
|
35
|
+
*
|
|
36
|
+
* NEVER logs or exports the normalized text; it is used only for match-time
|
|
37
|
+
* comparison. The audit record still surfaces the PHRASE that matched, not
|
|
38
|
+
* the normalized input.
|
|
39
|
+
*/
|
|
40
|
+
export declare function normalizeForMatch(input: string): string;
|
|
9
41
|
/**
|
|
10
42
|
* Base64-token scanner regex. The only regex the injection middleware runs
|
|
11
43
|
* against untrusted payloads; wrapped in `SafeRegex` at middleware creation
|
|
@@ -24,6 +56,11 @@ export declare const INJECTION_BASE64_SHAPE: RegExp;
|
|
|
24
56
|
* one invocation append to an array under this key.
|
|
25
57
|
*/
|
|
26
58
|
export declare const INJECTION_TIMEOUT_METADATA_KEY = "injection.regex_timeout";
|
|
59
|
+
/**
|
|
60
|
+
* Audit metadata key for the classifier verdict. The value is an
|
|
61
|
+
* `InjectionClassifierMetadata` object.
|
|
62
|
+
*/
|
|
63
|
+
export declare const INJECTION_METADATA_KEY = "injection";
|
|
27
64
|
export interface InjectionTimeoutEvent {
|
|
28
65
|
event: 'injection.regex_timeout';
|
|
29
66
|
pattern_source: 'default';
|
|
@@ -31,10 +68,103 @@ export interface InjectionTimeoutEvent {
|
|
|
31
68
|
input_bytes: number;
|
|
32
69
|
timeout_ms: number;
|
|
33
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* G9 — classifier verdict written under `ctx.metadata.injection`. The audit
|
|
73
|
+
* middleware exports `ctx.metadata` verbatim, so this object becomes the
|
|
74
|
+
* permanent record of why a call was allowed, warned, or denied.
|
|
75
|
+
*
|
|
76
|
+
* `verdict` —
|
|
77
|
+
* `clean`: no match, no metadata is written (this type exists only to
|
|
78
|
+
* describe the internal return of `classifyInjection`).
|
|
79
|
+
* `suspicious`: exactly one literal match at write/destructive tier, no
|
|
80
|
+
* base64 escalation. Warn-only by default; deny when
|
|
81
|
+
* `policy.injection.suspicious_blocks_writes === true`.
|
|
82
|
+
* `likely_injection`: always deny. Triggered by any of:
|
|
83
|
+
* - ≥2 distinct literal pattern matches
|
|
84
|
+
* - any match found after base64 decoding
|
|
85
|
+
* - any match at read tier (read-tier is permissive by design — a hit
|
|
86
|
+
* there is anomalous)
|
|
87
|
+
* - an unknown/missing tier (fail-closed)
|
|
88
|
+
* `error` (G9 follow-up): scanner failure — at least one regex call
|
|
89
|
+
* exceeded its worker-bounded timeout. Treated as inconclusive; the
|
|
90
|
+
* call is NOT denied on this signal alone, but the metadata record
|
|
91
|
+
* carries a stable `verdict` field so downstream audit consumers do
|
|
92
|
+
* not receive a timeout event without a verdict shape.
|
|
93
|
+
*
|
|
94
|
+
* `matched_patterns` — the distinct phrase strings from `INJECTION_PHRASES`
|
|
95
|
+
* that matched. Sorted for audit-log determinism. NEVER includes the input
|
|
96
|
+
* text itself (no payload leakage). On `verdict: 'error'` this array may be
|
|
97
|
+
* empty (the scan was abandoned).
|
|
98
|
+
*
|
|
99
|
+
* `base64_decoded` — true iff at least one match was found in content that
|
|
100
|
+
* was base64-decoded before matching. On `verdict: 'error'` this field
|
|
101
|
+
* reports whether any base64-decoded match was observed BEFORE the timeout.
|
|
102
|
+
*/
|
|
103
|
+
export interface InjectionClassifierMetadata {
|
|
104
|
+
verdict: 'suspicious' | 'likely_injection' | 'error';
|
|
105
|
+
matched_patterns: string[];
|
|
106
|
+
base64_decoded: boolean;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* G9 follow-up — zod schema for the `ctx.metadata.injection` record the
|
|
110
|
+
* middleware emits. Every emitted record has a `verdict` field; the schema
|
|
111
|
+
* exists so internal test code (and a follow-up public surface, once we
|
|
112
|
+
* decide how to expose audit-record types) can catch shape regressions —
|
|
113
|
+
* notably the pre-fix behavior where a regex-timeout emitted timing
|
|
114
|
+
* metadata under a different key without ever writing a verdict.
|
|
115
|
+
*
|
|
116
|
+
* INTERNAL today. Not reachable via the published package `exports` map
|
|
117
|
+
* (only `.`, `./policy`, `./middleware`, and `./audit` are public). If
|
|
118
|
+
* downstream consumers (e.g. Helix) need to validate audit records they
|
|
119
|
+
* read off `.rea/audit.jsonl`, we will promote this to a public entrypoint
|
|
120
|
+
* in a follow-up (filed as G9.2). Do not rely on this symbol from outside
|
|
121
|
+
* the rea repo yet.
|
|
122
|
+
*/
|
|
123
|
+
export declare const InjectionMetadataSchema: z.ZodObject<{
|
|
124
|
+
verdict: z.ZodEnum<["suspicious", "likely_injection", "error"]>;
|
|
125
|
+
matched_patterns: z.ZodArray<z.ZodString, "many">;
|
|
126
|
+
base64_decoded: z.ZodBoolean;
|
|
127
|
+
}, "strict", z.ZodTypeAny, {
|
|
128
|
+
verdict: "error" | "suspicious" | "likely_injection";
|
|
129
|
+
matched_patterns: string[];
|
|
130
|
+
base64_decoded: boolean;
|
|
131
|
+
}, {
|
|
132
|
+
verdict: "error" | "suspicious" | "likely_injection";
|
|
133
|
+
matched_patterns: string[];
|
|
134
|
+
base64_decoded: boolean;
|
|
135
|
+
}>;
|
|
34
136
|
interface CompiledInjectionPatterns {
|
|
35
137
|
base64Token: SafeRegex;
|
|
36
138
|
base64Shape: SafeRegex;
|
|
37
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* G9 — scan result split by match origin so the classifier can distinguish
|
|
142
|
+
* a single-literal hit (potentially `suspicious`) from a base64-decoded hit
|
|
143
|
+
* (always `likely_injection`). The Sets deduplicate by phrase; same phrase
|
|
144
|
+
* matched twice counts as one distinct pattern.
|
|
145
|
+
*/
|
|
146
|
+
export interface InjectionScanResult {
|
|
147
|
+
literalMatches: Set<string>;
|
|
148
|
+
base64DecodedMatches: Set<string>;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* G9 — pure helper that walks an arbitrary `unknown` value and returns every
|
|
152
|
+
* successfully decoded base64-looking string. Decoding is attempted only for
|
|
153
|
+
* strings that:
|
|
154
|
+
* - are ≥ `MIN_BASE64_PROBE_LENGTH` (24) chars
|
|
155
|
+
* - have length divisible by 4 (base64 framing)
|
|
156
|
+
* - match the `INJECTION_BASE64_SHAPE` (`^[A-Za-z0-9+/]+=*$`)
|
|
157
|
+
* - decode to a UTF-8 string that is ≥95% printable and contains no null bytes
|
|
158
|
+
*
|
|
159
|
+
* NOTE: This function is NOT called from the middleware body. The inline base64
|
|
160
|
+
* probe in `scanStringForInjection` (via `INJECTION_BASE64_PATTERN`) already
|
|
161
|
+
* covers embedded base64 token detection. Calling `decodeBase64Strings` as a
|
|
162
|
+
* second full-tree pass would duplicate that work and add an avoidable DoS
|
|
163
|
+
* amplification surface (full tree traversal + decoded-string allocation for
|
|
164
|
+
* every base64-shaped leaf). This function is exported for testing and external
|
|
165
|
+
* use only.
|
|
166
|
+
*/
|
|
167
|
+
export declare function decodeBase64Strings(input: unknown): string[];
|
|
38
168
|
export interface ScanForInjectionOptions {
|
|
39
169
|
onTimeout?: (patternId: string, input: string) => void;
|
|
40
170
|
}
|
|
@@ -44,36 +174,111 @@ export interface ScanForInjectionOptions {
|
|
|
44
174
|
*/
|
|
45
175
|
export declare function compileInjectionPatterns(timeoutMs: number, onTimeout?: (patternId: string, input: string) => void): CompiledInjectionPatterns;
|
|
46
176
|
/**
|
|
47
|
-
* Scan a string
|
|
48
|
-
*
|
|
49
|
-
*
|
|
177
|
+
* Scan a single string and record hits into the provided `InjectionScanResult`
|
|
178
|
+
* buckets. Exported for test surface and for callers who want to scan a known
|
|
179
|
+
* string without walking a tree.
|
|
50
180
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
181
|
+
* - Literal matches (case-insensitive substring) go into `literalMatches`.
|
|
182
|
+
* - Base64-decoded matches (tokens extracted via `INJECTION_BASE64_PATTERN`,
|
|
183
|
+
* decoded, then re-scanned for literals) go into `base64DecodedMatches`.
|
|
184
|
+
*
|
|
185
|
+
* Set semantics dedupe by phrase: the same phrase matched five times in one
|
|
186
|
+
* string counts as one distinct pattern, which is intentional for the
|
|
187
|
+
* classifier's "≥2 distinct patterns → likely" rule.
|
|
188
|
+
*/
|
|
189
|
+
export declare function scanStringForInjection(input: string, result: InjectionScanResult, safe: CompiledInjectionPatterns): void;
|
|
190
|
+
/**
|
|
191
|
+
* Back-compat wrapper: legacy callers (and the old audit-metadata consumer)
|
|
192
|
+
* received a flat `string[]` of "literal: …" / "base64-encoded: …" descriptions.
|
|
193
|
+
* Kept as an exported helper so `scripts/lint-safe-regex.mjs` and any external
|
|
194
|
+
* consumer that imported it continue to work. New code should call
|
|
195
|
+
* `scanStringForInjection` directly.
|
|
53
196
|
*/
|
|
54
197
|
export declare function scanForInjection(input: string, safe: CompiledInjectionPatterns): string[];
|
|
198
|
+
/**
|
|
199
|
+
* Recursively scan an unknown value (string, array, or plain object) and
|
|
200
|
+
* accumulate matches into the supplied `InjectionScanResult` buckets.
|
|
201
|
+
*/
|
|
202
|
+
export declare function scanValueForInjection(value: unknown, result: InjectionScanResult, safe: CompiledInjectionPatterns): void;
|
|
203
|
+
/**
|
|
204
|
+
* G9 — classify a scan result into `clean` / `suspicious` / `likely_injection`
|
|
205
|
+
* using the tier and the distinctness of literal matches.
|
|
206
|
+
*
|
|
207
|
+
* Decision table (first match wins):
|
|
208
|
+
*
|
|
209
|
+
* 1. No literal AND no base64-decoded matches
|
|
210
|
+
* → { verdict: 'clean' }
|
|
211
|
+
* 2. Any base64-decoded match (regardless of count/tier)
|
|
212
|
+
* → { verdict: 'likely_injection', base64_decoded: true }
|
|
213
|
+
* 3. ≥2 distinct literal matches
|
|
214
|
+
* → { verdict: 'likely_injection' }
|
|
215
|
+
* 4. Tier is Read (or undefined — fail closed)
|
|
216
|
+
* → { verdict: 'likely_injection' }
|
|
217
|
+
* 5. Exactly 1 literal match at Write/Destructive
|
|
218
|
+
* → { verdict: 'suspicious' }
|
|
219
|
+
*
|
|
220
|
+
* Extension point: a future "deny-tag" per-pattern metadata layer can force
|
|
221
|
+
* any match to `likely_injection`. Not wired in this PR — TODO below.
|
|
222
|
+
*/
|
|
223
|
+
export type InjectionClassification = {
|
|
224
|
+
verdict: 'clean';
|
|
225
|
+
} | InjectionClassifierMetadata;
|
|
226
|
+
export declare function classifyInjection(scan: InjectionScanResult, tier: Tier | undefined): InjectionClassification;
|
|
55
227
|
export type InjectionAction = 'block' | 'warn';
|
|
56
228
|
export interface InjectionMiddlewareOptions {
|
|
57
229
|
/** Timeout budget for each regex call. Default 100ms. */
|
|
58
230
|
matchTimeoutMs?: number;
|
|
231
|
+
/**
|
|
232
|
+
* G9 — governs whether `suspicious` classifications at write/destructive
|
|
233
|
+
* tier deny. `likely_injection` is ALWAYS deny regardless of this flag.
|
|
234
|
+
*
|
|
235
|
+
* Interpreted by `createInjectionMiddleware` with the `action` parameter:
|
|
236
|
+
*
|
|
237
|
+
* - `action: 'warn'` (legacy `injection_detection: warn`): this flag is
|
|
238
|
+
* IGNORED. Suspicious stays warn-only, 0.2.x parity. Operators who
|
|
239
|
+
* pinned warn mode never get a suspicious-deny, even if a profile
|
|
240
|
+
* layer flipped the flag to `true`.
|
|
241
|
+
* - `action: 'block'` + flag `true`: suspicious denies.
|
|
242
|
+
* - `action: 'block'` + flag `false`: suspicious warns only.
|
|
243
|
+
* - `action: 'block'` + flag `undefined` (UNSET): defaults to `false`
|
|
244
|
+
* to preserve 0.3.x behavior. Consumers who omit the `injection:`
|
|
245
|
+
* policy block will NOT be silently tightened on upgrade. To enable
|
|
246
|
+
* the stricter posture, set `injection.suspicious_blocks_writes: true`
|
|
247
|
+
* explicitly in policy (or use the bst-internal profile, which
|
|
248
|
+
* already sets it).
|
|
249
|
+
*
|
|
250
|
+
* Wired from `policy.injection.suspicious_blocks_writes` by the gateway.
|
|
251
|
+
* When the policy omits the `injection:` block, the field is `undefined`
|
|
252
|
+
* (the loader schema no longer applies a default) so this middleware can
|
|
253
|
+
* distinguish "not configured" from "explicitly false".
|
|
254
|
+
*/
|
|
255
|
+
suspiciousBlocksWrites?: boolean;
|
|
59
256
|
}
|
|
60
257
|
/**
|
|
61
|
-
* PostToolUse middleware:
|
|
258
|
+
* PostToolUse middleware: classifies tool results for prompt injection.
|
|
259
|
+
*
|
|
260
|
+
* G9 tiered classifier:
|
|
261
|
+
* - `clean` → allow, no log
|
|
262
|
+
* - `suspicious` → warn (stderr + audit metadata `injection.suspicious`).
|
|
263
|
+
* Denies only when `suspiciousBlocksWrites: true`.
|
|
264
|
+
* - `likely_injection` → always deny, always log.
|
|
62
265
|
*
|
|
63
266
|
* Operates on tool output (ctx.result) returned from downstream MCP servers.
|
|
64
|
-
* On detection:
|
|
65
|
-
* - Always logs to audit metadata and emits a warning to stderr.
|
|
66
|
-
* - If action is 'block' (default), sets ctx.status to Denied and blocks the result.
|
|
67
|
-
* - If action is 'warn', allows the result through with a warning only.
|
|
68
267
|
*
|
|
69
|
-
* SECURITY: Checking PostToolUse (after downstream execution, before the
|
|
70
|
-
* reaches the LLM) is the correct place to catch injection in tool
|
|
71
|
-
* and resource content coming from potentially untrusted
|
|
268
|
+
* SECURITY: Checking PostToolUse (after downstream execution, before the
|
|
269
|
+
* result reaches the LLM) is the correct place to catch injection in tool
|
|
270
|
+
* descriptions and resource content coming from potentially untrusted
|
|
271
|
+
* downstream servers.
|
|
72
272
|
*
|
|
73
273
|
* SECURITY (G3): The only regexes this middleware runs are wrapped in
|
|
74
274
|
* `SafeRegex` with a 100ms default per-call timeout. On timeout the scanner
|
|
75
275
|
* records an audit event and proceeds — blocking is governed by the literal
|
|
76
276
|
* substring checks (which have no ReDoS surface).
|
|
277
|
+
*
|
|
278
|
+
* The legacy `action` parameter (`'block' | 'warn'`) selects the fallback
|
|
279
|
+
* behavior for `suspicious` verdicts when the G9 flag is unset — preserving
|
|
280
|
+
* 0.2.x `injection_detection: 'warn'` semantics for operators who pinned it.
|
|
281
|
+
* `likely_injection` ignores this parameter.
|
|
77
282
|
*/
|
|
78
283
|
export declare function createInjectionMiddleware(action?: InjectionAction, opts?: InjectionMiddlewareOptions): Middleware;
|
|
79
284
|
export {};
|