@trac3er/oh-my-god 2.0.0 → 2.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 (243) hide show
  1. package/.claude-plugin/marketplace.json +8 -8
  2. package/.claude-plugin/plugin.json +5 -4
  3. package/.claude-plugin/scripts/uninstall.sh +74 -3
  4. package/.claude-plugin/scripts/update.sh +78 -3
  5. package/.coveragerc +26 -0
  6. package/.mcp.json +4 -4
  7. package/CHANGELOG.md +14 -0
  8. package/CODE_OF_CONDUCT.md +27 -0
  9. package/CONTRIBUTING.md +62 -0
  10. package/OMG-setup.sh +1201 -355
  11. package/README.md +77 -56
  12. package/SECURITY.md +25 -0
  13. package/agents/__init__.py +1 -0
  14. package/agents/model_roles.py +196 -0
  15. package/agents/omg-architect-mode.md +3 -5
  16. package/agents/omg-backend-engineer.md +3 -5
  17. package/agents/omg-database-engineer.md +3 -5
  18. package/agents/omg-frontend-designer.md +4 -5
  19. package/agents/omg-implement-mode.md +4 -5
  20. package/agents/omg-infra-engineer.md +3 -5
  21. package/agents/omg-research-mode.md +4 -6
  22. package/agents/omg-security-auditor.md +3 -5
  23. package/agents/omg-testing-engineer.md +3 -5
  24. package/build/lib/yaml.py +321 -0
  25. package/commands/OMG:ai-commit.md +101 -14
  26. package/commands/OMG:arch.md +302 -19
  27. package/commands/OMG:ccg.md +12 -7
  28. package/commands/OMG:compat.md +25 -17
  29. package/commands/OMG:cost.md +173 -13
  30. package/commands/OMG:crazy.md +1 -1
  31. package/commands/OMG:create-agent.md +170 -20
  32. package/commands/OMG:deps.md +235 -17
  33. package/commands/OMG:domain-init.md +1 -1
  34. package/commands/OMG:escalate.md +41 -12
  35. package/commands/OMG:health-check.md +37 -13
  36. package/commands/OMG:init.md +122 -14
  37. package/commands/OMG:project-init.md +1 -1
  38. package/commands/OMG:session-branch.md +76 -9
  39. package/commands/OMG:session-fork.md +42 -5
  40. package/commands/OMG:session-merge.md +124 -8
  41. package/commands/OMG:setup.md +69 -12
  42. package/commands/OMG:stats.md +215 -14
  43. package/commands/OMG:teams.md +19 -10
  44. package/config/lsp_languages.yaml +8 -0
  45. package/hooks/__init__.py +0 -0
  46. package/hooks/_agent_registry.py +423 -0
  47. package/hooks/_analytics.py +291 -0
  48. package/hooks/_budget.py +31 -0
  49. package/hooks/_common.py +569 -0
  50. package/hooks/_compression_optimizer.py +119 -0
  51. package/hooks/_cost_ledger.py +176 -0
  52. package/hooks/_learnings.py +126 -0
  53. package/hooks/_memory.py +103 -0
  54. package/hooks/_protected_context.py +150 -0
  55. package/hooks/_token_counter.py +221 -0
  56. package/hooks/branch_manager.py +236 -0
  57. package/hooks/budget_governor.py +232 -0
  58. package/hooks/circuit-breaker.py +270 -0
  59. package/hooks/compression_feedback.py +254 -0
  60. package/hooks/config-guard.py +216 -0
  61. package/hooks/context_pressure.py +53 -0
  62. package/hooks/credential_store.py +1020 -0
  63. package/hooks/fetch-rate-limits.py +212 -0
  64. package/hooks/firewall.py +48 -0
  65. package/hooks/hashline-formatter-bridge.py +224 -0
  66. package/hooks/hashline-injector.py +273 -0
  67. package/hooks/hashline-validator.py +216 -0
  68. package/hooks/idle-detector.py +95 -0
  69. package/hooks/intentgate-keyword-detector.py +188 -0
  70. package/hooks/magic-keyword-router.py +195 -0
  71. package/hooks/policy_engine.py +505 -0
  72. package/hooks/post-tool-failure.py +19 -0
  73. package/hooks/post-write.py +219 -0
  74. package/hooks/post_write.py +46 -0
  75. package/hooks/pre-compact.py +398 -0
  76. package/hooks/pre-tool-inject.py +98 -0
  77. package/hooks/prompt-enhancer.py +672 -0
  78. package/hooks/quality-runner.py +191 -0
  79. package/hooks/query.py +512 -0
  80. package/hooks/secret-guard.py +61 -0
  81. package/hooks/secret_audit.py +144 -0
  82. package/hooks/session-end-capture.py +137 -0
  83. package/hooks/session-start.py +277 -0
  84. package/hooks/setup_wizard.py +582 -0
  85. package/hooks/shadow_manager.py +297 -0
  86. package/hooks/state_migration.py +225 -0
  87. package/hooks/stop-gate.py +7 -0
  88. package/hooks/stop_dispatcher.py +945 -0
  89. package/hooks/test-validator.py +361 -0
  90. package/hooks/test_generator_hook.py +123 -0
  91. package/hooks/todo-state-tracker.py +114 -0
  92. package/hooks/tool-ledger.py +149 -0
  93. package/hooks/trust_review.py +585 -0
  94. package/hud/omg-hud.mjs +31 -1
  95. package/lab/__init__.py +1 -0
  96. package/lab/pipeline.py +75 -0
  97. package/lab/policies.py +52 -0
  98. package/package.json +7 -18
  99. package/plugins/README.md +33 -61
  100. package/plugins/advanced/commands/OMG:deep-plan.md +3 -3
  101. package/plugins/advanced/commands/OMG:learn.md +1 -1
  102. package/plugins/advanced/commands/OMG:security-review.md +3 -3
  103. package/plugins/advanced/commands/OMG:ship.md +1 -1
  104. package/plugins/advanced/plugin.json +1 -1
  105. package/plugins/core/plugin.json +8 -3
  106. package/plugins/dephealth/__init__.py +0 -0
  107. package/plugins/dephealth/cve_scanner.py +188 -0
  108. package/plugins/dephealth/license_checker.py +135 -0
  109. package/plugins/dephealth/manifest_detector.py +423 -0
  110. package/plugins/dephealth/vuln_analyzer.py +169 -0
  111. package/plugins/testgen/__init__.py +0 -0
  112. package/plugins/testgen/codamosa_engine.py +402 -0
  113. package/plugins/testgen/edge_case_synthesizer.py +184 -0
  114. package/plugins/testgen/framework_detector.py +271 -0
  115. package/plugins/testgen/skeleton_generator.py +219 -0
  116. package/plugins/viz/__init__.py +0 -0
  117. package/plugins/viz/ast_parser.py +139 -0
  118. package/plugins/viz/diagram_generator.py +192 -0
  119. package/plugins/viz/graph_builder.py +444 -0
  120. package/plugins/viz/native_parsers.py +259 -0
  121. package/plugins/viz/regex_parser.py +112 -0
  122. package/pyproject.toml +81 -0
  123. package/rules/contextual/write-verify.md +2 -2
  124. package/rules/core/00-truth.md +1 -1
  125. package/rules/core/01-surgical.md +1 -1
  126. package/rules/core/02-circuit-breaker.md +2 -2
  127. package/rules/core/03-ensemble.md +3 -3
  128. package/rules/core/04-testing.md +3 -3
  129. package/runtime/__init__.py +32 -0
  130. package/runtime/adapters/__init__.py +13 -0
  131. package/runtime/adapters/claude.py +60 -0
  132. package/runtime/adapters/gpt.py +53 -0
  133. package/runtime/adapters/local.py +53 -0
  134. package/runtime/adoption.py +212 -0
  135. package/runtime/business_workflow.py +220 -0
  136. package/runtime/cli_provider.py +85 -0
  137. package/runtime/compat.py +1299 -0
  138. package/runtime/custom_agent_loader.py +366 -0
  139. package/runtime/dispatcher.py +47 -0
  140. package/runtime/ecosystem.py +371 -0
  141. package/runtime/legacy_compat.py +7 -0
  142. package/runtime/mcp_config_writers.py +115 -0
  143. package/runtime/mcp_lifecycle.py +153 -0
  144. package/runtime/mcp_memory_server.py +135 -0
  145. package/runtime/memory_parsers/__init__.py +0 -0
  146. package/runtime/memory_parsers/chatgpt_parser.py +257 -0
  147. package/runtime/memory_parsers/claude_import.py +107 -0
  148. package/runtime/memory_parsers/export.py +97 -0
  149. package/runtime/memory_parsers/gemini_import.py +91 -0
  150. package/runtime/memory_parsers/kimi_import.py +91 -0
  151. package/runtime/memory_store.py +215 -0
  152. package/runtime/omc_compat.py +7 -0
  153. package/runtime/providers/__init__.py +0 -0
  154. package/runtime/providers/codex_provider.py +112 -0
  155. package/runtime/providers/gemini_provider.py +128 -0
  156. package/runtime/providers/kimi_provider.py +151 -0
  157. package/runtime/providers/opencode_provider.py +144 -0
  158. package/runtime/subagent_dispatcher.py +362 -0
  159. package/runtime/team_router.py +1167 -0
  160. package/runtime/tmux_session_manager.py +169 -0
  161. package/scripts/check-omg-compat-contract-snapshot.py +137 -0
  162. package/scripts/check-omg-contract-snapshot.py +12 -0
  163. package/scripts/check-omg-public-ready.py +193 -0
  164. package/scripts/check-omg-standalone-clean.py +103 -0
  165. package/scripts/legacy_to_omg_migrate.py +29 -0
  166. package/scripts/migrate-legacy.py +464 -0
  167. package/scripts/omc_to_omg_migrate.py +12 -0
  168. package/scripts/omg.py +492 -0
  169. package/scripts/settings-merge.py +283 -0
  170. package/scripts/verify-standalone.sh +8 -4
  171. package/settings.json +126 -29
  172. package/templates/profile.yaml +1 -1
  173. package/tools/__init__.py +2 -0
  174. package/tools/browser_consent.py +289 -0
  175. package/tools/browser_stealth.py +481 -0
  176. package/tools/browser_tool.py +448 -0
  177. package/tools/changelog_generator.py +347 -0
  178. package/tools/commit_splitter.py +746 -0
  179. package/tools/config_discovery.py +151 -0
  180. package/tools/config_merger.py +449 -0
  181. package/tools/dashboard_generator.py +300 -0
  182. package/tools/git_inspector.py +298 -0
  183. package/tools/lsp_client.py +275 -0
  184. package/tools/lsp_discovery.py +231 -0
  185. package/tools/lsp_operations.py +392 -0
  186. package/tools/pr_generator.py +404 -0
  187. package/tools/python_repl.py +656 -0
  188. package/tools/python_sandbox.py +609 -0
  189. package/tools/search_providers/__init__.py +77 -0
  190. package/tools/search_providers/brave.py +115 -0
  191. package/tools/search_providers/exa.py +116 -0
  192. package/tools/search_providers/jina.py +104 -0
  193. package/tools/search_providers/perplexity.py +139 -0
  194. package/tools/search_providers/synthetic.py +74 -0
  195. package/tools/session_snapshot.py +736 -0
  196. package/tools/ssh_manager.py +912 -0
  197. package/tools/theme_engine.py +294 -0
  198. package/tools/theme_selector.py +137 -0
  199. package/tools/web_search.py +622 -0
  200. package/yaml.py +321 -0
  201. package/.claude-plugin/scripts/install.sh +0 -9
  202. package/bun.lock +0 -23
  203. package/bunfig.toml +0 -3
  204. package/hooks/_budget.ts +0 -1
  205. package/hooks/_common.ts +0 -63
  206. package/hooks/circuit-breaker.ts +0 -101
  207. package/hooks/config-guard.ts +0 -4
  208. package/hooks/firewall.ts +0 -20
  209. package/hooks/policy_engine.ts +0 -156
  210. package/hooks/post-tool-failure.ts +0 -22
  211. package/hooks/post-write.ts +0 -4
  212. package/hooks/pre-tool-inject.ts +0 -4
  213. package/hooks/prompt-enhancer.ts +0 -46
  214. package/hooks/quality-runner.ts +0 -24
  215. package/hooks/secret-guard.ts +0 -4
  216. package/hooks/session-end-capture.ts +0 -19
  217. package/hooks/session-start.ts +0 -19
  218. package/hooks/shadow_manager.ts +0 -81
  219. package/hooks/stop-gate.ts +0 -22
  220. package/hooks/stop_dispatcher.ts +0 -147
  221. package/hooks/test-generator-hook.ts +0 -4
  222. package/hooks/tool-ledger.ts +0 -27
  223. package/hooks/trust_review.ts +0 -175
  224. package/lab/pipeline.ts +0 -75
  225. package/lab/policies.ts +0 -68
  226. package/runtime/common.ts +0 -111
  227. package/runtime/compat.ts +0 -174
  228. package/runtime/dispatcher.ts +0 -25
  229. package/runtime/ecosystem.ts +0 -186
  230. package/runtime/provider_bootstrap.ts +0 -99
  231. package/runtime/provider_smoke.ts +0 -34
  232. package/runtime/release_readiness.ts +0 -186
  233. package/runtime/team_router.ts +0 -144
  234. package/scripts/check-omg-compat-contract-snapshot.ts +0 -20
  235. package/scripts/check-omg-standalone-clean.ts +0 -12
  236. package/scripts/check-runtime-clean.ts +0 -94
  237. package/scripts/omg.ts +0 -352
  238. package/scripts/settings-merge.ts +0 -93
  239. package/tools/commit_splitter.ts +0 -23
  240. package/tools/git_inspector.ts +0 -18
  241. package/tools/session_snapshot.ts +0 -47
  242. package/trac3er-oh-my-god-2.0.0.tgz +0 -0
  243. package/tsconfig.json +0 -15
@@ -0,0 +1,585 @@
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_ALLOW_COMMANDS = (
20
+ "rm",
21
+ "sudo",
22
+ "curl",
23
+ "wget",
24
+ "ssh",
25
+ "nc",
26
+ "ncat",
27
+ )
28
+
29
+ _LATEST_TAG_PATTERN = re.compile(r"@latest(?:$|[/:])", re.IGNORECASE)
30
+
31
+
32
+ def _is_dangerous_allow_entry(entry: Any) -> bool:
33
+ if not isinstance(entry, str):
34
+ return False
35
+
36
+ normalized = re.sub(r"\s+", " ", entry.strip())
37
+ for command in DANGEROUS_ALLOW_COMMANDS:
38
+ patterns = (
39
+ f"Bash({command}:*)",
40
+ f"Bash({command} *)",
41
+ )
42
+ if normalized in patterns:
43
+ return True
44
+
45
+ return False
46
+
47
+
48
+ def _mcp_server_risk(server_name: str, config: Any) -> tuple[int, list[str], list[str]]:
49
+ if not isinstance(config, dict):
50
+ return 0, [], []
51
+
52
+ score = 0
53
+ reasons: list[str] = []
54
+ controls: list[str] = []
55
+
56
+ command = str(config.get("command", "")).strip()
57
+ args = config.get("args", [])
58
+ args_list = [str(arg) for arg in args] if isinstance(args, list) else []
59
+
60
+ if command == "npx" and any(arg in {"-y", "--yes"} for arg in args_list):
61
+ score += 35
62
+ reasons.append(f"MCP server {server_name} uses npx auto-install confirmation")
63
+ controls.append("pin-mcp-package")
64
+
65
+ if any(_LATEST_TAG_PATTERN.search(arg) for arg in args_list):
66
+ score += 45
67
+ reasons.append(f"MCP server {server_name} uses an unpinned @latest package")
68
+ controls.append("pin-mcp-package")
69
+
70
+ if "server-filesystem" in " ".join(args_list):
71
+ root = args_list[-1] if args_list else ""
72
+ if root not in {".", "./"}:
73
+ score += 45
74
+ reasons.append(f"MCP filesystem server {server_name} is scoped outside the project root")
75
+ controls.append("scope-filesystem-root")
76
+
77
+ return score, reasons, controls
78
+
79
+
80
+ def _safe_dict(value: Any) -> dict[str, Any]:
81
+ return value if isinstance(value, dict) else {}
82
+
83
+
84
+ def _safe_list(value: Any) -> list[Any]:
85
+ return value if isinstance(value, list) else []
86
+
87
+
88
+ def _collect_mcp_changes(old_cfg: dict[str, Any], new_cfg: dict[str, Any]) -> list[dict[str, Any]]:
89
+ old_servers = _safe_dict(old_cfg.get("mcpServers"))
90
+ new_servers = _safe_dict(new_cfg.get("mcpServers"))
91
+ changes: list[dict[str, Any]] = []
92
+
93
+ old_keys = set(old_servers.keys())
94
+ new_keys = set(new_servers.keys())
95
+
96
+ for name in sorted(new_keys - old_keys):
97
+ changes.append({"type": "added", "server": name, "new": new_servers.get(name)})
98
+ for name in sorted(old_keys - new_keys):
99
+ changes.append({"type": "removed", "server": name, "old": old_servers.get(name)})
100
+ for name in sorted(old_keys & new_keys):
101
+ if old_servers.get(name) != new_servers.get(name):
102
+ changes.append(
103
+ {
104
+ "type": "modified",
105
+ "server": name,
106
+ "old": old_servers.get(name),
107
+ "new": new_servers.get(name),
108
+ }
109
+ )
110
+ return changes
111
+
112
+
113
+ def _count_hooks(cfg: dict[str, Any]) -> int:
114
+ hooks = _safe_dict(cfg.get("hooks"))
115
+ total = 0
116
+ for event_entries in hooks.values():
117
+ if not isinstance(event_entries, list):
118
+ continue
119
+ for entry in event_entries:
120
+ if isinstance(entry, dict):
121
+ nested = _safe_list(entry.get("hooks"))
122
+ total += len(nested) if nested else 1
123
+ else:
124
+ total += 1
125
+ return total
126
+
127
+
128
+ def _collect_hook_changes(old_cfg: dict[str, Any], new_cfg: dict[str, Any]) -> dict[str, Any]:
129
+ old_hooks = _safe_dict(old_cfg.get("hooks"))
130
+ new_hooks = _safe_dict(new_cfg.get("hooks"))
131
+
132
+ old_events = set(old_hooks.keys())
133
+ new_events = set(new_hooks.keys())
134
+ removed_events = sorted(old_events - new_events)
135
+ added_events = sorted(new_events - old_events)
136
+ modified_events = sorted(
137
+ event for event in (old_events & new_events) if old_hooks.get(event) != new_hooks.get(event)
138
+ )
139
+
140
+ return {
141
+ "old_hook_count": _count_hooks(old_cfg),
142
+ "new_hook_count": _count_hooks(new_cfg),
143
+ "removed_events": removed_events,
144
+ "added_events": added_events,
145
+ "modified_events": modified_events,
146
+ }
147
+
148
+
149
+ def _collect_env_changes(old_cfg: dict[str, Any], new_cfg: dict[str, Any]) -> list[dict[str, Any]]:
150
+ old_env = _safe_dict(old_cfg.get("env"))
151
+ new_env = _safe_dict(new_cfg.get("env"))
152
+
153
+ changes: list[dict[str, Any]] = []
154
+ keys = sorted(set(old_env.keys()) | set(new_env.keys()))
155
+ for key in keys:
156
+ old = old_env.get(key)
157
+ new = new_env.get(key)
158
+ if old == new:
159
+ continue
160
+ changes.append({"key": key, "old": old, "new": new})
161
+ return changes
162
+
163
+
164
+ def _risk_from_permissions(old_cfg: dict[str, Any], new_cfg: dict[str, Any]) -> tuple[int, list[str], list[str]]:
165
+ old_perms = _safe_dict(old_cfg.get("permissions"))
166
+ new_perms = _safe_dict(new_cfg.get("permissions"))
167
+
168
+ old_allow = set(_safe_list(old_perms.get("allow")))
169
+ new_allow = set(_safe_list(new_perms.get("allow")))
170
+ added_allow = sorted(new_allow - old_allow)
171
+
172
+ score = 0
173
+ reasons: list[str] = []
174
+ controls: list[str] = []
175
+
176
+ for dangerous in added_allow:
177
+ if _is_dangerous_allow_entry(dangerous):
178
+ score += 80
179
+ reasons.append(f"Dangerous allow pattern added: {dangerous}")
180
+ controls.extend(["manual-trust-review", "deny-by-default"])
181
+
182
+ return score, reasons, controls
183
+
184
+
185
+ def _risk_from_hooks(hook_changes: dict[str, Any]) -> tuple[int, list[str], list[str]]:
186
+ score = 0
187
+ reasons: list[str] = []
188
+ controls: list[str] = []
189
+
190
+ old_count = int(hook_changes.get("old_hook_count", 0))
191
+ new_count = int(hook_changes.get("new_hook_count", 0))
192
+ removed_events = hook_changes.get("removed_events", [])
193
+ modified_events = hook_changes.get("modified_events", [])
194
+
195
+ if old_count and new_count < max(1, old_count - 2):
196
+ score += 35
197
+ reasons.append(f"Hook count reduced significantly ({old_count} -> {new_count})")
198
+ controls.append("require-hook-audit")
199
+
200
+ if removed_events:
201
+ score += 25
202
+ reasons.append(f"Hook events removed: {', '.join(removed_events)}")
203
+ controls.append("event-removal-review")
204
+
205
+ if modified_events:
206
+ score += 20
207
+ reasons.append(f"Hook definitions modified: {', '.join(modified_events)}")
208
+ controls.append("hook-diff-review")
209
+
210
+ return score, reasons, controls
211
+
212
+
213
+ def _risk_from_mcp(mcp_changes: list[dict[str, Any]]) -> tuple[int, list[str], list[str]]:
214
+ score = 0
215
+ reasons: list[str] = []
216
+ controls: list[str] = []
217
+
218
+ for change in mcp_changes:
219
+ ctype = change.get("type")
220
+ name = change.get("server")
221
+ server_cfg = change.get("new") if ctype in {"added", "modified"} else change.get("old")
222
+ if ctype == "added":
223
+ score += 30
224
+ reasons.append(f"New MCP server added: {name}")
225
+ controls.append("mcp-endpoint-review")
226
+ elif ctype == "modified":
227
+ score += 35
228
+ reasons.append(f"MCP server modified: {name}")
229
+ controls.append("mcp-diff-review")
230
+ elif ctype == "removed":
231
+ score += 10
232
+ reasons.append(f"MCP server removed: {name}")
233
+
234
+ extra_score, extra_reasons, extra_controls = _mcp_server_risk(str(name), server_cfg)
235
+ score += extra_score
236
+ reasons.extend(extra_reasons)
237
+ controls.extend(extra_controls)
238
+
239
+ return score, reasons, controls
240
+
241
+
242
+ def _risk_from_env(env_changes: list[dict[str, Any]]) -> tuple[int, list[str], list[str]]:
243
+ score = 0
244
+ reasons: list[str] = []
245
+ controls: list[str] = []
246
+
247
+ for change in env_changes:
248
+ key = str(change.get("key", ""))
249
+ if any(token in key.upper() for token in ["KEY", "TOKEN", "SECRET", "PASSWORD", "CREDENTIAL"]):
250
+ score += 20
251
+ reasons.append(f"Sensitive environment key modified: {key}")
252
+ controls.append("secret-env-review")
253
+ else:
254
+ score += 5
255
+ reasons.append(f"Environment key modified: {key}")
256
+
257
+ return score, reasons, controls
258
+
259
+
260
+ def score_to_verdict(score: int) -> tuple[str, str]:
261
+ if score >= 80:
262
+ return "deny", "critical"
263
+ if score >= 45:
264
+ return "ask", "high"
265
+ if score >= 20:
266
+ return "ask", "med"
267
+ return "allow", "low"
268
+
269
+
270
+ def review_config_change(
271
+ file_path: str,
272
+ old_config: dict[str, Any] | None,
273
+ new_config: dict[str, Any] | None,
274
+ ) -> dict[str, Any]:
275
+ old_cfg = old_config or {}
276
+ new_cfg = new_config or {}
277
+
278
+ mcp_changes = _collect_mcp_changes(old_cfg, new_cfg)
279
+ hook_changes = _collect_hook_changes(old_cfg, new_cfg)
280
+ env_changes = _collect_env_changes(old_cfg, new_cfg)
281
+
282
+ risk_score = 0
283
+ reasons: list[str] = []
284
+ controls: list[str] = []
285
+
286
+ for score, r, c in [
287
+ _risk_from_permissions(old_cfg, new_cfg),
288
+ _risk_from_hooks(hook_changes),
289
+ _risk_from_mcp(mcp_changes),
290
+ _risk_from_env(env_changes),
291
+ ]:
292
+ risk_score += score
293
+ reasons.extend(r)
294
+ controls.extend(c)
295
+
296
+ verdict, risk_level = score_to_verdict(risk_score)
297
+
298
+ return {
299
+ "ts": datetime.now(timezone.utc).isoformat(),
300
+ "changed_files": [file_path] if file_path else [],
301
+ "mcp_changes": mcp_changes,
302
+ "hook_changes": hook_changes,
303
+ "env_changes": env_changes,
304
+ "risk_score": risk_score,
305
+ "risk_level": risk_level,
306
+ "verdict": verdict,
307
+ "reasons": reasons,
308
+ "controls": sorted(set(controls)),
309
+ }
310
+
311
+
312
+ def format_review_summary(review: dict[str, Any]) -> str:
313
+ verdict = review.get("verdict", "allow")
314
+ score = review.get("risk_score", 0)
315
+ risk_level = review.get("risk_level", "low")
316
+ reasons = review.get("reasons", []) or []
317
+
318
+ lines = [f"Trust Review: verdict={verdict} risk={risk_level} score={score}"]
319
+ if reasons:
320
+ lines.extend([f" - {reason}" for reason in reasons[:6]])
321
+ return "\n".join(lines)
322
+
323
+
324
+ def write_trust_manifest(project_dir: str, review: dict[str, Any]) -> str:
325
+ trust_dir = os.path.join(project_dir, ".omg", "trust")
326
+ os.makedirs(trust_dir, exist_ok=True)
327
+ manifest_path = os.path.join(trust_dir, "manifest.lock.json")
328
+
329
+ payload = {
330
+ "version": "omg-v1",
331
+ "updated_at": datetime.now(timezone.utc).isoformat(),
332
+ "last_review": review,
333
+ }
334
+ digest_input = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
335
+ payload["signature"] = hashlib.sha256(digest_input).hexdigest()
336
+
337
+ with open(manifest_path, "w", encoding="utf-8") as f:
338
+ json.dump(payload, f, indent=2, ensure_ascii=True)
339
+
340
+ return manifest_path
341
+
342
+
343
+ def _load_json_file(path: str) -> dict[str, Any]:
344
+ if not path or not os.path.exists(path):
345
+ return {}
346
+ try:
347
+ with open(path, "r", encoding="utf-8") as f:
348
+ data = json.load(f)
349
+ return data if isinstance(data, dict) else {}
350
+ except Exception:
351
+ return {}
352
+
353
+
354
+
355
+ # === Config Discovery Integration ============================================
356
+
357
+ # Suspicious code patterns that should block config import
358
+ _DANGEROUS_PATTERNS = [
359
+ re.compile(r'\beval\s*\('),
360
+ re.compile(r'\bexec\s*\('),
361
+ re.compile(r'\b__import__\s*\('),
362
+ re.compile(r'\bsubprocess\b'),
363
+ re.compile(r'\bos\.system\s*\('),
364
+ ]
365
+
366
+ # Credential patterns that produce warnings (not blocking)
367
+ _CREDENTIAL_PATTERNS = [
368
+ re.compile(r'\bpassword\b', re.IGNORECASE),
369
+ re.compile(r'\bsecret\b', re.IGNORECASE),
370
+ re.compile(r'\bapi_key\b', re.IGNORECASE),
371
+ re.compile(r'\btoken\b', re.IGNORECASE),
372
+ ]
373
+
374
+ # Max config size before warning (100KB)
375
+ _MAX_CONFIG_SIZE_BYTES = 100 * 1024
376
+
377
+
378
+ def _validate_config_security(config_path: str, content: str) -> Dict[str, Any]:
379
+ """Validate a config file's content for security issues.
380
+
381
+ Returns:
382
+ {"safe": bool, "issues": list[str], "warnings": list[str]}
383
+ """
384
+ issues: List[str] = []
385
+ warnings: List[str] = []
386
+
387
+ # Check for dangerous code patterns
388
+ for pattern in _DANGEROUS_PATTERNS:
389
+ if pattern.search(content):
390
+ issues.append(f"Dangerous pattern '{pattern.pattern}' found in {config_path}")
391
+
392
+ # Check for credential patterns (warn only)
393
+ for pattern in _CREDENTIAL_PATTERNS:
394
+ if pattern.search(content):
395
+ warnings.append(f"Credential pattern '{pattern.pattern}' found in {config_path}")
396
+
397
+ # Check file size
398
+ try:
399
+ size = os.path.getsize(config_path) if os.path.isfile(config_path) else 0
400
+ if size > _MAX_CONFIG_SIZE_BYTES:
401
+ warnings.append(f"Config file is large ({size} bytes): {config_path}")
402
+ except OSError:
403
+ pass
404
+
405
+ return {
406
+ "safe": len(issues) == 0,
407
+ "issues": issues,
408
+ "warnings": warnings,
409
+ }
410
+
411
+
412
+ def _log_config_import(config_path: str, tool: str, approved: bool, project_dir: str = ".") -> None:
413
+ """Log a config import decision to .omg/trust/config_imports.json.
414
+
415
+ Uses atomic_json_write() from _common for safe writes.
416
+ """
417
+ # Lazy import _common utilities
418
+ hooks_dir = os.path.dirname(os.path.abspath(__file__))
419
+ if hooks_dir not in sys.path:
420
+ sys.path.insert(0, hooks_dir)
421
+ try:
422
+ from _common import atomic_json_write # type: ignore[import-untyped]
423
+ except ImportError:
424
+ return # silently fail if _common unavailable
425
+
426
+ # Compute SHA-256 hash of the config file
427
+ sha256_hash = ""
428
+ try:
429
+ abs_path = os.path.join(project_dir, config_path) if not os.path.isabs(config_path) else config_path
430
+ if os.path.isfile(abs_path):
431
+ with open(abs_path, "rb") as f:
432
+ sha256_hash = hashlib.sha256(f.read()).hexdigest()
433
+ except (OSError, IOError):
434
+ sha256_hash = "unreadable"
435
+
436
+ # Build log entry
437
+ entry = {
438
+ "timestamp": datetime.now(timezone.utc).isoformat(),
439
+ "config_path": config_path,
440
+ "tool": tool,
441
+ "approved": approved,
442
+ "sha256_hash": sha256_hash,
443
+ }
444
+
445
+ # Load existing log, append, write back
446
+ log_path = os.path.join(project_dir, ".omg", "trust", "config_imports.json")
447
+ existing: List[Dict[str, Any]] = []
448
+ try:
449
+ if os.path.exists(log_path):
450
+ with open(log_path, "r", encoding="utf-8") as f:
451
+ data = json.load(f)
452
+ if isinstance(data, list):
453
+ existing = data
454
+ except (json.JSONDecodeError, OSError):
455
+ existing = []
456
+
457
+ existing.append(entry)
458
+ atomic_json_write(log_path, existing)
459
+
460
+
461
+ def review_discovered_configs(project_dir: str = ".") -> Dict[str, Any]:
462
+ """Scan, validate, and review discovered AI tool configurations.
463
+
464
+ Feature flag: OMG_CONFIG_DISCOVERY_ENABLED (default: False)
465
+
466
+ Returns:
467
+ {
468
+ "skipped": bool (if feature disabled),
469
+ "reason": str (if skipped),
470
+ "approved": list,
471
+ "rejected": list,
472
+ "warnings": list,
473
+ "pending": list,
474
+ }
475
+ """
476
+ # Check feature flag via lazy import
477
+ hooks_dir = os.path.dirname(os.path.abspath(__file__))
478
+ if hooks_dir not in sys.path:
479
+ sys.path.insert(0, hooks_dir)
480
+ try:
481
+ from _common import get_feature_flag # type: ignore[import-untyped]
482
+ enabled = get_feature_flag("CONFIG_DISCOVERY", default=False)
483
+ except ImportError:
484
+ enabled = os.getenv("OMG_CONFIG_DISCOVERY_ENABLED", "false").lower() in ("1", "true", "yes")
485
+
486
+ if not enabled:
487
+ return {"skipped": True, "reason": "feature disabled"}
488
+
489
+ # Lazy import config discovery from tools/
490
+ tools_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "tools")
491
+ tools_dir = os.path.normpath(tools_dir)
492
+ if tools_dir not in sys.path:
493
+ sys.path.insert(0, tools_dir)
494
+ try:
495
+ from config_discovery import discover_configs # type: ignore[import-untyped]
496
+ except ImportError:
497
+ return {
498
+ "skipped": True,
499
+ "reason": "config_discovery module not available",
500
+ }
501
+
502
+ # Run discovery
503
+ discovery_result = discover_configs(project_dir)
504
+ discovered = discovery_result.get("discovered", [])
505
+
506
+ approved: List[Dict[str, Any]] = []
507
+ rejected: List[Dict[str, Any]] = []
508
+ warnings: List[str] = []
509
+ pending: List[Dict[str, Any]] = []
510
+
511
+ for config in discovered:
512
+ tool = config.get("tool", "unknown")
513
+ paths = config.get("paths", [])
514
+ readable = config.get("readable", False)
515
+ size_bytes = config.get("size_bytes", 0)
516
+
517
+ if not paths:
518
+ continue
519
+
520
+ # Use the first path for validation
521
+ rel_path = paths[0]
522
+ abs_path = os.path.join(project_dir, rel_path)
523
+
524
+ # Read content for security validation
525
+ content = ""
526
+ if readable and os.path.isfile(abs_path):
527
+ try:
528
+ with open(abs_path, "r", encoding="utf-8", errors="ignore") as f:
529
+ content = f.read(256 * 1024) # Read up to 256KB for analysis
530
+ except (OSError, IOError):
531
+ content = ""
532
+
533
+ # Validate security
534
+ validation = _validate_config_security(abs_path, content)
535
+ entry = {
536
+ "tool": tool,
537
+ "path": rel_path,
538
+ "format": config.get("format", "unknown"),
539
+ "size_bytes": size_bytes,
540
+ "validation": validation,
541
+ }
542
+
543
+ if not validation["safe"]:
544
+ entry["reason"] = "; ".join(validation["issues"])
545
+ rejected.append(entry)
546
+ _log_config_import(rel_path, tool, approved=False, project_dir=project_dir)
547
+ else:
548
+ if validation["warnings"]:
549
+ warnings.extend(validation["warnings"])
550
+ approved.append(entry)
551
+ _log_config_import(rel_path, tool, approved=True, project_dir=project_dir)
552
+
553
+ return {
554
+ "skipped": False,
555
+ "approved": approved,
556
+ "rejected": rejected,
557
+ "warnings": warnings,
558
+ "pending": pending,
559
+ "scan_dir": discovery_result.get("scan_dir", project_dir),
560
+ "timestamp": discovery_result.get("timestamp", datetime.now(timezone.utc).isoformat()),
561
+ }
562
+
563
+
564
+ def _main() -> int:
565
+ try:
566
+ payload = json.load(__import__("sys").stdin)
567
+ except Exception:
568
+ return 0
569
+
570
+ file_path = payload.get("file_path", "")
571
+ old_config = payload.get("old_config")
572
+ new_config = payload.get("new_config")
573
+
574
+ if isinstance(old_config, str):
575
+ old_config = _load_json_file(old_config)
576
+ if isinstance(new_config, str):
577
+ new_config = _load_json_file(new_config)
578
+
579
+ review = review_config_change(file_path, old_config, new_config)
580
+ __import__("json").dump(review, __import__("sys").stdout)
581
+ return 0
582
+
583
+
584
+ if __name__ == "__main__":
585
+ raise SystemExit(_main())
package/hud/omg-hud.mjs CHANGED
@@ -219,6 +219,36 @@ const PRESET_CONFIGS = {
219
219
  safeMode: true,
220
220
  inventory: true,
221
221
  },
222
+ opencode: {
223
+ cwd: true,
224
+ cwdFormat: "relative",
225
+ gitRepo: false,
226
+ gitBranch: true,
227
+ model: false,
228
+ modelFormat: "short",
229
+ omcLabel: true,
230
+ rateLimits: false,
231
+ activeSkills: true,
232
+ lastSkill: true,
233
+ contextBar: true,
234
+ promptTime: true,
235
+ sessionHealth: true,
236
+ ralph: true,
237
+ autopilot: true,
238
+ prdStory: false,
239
+ agents: true,
240
+ agentsFormat: "count",
241
+ backgroundTasks: false,
242
+ todos: true,
243
+ thinking: true,
244
+ thinkingFormat: "text",
245
+ permissionStatus: false,
246
+ useBars: false,
247
+ showCallCounts: false,
248
+ maxOutputLines: 4,
249
+ safeMode: true,
250
+ inventory: true,
251
+ },
222
252
  };
223
253
 
224
254
  function countByExt(dirPath, ext) {
@@ -236,7 +266,7 @@ function getRuntimeInventory() {
236
266
  const claudeDir = getClaudeConfigDir();
237
267
  return {
238
268
  agents: countByExt(join(claudeDir, "agents"), ".md"),
239
- hooks: countByExt(join(claudeDir, "hooks"), ".ts"),
269
+ hooks: countByExt(join(claudeDir, "hooks"), ".py"),
240
270
  commands: countByExt(join(claudeDir, "commands"), ".md"),
241
271
  rules: countByExt(join(claudeDir, "rules"), ".md"),
242
272
  };
@@ -0,0 +1 @@
1
+ """Lab package for OMG."""
@@ -0,0 +1,75 @@
1
+ """OMG full AI pipeline stub: data -> refine -> train/distill -> evaluate -> regression."""
2
+ from __future__ import annotations
3
+
4
+ from datetime import datetime, timezone
5
+ from typing import Any
6
+
7
+ from .policies import validate_job_request
8
+
9
+
10
+ def _now() -> str:
11
+ return datetime.now(timezone.utc).isoformat()
12
+
13
+
14
+ def run_pipeline(job: dict[str, Any]) -> dict[str, Any]:
15
+ ok, reason = validate_job_request(job)
16
+ if not ok:
17
+ return {
18
+ "status": "blocked",
19
+ "stage": "policy",
20
+ "reason": reason,
21
+ "published": False,
22
+ "evaluation_report": None,
23
+ }
24
+
25
+ target_metric = float(job.get("target_metric", 0.8))
26
+ simulated_metric = float(job.get("simulated_metric", target_metric))
27
+
28
+ stages = [
29
+ {"name": "data_prepare", "status": "ok"},
30
+ {"name": "synthetic_refine", "status": "ok"},
31
+ {"name": "train_distill", "status": "ok"},
32
+ {"name": "evaluate", "status": "ok" if simulated_metric >= target_metric else "fail"},
33
+ {"name": "regression_test", "status": "ok" if simulated_metric >= target_metric else "fail"},
34
+ ]
35
+
36
+ report = {
37
+ "created_at": _now(),
38
+ "metric": simulated_metric,
39
+ "target_metric": target_metric,
40
+ "passed": simulated_metric >= target_metric,
41
+ "notes": job.get("evaluation_notes", ""),
42
+ }
43
+
44
+ if not report["passed"]:
45
+ return {
46
+ "status": "failed_evaluation",
47
+ "stage": "evaluate",
48
+ "stages": stages,
49
+ "published": False,
50
+ "evaluation_report": report,
51
+ }
52
+
53
+ return {
54
+ "status": "ready",
55
+ "stage": "complete",
56
+ "stages": stages,
57
+ "published": False,
58
+ "evaluation_report": report,
59
+ }
60
+
61
+
62
+ def publish_artifact(result: dict[str, Any]) -> dict[str, Any]:
63
+ report = result.get("evaluation_report")
64
+ if not isinstance(report, dict) or not report.get("passed"):
65
+ return {
66
+ "status": "blocked",
67
+ "reason": "evaluation report missing or not passed",
68
+ "published": False,
69
+ }
70
+
71
+ out = dict(result)
72
+ out["status"] = "published"
73
+ out["published"] = True
74
+ out["published_at"] = _now()
75
+ return out