mcpower-proxy 0.0.65__py3-none-any.whl → 0.0.79__py3-none-any.whl
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.
Potentially problematic release.
This version of mcpower-proxy might be problematic. Click here for more details.
- ide_tools/__init__.py +12 -0
- ide_tools/common/__init__.py +5 -0
- ide_tools/common/hooks/__init__.py +5 -0
- ide_tools/common/hooks/init.py +130 -0
- ide_tools/common/hooks/output.py +63 -0
- ide_tools/common/hooks/prompt_submit.py +136 -0
- ide_tools/common/hooks/read_file.py +170 -0
- ide_tools/common/hooks/shell_execution.py +257 -0
- ide_tools/common/hooks/shell_parser_bashlex.py +394 -0
- ide_tools/common/hooks/types.py +34 -0
- ide_tools/common/hooks/utils.py +286 -0
- ide_tools/cursor/__init__.py +11 -0
- ide_tools/cursor/constants.py +77 -0
- ide_tools/cursor/format.py +35 -0
- ide_tools/cursor/router.py +107 -0
- ide_tools/router.py +48 -0
- main.py +11 -4
- {mcpower_proxy-0.0.65.dist-info → mcpower_proxy-0.0.79.dist-info}/METADATA +4 -3
- mcpower_proxy-0.0.79.dist-info/RECORD +62 -0
- {mcpower_proxy-0.0.65.dist-info → mcpower_proxy-0.0.79.dist-info}/top_level.txt +1 -0
- modules/apis/security_policy.py +11 -6
- modules/decision_handler.py +219 -0
- modules/logs/audit_trail.py +20 -18
- modules/logs/logger.py +14 -18
- modules/redaction/gitleaks_rules.py +1 -1
- modules/redaction/pii_rules.py +0 -48
- modules/redaction/redactor.py +112 -107
- modules/ui/__init__.py +1 -1
- modules/ui/confirmation.py +0 -1
- modules/utils/cli.py +36 -6
- modules/utils/ids.py +55 -10
- modules/utils/json.py +3 -3
- modules/utils/platform.py +23 -0
- modules/utils/string.py +17 -0
- wrapper/__version__.py +1 -1
- wrapper/middleware.py +144 -221
- wrapper/server.py +19 -11
- mcpower_proxy-0.0.65.dist-info/RECORD +0 -43
- {mcpower_proxy-0.0.65.dist-info → mcpower_proxy-0.0.79.dist-info}/WHEEL +0 -0
- {mcpower_proxy-0.0.65.dist-info → mcpower_proxy-0.0.79.dist-info}/entry_points.txt +0 -0
- {mcpower_proxy-0.0.65.dist-info → mcpower_proxy-0.0.79.dist-info}/licenses/LICENSE +0 -0
modules/redaction/redactor.py
CHANGED
|
@@ -20,7 +20,6 @@ from .constants import (
|
|
|
20
20
|
)
|
|
21
21
|
from .pii_rules import detect_pii
|
|
22
22
|
|
|
23
|
-
|
|
24
23
|
# Type alias for JSONPath cache values: (matches_set, prefix_tree)
|
|
25
24
|
JSONPathCacheValue = Tuple[Set[str], Dict[str, Any]]
|
|
26
25
|
|
|
@@ -30,11 +29,11 @@ class LRUCache:
|
|
|
30
29
|
Least Recently Used cache with size limit to prevent unbounded memory growth.
|
|
31
30
|
Thread-safe for single-threaded usage (not thread-safe for concurrent access).
|
|
32
31
|
"""
|
|
33
|
-
|
|
32
|
+
|
|
34
33
|
def __init__(self, max_size: int = 128):
|
|
35
34
|
self.cache = OrderedDict()
|
|
36
35
|
self.max_size = max_size
|
|
37
|
-
|
|
36
|
+
|
|
38
37
|
def get(self, key) -> Optional[JSONPathCacheValue]:
|
|
39
38
|
"""Get value and mark as recently used."""
|
|
40
39
|
if key in self.cache:
|
|
@@ -42,7 +41,7 @@ class LRUCache:
|
|
|
42
41
|
self.cache.move_to_end(key)
|
|
43
42
|
return self.cache[key]
|
|
44
43
|
return None
|
|
45
|
-
|
|
44
|
+
|
|
46
45
|
def set(self, key, value: JSONPathCacheValue) -> None:
|
|
47
46
|
"""Set value and evict oldest if necessary."""
|
|
48
47
|
if key in self.cache:
|
|
@@ -55,11 +54,11 @@ class LRUCache:
|
|
|
55
54
|
# Evict oldest if over limit
|
|
56
55
|
if len(self.cache) > self.max_size:
|
|
57
56
|
self.cache.popitem(last=False) # Remove oldest (first) item
|
|
58
|
-
|
|
57
|
+
|
|
59
58
|
def clear(self) -> None:
|
|
60
59
|
"""Clear all cached entries."""
|
|
61
60
|
self.cache.clear()
|
|
62
|
-
|
|
61
|
+
|
|
63
62
|
def size(self) -> int:
|
|
64
63
|
"""Get current cache size."""
|
|
65
64
|
return len(self.cache)
|
|
@@ -73,7 +72,7 @@ class RedactionSpan:
|
|
|
73
72
|
end: int
|
|
74
73
|
replacement: str
|
|
75
74
|
source: str # 'secrets' or 'pii'
|
|
76
|
-
|
|
75
|
+
|
|
77
76
|
@property
|
|
78
77
|
def length(self) -> int:
|
|
79
78
|
return self.end - self.start
|
|
@@ -81,20 +80,20 @@ class RedactionSpan:
|
|
|
81
80
|
|
|
82
81
|
class RedactionEngine:
|
|
83
82
|
"""Core redaction engine using PII detection and Gitleaks."""
|
|
84
|
-
|
|
83
|
+
|
|
85
84
|
# Performance optimization: Simple bounded cache for compiled JSONPath expressions
|
|
86
85
|
_jsonpath_expr_cache = {}
|
|
87
86
|
_MAX_EXPR_CACHE = 64
|
|
88
|
-
|
|
87
|
+
|
|
89
88
|
# Performance optimization: LRU cache for JSONPath matches to prevent memory leaks
|
|
90
89
|
_jsonpath_cache = LRUCache(max_size=128)
|
|
91
|
-
|
|
90
|
+
|
|
92
91
|
# Performance optimization: Pre-compiled regex patterns
|
|
93
92
|
_compiled_regexes = {
|
|
94
93
|
'placeholder': re.compile(REDACTION_PLACEHOLDER_PATTERN),
|
|
95
94
|
'path_brackets': re.compile(r'\.\[(\d+)\]')
|
|
96
95
|
}
|
|
97
|
-
|
|
96
|
+
|
|
98
97
|
@staticmethod
|
|
99
98
|
def redact(value: Any, ignored_keys: List[str] = None, include_keys: List[str] = None) -> Any:
|
|
100
99
|
"""
|
|
@@ -113,55 +112,56 @@ class RedactionEngine:
|
|
|
113
112
|
"""
|
|
114
113
|
if ignored_keys and include_keys:
|
|
115
114
|
raise ValueError("Cannot specify both ignored_keys and include_keys - use one or the other")
|
|
116
|
-
|
|
115
|
+
|
|
117
116
|
return RedactionEngine._redact_with_path(value, ignored_keys or [], include_keys or [], "", value)
|
|
118
|
-
|
|
117
|
+
|
|
119
118
|
@staticmethod
|
|
120
|
-
def _redact_with_path(value: Any, ignored_keys: List[str], include_keys: List[str], current_path: str,
|
|
119
|
+
def _redact_with_path(value: Any, ignored_keys: List[str], include_keys: List[str], current_path: str,
|
|
120
|
+
root_data: Any) -> Any:
|
|
121
121
|
"""
|
|
122
122
|
Internal redaction function that tracks the current path.
|
|
123
123
|
"""
|
|
124
124
|
# Performance optimization: fastest type checks first
|
|
125
125
|
if value is None:
|
|
126
126
|
return None
|
|
127
|
-
|
|
127
|
+
|
|
128
128
|
if isinstance(value, bool):
|
|
129
129
|
return value # Booleans can never contain sensitive data - fastest check
|
|
130
|
-
|
|
130
|
+
|
|
131
131
|
if isinstance(value, (int, float)):
|
|
132
132
|
# Preserve type if no redaction needed
|
|
133
133
|
str_value = str(value)
|
|
134
134
|
redacted_str = RedactionEngine._redact_string(str_value)
|
|
135
135
|
return value if str_value == redacted_str else redacted_str
|
|
136
|
-
|
|
136
|
+
|
|
137
137
|
if isinstance(value, str):
|
|
138
138
|
return RedactionEngine._redact_string(value)
|
|
139
|
-
|
|
139
|
+
|
|
140
140
|
if isinstance(value, dict):
|
|
141
141
|
return RedactionEngine._redact_dict(value, ignored_keys, include_keys, current_path, root_data)
|
|
142
|
-
|
|
142
|
+
|
|
143
143
|
if isinstance(value, list):
|
|
144
144
|
return RedactionEngine._redact_list(value, ignored_keys, include_keys, current_path, root_data)
|
|
145
|
-
|
|
145
|
+
|
|
146
146
|
# For other types, convert to string and redact
|
|
147
147
|
return RedactionEngine._redact_string(str(value))
|
|
148
|
-
|
|
148
|
+
|
|
149
149
|
@staticmethod
|
|
150
150
|
def _normalize_text(text: str) -> str:
|
|
151
151
|
"""Strip zero-width characters efficiently."""
|
|
152
152
|
# Fast path - check if normalization is needed
|
|
153
153
|
if not any(char in text for char in ZERO_WIDTH_CHARS):
|
|
154
154
|
return text
|
|
155
|
-
|
|
155
|
+
|
|
156
156
|
# Single pass with translation table (much faster)
|
|
157
157
|
translation_table = str.maketrans('', '', ''.join(ZERO_WIDTH_CHARS))
|
|
158
158
|
return text.translate(translation_table)
|
|
159
|
-
|
|
159
|
+
|
|
160
160
|
@staticmethod
|
|
161
161
|
def _detect_secrets(text: str) -> List[RedactionSpan]:
|
|
162
162
|
"""Detect secrets using Gitleaks-based patterns."""
|
|
163
163
|
spans = []
|
|
164
|
-
|
|
164
|
+
|
|
165
165
|
# Use compiled rules (based on Gitleaks patterns) for secrets detection
|
|
166
166
|
from . import gitleaks_rules as GL
|
|
167
167
|
# Scan line-by-line for performance and to reduce pathological matches
|
|
@@ -181,7 +181,7 @@ class RedactionEngine:
|
|
|
181
181
|
s, e = match.span(0)
|
|
182
182
|
except (IndexError, AttributeError):
|
|
183
183
|
s, e = match.span(0)
|
|
184
|
-
|
|
184
|
+
|
|
185
185
|
if s < e:
|
|
186
186
|
spans.append(RedactionSpan(
|
|
187
187
|
start=offset + s,
|
|
@@ -190,47 +190,47 @@ class RedactionEngine:
|
|
|
190
190
|
source='secrets'
|
|
191
191
|
))
|
|
192
192
|
offset += len(line) + 1 # +1 for the split '\n'
|
|
193
|
-
|
|
193
|
+
|
|
194
194
|
return spans
|
|
195
|
-
|
|
195
|
+
|
|
196
196
|
@staticmethod
|
|
197
197
|
def _detect_pii(text: str) -> List[RedactionSpan]:
|
|
198
198
|
"""Detect PII using regex patterns."""
|
|
199
199
|
spans = []
|
|
200
|
-
|
|
200
|
+
|
|
201
201
|
# Use PII detection
|
|
202
202
|
results = detect_pii(text)
|
|
203
|
-
|
|
203
|
+
|
|
204
204
|
# Convert results to redaction spans
|
|
205
205
|
for result in results:
|
|
206
206
|
placeholder = PII_PLACEHOLDERS.get(
|
|
207
|
-
result.entity_type,
|
|
207
|
+
result.entity_type,
|
|
208
208
|
PII_PLACEHOLDERS["DEFAULT"]
|
|
209
209
|
)
|
|
210
|
-
|
|
210
|
+
|
|
211
211
|
spans.append(RedactionSpan(
|
|
212
212
|
start=result.start,
|
|
213
213
|
end=result.end,
|
|
214
214
|
replacement=placeholder,
|
|
215
215
|
source='pii'
|
|
216
216
|
))
|
|
217
|
-
|
|
217
|
+
|
|
218
218
|
return spans
|
|
219
|
-
|
|
219
|
+
|
|
220
220
|
@staticmethod
|
|
221
221
|
def _resolve_overlaps(spans: List[RedactionSpan]) -> List[RedactionSpan]:
|
|
222
222
|
"""Resolve overlapping spans - longest span wins."""
|
|
223
223
|
if not spans:
|
|
224
224
|
return []
|
|
225
|
-
|
|
225
|
+
|
|
226
226
|
# Sort by start position
|
|
227
227
|
sorted_spans = sorted(spans, key=lambda s: s.start)
|
|
228
228
|
resolved = []
|
|
229
|
-
|
|
229
|
+
|
|
230
230
|
for current_span in sorted_spans:
|
|
231
231
|
# Check for overlaps with already resolved spans
|
|
232
232
|
overlaps = False
|
|
233
|
-
|
|
233
|
+
|
|
234
234
|
for i, existing_span in enumerate(resolved):
|
|
235
235
|
if RedactionEngine._spans_overlap(current_span, existing_span):
|
|
236
236
|
overlaps = True
|
|
@@ -238,121 +238,125 @@ class RedactionEngine:
|
|
|
238
238
|
if current_span.length > existing_span.length:
|
|
239
239
|
resolved[i] = current_span
|
|
240
240
|
break
|
|
241
|
-
|
|
241
|
+
|
|
242
242
|
if not overlaps:
|
|
243
243
|
resolved.append(current_span)
|
|
244
|
-
|
|
244
|
+
|
|
245
245
|
return resolved
|
|
246
|
-
|
|
246
|
+
|
|
247
247
|
@staticmethod
|
|
248
248
|
def _spans_overlap(span1: RedactionSpan, span2: RedactionSpan) -> bool:
|
|
249
249
|
"""Check if two spans overlap."""
|
|
250
250
|
return not (span1.end <= span2.start or span2.end <= span1.start)
|
|
251
|
-
|
|
251
|
+
|
|
252
252
|
@staticmethod
|
|
253
253
|
def _apply_idempotency_guard(text: str, spans: List[RedactionSpan]) -> List[RedactionSpan]:
|
|
254
254
|
"""Remove spans that would redact inside existing placeholders."""
|
|
255
255
|
if not spans:
|
|
256
256
|
return []
|
|
257
|
-
|
|
257
|
+
|
|
258
258
|
# Find existing redaction placeholders
|
|
259
259
|
placeholder_spans = []
|
|
260
260
|
# Performance optimization: Use pre-compiled regex
|
|
261
261
|
for match in RedactionEngine._compiled_regexes['placeholder'].finditer(text):
|
|
262
262
|
placeholder_spans.append((match.start(), match.end()))
|
|
263
|
-
|
|
263
|
+
|
|
264
264
|
# Filter out spans that overlap with existing placeholders
|
|
265
265
|
filtered_spans = []
|
|
266
266
|
for span in spans:
|
|
267
267
|
overlaps_placeholder = False
|
|
268
|
-
|
|
268
|
+
|
|
269
269
|
for ph_start, ph_end in placeholder_spans:
|
|
270
270
|
if not (span.end <= ph_start or span.start >= ph_end):
|
|
271
271
|
overlaps_placeholder = True
|
|
272
272
|
break
|
|
273
|
-
|
|
273
|
+
|
|
274
274
|
if not overlaps_placeholder:
|
|
275
275
|
filtered_spans.append(span)
|
|
276
|
-
|
|
276
|
+
|
|
277
277
|
return filtered_spans
|
|
278
|
-
|
|
278
|
+
|
|
279
279
|
@staticmethod
|
|
280
280
|
def _apply_redactions(text: str, spans: List[RedactionSpan]) -> str:
|
|
281
281
|
"""Apply redactions right-to-left to avoid index shifting, preserving JSON structure."""
|
|
282
282
|
if not spans:
|
|
283
283
|
return text
|
|
284
|
-
|
|
284
|
+
|
|
285
285
|
# Sort spans by start position (descending) for right-to-left processing
|
|
286
286
|
# Processing right-to-left ensures earlier spans' positions remain valid
|
|
287
287
|
# since replacements to the right don't affect positions to the left
|
|
288
288
|
sorted_spans = sorted(spans, key=lambda s: s.start, reverse=True)
|
|
289
|
-
|
|
289
|
+
|
|
290
290
|
result = text
|
|
291
|
-
|
|
291
|
+
|
|
292
292
|
for span in sorted_spans:
|
|
293
293
|
# Replacements to the right don't shift positions to the left
|
|
294
294
|
start_pos = span.start
|
|
295
295
|
end_pos = span.end
|
|
296
|
-
|
|
296
|
+
|
|
297
297
|
# Bounds check
|
|
298
298
|
if start_pos < 0 or end_pos > len(result) or start_pos >= len(result):
|
|
299
299
|
continue
|
|
300
|
-
|
|
300
|
+
|
|
301
301
|
replacement = span.replacement
|
|
302
|
-
|
|
302
|
+
|
|
303
303
|
# Simply replace the matched text with the redaction placeholder
|
|
304
304
|
# Do NOT expand to surrounding quotes - this breaks nested JSON
|
|
305
305
|
# The redaction placeholders are designed to be valid string content as-is
|
|
306
|
-
|
|
306
|
+
|
|
307
307
|
# Apply the redaction
|
|
308
308
|
result = result[:start_pos] + replacement + result[end_pos:]
|
|
309
|
-
|
|
309
|
+
|
|
310
310
|
return result
|
|
311
311
|
|
|
312
312
|
@staticmethod
|
|
313
|
-
def _redact_dict(d: dict, ignored_keys: List[str], include_keys: List[str], current_path: str,
|
|
313
|
+
def _redact_dict(d: dict, ignored_keys: List[str], include_keys: List[str], current_path: str,
|
|
314
|
+
root_data: Any) -> dict:
|
|
314
315
|
"""Redact dictionary values, respecting ignored_keys or include_keys."""
|
|
315
316
|
result = {}
|
|
316
|
-
|
|
317
|
+
|
|
317
318
|
for key, value in d.items():
|
|
318
319
|
# Build the path for this key
|
|
319
320
|
key_path = RedactionEngine._build_path(current_path, str(key))
|
|
320
|
-
|
|
321
|
+
|
|
321
322
|
# Determine if this path should be redacted
|
|
322
|
-
if RedactionEngine._should_redact_path_enhanced(root_data, key_path, ignored_keys, include_keys,
|
|
323
|
+
if RedactionEngine._should_redact_path_enhanced(root_data, key_path, ignored_keys, include_keys,
|
|
324
|
+
current_path):
|
|
323
325
|
# Recursively redact the value with updated path context
|
|
324
326
|
result[key] = RedactionEngine._redact_with_path(value, ignored_keys, include_keys, key_path, root_data)
|
|
325
327
|
else:
|
|
326
328
|
# Keep the value as-is (no redaction for this path or any nested paths)
|
|
327
329
|
result[key] = value
|
|
328
|
-
|
|
330
|
+
|
|
329
331
|
return result
|
|
330
|
-
|
|
332
|
+
|
|
331
333
|
@staticmethod
|
|
332
|
-
def _redact_list(lst: list, ignored_keys: List[str], include_keys: List[str], current_path: str,
|
|
334
|
+
def _redact_list(lst: list, ignored_keys: List[str], include_keys: List[str], current_path: str,
|
|
335
|
+
root_data: Any) -> list:
|
|
333
336
|
"""Redact list items, respecting ignored_keys or include_keys."""
|
|
334
337
|
result = []
|
|
335
|
-
|
|
338
|
+
|
|
336
339
|
for i, item in enumerate(lst):
|
|
337
340
|
# Build the path for this index
|
|
338
341
|
item_path = RedactionEngine._build_path(current_path, str(i))
|
|
339
|
-
|
|
342
|
+
|
|
340
343
|
# Determine if this path should be redacted
|
|
341
|
-
if RedactionEngine._should_redact_path_enhanced(root_data, item_path, ignored_keys, include_keys,
|
|
344
|
+
if RedactionEngine._should_redact_path_enhanced(root_data, item_path, ignored_keys, include_keys,
|
|
345
|
+
current_path):
|
|
342
346
|
# Recursively redact the item with updated path context
|
|
343
347
|
result.append(RedactionEngine._redact_with_path(item, ignored_keys, include_keys, item_path, root_data))
|
|
344
348
|
else:
|
|
345
349
|
# Keep the item as-is (no redaction for this path or any nested paths)
|
|
346
350
|
result.append(item)
|
|
347
|
-
|
|
351
|
+
|
|
348
352
|
return result
|
|
349
|
-
|
|
353
|
+
|
|
350
354
|
@staticmethod
|
|
351
355
|
def _redact_string(text: str) -> str:
|
|
352
356
|
"""Redact a string using the existing redaction pipeline."""
|
|
353
357
|
if not text or not isinstance(text, str):
|
|
354
358
|
return text
|
|
355
|
-
|
|
359
|
+
|
|
356
360
|
normalized_text = RedactionEngine._normalize_text(text)
|
|
357
361
|
redaction_spans: List[RedactionSpan] = []
|
|
358
362
|
redaction_spans.extend(RedactionEngine._detect_secrets(normalized_text))
|
|
@@ -360,7 +364,7 @@ class RedactionEngine:
|
|
|
360
364
|
resolved_spans = RedactionEngine._resolve_overlaps(redaction_spans)
|
|
361
365
|
final_spans = RedactionEngine._apply_idempotency_guard(normalized_text, resolved_spans)
|
|
362
366
|
return RedactionEngine._apply_redactions(normalized_text, final_spans)
|
|
363
|
-
|
|
367
|
+
|
|
364
368
|
@staticmethod
|
|
365
369
|
def _normalize_numeric_key(key: str) -> str:
|
|
366
370
|
"""Normalize numeric keys efficiently - only if needed."""
|
|
@@ -368,7 +372,7 @@ class RedactionEngine:
|
|
|
368
372
|
# Only normalize if there are leading zeros
|
|
369
373
|
return str(int(key))
|
|
370
374
|
return key
|
|
371
|
-
|
|
375
|
+
|
|
372
376
|
@staticmethod
|
|
373
377
|
def _build_path(current_path: str, key: str) -> str:
|
|
374
378
|
"""
|
|
@@ -378,19 +382,19 @@ class RedactionEngine:
|
|
|
378
382
|
"""
|
|
379
383
|
# Normalize numeric keys efficiently
|
|
380
384
|
normalized_key = RedactionEngine._normalize_numeric_key(key)
|
|
381
|
-
|
|
385
|
+
|
|
382
386
|
# Handle empty keys to prevent paths like "user..email"
|
|
383
387
|
if not normalized_key:
|
|
384
388
|
return current_path
|
|
385
|
-
|
|
389
|
+
|
|
386
390
|
if not current_path:
|
|
387
391
|
return normalized_key
|
|
388
|
-
|
|
392
|
+
|
|
389
393
|
return f"{current_path}.{normalized_key}"
|
|
390
|
-
|
|
391
|
-
|
|
394
|
+
|
|
392
395
|
@staticmethod
|
|
393
|
-
def _should_redact_path_enhanced(root_data: dict, path: str, ignored_keys: List[str], include_keys: List[str],
|
|
396
|
+
def _should_redact_path_enhanced(root_data: dict, path: str, ignored_keys: List[str], include_keys: List[str],
|
|
397
|
+
current_path: str) -> bool:
|
|
394
398
|
"""
|
|
395
399
|
Enhanced path matching using JSONPath when available, fallback to custom logic.
|
|
396
400
|
|
|
@@ -405,9 +409,10 @@ class RedactionEngine:
|
|
|
405
409
|
True if the path should be redacted, False otherwise
|
|
406
410
|
"""
|
|
407
411
|
return RedactionEngine._should_redact_path_jsonpath(root_data, path, ignored_keys, include_keys)
|
|
408
|
-
|
|
412
|
+
|
|
409
413
|
@staticmethod
|
|
410
|
-
def _should_redact_path_jsonpath(root_data: dict, path: str, ignored_keys: List[str],
|
|
414
|
+
def _should_redact_path_jsonpath(root_data: dict, path: str, ignored_keys: List[str],
|
|
415
|
+
include_keys: List[str]) -> bool:
|
|
411
416
|
"""
|
|
412
417
|
JSONPath-based path matching with LRU cache to prevent memory leaks.
|
|
413
418
|
"""
|
|
@@ -417,21 +422,21 @@ class RedactionEngine:
|
|
|
417
422
|
frozenset(ignored_keys) if ignored_keys else None,
|
|
418
423
|
frozenset(include_keys) if include_keys else None
|
|
419
424
|
)
|
|
420
|
-
|
|
425
|
+
|
|
421
426
|
# Try to get from LRU cache
|
|
422
427
|
cached_result = RedactionEngine._jsonpath_cache.get(cache_key)
|
|
423
|
-
|
|
428
|
+
|
|
424
429
|
if cached_result is None:
|
|
425
430
|
# Pre-compute all matches for this data/pattern combination
|
|
426
431
|
all_matches = set()
|
|
427
432
|
patterns = include_keys or ignored_keys or []
|
|
428
|
-
|
|
433
|
+
|
|
429
434
|
for pattern in patterns:
|
|
430
435
|
try:
|
|
431
436
|
# Expect proper JSONPath format (starting with $)
|
|
432
437
|
if not pattern.startswith('$'):
|
|
433
438
|
raise ValueError(f"Pattern must be in JSONPath format (start with $): {pattern}")
|
|
434
|
-
|
|
439
|
+
|
|
435
440
|
# Performance optimization: Use cached compiled expressions
|
|
436
441
|
if pattern not in RedactionEngine._jsonpath_expr_cache:
|
|
437
442
|
# Simple FIFO eviction if cache is full
|
|
@@ -441,29 +446,29 @@ class RedactionEngine:
|
|
|
441
446
|
del RedactionEngine._jsonpath_expr_cache[oldest_key]
|
|
442
447
|
RedactionEngine._jsonpath_expr_cache[pattern] = jsonpath_parse(pattern)
|
|
443
448
|
jsonpath_expr = RedactionEngine._jsonpath_expr_cache[pattern]
|
|
444
|
-
|
|
449
|
+
|
|
445
450
|
matches = jsonpath_expr.find(root_data)
|
|
446
|
-
|
|
451
|
+
|
|
447
452
|
for match in matches:
|
|
448
453
|
# Performance optimization: Single regex for path conversion
|
|
449
454
|
match_path = RedactionEngine._compiled_regexes['path_brackets'].sub(
|
|
450
455
|
r'.\1', str(match.full_path)
|
|
451
456
|
)
|
|
452
457
|
all_matches.add(match_path)
|
|
453
|
-
|
|
458
|
+
|
|
454
459
|
except Exception:
|
|
455
460
|
# Fallback for invalid JSONPath patterns
|
|
456
461
|
continue
|
|
457
|
-
|
|
462
|
+
|
|
458
463
|
# Build prefix tree for O(k) child matching
|
|
459
464
|
prefix_tree = RedactionEngine._build_prefix_tree(all_matches)
|
|
460
465
|
cached_result = (all_matches, prefix_tree)
|
|
461
|
-
|
|
466
|
+
|
|
462
467
|
# Store in LRU cache (will evict oldest if necessary)
|
|
463
468
|
RedactionEngine._jsonpath_cache.set(cache_key, cached_result)
|
|
464
|
-
|
|
469
|
+
|
|
465
470
|
matches, prefix_tree = cached_result
|
|
466
|
-
|
|
471
|
+
|
|
467
472
|
if include_keys:
|
|
468
473
|
# include_keys mode: only redact if path matches or has matching children
|
|
469
474
|
return RedactionEngine._path_matches_or_has_children(path, prefix_tree)
|
|
@@ -473,7 +478,7 @@ class RedactionEngine:
|
|
|
473
478
|
else:
|
|
474
479
|
# No filtering: redact everything
|
|
475
480
|
return True
|
|
476
|
-
|
|
481
|
+
|
|
477
482
|
@staticmethod
|
|
478
483
|
def _build_prefix_tree(matches: Set[str]) -> Dict[str, Any]:
|
|
479
484
|
"""
|
|
@@ -481,23 +486,23 @@ class RedactionEngine:
|
|
|
481
486
|
Stores both exact matches and children for single-traversal lookup.
|
|
482
487
|
"""
|
|
483
488
|
prefix_tree = {}
|
|
484
|
-
|
|
489
|
+
|
|
485
490
|
for match in matches:
|
|
486
491
|
parts = match.split('.')
|
|
487
492
|
node = prefix_tree
|
|
488
|
-
|
|
493
|
+
|
|
489
494
|
# Build tree path
|
|
490
495
|
for i, part in enumerate(parts):
|
|
491
496
|
if part not in node:
|
|
492
497
|
node[part] = {'_children': {}, '_is_match': False}
|
|
493
|
-
|
|
498
|
+
|
|
494
499
|
if i == len(parts) - 1:
|
|
495
500
|
node[part]['_is_match'] = True # Mark exact matches
|
|
496
|
-
|
|
501
|
+
|
|
497
502
|
node = node[part]['_children']
|
|
498
|
-
|
|
503
|
+
|
|
499
504
|
return prefix_tree
|
|
500
|
-
|
|
505
|
+
|
|
501
506
|
@staticmethod
|
|
502
507
|
def _has_matching_children_optimized(path: str, prefix_tree: Dict[str, Any]) -> bool:
|
|
503
508
|
"""
|
|
@@ -507,20 +512,20 @@ class RedactionEngine:
|
|
|
507
512
|
if not path:
|
|
508
513
|
# Root path - check if tree has any entries
|
|
509
514
|
return bool(prefix_tree)
|
|
510
|
-
|
|
515
|
+
|
|
511
516
|
# Navigate to the node representing this path
|
|
512
517
|
parts = path.split('.')
|
|
513
518
|
node = prefix_tree
|
|
514
|
-
|
|
519
|
+
|
|
515
520
|
for part in parts:
|
|
516
521
|
if part not in node:
|
|
517
522
|
# Path doesn't exist in tree
|
|
518
523
|
return False
|
|
519
524
|
node = node[part]['_children']
|
|
520
|
-
|
|
525
|
+
|
|
521
526
|
# Check if there are any children under this path
|
|
522
527
|
return bool(node)
|
|
523
|
-
|
|
528
|
+
|
|
524
529
|
@staticmethod
|
|
525
530
|
def _path_matches_or_has_children(path: str, prefix_tree: Dict[str, Any]) -> bool:
|
|
526
531
|
"""
|
|
@@ -529,22 +534,22 @@ class RedactionEngine:
|
|
|
529
534
|
"""
|
|
530
535
|
if not path:
|
|
531
536
|
return bool(prefix_tree)
|
|
532
|
-
|
|
537
|
+
|
|
533
538
|
parts = path.split('.')
|
|
534
539
|
node = prefix_tree
|
|
535
|
-
|
|
540
|
+
|
|
536
541
|
for i, part in enumerate(parts):
|
|
537
542
|
if part not in node:
|
|
538
543
|
return False
|
|
539
|
-
|
|
544
|
+
|
|
540
545
|
if i == len(parts) - 1:
|
|
541
546
|
# Check if this exact path matches OR has children
|
|
542
547
|
return node[part].get('_is_match', False) or bool(node[part]['_children'])
|
|
543
|
-
|
|
548
|
+
|
|
544
549
|
node = node[part]['_children']
|
|
545
|
-
|
|
550
|
+
|
|
546
551
|
return False
|
|
547
|
-
|
|
552
|
+
|
|
548
553
|
@staticmethod
|
|
549
554
|
def _has_matching_children_cached(path: str, matches: Set[str]) -> bool:
|
|
550
555
|
"""
|
|
@@ -553,7 +558,7 @@ class RedactionEngine:
|
|
|
553
558
|
"""
|
|
554
559
|
path_prefix = path + "." if path else ""
|
|
555
560
|
return any(match.startswith(path_prefix) for match in matches)
|
|
556
|
-
|
|
561
|
+
|
|
557
562
|
@classmethod
|
|
558
563
|
def clear_caches(cls) -> None:
|
|
559
564
|
"""
|
|
@@ -561,7 +566,7 @@ class RedactionEngine:
|
|
|
561
566
|
"""
|
|
562
567
|
cls._jsonpath_expr_cache.clear()
|
|
563
568
|
cls._jsonpath_cache.clear()
|
|
564
|
-
|
|
569
|
+
|
|
565
570
|
@classmethod
|
|
566
571
|
def get_cache_stats(cls) -> Dict[str, int]:
|
|
567
572
|
"""
|
modules/ui/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
# UI modules for user interaction
|
|
1
|
+
# UI modules for user interaction
|
modules/ui/confirmation.py
CHANGED
modules/utils/cli.py
CHANGED
|
@@ -7,7 +7,7 @@ import argparse
|
|
|
7
7
|
def parse_args():
|
|
8
8
|
"""Parse command line arguments"""
|
|
9
9
|
parser = argparse.ArgumentParser(
|
|
10
|
-
description="
|
|
10
|
+
description="Transparent MCP wrapper with security middleware for real-time policy enforcement and monitoring.",
|
|
11
11
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
12
12
|
epilog="""
|
|
13
13
|
Examples:
|
|
@@ -22,18 +22,34 @@ Examples:
|
|
|
22
22
|
|
|
23
23
|
# With custom name
|
|
24
24
|
%(prog)s --wrapped-config '{"command": "node", "args": ["server.js"]}' --name MyWrapper
|
|
25
|
+
|
|
26
|
+
# IDE Tools mode
|
|
27
|
+
%(prog)s --ide-tool --ide cursor --context beforeShellExecution
|
|
25
28
|
|
|
26
29
|
Reference Links:
|
|
27
|
-
•
|
|
28
|
-
• FastMCP Middleware: https://gofastmcp.com/servers/middleware
|
|
30
|
+
• MCPower Proxy: https://github.com/ai-mcpower/mcpower-proxy
|
|
29
31
|
• MCP Official: https://modelcontextprotocol.io
|
|
30
|
-
• Claude MCP Config: https://docs.anthropic.com/en/docs/claude-code/mcp
|
|
31
32
|
"""
|
|
32
33
|
)
|
|
33
34
|
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
'--ide-tool',
|
|
37
|
+
action='store_true',
|
|
38
|
+
help='Run in IDE tools mode'
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
'--ide',
|
|
43
|
+
help='IDE name (required with --ide-tool, e.g., "cursor")'
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
'--context',
|
|
48
|
+
help='Additional context (to be verified as optional by the associated --ide handler)'
|
|
49
|
+
)
|
|
50
|
+
|
|
34
51
|
parser.add_argument(
|
|
35
52
|
'--wrapped-config',
|
|
36
|
-
required=True,
|
|
37
53
|
help='JSON/JSONC configuration for the wrapped MCP server (FastMCP will handle validation)'
|
|
38
54
|
)
|
|
39
55
|
|
|
@@ -43,4 +59,18 @@ Reference Links:
|
|
|
43
59
|
help='Name for the wrapper MCP server (default: MCPWrapper)'
|
|
44
60
|
)
|
|
45
61
|
|
|
46
|
-
|
|
62
|
+
args = parser.parse_args()
|
|
63
|
+
|
|
64
|
+
# Validate: either --ide-tool or --wrapped-config is required
|
|
65
|
+
if not args.ide_tool and not args.wrapped_config:
|
|
66
|
+
parser.error("either --ide-tool or --wrapped-config is required")
|
|
67
|
+
|
|
68
|
+
# Validate: --ide-tool and --wrapped-config are mutually exclusive
|
|
69
|
+
if args.ide_tool and args.wrapped_config:
|
|
70
|
+
parser.error("--ide-tool and --wrapped-config are mutually exclusive")
|
|
71
|
+
|
|
72
|
+
# Validate: --ide-tool requires --ide
|
|
73
|
+
if args.ide_tool and not args.ide:
|
|
74
|
+
parser.error("--ide-tool requires --ide argument")
|
|
75
|
+
|
|
76
|
+
return args
|