synapse-hub 2.0.1 → 2.1.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.
@@ -109,6 +109,119 @@ class NetworkMockShield:
109
109
  delay_opt = f", delay: {delay_ms}" if delay_ms > 0 else ""
110
110
  return f"cy.intercept('{method}', '{url_pattern}', {{ statusCode: {status}, body: {body_json}{delay_opt} }}).as('mock_{url_pattern.split('/')[-1]}');"
111
111
 
112
+ @classmethod
113
+ def generate_playwright_har_harness(cls, har_path: str, mode: str = "replay", url_glob: str = "**/*", lang: str = "ts") -> str:
114
+ """
115
+ Generates Playwright Native Zero-Latency HAR Record & Replay harness.
116
+ - record: Dumps real network traffic to HAR file.
117
+ - replay: Intercepts all matching requests and returns responses from HAR with 0ms delay.
118
+ """
119
+ if mode == "record":
120
+ if lang == "python":
121
+ return "\n".join([
122
+ f"# --- ZERO-LATENCY HAR RECORDING HARNESS (Playwright Python) ---",
123
+ f"# Dumps live network traffic to '{har_path}'",
124
+ f"context = browser.new_context(",
125
+ f" record_har_path='{har_path}',",
126
+ f" record_har_mode='minimal',",
127
+ f" record_har_url_filter='{url_glob}'",
128
+ f")",
129
+ f"page = context.new_page()",
130
+ f"# Note: Run 'python tools/network_mock_shield.py --har-sanitize {har_path}' before committing to git!"
131
+ ])
132
+ else:
133
+ return "\n".join([
134
+ f"// --- ZERO-LATENCY HAR RECORDING HARNESS (Playwright TS) ---",
135
+ f"// Dumps live network traffic to '{har_path}'",
136
+ f"const context = await browser.newContext({{",
137
+ f" recordHar: {{",
138
+ f" path: '{har_path}',",
139
+ f" mode: 'minimal',",
140
+ f" urlFilter: '{url_glob}'",
141
+ f" }}",
142
+ f"}});",
143
+ f"const page = await context.newPage();",
144
+ f"// Note: Run 'python tools/network_mock_shield.py --har-sanitize {har_path}' before committing to git!"
145
+ ])
146
+ else:
147
+ # Replay Mode
148
+ if lang == "python":
149
+ return "\n".join([
150
+ f"# --- ZERO-LATENCY HAR REPLAY HARNESS (Playwright Python) ---",
151
+ f"# Replays 100% network responses from local HAR with 0ms delay (Immune to backend downtime)",
152
+ f"page.route_from_har(",
153
+ f" har_id='{har_path}',",
154
+ f" url='{url_glob}',",
155
+ f" not_found='fallback',",
156
+ f" update=False",
157
+ f")"
158
+ ])
159
+ else:
160
+ return "\n".join([
161
+ f"// --- ZERO-LATENCY HAR REPLAY HARNESS (Playwright TS) ---",
162
+ f"// Replays 100% network responses from local HAR with 0ms delay (Immune to backend downtime)",
163
+ f"await page.routeFromHAR('{har_path}', {{",
164
+ f" url: '{url_glob}',",
165
+ f" notFound: 'fallback',",
166
+ f" update: false",
167
+ f"}});"
168
+ ])
169
+
170
+ @classmethod
171
+ def sanitize_har_file(cls, input_har_path: str, output_har_path: Optional[str] = None) -> int:
172
+ """
173
+ Parses a recorded HAR file and redacts sensitive PII & credentials:
174
+ Bearer tokens, Authorization headers, Cookie headers, passwords, API keys.
175
+ Returns the count of redacted entries.
176
+ """
177
+ import re
178
+ if not os.path.exists(input_har_path):
179
+ raise FileNotFoundError(f"HAR file not found: {input_har_path}")
180
+
181
+ with open(input_har_path, "r", encoding="utf-8", errors="replace") as fp:
182
+ data = json.load(fp)
183
+
184
+ entries = data.get("log", {}).get("entries", [])
185
+ redacted_count = 0
186
+
187
+ pii_patterns = [
188
+ (re.compile(r'"(password|pass|secret|token|api_?key|access_?token)"\s*:\s*"[^"]+"', re.IGNORECASE), r'"\1": "REDACTED_SECRET"'),
189
+ (re.compile(r'"(credit_?card|card_?number|pan)"\s*:\s*"[^"]+"', re.IGNORECASE), r'"\1": "411111******1111"'),
190
+ (re.compile(r'"(cvv|cvc)"\s*:\s*"[^"]+"', re.IGNORECASE), r'"\1": "999"'),
191
+ (re.compile(r'"(email)"\s*:\s*"[^"]+"', re.IGNORECASE), r'"\1": "auto_user@enterprise.local"')
192
+ ]
193
+
194
+ for entry in entries:
195
+ req = entry.get("request", {})
196
+ # 1. Scrub Request Headers
197
+ for header in req.get("headers", []):
198
+ h_name = header.get("name", "").lower()
199
+ if h_name in ("authorization", "cookie", "x-api-key", "token", "proxy-authorization"):
200
+ header["value"] = "Bearer REDACTED_TOKEN" if "bearer" in header.get("value", "").lower() else "REDACTED"
201
+ redacted_count += 1
202
+
203
+ # 2. Scrub Request Post Data
204
+ post_data = req.get("postData", {})
205
+ if "text" in post_data:
206
+ txt = post_data["text"]
207
+ for pat, rep in pii_patterns:
208
+ txt = pat.sub(rep, txt)
209
+ post_data["text"] = txt
210
+
211
+ # 3. Scrub Response Headers (Set-Cookie)
212
+ resp = entry.get("response", {})
213
+ for header in resp.get("headers", []):
214
+ h_name = header.get("name", "").lower()
215
+ if h_name in ("set-cookie", "authorization"):
216
+ header["value"] = "REDACTED_COOKIE"
217
+ redacted_count += 1
218
+
219
+ out_path = output_har_path or input_har_path
220
+ with open(out_path, "w", encoding="utf-8") as fp:
221
+ json.dump(data, fp, indent=2)
222
+
223
+ return redacted_count
224
+
112
225
  @classmethod
113
226
  def get_preset(cls, preset_name: str) -> Dict[str, Any]:
114
227
  """Returns preset details or raises ValueError."""
@@ -127,8 +240,36 @@ def main():
127
240
  parser.add_argument("--lang", choices=["ts", "python", "cypress"], default="ts", help="Target script language")
128
241
  parser.add_argument("--list-presets", action="store_true", help="List all available presets")
129
242
 
243
+ # HAR Record & Replay options
244
+ parser.add_argument("--har-mode", choices=["record", "replay"], default=None, help="Generate Playwright HAR record or replay harness")
245
+ parser.add_argument("--har-path", default="fixtures/network_trace.har", help="Path to HAR archive file")
246
+ parser.add_argument("--har-sanitize", default=None, help="Sanitize PII, tokens, and cookies from recorded HAR file")
247
+ parser.add_argument("--har-out", default=None, help="Optional output path for sanitized HAR")
248
+
130
249
  args = parser.parse_args()
131
250
 
251
+ # Handle HAR Sanitization
252
+ if args.har_sanitize:
253
+ redacted = NetworkMockShield.sanitize_har_file(args.har_sanitize, output_har_path=args.har_out)
254
+ target = args.har_out or args.har_sanitize
255
+ print(f"āœ… [HAR SANITIZED] Scrubbed {redacted} sensitive headers/tokens in: {target}")
256
+ return
257
+
258
+ # Handle HAR Harness Generation
259
+ if args.har_mode:
260
+ code = NetworkMockShield.generate_playwright_har_harness(
261
+ har_path=args.har_path,
262
+ mode=args.har_mode,
263
+ url_glob=args.url,
264
+ lang=args.lang
265
+ )
266
+ print("======================================================================")
267
+ print(f" PLAYWRIGHT ZERO-LATENCY HAR {args.har_mode.upper()} HARNESS ({args.lang.upper()})")
268
+ print("======================================================================")
269
+ print(code)
270
+ print("======================================================================")
271
+ return
272
+
132
273
  if args.list_presets:
133
274
  print("======================================================================")
134
275
  print(" AVAILABLE NETWORK MOCK & CHAOS FAULT PRESETS")
@@ -1,5 +1,5 @@
1
1
  {
2
- "generated_at": "2026-10-29 20:23:27",
2
+ "generated_at": "2026-10-29 21:59:04",
3
3
  "target_repository": "/Users/phungcao/Documents/Agent Skill",
4
4
  "classes_count": 0,
5
5
  "methods_count": 0,
@@ -1,6 +1,6 @@
1
1
  # ==============================================================================
2
2
  # ENTERPRISE AUTOMATION REPOSITORY MAP (Aider-Style AST Compression)
3
- # Generated : 2026-10-29 20:23:27
3
+ # Generated : 2026-10-29 21:59:04
4
4
  # Target : /Users/phungcao/Documents/Agent Skill
5
5
  # Scope : 0 Classes (0 source lines, 0.0 KB)
6
6
  # Purpose : Token-budgeted index (< 600 tokens) for instant method lookup
@@ -27,12 +27,13 @@ import sqlite3
27
27
  import argparse
28
28
  from typing import Dict, Any, List, Optional, Tuple, Union
29
29
  try:
30
- from PIL import Image, ImageDraw, ImageFont
30
+ from PIL import Image, ImageDraw, ImageFont, ImageFilter
31
31
  HAS_PIL = True
32
32
  except ImportError:
33
33
  Image = None
34
34
  ImageDraw = None
35
35
  ImageFont = None
36
+ ImageFilter = None
36
37
  HAS_PIL = False
37
38
 
38
39
  # Enforce UTF-8 stdout encoding for Windows console compatibility
@@ -229,6 +230,89 @@ class VisualSoMHelper:
229
230
 
230
231
  return visual_hash
231
232
 
233
+ def detect_canvas_interactive_regions(
234
+ self,
235
+ image_path: str,
236
+ stride: int = 16,
237
+ edge_threshold: int = 28,
238
+ min_pixels_per_cell: int = 6,
239
+ min_w: int = 16,
240
+ min_h: int = 16,
241
+ max_ratio: float = 0.88
242
+ ) -> List[Dict[str, Any]]:
243
+ """
244
+ Standalone Computer Vision Contour / Active Blob Detector for Canvas/GIS.
245
+ Detects interactive visual components (buttons, polygon nodes, markers, sliders, chart icons)
246
+ without requiring DOM elements or accessibility nodes.
247
+ Uses pure PIL edge filtering and connected-grid component clustering (Zero-Dependency).
248
+ """
249
+ if not os.path.exists(image_path) or not HAS_PIL:
250
+ return []
251
+
252
+ img = Image.open(image_path).convert("L")
253
+ w, h = img.size
254
+ max_allowed_w = int(w * max_ratio)
255
+ max_allowed_h = int(h * max_ratio)
256
+
257
+ # Detect high-contrast edges & contours
258
+ edges = img.filter(ImageFilter.FIND_EDGES)
259
+ edges = edges.point(lambda p: 255 if p > edge_threshold else 0)
260
+ pixels = edges.load()
261
+
262
+ # Identify active cells with high edge density
263
+ active_cells = set()
264
+ for y in range(0, h, stride):
265
+ for x in range(0, w, stride):
266
+ count = 0
267
+ for cy in range(y, min(h, y + stride)):
268
+ for cx in range(x, min(w, x + stride)):
269
+ if pixels[cx, cy] > 0:
270
+ count += 1
271
+ if count >= min_pixels_per_cell:
272
+ active_cells.add((x // stride, y // stride))
273
+
274
+ # 8-direction BFS clustering to group connected components
275
+ visited = set()
276
+ detected_elements = []
277
+ cluster_id = 1
278
+
279
+ for cell in sorted(active_cells):
280
+ if cell in visited:
281
+ continue
282
+ queue = [cell]
283
+ visited.add(cell)
284
+ cluster = [cell]
285
+
286
+ while queue:
287
+ curr = queue.pop(0)
288
+ cx, cy = curr
289
+ for dx in (-1, 0, 1):
290
+ for dy in (-1, 0, 1):
291
+ nb = (cx + dx, cy + dy)
292
+ if nb in active_cells and nb not in visited:
293
+ visited.add(nb)
294
+ queue.append(nb)
295
+ cluster.append(nb)
296
+
297
+ min_x = max(0, min(c[0] for c in cluster) * stride - 4)
298
+ min_y = max(0, min(c[1] for c in cluster) * stride - 4)
299
+ box_w = min(w - min_x, (max(c[0] for c in cluster) + 1) * stride - min_x + 8)
300
+ box_h = min(h - min_y, (max(c[1] for c in cluster) + 1) * stride - min_y + 8)
301
+
302
+ # Filter out whole-canvas background borders or tiny noise
303
+ if box_w >= min_w and box_h >= min_h and (box_w < max_allowed_w or box_h < max_allowed_h):
304
+ detected_elements.append({
305
+ "x": min_x,
306
+ "y": min_y,
307
+ "width": box_w,
308
+ "height": box_h,
309
+ "label": f"Canvas_Target_{cluster_id}",
310
+ "tag": "canvas_interactive_node"
311
+ })
312
+ cluster_id += 1
313
+
314
+ return detected_elements
315
+
232
316
  def generate_som_overlay(
233
317
  self,
234
318
  image_path: str,
@@ -237,22 +321,27 @@ class VisualSoMHelper:
237
321
  ) -> Tuple[str, Dict[int, Dict[str, Any]]]:
238
322
  """
239
323
  Draws high-contrast numbered badges (Set-of-Marks) over each element bounding box.
324
+ If elements is omitted, autonomously runs detect_canvas_interactive_regions.
240
325
  Returns (marked_image_path, marks_map).
241
326
  """
327
+ if not os.path.exists(image_path):
328
+ img = Image.new("RGB", (self.viewport_width, self.viewport_height), color="#1E1E2E")
329
+ draw = ImageDraw.Draw(img)
330
+ draw.text((50, 50), "Mock Financial Dashboard (HTML5 Canvas)", fill="#FFFFFF")
331
+ draw.rectangle([100, 150, 220, 190], outline="#00CCFF", width=2)
332
+ draw.rectangle([240, 150, 360, 190], outline="#FF0055", width=2)
333
+ draw.rectangle([500, 300, 700, 350], outline="#00FF66", width=2)
334
+ img.save(image_path)
335
+
242
336
  if not elements:
243
- elements = [
337
+ detected = self.detect_canvas_interactive_regions(image_path)
338
+ elements = detected if detected else [
244
339
  {"x": 100, "y": 150, "width": 120, "height": 40, "label": "TradingView Zoom In", "tag": "canvas_control"},
245
340
  {"x": 240, "y": 150, "width": 120, "height": 40, "label": "TradingView Zoom Out", "tag": "canvas_control"},
246
341
  {"x": 500, "y": 300, "width": 200, "height": 50, "label": "Execute Trade Order", "tag": "shadow_button"},
247
342
  {"x": 720, "y": 300, "width": 150, "height": 50, "label": "Cancel Order", "tag": "shadow_button"}
248
343
  ]
249
344
 
250
- if not os.path.exists(image_path):
251
- img = Image.new("RGB", (self.viewport_width, self.viewport_height), color="#1E1E2E")
252
- draw = ImageDraw.Draw(img)
253
- draw.text((50, 50), "Mock Financial Dashboard (HTML5 Canvas)", fill="#FFFFFF")
254
- img.save(image_path)
255
-
256
345
  img = Image.open(image_path).convert("RGBA")
257
346
  overlay = Image.new("RGBA", img.size, (255, 255, 255, 0))
258
347
  draw = ImageDraw.Draw(overlay)
@@ -342,6 +431,7 @@ def main():
342
431
  parser.add_argument("--cache-lookup", action="store_true", help="Attempt zero-token visual cache resolution first")
343
432
  parser.add_argument("--cache-save", action="store_true", help="Save resolved coordinates to visual cache")
344
433
  parser.add_argument("--threshold", type=float, default=0.98, help="Visual similarity threshold for cache hit (default: 0.98)")
434
+ parser.add_argument("--detect-only", action="store_true", help="Run pure contour/blob detector and print detected bounding boxes")
345
435
  parser.add_argument("--json", action="store_true", help="Output results in JSON format")
346
436
  args = parser.parse_args()
347
437
 
@@ -364,6 +454,27 @@ def main():
364
454
  print(f" Action : {cached_result['playwright_action']}\n")
365
455
  return 0
366
456
 
457
+ # Auto-generate mock canvas if image doesn't exist
458
+ if not os.path.exists(args.image):
459
+ img = Image.new("RGB", (helper.viewport_width, helper.viewport_height), color="#1E1E2E")
460
+ draw = ImageDraw.Draw(img)
461
+ draw.text((50, 50), "Mock GIS & Financial Dashboard (HTML5 Canvas)", fill="#FFFFFF")
462
+ draw.rectangle([100, 150, 220, 190], fill="#00CCFF", outline="#FFFFFF", width=2)
463
+ draw.rectangle([240, 150, 360, 190], fill="#FF0055", outline="#FFFFFF", width=2)
464
+ draw.rectangle([500, 300, 700, 350], fill="#00FF66", outline="#FFFFFF", width=2)
465
+ img.save(args.image)
466
+
467
+ # Detect Only
468
+ if args.detect_only:
469
+ blobs = helper.detect_canvas_interactive_regions(args.image)
470
+ if args.json:
471
+ print(json.dumps(blobs, indent=2))
472
+ else:
473
+ print(f"\nšŸ” [CANVAS CONTOURS DETECTED] Found {len(blobs)} interactive regions:")
474
+ for b in blobs:
475
+ print(f" • {b['label']}: Bounds {b['x']},{b['y']} ({b['width']}x{b['height']} px)")
476
+ return 0
477
+
367
478
  # Load elements
368
479
  elements = []
369
480
  if args.elements_json:
@@ -377,7 +488,8 @@ def main():
377
488
  pass
378
489
 
379
490
  if not elements:
380
- elements = [
491
+ detected = helper.detect_canvas_interactive_regions(args.image)
492
+ elements = detected if detected else [
381
493
  {"x": 100, "y": 150, "width": 120, "height": 40, "label": "TradingView Zoom In", "tag": "canvas_control"},
382
494
  {"x": 240, "y": 150, "width": 120, "height": 40, "label": "TradingView Zoom Out", "tag": "canvas_control"},
383
495
  {"x": 500, "y": 300, "width": 200, "height": 50, "label": "Execute Trade Order", "tag": "shadow_button"},