torusguard 1.4.0 → 2.0.0-alpha

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.
Files changed (39) hide show
  1. package/.torusguard/auth.json +5 -0
  2. package/.torusguard/rules_catalog.json +242 -518
  3. package/.torusguard/scripts/__pycache__/apply_runner.cpython-314.pyc +0 -0
  4. package/.torusguard/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
  5. package/.torusguard/scripts/__pycache__/harden_runner.cpython-314.pyc +0 -0
  6. package/.torusguard/scripts/__pycache__/html_reporter.cpython-314.pyc +0 -0
  7. package/.torusguard/scripts/__pycache__/manifest_builder.cpython-314.pyc +0 -0
  8. package/.torusguard/scripts/__pycache__/recipes_runner.cpython-314.pyc +0 -0
  9. package/.torusguard/scripts/__pycache__/report_sync.cpython-314.pyc +0 -0
  10. package/.torusguard/snapshots/20260921-182638/go.mod.bak +11 -0
  11. package/.torusguard/snapshots/20260921-185213/file.go.bak +5 -0
  12. package/README.md +315 -393
  13. package/package.json +1 -1
  14. package/skills/torusguard/__pycache__/bootstrap.cpython-314.pyc +0 -0
  15. package/skills/torusguard/payload/TORUSGUARD.md +145 -145
  16. package/skills/torusguard/payload/agents/auditor.md +41 -41
  17. package/skills/torusguard/payload/agents/profiler.md +49 -49
  18. package/skills/torusguard/payload/agents/remediator.md +41 -41
  19. package/skills/torusguard/payload/agents/reviewer.md +39 -39
  20. package/skills/torusguard/payload/agents/validator.md +46 -46
  21. package/skills/torusguard/payload/config/scope.json +24 -24
  22. package/skills/torusguard/payload/references/csharp-security.md +41 -41
  23. package/skills/torusguard/payload/references/go-security.md +41 -41
  24. package/skills/torusguard/payload/references/java-security.md +40 -40
  25. package/skills/torusguard/payload/references/polyglot-security-matrix.md +25 -25
  26. package/skills/torusguard/payload/references/rust-security.md +40 -40
  27. package/skills/torusguard/payload/rules/custom/.gitkeep +1 -1
  28. package/skills/torusguard/payload/rules/custom/README.md +30 -30
  29. package/skills/torusguard/payload/scripts/rules_sync.py +321 -321
  30. package/skills/torusguard/payload/scripts/safety_gate.py +64 -64
  31. package/skills/torusguard/payload/skills/torusguard/references/csharp-security.md +41 -41
  32. package/skills/torusguard/payload/skills/torusguard/references/go-security.md +41 -41
  33. package/skills/torusguard/payload/skills/torusguard/references/java-security.md +40 -40
  34. package/skills/torusguard/payload/skills/torusguard/references/polyglot-security-matrix.md +25 -25
  35. package/skills/torusguard/payload/skills/torusguard/references/rust-security.md +40 -40
  36. package/skills/torusguard/payload/templates/audit-report.template.md +54 -54
  37. package/skills/torusguard/payload/templates/authorization.template.md +34 -34
  38. package/skills/torusguard/payload/templates/finding-card.template.md +32 -32
  39. package/skills/torusguard/payload/templates/remediation-bundle.template.md +35 -35
@@ -1,321 +1,321 @@
1
- #!/usr/bin/env python3
2
- """
3
- TorusGuard AI IDE Rules Auto-Sync Engine (v1.2.0)
4
- Compiles project security memory, golden fix recipes, and active guardrails
5
- into prompt-optimized rule files for Cursor, Claude Code, Antigravity, and Windsurf.
6
- Non-destructive: preserves existing user rules via demarcated comment fences.
7
- Zero external dependencies (Python 3.10+ stdlib).
8
- """
9
-
10
- import sys
11
- import json
12
- import re
13
- import argparse
14
- from pathlib import Path
15
- from typing import Any
16
-
17
- # Windows console UTF-8 support
18
- if sys.stdout and hasattr(sys.stdout, "reconfigure"):
19
- try:
20
- getattr(sys.stdout, "reconfigure")(encoding="utf-8", errors="replace")
21
- except (AttributeError, OSError):
22
- pass
23
- if sys.stderr and hasattr(sys.stderr, "reconfigure"):
24
- try:
25
- getattr(sys.stderr, "reconfigure")(encoding="utf-8", errors="replace")
26
- except (AttributeError, OSError):
27
- pass
28
-
29
- START_MARKER = "<!-- TORUSGUARD-SECURITY-GUARDRAILS:START -->"
30
- END_MARKER = "<!-- TORUSGUARD-SECURITY-GUARDRAILS:END -->"
31
- MAX_PROMPT_TOKENS = 400
32
-
33
-
34
- def find_project_root(start_dir: str | Path | None = None) -> Path:
35
- """Detect project root directory."""
36
- current = Path(start_dir or Path.cwd()).resolve()
37
- markers = [".git", "package.json", "pyproject.toml", ".torusguard"]
38
- for m in markers:
39
- if (current / m).exists():
40
- return current
41
- for parent in current.parents:
42
- for m in markers:
43
- if (parent / m).exists():
44
- return parent
45
- return current
46
-
47
-
48
- def load_security_context(root_dir: Path) -> dict[str, Any]:
49
- """Load security profile and patterns from memory subsystem."""
50
- tg_dir = root_dir / ".torusguard"
51
- memory_dir = tg_dir / "memory"
52
-
53
- context_file = memory_dir / "context.json"
54
- patterns_file = memory_dir / "patterns.json"
55
- profile_file = memory_dir / "profile.json"
56
-
57
- context_data: dict[str, Any] = {}
58
- patterns: list[dict[str, Any]] = []
59
- profile: dict[str, Any] = {}
60
-
61
- if context_file.exists():
62
- try:
63
- context_data = json.loads(context_file.read_text(encoding="utf-8"))
64
- except (json.JSONDecodeError, OSError):
65
- context_data = {}
66
-
67
- if patterns_file.exists():
68
- try:
69
- patterns = json.loads(patterns_file.read_text(encoding="utf-8"))
70
- except (json.JSONDecodeError, OSError):
71
- patterns = []
72
-
73
- if profile_file.exists():
74
- try:
75
- profile = json.loads(profile_file.read_text(encoding="utf-8"))
76
- except (json.JSONDecodeError, OSError):
77
- profile = {}
78
-
79
- cfg_file = tg_dir / "config" / "torusguard.json"
80
- if cfg_file.exists():
81
- try:
82
- cfg = json.loads(cfg_file.read_text(encoding="utf-8"))
83
- det = cfg.get("detected_stack", {})
84
- if not profile.get("stack"):
85
- lang = det.get("language")
86
- fw = det.get("framework")
87
- if lang and lang != "Unknown":
88
- stk = [lang]
89
- if fw and fw != "None":
90
- stk.append(fw)
91
- profile["stack"] = stk
92
- except (json.JSONDecodeError, OSError):
93
- pass
94
-
95
- # Dynamic fallback to stack_detect if stack is still unknown
96
- if not profile.get("stack"):
97
- try:
98
- import stack_detect # type: ignore
99
- det_stk = stack_detect.detect_stack(root_dir)
100
- lang = det_stk.get("language")
101
- fw = det_stk.get("framework")
102
- if lang and lang != "Unknown":
103
- stk = [lang]
104
- if fw and fw != "None":
105
- stk.append(fw)
106
- profile["stack"] = stk
107
- except Exception:
108
- pass
109
-
110
- return {
111
- "context": context_data,
112
- "patterns": patterns,
113
- "profile": profile
114
- }
115
-
116
-
117
- def format_dense_security_rules(sec_data: dict[str, Any]) -> str:
118
- """
119
- Format active security memory into a dense, token-budgeted markdown block (<=400 tokens),
120
- dynamically tailored to the detected language and framework family.
121
- """
122
- patterns = sec_data.get("patterns", [])
123
- profile = sec_data.get("profile", {})
124
- stack = profile.get("stack", [])
125
-
126
- golden_recipes: list[dict[str, Any]] = []
127
- regressions: list[dict[str, Any]] = []
128
- common_vulns: list[dict[str, Any]] = []
129
-
130
- for p in patterns:
131
- ptype = p.get("pattern_type")
132
- if ptype == "golden_fix_recipe":
133
- golden_recipes.append(p)
134
- elif ptype == "regression_watch":
135
- regressions.append(p)
136
- elif ptype in ("common_vulnerability", "recurring_fix"):
137
- common_vulns.append(p)
138
-
139
- lines: list[str] = [
140
- START_MARKER,
141
- "## 🛡️ TorusGuard Active Project Security Invariants",
142
- "> Auto-synced from `.torusguard/memory/`. Adhere to these guardrails in all generated code.",
143
- ""
144
- ]
145
-
146
- if stack:
147
- lines.append(f"**Detected Stack:** {', '.join(stack)}")
148
- lines.append("")
149
-
150
- # Core Behavioral Invariants (Stack-Adaptive & Token-Optimized)
151
- stack_lower = " ".join(stack).lower()
152
- sql_guards = []
153
- bypass_guards = []
154
-
155
- if "go" in stack_lower:
156
- sql_guards.append("`db.Query(..., args)` / GORM")
157
- bypass_guards.append("`InsecureSkipVerify: true`")
158
- if "rust" in stack_lower:
159
- sql_guards.append("`sqlx::query!` / Diesel")
160
- bypass_guards.append("`unsafe { ... }`")
161
- if "java" in stack_lower or "kotlin" in stack_lower:
162
- sql_guards.append("JPA / PreparedStatement")
163
- bypass_guards.append("`csrf().disable()`")
164
- if "c#" in stack_lower or "dotnet" in stack_lower:
165
- sql_guards.append("EF Core LINQ / `FromSqlInterpolated`")
166
- bypass_guards.append("`[AllowAnonymous]`")
167
- if "php" in stack_lower:
168
- sql_guards.append("PDO prepared statements / Eloquent")
169
- bypass_guards.append("`CURLOPT_SSL_VERIFYPEER => false`")
170
- if "typescript" in stack_lower or "javascript" in stack_lower or "next" in stack_lower or "express" in stack_lower:
171
- sql_guards.append("Prisma / Drizzle binders")
172
- bypass_guards.append("`rejectUnauthorized: false`")
173
-
174
- if not sql_guards:
175
- sql_rule = "1. **Zero Raw SQL / String Formats:** Always use parameterized queries (e.g. `%s`, `?`, or ORM bound params)."
176
- else:
177
- sql_rule = f"1. **Zero Raw SQL:** Always use parameterized queries ({' / '.join(sql_guards[:2])})."
178
-
179
- if not bypass_guards:
180
- bypass_rule = "4. **No Security Bypasses:** Never insert `# nosec`, `verify=False`, `@csrf_exempt`, or `bypass_auth=True`."
181
- else:
182
- bypass_rule = f"4. **No Security Bypasses:** Never insert `# nosec`, `verify=False`, or {', '.join(bypass_guards[:2])}."
183
-
184
- lines.extend([
185
- "### Universal Guardrails",
186
- sql_rule,
187
- "2. **Tenant Isolation:** Enforce tenant/owner filtering on every database lookup (never query without `tenant_id` or owner scope).",
188
- "3. **Ponytail Bound Rule:** Remediation patches must be minimal (<= 35 additions, <= 25 deletions).",
189
- bypass_rule,
190
- ""
191
- ])
192
-
193
- # Golden Fix Recipes (Top 2 to preserve tokens)
194
- if golden_recipes:
195
- lines.append("### Verified Golden Fix Patterns")
196
- for rec in golden_recipes[:2]:
197
- rule = rec.get("rule_id", "TG-SEC")
198
- desc = rec.get("description", "Verified AST fix")
199
- r_data = rec.get("recipe_data", {})
200
- before = r_data.get("before_snippet", "")
201
- after = r_data.get("after_snippet", "")
202
- lines.append(f"- **{rule}** ({desc}):")
203
- if before and after:
204
- lines.append(f" - ❌ Avoid: `{before[:70]}`")
205
- lines.append(f" - ✅ Prefer: `{after[:70]}`")
206
- lines.append("")
207
-
208
- # High-Risk Regression Watches
209
- if regressions:
210
- lines.append("### Active Regression Watches (Do Not Re-Introduce)")
211
- for reg in regressions[:3]:
212
- rule = reg.get("rule_id", "TG-REG")
213
- files = ", ".join(reg.get("affected_files", [])[:2]) or "tracked files"
214
- lines.append(f"- **{rule}**: Recurrence risk in `{files}`.")
215
- lines.append("")
216
-
217
- lines.append(END_MARKER)
218
- return "\n".join(lines)
219
-
220
-
221
- def inject_rules_into_file(file_path: Path, rules_block: str) -> bool:
222
- """
223
- Non-destructively inject or replace the TorusGuard block in a target rules file.
224
- Creates parent directories if necessary.
225
- """
226
- file_path.parent.mkdir(parents=True, exist_ok=True)
227
- existing_content = ""
228
-
229
- if file_path.exists():
230
- try:
231
- existing_content = file_path.read_text(encoding="utf-8")
232
- except OSError:
233
- existing_content = ""
234
-
235
- pattern = re.compile(
236
- rf"{re.escape(START_MARKER)}.*?{re.escape(END_MARKER)}",
237
- re.DOTALL
238
- )
239
-
240
- if pattern.search(existing_content):
241
- updated_content = pattern.sub(rules_block, existing_content)
242
- else:
243
- if existing_content.strip():
244
- updated_content = existing_content.rstrip() + "\n\n" + rules_block + "\n"
245
- else:
246
- updated_content = rules_block + "\n"
247
-
248
- try:
249
- file_path.write_text(updated_content, encoding="utf-8")
250
- return True
251
- except OSError as err:
252
- print(f"Error writing rules to {file_path}: {err}", file=sys.stderr)
253
- return False
254
-
255
-
256
- def sync_rules(
257
- target_format: str = "all",
258
- root_dir: str | Path | None = None
259
- ) -> dict[str, Any]:
260
- """
261
- Synchronize project security invariants into target IDE rules files.
262
- Supported targets: 'cursor', 'claude', 'agent', 'windsurf', 'all'.
263
- """
264
- root = Path(root_dir or find_project_root()).resolve()
265
- sec_data = load_security_context(root)
266
- rules_block = format_dense_security_rules(sec_data)
267
-
268
- target_files: dict[str, Path] = {
269
- "cursor": root / ".cursorrules",
270
- "claude": root / "CLAUDE.md",
271
- "agent": root / ".agent" / "rules" / "torusguard.md",
272
- "windsurf": root / ".windsurfrules"
273
- }
274
-
275
- synced_targets: list[str] = []
276
- formats_to_sync = list(target_files.keys()) if target_format == "all" else [target_format]
277
-
278
- for fmt in formats_to_sync:
279
- if fmt in target_files:
280
- target_path = target_files[fmt]
281
- success = inject_rules_into_file(target_path, rules_block)
282
- if success:
283
- synced_targets.append(str(target_path.relative_to(root)))
284
-
285
- # Estimate token overhead (approx 4 chars/token)
286
- token_est = max(1, len(rules_block) // 4)
287
-
288
- return {
289
- "status": "success" if synced_targets else "skipped",
290
- "synced_files": synced_targets,
291
- "token_estimate": token_est,
292
- "within_budget": token_est <= MAX_PROMPT_TOKENS
293
- }
294
-
295
-
296
- def main():
297
- parser = argparse.ArgumentParser(description="TorusGuard AI IDE Rules Auto-Sync Engine")
298
- parser.add_argument(
299
- "--format",
300
- choices=["all", "cursor", "claude", "agent", "windsurf"],
301
- default="all",
302
- help="Target AI IDE rules format to sync"
303
- )
304
- parser.add_argument("--root", help="Project root override")
305
- parser.add_argument("--json", action="store_true", help="Output raw JSON")
306
-
307
- args = parser.parse_args()
308
- root = Path(args.root).resolve() if args.root else None
309
-
310
- result = sync_rules(target_format=args.format, root_dir=root)
311
- if args.json:
312
- print(json.dumps(result, indent=2))
313
- else:
314
- print(f"[SUCCESS] Synced TorusGuard security invariants to {len(result['synced_files'])} file(s):")
315
- for f in result["synced_files"]:
316
- print(f" - {f}")
317
- print(f"Token estimate: ~{result['token_estimate']} tokens (budget <= {MAX_PROMPT_TOKENS})")
318
-
319
-
320
- if __name__ == "__main__":
321
- main()
1
+ #!/usr/bin/env python3
2
+ """
3
+ TorusGuard AI IDE Rules Auto-Sync Engine (v1.2.0)
4
+ Compiles project security memory, golden fix recipes, and active guardrails
5
+ into prompt-optimized rule files for Cursor, Claude Code, Antigravity, and Windsurf.
6
+ Non-destructive: preserves existing user rules via demarcated comment fences.
7
+ Zero external dependencies (Python 3.10+ stdlib).
8
+ """
9
+
10
+ import sys
11
+ import json
12
+ import re
13
+ import argparse
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ # Windows console UTF-8 support
18
+ if sys.stdout and hasattr(sys.stdout, "reconfigure"):
19
+ try:
20
+ getattr(sys.stdout, "reconfigure")(encoding="utf-8", errors="replace")
21
+ except (AttributeError, OSError):
22
+ pass
23
+ if sys.stderr and hasattr(sys.stderr, "reconfigure"):
24
+ try:
25
+ getattr(sys.stderr, "reconfigure")(encoding="utf-8", errors="replace")
26
+ except (AttributeError, OSError):
27
+ pass
28
+
29
+ START_MARKER = "<!-- TORUSGUARD-SECURITY-GUARDRAILS:START -->"
30
+ END_MARKER = "<!-- TORUSGUARD-SECURITY-GUARDRAILS:END -->"
31
+ MAX_PROMPT_TOKENS = 400
32
+
33
+
34
+ def find_project_root(start_dir: str | Path | None = None) -> Path:
35
+ """Detect project root directory."""
36
+ current = Path(start_dir or Path.cwd()).resolve()
37
+ markers = [".git", "package.json", "pyproject.toml", ".torusguard"]
38
+ for m in markers:
39
+ if (current / m).exists():
40
+ return current
41
+ for parent in current.parents:
42
+ for m in markers:
43
+ if (parent / m).exists():
44
+ return parent
45
+ return current
46
+
47
+
48
+ def load_security_context(root_dir: Path) -> dict[str, Any]:
49
+ """Load security profile and patterns from memory subsystem."""
50
+ tg_dir = root_dir / ".torusguard"
51
+ memory_dir = tg_dir / "memory"
52
+
53
+ context_file = memory_dir / "context.json"
54
+ patterns_file = memory_dir / "patterns.json"
55
+ profile_file = memory_dir / "profile.json"
56
+
57
+ context_data: dict[str, Any] = {}
58
+ patterns: list[dict[str, Any]] = []
59
+ profile: dict[str, Any] = {}
60
+
61
+ if context_file.exists():
62
+ try:
63
+ context_data = json.loads(context_file.read_text(encoding="utf-8"))
64
+ except (json.JSONDecodeError, OSError):
65
+ context_data = {}
66
+
67
+ if patterns_file.exists():
68
+ try:
69
+ patterns = json.loads(patterns_file.read_text(encoding="utf-8"))
70
+ except (json.JSONDecodeError, OSError):
71
+ patterns = []
72
+
73
+ if profile_file.exists():
74
+ try:
75
+ profile = json.loads(profile_file.read_text(encoding="utf-8"))
76
+ except (json.JSONDecodeError, OSError):
77
+ profile = {}
78
+
79
+ cfg_file = tg_dir / "config" / "torusguard.json"
80
+ if cfg_file.exists():
81
+ try:
82
+ cfg = json.loads(cfg_file.read_text(encoding="utf-8"))
83
+ det = cfg.get("detected_stack", {})
84
+ if not profile.get("stack"):
85
+ lang = det.get("language")
86
+ fw = det.get("framework")
87
+ if lang and lang != "Unknown":
88
+ stk = [lang]
89
+ if fw and fw != "None":
90
+ stk.append(fw)
91
+ profile["stack"] = stk
92
+ except (json.JSONDecodeError, OSError):
93
+ pass
94
+
95
+ # Dynamic fallback to stack_detect if stack is still unknown
96
+ if not profile.get("stack"):
97
+ try:
98
+ import stack_detect # type: ignore
99
+ det_stk = stack_detect.detect_stack(root_dir)
100
+ lang = det_stk.get("language")
101
+ fw = det_stk.get("framework")
102
+ if lang and lang != "Unknown":
103
+ stk = [lang]
104
+ if fw and fw != "None":
105
+ stk.append(fw)
106
+ profile["stack"] = stk
107
+ except Exception:
108
+ pass
109
+
110
+ return {
111
+ "context": context_data,
112
+ "patterns": patterns,
113
+ "profile": profile
114
+ }
115
+
116
+
117
+ def format_dense_security_rules(sec_data: dict[str, Any]) -> str:
118
+ """
119
+ Format active security memory into a dense, token-budgeted markdown block (<=400 tokens),
120
+ dynamically tailored to the detected language and framework family.
121
+ """
122
+ patterns = sec_data.get("patterns", [])
123
+ profile = sec_data.get("profile", {})
124
+ stack = profile.get("stack", [])
125
+
126
+ golden_recipes: list[dict[str, Any]] = []
127
+ regressions: list[dict[str, Any]] = []
128
+ common_vulns: list[dict[str, Any]] = []
129
+
130
+ for p in patterns:
131
+ ptype = p.get("pattern_type")
132
+ if ptype == "golden_fix_recipe":
133
+ golden_recipes.append(p)
134
+ elif ptype == "regression_watch":
135
+ regressions.append(p)
136
+ elif ptype in ("common_vulnerability", "recurring_fix"):
137
+ common_vulns.append(p)
138
+
139
+ lines: list[str] = [
140
+ START_MARKER,
141
+ "## 🛡️ TorusGuard Active Project Security Invariants",
142
+ "> Auto-synced from `.torusguard/memory/`. Adhere to these guardrails in all generated code.",
143
+ ""
144
+ ]
145
+
146
+ if stack:
147
+ lines.append(f"**Detected Stack:** {', '.join(stack)}")
148
+ lines.append("")
149
+
150
+ # Core Behavioral Invariants (Stack-Adaptive & Token-Optimized)
151
+ stack_lower = " ".join(stack).lower()
152
+ sql_guards = []
153
+ bypass_guards = []
154
+
155
+ if "go" in stack_lower:
156
+ sql_guards.append("`db.Query(..., args)` / GORM")
157
+ bypass_guards.append("`InsecureSkipVerify: true`")
158
+ if "rust" in stack_lower:
159
+ sql_guards.append("`sqlx::query!` / Diesel")
160
+ bypass_guards.append("`unsafe { ... }`")
161
+ if "java" in stack_lower or "kotlin" in stack_lower:
162
+ sql_guards.append("JPA / PreparedStatement")
163
+ bypass_guards.append("`csrf().disable()`")
164
+ if "c#" in stack_lower or "dotnet" in stack_lower:
165
+ sql_guards.append("EF Core LINQ / `FromSqlInterpolated`")
166
+ bypass_guards.append("`[AllowAnonymous]`")
167
+ if "php" in stack_lower:
168
+ sql_guards.append("PDO prepared statements / Eloquent")
169
+ bypass_guards.append("`CURLOPT_SSL_VERIFYPEER => false`")
170
+ if "typescript" in stack_lower or "javascript" in stack_lower or "next" in stack_lower or "express" in stack_lower:
171
+ sql_guards.append("Prisma / Drizzle binders")
172
+ bypass_guards.append("`rejectUnauthorized: false`")
173
+
174
+ if not sql_guards:
175
+ sql_rule = "1. **Zero Raw SQL / String Formats:** Always use parameterized queries (e.g. `%s`, `?`, or ORM bound params)."
176
+ else:
177
+ sql_rule = f"1. **Zero Raw SQL:** Always use parameterized queries ({' / '.join(sql_guards[:2])})."
178
+
179
+ if not bypass_guards:
180
+ bypass_rule = "4. **No Security Bypasses:** Never insert `# nosec`, `verify=False`, `@csrf_exempt`, or `bypass_auth=True`."
181
+ else:
182
+ bypass_rule = f"4. **No Security Bypasses:** Never insert `# nosec`, `verify=False`, or {', '.join(bypass_guards[:2])}."
183
+
184
+ lines.extend([
185
+ "### Universal Guardrails",
186
+ sql_rule,
187
+ "2. **Tenant Isolation:** Enforce tenant/owner filtering on every database lookup (never query without `tenant_id` or owner scope).",
188
+ "3. **Ponytail Bound Rule:** Remediation patches must be minimal (<= 35 additions, <= 25 deletions).",
189
+ bypass_rule,
190
+ ""
191
+ ])
192
+
193
+ # Golden Fix Recipes (Top 2 to preserve tokens)
194
+ if golden_recipes:
195
+ lines.append("### Verified Golden Fix Patterns")
196
+ for rec in golden_recipes[:2]:
197
+ rule = rec.get("rule_id", "TG-SEC")
198
+ desc = rec.get("description", "Verified AST fix")
199
+ r_data = rec.get("recipe_data", {})
200
+ before = r_data.get("before_snippet", "")
201
+ after = r_data.get("after_snippet", "")
202
+ lines.append(f"- **{rule}** ({desc}):")
203
+ if before and after:
204
+ lines.append(f" - ❌ Avoid: `{before[:70]}`")
205
+ lines.append(f" - ✅ Prefer: `{after[:70]}`")
206
+ lines.append("")
207
+
208
+ # High-Risk Regression Watches
209
+ if regressions:
210
+ lines.append("### Active Regression Watches (Do Not Re-Introduce)")
211
+ for reg in regressions[:3]:
212
+ rule = reg.get("rule_id", "TG-REG")
213
+ files = ", ".join(reg.get("affected_files", [])[:2]) or "tracked files"
214
+ lines.append(f"- **{rule}**: Recurrence risk in `{files}`.")
215
+ lines.append("")
216
+
217
+ lines.append(END_MARKER)
218
+ return "\n".join(lines)
219
+
220
+
221
+ def inject_rules_into_file(file_path: Path, rules_block: str) -> bool:
222
+ """
223
+ Non-destructively inject or replace the TorusGuard block in a target rules file.
224
+ Creates parent directories if necessary.
225
+ """
226
+ file_path.parent.mkdir(parents=True, exist_ok=True)
227
+ existing_content = ""
228
+
229
+ if file_path.exists():
230
+ try:
231
+ existing_content = file_path.read_text(encoding="utf-8")
232
+ except OSError:
233
+ existing_content = ""
234
+
235
+ pattern = re.compile(
236
+ rf"{re.escape(START_MARKER)}.*?{re.escape(END_MARKER)}",
237
+ re.DOTALL
238
+ )
239
+
240
+ if pattern.search(existing_content):
241
+ updated_content = pattern.sub(rules_block, existing_content)
242
+ else:
243
+ if existing_content.strip():
244
+ updated_content = existing_content.rstrip() + "\n\n" + rules_block + "\n"
245
+ else:
246
+ updated_content = rules_block + "\n"
247
+
248
+ try:
249
+ file_path.write_text(updated_content, encoding="utf-8")
250
+ return True
251
+ except OSError as err:
252
+ print(f"Error writing rules to {file_path}: {err}", file=sys.stderr)
253
+ return False
254
+
255
+
256
+ def sync_rules(
257
+ target_format: str = "all",
258
+ root_dir: str | Path | None = None
259
+ ) -> dict[str, Any]:
260
+ """
261
+ Synchronize project security invariants into target IDE rules files.
262
+ Supported targets: 'cursor', 'claude', 'agent', 'windsurf', 'all'.
263
+ """
264
+ root = Path(root_dir or find_project_root()).resolve()
265
+ sec_data = load_security_context(root)
266
+ rules_block = format_dense_security_rules(sec_data)
267
+
268
+ target_files: dict[str, Path] = {
269
+ "cursor": root / ".cursorrules",
270
+ "claude": root / "CLAUDE.md",
271
+ "agent": root / ".agent" / "rules" / "torusguard.md",
272
+ "windsurf": root / ".windsurfrules"
273
+ }
274
+
275
+ synced_targets: list[str] = []
276
+ formats_to_sync = list(target_files.keys()) if target_format == "all" else [target_format]
277
+
278
+ for fmt in formats_to_sync:
279
+ if fmt in target_files:
280
+ target_path = target_files[fmt]
281
+ success = inject_rules_into_file(target_path, rules_block)
282
+ if success:
283
+ synced_targets.append(str(target_path.relative_to(root)))
284
+
285
+ # Estimate token overhead (approx 4 chars/token)
286
+ token_est = max(1, len(rules_block) // 4)
287
+
288
+ return {
289
+ "status": "success" if synced_targets else "skipped",
290
+ "synced_files": synced_targets,
291
+ "token_estimate": token_est,
292
+ "within_budget": token_est <= MAX_PROMPT_TOKENS
293
+ }
294
+
295
+
296
+ def main():
297
+ parser = argparse.ArgumentParser(description="TorusGuard AI IDE Rules Auto-Sync Engine")
298
+ parser.add_argument(
299
+ "--format",
300
+ choices=["all", "cursor", "claude", "agent", "windsurf"],
301
+ default="all",
302
+ help="Target AI IDE rules format to sync"
303
+ )
304
+ parser.add_argument("--root", help="Project root override")
305
+ parser.add_argument("--json", action="store_true", help="Output raw JSON")
306
+
307
+ args = parser.parse_args()
308
+ root = Path(args.root).resolve() if args.root else None
309
+
310
+ result = sync_rules(target_format=args.format, root_dir=root)
311
+ if args.json:
312
+ print(json.dumps(result, indent=2))
313
+ else:
314
+ print(f"[SUCCESS] Synced TorusGuard security invariants to {len(result['synced_files'])} file(s):")
315
+ for f in result["synced_files"]:
316
+ print(f" - {f}")
317
+ print(f"Token estimate: ~{result['token_estimate']} tokens (budget <= {MAX_PROMPT_TOKENS})")
318
+
319
+
320
+ if __name__ == "__main__":
321
+ main()