@trac3er/oh-my-god 1.0.2

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 (229) hide show
  1. package/.claude-plugin/marketplace.json +36 -0
  2. package/.claude-plugin/plugin.json +23 -0
  3. package/.claude-plugin/scripts/install.sh +49 -0
  4. package/.claude-plugin/scripts/uninstall.sh +80 -0
  5. package/.claude-plugin/scripts/update.sh +84 -0
  6. package/.mcp.json +20 -0
  7. package/LICENSE +21 -0
  8. package/OMG-setup.sh +1093 -0
  9. package/README.md +335 -0
  10. package/THIRD_PARTY_NOTICES.md +24 -0
  11. package/UPSTREAM_DIFF.md +20 -0
  12. package/agents/__init__.py +1 -0
  13. package/agents/_model_roles.yaml +26 -0
  14. package/agents/designer.md +67 -0
  15. package/agents/explore.md +60 -0
  16. package/agents/model_roles.py +196 -0
  17. package/agents/omg-api-builder.md +23 -0
  18. package/agents/omg-architect-mode.md +43 -0
  19. package/agents/omg-architect.md +13 -0
  20. package/agents/omg-backend-engineer.md +43 -0
  21. package/agents/omg-critic.md +16 -0
  22. package/agents/omg-database-engineer.md +43 -0
  23. package/agents/omg-escalation-router.md +17 -0
  24. package/agents/omg-executor.md +12 -0
  25. package/agents/omg-frontend-designer.md +42 -0
  26. package/agents/omg-implement-mode.md +50 -0
  27. package/agents/omg-infra-engineer.md +43 -0
  28. package/agents/omg-qa-tester.md +16 -0
  29. package/agents/omg-research-mode.md +43 -0
  30. package/agents/omg-security-auditor.md +43 -0
  31. package/agents/omg-testing-engineer.md +43 -0
  32. package/agents/plan.md +80 -0
  33. package/agents/quick_task.md +64 -0
  34. package/agents/reviewer.md +83 -0
  35. package/agents/task.md +71 -0
  36. package/commands/OMG:ccg.md +22 -0
  37. package/commands/OMG:compat.md +57 -0
  38. package/commands/OMG:crazy.md +125 -0
  39. package/commands/OMG:domain-init.md +11 -0
  40. package/commands/OMG:escalate.md +52 -0
  41. package/commands/OMG:health-check.md +45 -0
  42. package/commands/OMG:init.md +134 -0
  43. package/commands/OMG:mode.md +44 -0
  44. package/commands/OMG:project-init.md +11 -0
  45. package/commands/OMG:ralph-start.md +43 -0
  46. package/commands/OMG:ralph-stop.md +23 -0
  47. package/commands/OMG:teams.md +39 -0
  48. package/commands/ai-commit.md +113 -0
  49. package/commands/ccg.md +9 -0
  50. package/commands/create-agent.md +183 -0
  51. package/commands/omc-teams.md +9 -0
  52. package/commands/session-branch.md +85 -0
  53. package/commands/session-fork.md +53 -0
  54. package/commands/session-merge.md +134 -0
  55. package/commands/theme.md +44 -0
  56. package/config/lsp_languages.yaml +324 -0
  57. package/config/themes/catppuccin-frappe.yaml +14 -0
  58. package/config/themes/catppuccin-latte.yaml +14 -0
  59. package/config/themes/catppuccin-macchiato.yaml +14 -0
  60. package/config/themes/catppuccin-mocha.yaml +14 -0
  61. package/config/themes/dracula.yaml +14 -0
  62. package/config/themes/gruvbox-dark.yaml +14 -0
  63. package/config/themes/nord.yaml +14 -0
  64. package/config/themes/one-dark.yaml +14 -0
  65. package/config/themes/solarized-dark.yaml +14 -0
  66. package/config/themes/tokyo-night.yaml +14 -0
  67. package/control_plane/__init__.py +2 -0
  68. package/control_plane/openapi.yaml +109 -0
  69. package/control_plane/server.py +107 -0
  70. package/control_plane/service.py +148 -0
  71. package/crates/omg-natives/Cargo.toml +17 -0
  72. package/crates/omg-natives/src/clipboard.rs +5 -0
  73. package/crates/omg-natives/src/glob.rs +15 -0
  74. package/crates/omg-natives/src/grep.rs +15 -0
  75. package/crates/omg-natives/src/highlight.rs +15 -0
  76. package/crates/omg-natives/src/html.rs +14 -0
  77. package/crates/omg-natives/src/image.rs +5 -0
  78. package/crates/omg-natives/src/keys.rs +5 -0
  79. package/crates/omg-natives/src/lib.rs +36 -0
  80. package/crates/omg-natives/src/prof.rs +5 -0
  81. package/crates/omg-natives/src/ps.rs +5 -0
  82. package/crates/omg-natives/src/shell.rs +5 -0
  83. package/crates/omg-natives/src/task.rs +5 -0
  84. package/crates/omg-natives/src/text.rs +14 -0
  85. package/hooks/_agent_registry.py +421 -0
  86. package/hooks/_budget.py +31 -0
  87. package/hooks/_common.py +476 -0
  88. package/hooks/_learnings.py +126 -0
  89. package/hooks/_memory.py +103 -0
  90. package/hooks/circuit-breaker.py +270 -0
  91. package/hooks/config-guard.py +163 -0
  92. package/hooks/context_pressure.py +53 -0
  93. package/hooks/credential_store.py +801 -0
  94. package/hooks/fetch-rate-limits.py +212 -0
  95. package/hooks/firewall.py +48 -0
  96. package/hooks/hashline-formatter-bridge.py +224 -0
  97. package/hooks/hashline-injector.py +273 -0
  98. package/hooks/hashline-validator.py +216 -0
  99. package/hooks/idle-detector.py +95 -0
  100. package/hooks/intentgate-keyword-detector.py +188 -0
  101. package/hooks/magic-keyword-router.py +195 -0
  102. package/hooks/policy_engine.py +310 -0
  103. package/hooks/post-tool-failure.py +19 -0
  104. package/hooks/post-write.py +199 -0
  105. package/hooks/pre-compact.py +204 -0
  106. package/hooks/pre-tool-inject.py +98 -0
  107. package/hooks/prompt-enhancer.py +672 -0
  108. package/hooks/quality-runner.py +191 -0
  109. package/hooks/secret-guard.py +47 -0
  110. package/hooks/session-end-capture.py +137 -0
  111. package/hooks/session-start.py +275 -0
  112. package/hooks/shadow_manager.py +297 -0
  113. package/hooks/state_migration.py +209 -0
  114. package/hooks/stop-gate.py +7 -0
  115. package/hooks/stop_dispatcher.py +929 -0
  116. package/hooks/test-validator.py +138 -0
  117. package/hooks/todo-state-tracker.py +114 -0
  118. package/hooks/tool-ledger.py +126 -0
  119. package/hooks/trust_review.py +524 -0
  120. package/install.sh +9 -0
  121. package/omg_natives/__init__.py +186 -0
  122. package/omg_natives/_bindings.py +165 -0
  123. package/omg_natives/clipboard.py +36 -0
  124. package/omg_natives/glob.py +42 -0
  125. package/omg_natives/grep.py +61 -0
  126. package/omg_natives/highlight.py +54 -0
  127. package/omg_natives/html.py +157 -0
  128. package/omg_natives/image.py +51 -0
  129. package/omg_natives/keys.py +46 -0
  130. package/omg_natives/prof.py +39 -0
  131. package/omg_natives/ps.py +93 -0
  132. package/omg_natives/shell.py +58 -0
  133. package/omg_natives/task.py +41 -0
  134. package/omg_natives/text.py +50 -0
  135. package/package.json +26 -0
  136. package/plugins/README.md +82 -0
  137. package/plugins/advanced/commands/OMG:code-review.md +114 -0
  138. package/plugins/advanced/commands/OMG:deep-plan.md +221 -0
  139. package/plugins/advanced/commands/OMG:handoff.md +115 -0
  140. package/plugins/advanced/commands/OMG:learn.md +110 -0
  141. package/plugins/advanced/commands/OMG:maintainer.md +31 -0
  142. package/plugins/advanced/commands/OMG:ralph-start.md +43 -0
  143. package/plugins/advanced/commands/OMG:ralph-stop.md +23 -0
  144. package/plugins/advanced/commands/OMG:security-review.md +119 -0
  145. package/plugins/advanced/commands/OMG:sequential-thinking.md +20 -0
  146. package/plugins/advanced/commands/OMG:ship.md +46 -0
  147. package/plugins/advanced/plugin.json +96 -0
  148. package/plugins/core/plugin.json +82 -0
  149. package/pytest.ini +5 -0
  150. package/registry/__init__.py +1 -0
  151. package/registry/verify_artifact.py +90 -0
  152. package/rules/contextual/architect-mode.md +9 -0
  153. package/rules/contextual/big-picture.md +20 -0
  154. package/rules/contextual/code-hygiene.md +26 -0
  155. package/rules/contextual/context-management.md +19 -0
  156. package/rules/contextual/context-minimization.md +32 -0
  157. package/rules/contextual/ddd-sdd.md +28 -0
  158. package/rules/contextual/dependency-safety.md +16 -0
  159. package/rules/contextual/doc-check.md +13 -0
  160. package/rules/contextual/implement-mode.md +9 -0
  161. package/rules/contextual/infra-safety.md +14 -0
  162. package/rules/contextual/outside-in.md +13 -0
  163. package/rules/contextual/persistent-mode.md +24 -0
  164. package/rules/contextual/research-mode.md +9 -0
  165. package/rules/contextual/security-domains.md +25 -0
  166. package/rules/contextual/vision-detection.md +27 -0
  167. package/rules/contextual/web-search.md +25 -0
  168. package/rules/contextual/write-verify.md +23 -0
  169. package/rules/core/00-truth.md +20 -0
  170. package/rules/core/01-surgical.md +19 -0
  171. package/rules/core/02-circuit-breaker.md +22 -0
  172. package/rules/core/03-ensemble.md +28 -0
  173. package/rules/core/04-testing.md +30 -0
  174. package/runtime/__init__.py +32 -0
  175. package/runtime/adapters/__init__.py +13 -0
  176. package/runtime/adapters/claude.py +60 -0
  177. package/runtime/adapters/gpt.py +53 -0
  178. package/runtime/adapters/local.py +53 -0
  179. package/runtime/business_workflow.py +220 -0
  180. package/runtime/compat.py +1299 -0
  181. package/runtime/custom_agent_loader.py +366 -0
  182. package/runtime/dispatcher.py +47 -0
  183. package/runtime/ecosystem.py +371 -0
  184. package/runtime/legacy_compat.py +7 -0
  185. package/runtime/omc_compat.py +7 -0
  186. package/runtime/omc_contract_snapshot.json +916 -0
  187. package/runtime/omg_compat_contract_snapshot.json +916 -0
  188. package/runtime/subagent_dispatcher.py +362 -0
  189. package/runtime/team_router.py +838 -0
  190. package/scripts/check-omc-contract-snapshot.py +12 -0
  191. package/scripts/check-omg-compat-contract-snapshot.py +137 -0
  192. package/scripts/check-omg-standalone-clean.py +102 -0
  193. package/scripts/legacy_to_omg_migrate.py +29 -0
  194. package/scripts/migrate-omc.py +464 -0
  195. package/scripts/omc_to_omg_migrate.py +12 -0
  196. package/scripts/omg.py +493 -0
  197. package/scripts/settings-merge.py +224 -0
  198. package/scripts/verify-no-omc.sh +5 -0
  199. package/scripts/verify-standalone.sh +21 -0
  200. package/templates/idea.yml +30 -0
  201. package/templates/policy.yaml +15 -0
  202. package/templates/profile.yaml +25 -0
  203. package/templates/runtime.yaml +12 -0
  204. package/templates/working-memory.md +17 -0
  205. package/tools/__init__.py +2 -0
  206. package/tools/browser_consent.py +289 -0
  207. package/tools/browser_stealth.py +481 -0
  208. package/tools/browser_tool.py +448 -0
  209. package/tools/changelog_generator.py +268 -0
  210. package/tools/commit_splitter.py +361 -0
  211. package/tools/config_discovery.py +151 -0
  212. package/tools/config_merger.py +449 -0
  213. package/tools/git_inspector.py +298 -0
  214. package/tools/lsp_client.py +275 -0
  215. package/tools/lsp_discovery.py +231 -0
  216. package/tools/lsp_operations.py +392 -0
  217. package/tools/python_repl.py +656 -0
  218. package/tools/python_sandbox.py +609 -0
  219. package/tools/search_providers/__init__.py +77 -0
  220. package/tools/search_providers/brave.py +115 -0
  221. package/tools/search_providers/exa.py +116 -0
  222. package/tools/search_providers/jina.py +104 -0
  223. package/tools/search_providers/perplexity.py +139 -0
  224. package/tools/search_providers/synthetic.py +74 -0
  225. package/tools/session_snapshot.py +736 -0
  226. package/tools/ssh_manager.py +912 -0
  227. package/tools/theme_engine.py +294 -0
  228. package/tools/theme_selector.py +137 -0
  229. package/tools/web_search.py +622 -0
@@ -0,0 +1,524 @@
1
+ #!/usr/bin/env python3
2
+ """OMG v1 Trust Review
3
+
4
+ Analyzes high-risk configuration changes (hooks/MCP/env/permissions) and emits
5
+ structured trust review artifacts. Also integrates with config discovery to
6
+ validate and approve discovered AI tool configurations.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import json
12
+ import os
13
+ from datetime import datetime, timezone
14
+ from typing import Any, Dict, List
15
+
16
+ import re
17
+ import sys
18
+
19
+ DANGEROUS_IN_ALLOW = [
20
+ "Bash(rm:*)", "Bash(sudo:*)", "Bash(curl:*)", "Bash(wget:*)",
21
+ "Bash(ssh:*)", "Bash(nc:*)", "Bash(ncat:*)",
22
+ ]
23
+
24
+
25
+ def _safe_dict(value: Any) -> dict[str, Any]:
26
+ return value if isinstance(value, dict) else {}
27
+
28
+
29
+ def _safe_list(value: Any) -> list[Any]:
30
+ return value if isinstance(value, list) else []
31
+
32
+
33
+ def _collect_mcp_changes(old_cfg: dict[str, Any], new_cfg: dict[str, Any]) -> list[dict[str, Any]]:
34
+ old_servers = _safe_dict(old_cfg.get("mcpServers"))
35
+ new_servers = _safe_dict(new_cfg.get("mcpServers"))
36
+ changes: list[dict[str, Any]] = []
37
+
38
+ old_keys = set(old_servers.keys())
39
+ new_keys = set(new_servers.keys())
40
+
41
+ for name in sorted(new_keys - old_keys):
42
+ changes.append({"type": "added", "server": name, "new": new_servers.get(name)})
43
+ for name in sorted(old_keys - new_keys):
44
+ changes.append({"type": "removed", "server": name, "old": old_servers.get(name)})
45
+ for name in sorted(old_keys & new_keys):
46
+ if old_servers.get(name) != new_servers.get(name):
47
+ changes.append(
48
+ {
49
+ "type": "modified",
50
+ "server": name,
51
+ "old": old_servers.get(name),
52
+ "new": new_servers.get(name),
53
+ }
54
+ )
55
+ return changes
56
+
57
+
58
+ def _count_hooks(cfg: dict[str, Any]) -> int:
59
+ hooks = _safe_dict(cfg.get("hooks"))
60
+ total = 0
61
+ for event_entries in hooks.values():
62
+ if not isinstance(event_entries, list):
63
+ continue
64
+ for entry in event_entries:
65
+ if isinstance(entry, dict):
66
+ nested = _safe_list(entry.get("hooks"))
67
+ total += len(nested) if nested else 1
68
+ else:
69
+ total += 1
70
+ return total
71
+
72
+
73
+ def _collect_hook_changes(old_cfg: dict[str, Any], new_cfg: dict[str, Any]) -> dict[str, Any]:
74
+ old_hooks = _safe_dict(old_cfg.get("hooks"))
75
+ new_hooks = _safe_dict(new_cfg.get("hooks"))
76
+
77
+ old_events = set(old_hooks.keys())
78
+ new_events = set(new_hooks.keys())
79
+ removed_events = sorted(old_events - new_events)
80
+ added_events = sorted(new_events - old_events)
81
+ modified_events = sorted(
82
+ event for event in (old_events & new_events) if old_hooks.get(event) != new_hooks.get(event)
83
+ )
84
+
85
+ return {
86
+ "old_hook_count": _count_hooks(old_cfg),
87
+ "new_hook_count": _count_hooks(new_cfg),
88
+ "removed_events": removed_events,
89
+ "added_events": added_events,
90
+ "modified_events": modified_events,
91
+ }
92
+
93
+
94
+ def _collect_env_changes(old_cfg: dict[str, Any], new_cfg: dict[str, Any]) -> list[dict[str, Any]]:
95
+ old_env = _safe_dict(old_cfg.get("env"))
96
+ new_env = _safe_dict(new_cfg.get("env"))
97
+
98
+ changes: list[dict[str, Any]] = []
99
+ keys = sorted(set(old_env.keys()) | set(new_env.keys()))
100
+ for key in keys:
101
+ old = old_env.get(key)
102
+ new = new_env.get(key)
103
+ if old == new:
104
+ continue
105
+ changes.append({"key": key, "old": old, "new": new})
106
+ return changes
107
+
108
+
109
+ def _risk_from_permissions(old_cfg: dict[str, Any], new_cfg: dict[str, Any]) -> tuple[int, list[str], list[str]]:
110
+ old_perms = _safe_dict(old_cfg.get("permissions"))
111
+ new_perms = _safe_dict(new_cfg.get("permissions"))
112
+
113
+ old_allow = set(_safe_list(old_perms.get("allow")))
114
+ new_allow = set(_safe_list(new_perms.get("allow")))
115
+ added_allow = sorted(new_allow - old_allow)
116
+
117
+ score = 0
118
+ reasons: list[str] = []
119
+ controls: list[str] = []
120
+
121
+ for dangerous in DANGEROUS_IN_ALLOW:
122
+ if dangerous in added_allow:
123
+ score += 80
124
+ reasons.append(f"Dangerous allow pattern added: {dangerous}")
125
+ controls.extend(["manual-trust-review", "deny-by-default"])
126
+
127
+ return score, reasons, controls
128
+
129
+
130
+ def _risk_from_hooks(hook_changes: dict[str, Any]) -> tuple[int, list[str], list[str]]:
131
+ score = 0
132
+ reasons: list[str] = []
133
+ controls: list[str] = []
134
+
135
+ old_count = int(hook_changes.get("old_hook_count", 0))
136
+ new_count = int(hook_changes.get("new_hook_count", 0))
137
+ removed_events = hook_changes.get("removed_events", [])
138
+ modified_events = hook_changes.get("modified_events", [])
139
+
140
+ if old_count and new_count < max(1, old_count - 2):
141
+ score += 35
142
+ reasons.append(f"Hook count reduced significantly ({old_count} -> {new_count})")
143
+ controls.append("require-hook-audit")
144
+
145
+ if removed_events:
146
+ score += 25
147
+ reasons.append(f"Hook events removed: {', '.join(removed_events)}")
148
+ controls.append("event-removal-review")
149
+
150
+ if modified_events:
151
+ score += 20
152
+ reasons.append(f"Hook definitions modified: {', '.join(modified_events)}")
153
+ controls.append("hook-diff-review")
154
+
155
+ return score, reasons, controls
156
+
157
+
158
+ def _risk_from_mcp(mcp_changes: list[dict[str, Any]]) -> tuple[int, list[str], list[str]]:
159
+ score = 0
160
+ reasons: list[str] = []
161
+ controls: list[str] = []
162
+
163
+ for change in mcp_changes:
164
+ ctype = change.get("type")
165
+ name = change.get("server")
166
+ if ctype == "added":
167
+ score += 30
168
+ reasons.append(f"New MCP server added: {name}")
169
+ controls.append("mcp-endpoint-review")
170
+ elif ctype == "modified":
171
+ score += 35
172
+ reasons.append(f"MCP server modified: {name}")
173
+ controls.append("mcp-diff-review")
174
+ elif ctype == "removed":
175
+ score += 10
176
+ reasons.append(f"MCP server removed: {name}")
177
+
178
+ return score, reasons, controls
179
+
180
+
181
+ def _risk_from_env(env_changes: list[dict[str, Any]]) -> tuple[int, list[str], list[str]]:
182
+ score = 0
183
+ reasons: list[str] = []
184
+ controls: list[str] = []
185
+
186
+ for change in env_changes:
187
+ key = str(change.get("key", ""))
188
+ if any(token in key.upper() for token in ["KEY", "TOKEN", "SECRET", "PASSWORD", "CREDENTIAL"]):
189
+ score += 20
190
+ reasons.append(f"Sensitive environment key modified: {key}")
191
+ controls.append("secret-env-review")
192
+ else:
193
+ score += 5
194
+ reasons.append(f"Environment key modified: {key}")
195
+
196
+ return score, reasons, controls
197
+
198
+
199
+ def score_to_verdict(score: int) -> tuple[str, str]:
200
+ if score >= 80:
201
+ return "deny", "critical"
202
+ if score >= 45:
203
+ return "ask", "high"
204
+ if score >= 20:
205
+ return "ask", "med"
206
+ return "allow", "low"
207
+
208
+
209
+ def review_config_change(
210
+ file_path: str,
211
+ old_config: dict[str, Any] | None,
212
+ new_config: dict[str, Any] | None,
213
+ ) -> dict[str, Any]:
214
+ old_cfg = old_config or {}
215
+ new_cfg = new_config or {}
216
+
217
+ mcp_changes = _collect_mcp_changes(old_cfg, new_cfg)
218
+ hook_changes = _collect_hook_changes(old_cfg, new_cfg)
219
+ env_changes = _collect_env_changes(old_cfg, new_cfg)
220
+
221
+ risk_score = 0
222
+ reasons: list[str] = []
223
+ controls: list[str] = []
224
+
225
+ for score, r, c in [
226
+ _risk_from_permissions(old_cfg, new_cfg),
227
+ _risk_from_hooks(hook_changes),
228
+ _risk_from_mcp(mcp_changes),
229
+ _risk_from_env(env_changes),
230
+ ]:
231
+ risk_score += score
232
+ reasons.extend(r)
233
+ controls.extend(c)
234
+
235
+ verdict, risk_level = score_to_verdict(risk_score)
236
+
237
+ return {
238
+ "ts": datetime.now(timezone.utc).isoformat(),
239
+ "changed_files": [file_path] if file_path else [],
240
+ "mcp_changes": mcp_changes,
241
+ "hook_changes": hook_changes,
242
+ "env_changes": env_changes,
243
+ "risk_score": risk_score,
244
+ "risk_level": risk_level,
245
+ "verdict": verdict,
246
+ "reasons": reasons,
247
+ "controls": sorted(set(controls)),
248
+ }
249
+
250
+
251
+ def format_review_summary(review: dict[str, Any]) -> str:
252
+ verdict = review.get("verdict", "allow")
253
+ score = review.get("risk_score", 0)
254
+ risk_level = review.get("risk_level", "low")
255
+ reasons = review.get("reasons", []) or []
256
+
257
+ lines = [f"Trust Review: verdict={verdict} risk={risk_level} score={score}"]
258
+ if reasons:
259
+ lines.extend([f" - {reason}" for reason in reasons[:6]])
260
+ return "\n".join(lines)
261
+
262
+
263
+ def write_trust_manifest(project_dir: str, review: dict[str, Any]) -> str:
264
+ trust_dir = os.path.join(project_dir, ".omg", "trust")
265
+ os.makedirs(trust_dir, exist_ok=True)
266
+ manifest_path = os.path.join(trust_dir, "manifest.lock.json")
267
+
268
+ payload = {
269
+ "version": "omg-v1",
270
+ "updated_at": datetime.now(timezone.utc).isoformat(),
271
+ "last_review": review,
272
+ }
273
+ digest_input = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
274
+ payload["signature"] = hashlib.sha256(digest_input).hexdigest()
275
+
276
+ with open(manifest_path, "w", encoding="utf-8") as f:
277
+ json.dump(payload, f, indent=2, ensure_ascii=True)
278
+
279
+ return manifest_path
280
+
281
+
282
+ def _load_json_file(path: str) -> dict[str, Any]:
283
+ if not path or not os.path.exists(path):
284
+ return {}
285
+ try:
286
+ with open(path, "r", encoding="utf-8") as f:
287
+ data = json.load(f)
288
+ return data if isinstance(data, dict) else {}
289
+ except Exception:
290
+ return {}
291
+
292
+
293
+
294
+ # === Config Discovery Integration ============================================
295
+
296
+ # Suspicious code patterns that should block config import
297
+ _DANGEROUS_PATTERNS = [
298
+ re.compile(r'\beval\s*\('),
299
+ re.compile(r'\bexec\s*\('),
300
+ re.compile(r'\b__import__\s*\('),
301
+ re.compile(r'\bsubprocess\b'),
302
+ re.compile(r'\bos\.system\s*\('),
303
+ ]
304
+
305
+ # Credential patterns that produce warnings (not blocking)
306
+ _CREDENTIAL_PATTERNS = [
307
+ re.compile(r'\bpassword\b', re.IGNORECASE),
308
+ re.compile(r'\bsecret\b', re.IGNORECASE),
309
+ re.compile(r'\bapi_key\b', re.IGNORECASE),
310
+ re.compile(r'\btoken\b', re.IGNORECASE),
311
+ ]
312
+
313
+ # Max config size before warning (100KB)
314
+ _MAX_CONFIG_SIZE_BYTES = 100 * 1024
315
+
316
+
317
+ def _validate_config_security(config_path: str, content: str) -> Dict[str, Any]:
318
+ """Validate a config file's content for security issues.
319
+
320
+ Returns:
321
+ {"safe": bool, "issues": list[str], "warnings": list[str]}
322
+ """
323
+ issues: List[str] = []
324
+ warnings: List[str] = []
325
+
326
+ # Check for dangerous code patterns
327
+ for pattern in _DANGEROUS_PATTERNS:
328
+ if pattern.search(content):
329
+ issues.append(f"Dangerous pattern '{pattern.pattern}' found in {config_path}")
330
+
331
+ # Check for credential patterns (warn only)
332
+ for pattern in _CREDENTIAL_PATTERNS:
333
+ if pattern.search(content):
334
+ warnings.append(f"Credential pattern '{pattern.pattern}' found in {config_path}")
335
+
336
+ # Check file size
337
+ try:
338
+ size = os.path.getsize(config_path) if os.path.isfile(config_path) else 0
339
+ if size > _MAX_CONFIG_SIZE_BYTES:
340
+ warnings.append(f"Config file is large ({size} bytes): {config_path}")
341
+ except OSError:
342
+ pass
343
+
344
+ return {
345
+ "safe": len(issues) == 0,
346
+ "issues": issues,
347
+ "warnings": warnings,
348
+ }
349
+
350
+
351
+ def _log_config_import(config_path: str, tool: str, approved: bool, project_dir: str = ".") -> None:
352
+ """Log a config import decision to .omg/trust/config_imports.json.
353
+
354
+ Uses atomic_json_write() from _common for safe writes.
355
+ """
356
+ # Lazy import _common utilities
357
+ hooks_dir = os.path.dirname(os.path.abspath(__file__))
358
+ if hooks_dir not in sys.path:
359
+ sys.path.insert(0, hooks_dir)
360
+ try:
361
+ from _common import atomic_json_write # type: ignore[import-untyped]
362
+ except ImportError:
363
+ return # silently fail if _common unavailable
364
+
365
+ # Compute SHA-256 hash of the config file
366
+ sha256_hash = ""
367
+ try:
368
+ abs_path = os.path.join(project_dir, config_path) if not os.path.isabs(config_path) else config_path
369
+ if os.path.isfile(abs_path):
370
+ with open(abs_path, "rb") as f:
371
+ sha256_hash = hashlib.sha256(f.read()).hexdigest()
372
+ except (OSError, IOError):
373
+ sha256_hash = "unreadable"
374
+
375
+ # Build log entry
376
+ entry = {
377
+ "timestamp": datetime.now(timezone.utc).isoformat(),
378
+ "config_path": config_path,
379
+ "tool": tool,
380
+ "approved": approved,
381
+ "sha256_hash": sha256_hash,
382
+ }
383
+
384
+ # Load existing log, append, write back
385
+ log_path = os.path.join(project_dir, ".omg", "trust", "config_imports.json")
386
+ existing: List[Dict[str, Any]] = []
387
+ try:
388
+ if os.path.exists(log_path):
389
+ with open(log_path, "r", encoding="utf-8") as f:
390
+ data = json.load(f)
391
+ if isinstance(data, list):
392
+ existing = data
393
+ except (json.JSONDecodeError, OSError):
394
+ existing = []
395
+
396
+ existing.append(entry)
397
+ atomic_json_write(log_path, existing)
398
+
399
+
400
+ def review_discovered_configs(project_dir: str = ".") -> Dict[str, Any]:
401
+ """Scan, validate, and review discovered AI tool configurations.
402
+
403
+ Feature flag: OMG_CONFIG_DISCOVERY_ENABLED (default: False)
404
+
405
+ Returns:
406
+ {
407
+ "skipped": bool (if feature disabled),
408
+ "reason": str (if skipped),
409
+ "approved": list,
410
+ "rejected": list,
411
+ "warnings": list,
412
+ "pending": list,
413
+ }
414
+ """
415
+ # Check feature flag via lazy import
416
+ hooks_dir = os.path.dirname(os.path.abspath(__file__))
417
+ if hooks_dir not in sys.path:
418
+ sys.path.insert(0, hooks_dir)
419
+ try:
420
+ from _common import get_feature_flag # type: ignore[import-untyped]
421
+ enabled = get_feature_flag("CONFIG_DISCOVERY", default=False)
422
+ except ImportError:
423
+ enabled = os.getenv("OMG_CONFIG_DISCOVERY_ENABLED", "false").lower() in ("1", "true", "yes")
424
+
425
+ if not enabled:
426
+ return {"skipped": True, "reason": "feature disabled"}
427
+
428
+ # Lazy import config discovery from tools/
429
+ tools_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "tools")
430
+ tools_dir = os.path.normpath(tools_dir)
431
+ if tools_dir not in sys.path:
432
+ sys.path.insert(0, tools_dir)
433
+ try:
434
+ from config_discovery import discover_configs # type: ignore[import-untyped]
435
+ except ImportError:
436
+ return {
437
+ "skipped": True,
438
+ "reason": "config_discovery module not available",
439
+ }
440
+
441
+ # Run discovery
442
+ discovery_result = discover_configs(project_dir)
443
+ discovered = discovery_result.get("discovered", [])
444
+
445
+ approved: List[Dict[str, Any]] = []
446
+ rejected: List[Dict[str, Any]] = []
447
+ warnings: List[str] = []
448
+ pending: List[Dict[str, Any]] = []
449
+
450
+ for config in discovered:
451
+ tool = config.get("tool", "unknown")
452
+ paths = config.get("paths", [])
453
+ readable = config.get("readable", False)
454
+ size_bytes = config.get("size_bytes", 0)
455
+
456
+ if not paths:
457
+ continue
458
+
459
+ # Use the first path for validation
460
+ rel_path = paths[0]
461
+ abs_path = os.path.join(project_dir, rel_path)
462
+
463
+ # Read content for security validation
464
+ content = ""
465
+ if readable and os.path.isfile(abs_path):
466
+ try:
467
+ with open(abs_path, "r", encoding="utf-8", errors="ignore") as f:
468
+ content = f.read(256 * 1024) # Read up to 256KB for analysis
469
+ except (OSError, IOError):
470
+ content = ""
471
+
472
+ # Validate security
473
+ validation = _validate_config_security(abs_path, content)
474
+ entry = {
475
+ "tool": tool,
476
+ "path": rel_path,
477
+ "format": config.get("format", "unknown"),
478
+ "size_bytes": size_bytes,
479
+ "validation": validation,
480
+ }
481
+
482
+ if not validation["safe"]:
483
+ entry["reason"] = "; ".join(validation["issues"])
484
+ rejected.append(entry)
485
+ _log_config_import(rel_path, tool, approved=False, project_dir=project_dir)
486
+ else:
487
+ if validation["warnings"]:
488
+ warnings.extend(validation["warnings"])
489
+ approved.append(entry)
490
+ _log_config_import(rel_path, tool, approved=True, project_dir=project_dir)
491
+
492
+ return {
493
+ "skipped": False,
494
+ "approved": approved,
495
+ "rejected": rejected,
496
+ "warnings": warnings,
497
+ "pending": pending,
498
+ "scan_dir": discovery_result.get("scan_dir", project_dir),
499
+ "timestamp": discovery_result.get("timestamp", datetime.now(timezone.utc).isoformat()),
500
+ }
501
+
502
+
503
+ def _main() -> int:
504
+ try:
505
+ payload = json.load(__import__("sys").stdin)
506
+ except Exception:
507
+ return 0
508
+
509
+ file_path = payload.get("file_path", "")
510
+ old_config = payload.get("old_config")
511
+ new_config = payload.get("new_config")
512
+
513
+ if isinstance(old_config, str):
514
+ old_config = _load_json_file(old_config)
515
+ if isinstance(new_config, str):
516
+ new_config = _load_json_file(new_config)
517
+
518
+ review = review_config_change(file_path, old_config, new_config)
519
+ __import__("json").dump(review, __import__("sys").stdout)
520
+ return 0
521
+
522
+
523
+ if __name__ == "__main__":
524
+ raise SystemExit(_main())
package/install.sh ADDED
@@ -0,0 +1,9 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ SETUP_SCRIPT="$SCRIPT_DIR/OMG-setup.sh"
6
+
7
+ echo "[DEPRECATED] install.sh is deprecated. Use OMG-setup.sh instead." >&2
8
+
9
+ exec bash "$SETUP_SCRIPT" "$@"