synapse-hub 2.0.0 → 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 19:59:47",
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 19:59:47
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
@@ -26,7 +26,15 @@ import json
26
26
  import sqlite3
27
27
  import argparse
28
28
  from typing import Dict, Any, List, Optional, Tuple, Union
29
- from PIL import Image, ImageDraw, ImageFont
29
+ try:
30
+ from PIL import Image, ImageDraw, ImageFont, ImageFilter
31
+ HAS_PIL = True
32
+ except ImportError:
33
+ Image = None
34
+ ImageDraw = None
35
+ ImageFont = None
36
+ ImageFilter = None
37
+ HAS_PIL = False
30
38
 
31
39
  # Enforce UTF-8 stdout encoding for Windows console compatibility
32
40
  if hasattr(sys.stdout, 'reconfigure'):
@@ -222,6 +230,89 @@ class VisualSoMHelper:
222
230
 
223
231
  return visual_hash
224
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
+
225
316
  def generate_som_overlay(
226
317
  self,
227
318
  image_path: str,
@@ -230,22 +321,27 @@ class VisualSoMHelper:
230
321
  ) -> Tuple[str, Dict[int, Dict[str, Any]]]:
231
322
  """
232
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.
233
325
  Returns (marked_image_path, marks_map).
234
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
+
235
336
  if not elements:
236
- elements = [
337
+ detected = self.detect_canvas_interactive_regions(image_path)
338
+ elements = detected if detected else [
237
339
  {"x": 100, "y": 150, "width": 120, "height": 40, "label": "TradingView Zoom In", "tag": "canvas_control"},
238
340
  {"x": 240, "y": 150, "width": 120, "height": 40, "label": "TradingView Zoom Out", "tag": "canvas_control"},
239
341
  {"x": 500, "y": 300, "width": 200, "height": 50, "label": "Execute Trade Order", "tag": "shadow_button"},
240
342
  {"x": 720, "y": 300, "width": 150, "height": 50, "label": "Cancel Order", "tag": "shadow_button"}
241
343
  ]
242
344
 
243
- if not os.path.exists(image_path):
244
- img = Image.new("RGB", (self.viewport_width, self.viewport_height), color="#1E1E2E")
245
- draw = ImageDraw.Draw(img)
246
- draw.text((50, 50), "Mock Financial Dashboard (HTML5 Canvas)", fill="#FFFFFF")
247
- img.save(image_path)
248
-
249
345
  img = Image.open(image_path).convert("RGBA")
250
346
  overlay = Image.new("RGBA", img.size, (255, 255, 255, 0))
251
347
  draw = ImageDraw.Draw(overlay)
@@ -335,6 +431,7 @@ def main():
335
431
  parser.add_argument("--cache-lookup", action="store_true", help="Attempt zero-token visual cache resolution first")
336
432
  parser.add_argument("--cache-save", action="store_true", help="Save resolved coordinates to visual cache")
337
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")
338
435
  parser.add_argument("--json", action="store_true", help="Output results in JSON format")
339
436
  args = parser.parse_args()
340
437
 
@@ -357,6 +454,27 @@ def main():
357
454
  print(f" Action : {cached_result['playwright_action']}\n")
358
455
  return 0
359
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
+
360
478
  # Load elements
361
479
  elements = []
362
480
  if args.elements_json:
@@ -370,7 +488,8 @@ def main():
370
488
  pass
371
489
 
372
490
  if not elements:
373
- elements = [
491
+ detected = helper.detect_canvas_interactive_regions(args.image)
492
+ elements = detected if detected else [
374
493
  {"x": 100, "y": 150, "width": 120, "height": 40, "label": "TradingView Zoom In", "tag": "canvas_control"},
375
494
  {"x": 240, "y": 150, "width": 120, "height": 40, "label": "TradingView Zoom Out", "tag": "canvas_control"},
376
495
  {"x": 500, "y": 300, "width": 200, "height": 50, "label": "Execute Trade Order", "tag": "shadow_button"},