1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
//! Secret scrubbing for tool output.
//!
//! [`RedactingMiddleware`] wraps the dispatch pipeline and rewrites every
//! text part of the returned [`ToolOutput`](crate::tool::ToolOutput),
//! replacing anything matching a [`SecretPatternSet`] with a
//! `[REDACTED:<kind>]` placeholder. Tools that capture external content
//! — shell stdout, fetched URL bodies — can emit credentials; without
//! scrubbing those flow back into the model's context and into whatever
//! the host persists. The mechanism is generic; the policy (which
//! patterns, how strict) is the host's via
//! [`SecretPatternSet::default_common`] plus [`SecretPatternSet::with_pattern`].
//!
//! The rewrite is post-tool and pre-result-re-entry: loop semantics
//! (compaction, loop-detection hashing, turn counting) are unaffected,
//! and a redacted output is still a successful tool result.
//!
//! # Example
//!
//! ```rust,ignore
//! use loopctl::middleware::{RedactingMiddleware, SecretPatternSet, ToolPipeline};
//! use loopctl::tool::ToolRegistry;
//! use std::sync::Arc;
//!
//! let pipeline = ToolPipeline::builder()
//! .with_middleware(RedactingMiddleware::new(SecretPatternSet::default_common()))
//! .with_core(Arc::new(ToolRegistry::new()))
//! .build()?;
//! ```
use Future;
use Pin;
use ;
use crate;
/// Matches `Authorization: Bearer …` header values.
///
/// Case-insensitive scheme and header name, RFC 3986 unreserved
/// characters plus separators in the credential — the shape HTTP
/// clients echo in verbose logs and fetched-error bodies.
const BEARER: &str = r#"(?i)authorization:\s*bearer\s+[A-Za-z0-9\-._~+/=]+"#;
/// Matches `api_key=` / `token:` / `secret=` style key-value tokens.
///
/// Covers `.env` dumps and config prints: the key with `_`/`-`
/// separators, either `=` or `:` as the separator, and an optionally
/// quoted value of at least 16 alphanumerics.
const API_KEY_KV: &str = r#"(?i)(?:api[_-]?key|token|secret)\s*[=:]\s*["']?[A-Za-z0-9]{16,}["']?"#;
/// Matches AWS access-key IDs (`AKIA`, `ASIA`, `AGPA` prefixes).
///
/// The four-letter prefix plus 16 uppercase alphanumerics is the
/// documented access-key-id shape; the paired secret key is left to
/// the entropy heuristic (it is a contextless 40-char base64 string).
const AWS_ACCESS_KEY: &str = r"A(?:KIA|SIA|GPA)[0-9A-Z]{16}";
/// Matches whole PEM private-key blocks, header through footer.
///
/// Any key type (`RSA`, `EC`, `OPENSSH`, …) between the `BEGIN`/`END`
/// markers; the non-greedy body keeps two blocks in one output
/// separate, and `scrub` collapses each to a single placeholder.
const PEM_PRIVATE_KEY: &str =
r"-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----";
/// Matches the `GitHub` PAT prefix family (`ghp_`, `gho_`, …).
///
/// The fine-grained (`gh[pousr]_`) prefixes followed by at least 36
/// alphanumerics — the shape `gh auth token` and CI masks print.
const GITHUB_PAT: &str = r"gh[pousr]_[A-Za-z0-9]{36,}";
/// Matches `GitLab` PATs (`glpat-…`).
///
/// The `glpat-` prefix plus 20 token characters, the personal access
/// token shape `GitLab`'s UI creates by default.
const GITLAB_PAT: &str = r"glpat-[A-Za-z0-9_\-]{20}";
/// Minimum token length considered by the high-entropy heuristic.
///
/// Shorter tokens are ignored even when their byte distribution looks
/// random — most false positives (IDs, short hashes) fall below this.
const MIN_ENTROPY_TOKEN_LEN: usize = 32;
/// Shannon-entropy threshold, in bits per byte, for the heuristic.
///
/// A hex string tops out at 4.0 (16 symbols), so commit SHAs and hex
/// hashes stay visible; base64-family tokens sit near 6.0 and are
/// redacted. The value is the one truffleHog/gitleaks-style tools
/// converged on.
const ENTROPY_THRESHOLD: f64 = 4.5;
/// One secret-detection rule: a compiled pattern and the label that
/// replaces its matches.
///
/// `kind` is the short string substituted into the output as
/// `[REDACTED:<kind>]` (e.g. `"aws_access_key"`, `"github_pat"`,
/// `"bearer"`). It is also the key a host uses when reasoning about its
/// own extensions, so pick names that are stable and grep-able.
/// A collection of secret-detection rules applied to tool output.
///
/// Construct with [`SecretPatternSet::default_common`] for the curated
/// set (Authorization headers, key-value tokens, AWS keys, PEM
/// private-key blocks, GitHub/GitLab PATs, and a high-entropy heuristic
/// for unknown formats), then extend with
/// [`SecretPatternSet::with_pattern`] for host-specific shapes. The set
/// is `Send + Sync` (each [`regex::Regex`] is), so it can live behind
/// an `Arc` in a shared pipeline.
/// Compile one curated literal, returning `None` if it fails to compile.
///
/// The shipped literals are known-good, so `None` is unreachable in
/// practice; the `debug_assert` turns a broken literal into a test-time
/// failure instead of a silent gap.
/// Redact high-entropy tokens no explicit pattern matched.
///
/// Splits on spaces (preserving all other structure), trims
/// non-token characters from each piece's edges, and replaces any
/// remaining core of [`MIN_ENTROPY_TOKEN_LEN`] or more characters whose
/// Shannon entropy reaches [`ENTROPY_THRESHOLD`]. Returns the number of
/// tokens redacted.
/// Redact the token core of one space-delimited piece, if it qualifies.
///
/// Returns the (possibly rewritten) piece and whether a substitution
/// happened — the flag, not marker sniffing, is what counts, so a
/// piece already carrying a placeholder (an echoed earlier redaction)
/// is passed through without being counted again.
/// Whether `c` appears in the token alphabet the heuristic scans.
///
/// Alphanumerics plus the base64 and common credential separators
/// (`+ / = - _`); everything else — quotes, brackets, colons — bounds a
/// token.
/// Shannon entropy of `token`'s bytes, in bits per byte.
///
/// A uniform sample over `n` distinct symbols scores `log2(n)`: hex
/// tops out at 4.0, base64 near 6.0 — the spread
/// [`ENTROPY_THRESHOLD`] sits between.
/// Middleware that scrubs secrets from tool output after execution.
///
/// Wraps the dispatch pipeline and rewrites each text part of the
/// returned [`ToolOutput`](crate::tool::ToolOutput) using a
/// [`SecretPatternSet`], replacing matches with `[REDACTED:<kind>]`.
/// Image and other non-text multipart parts are left unchanged. The
/// rewrite is post-tool, pre-result-re-entry — it does not affect loop
/// semantics (compaction, loop-detection hashing, turn counting), never
/// sets `is_error`, and preserves any `DisplayHint`.
///
/// Default off: register it explicitly in the pipeline. A host that
/// does not register it sees today's behaviour (no scrubbing).
///
/// # Example
///
/// ```rust,ignore
/// use loopctl::middleware::{RedactingMiddleware, SecretPatternSet, ToolPipeline};
/// use loopctl::tool::ToolRegistry;
/// use std::sync::Arc;
///
/// let pipeline = ToolPipeline::builder()
/// .with_middleware(RedactingMiddleware::new(SecretPatternSet::default_common()))
/// .with_core(Arc::new(ToolRegistry::new()))
/// .build()?;
/// ```
/// Rewrite every text part of `result.output` through `patterns`.
///
/// `ToolContent::Text` scrubs the single string;
/// `ToolContent::Multipart` scrubs each `ToolContentPart::Text` in
/// place and leaves image and other parts untouched. Substitutions are
/// applied silently — the `[REDACTED:<kind>]` placeholder is the
/// model-visible signal.