@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,188 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ IntentGate Keyword Detection Hook — OMG v1.2
4
+
5
+ UserPromptSubmit hook that detects magic keywords in user prompts,
6
+ maps them to intents with confidence scoring, and injects a LEADER_HINT
7
+ into the hook output for downstream routing. Detection only — no execution.
8
+
9
+ Classification v1.2:
10
+ - Each detected intent includes a confidence score (0.0–1.0)
11
+ - Compound intent parsing: multiple keywords → multiple intents
12
+
13
+ Confidence rules:
14
+ - Exact keyword match (standalone): 0.95
15
+ - Keyword embedded in context: 0.90
16
+ - Keyword in compound phrase (multi-intent): 0.85
17
+ - Multiple occurrences of same keyword: 0.98 (cap)
18
+
19
+ Magic keywords:
20
+ - ultrawork, autopilot, ralph → execution modes
21
+ - plan this, tdd, search → task types
22
+ - stop, crazy → special directives
23
+ """
24
+ import json
25
+ import sys
26
+ import os
27
+ import re
28
+ import time
29
+
30
+ HOOKS_DIR = os.path.dirname(__file__)
31
+ if HOOKS_DIR not in sys.path:
32
+ sys.path.insert(0, HOOKS_DIR)
33
+
34
+ try:
35
+ from hooks._common import (
36
+ setup_crash_handler,
37
+ json_input,
38
+ get_feature_flag,
39
+ _resolve_project_dir,
40
+ check_performance_budget,
41
+ PRE_TOOL_INJECT_MAX_MS,
42
+ )
43
+ except ImportError:
44
+ import importlib
45
+ _common = importlib.import_module("_common")
46
+ setup_crash_handler = _common.setup_crash_handler
47
+ json_input = _common.json_input
48
+ get_feature_flag = _common.get_feature_flag
49
+ _resolve_project_dir = _common._resolve_project_dir
50
+ check_performance_budget = _common.check_performance_budget
51
+ PRE_TOOL_INJECT_MAX_MS = _common.PRE_TOOL_INJECT_MAX_MS
52
+
53
+ setup_crash_handler("intentgate-keyword-detector", fail_closed=False)
54
+
55
+ # ═══════════════════════════════════════════════════════════
56
+ # KEYWORD → INTENT MAPPING
57
+ # ═══════════════════════════════════════════════════════════
58
+ KEYWORD_INTENT_MAP = {
59
+ "ultrawork": "INTENT_MAX_EFFORT",
60
+ "autopilot": "INTENT_AUTONOMOUS",
61
+ "ralph": "INTENT_LOOP",
62
+ "plan this": "INTENT_PLAN",
63
+ "tdd": "INTENT_TEST_DRIVEN",
64
+ "search": "INTENT_SEARCH",
65
+ "stop": "INTENT_STOP",
66
+ "crazy": "INTENT_CRAZY",
67
+ }
68
+
69
+
70
+ # ═══════════════════════════════════════════════════════════
71
+ # CONFIDENCE SCORING ENGINE
72
+ # ═══════════════════════════════════════════════════════════
73
+
74
+ def _count_keyword_occurrences(keyword, prompt_lower):
75
+ """Count occurrences of keyword in prompt (respecting matching rules)."""
76
+ if " " in keyword:
77
+ # Multi-word: non-overlapping substring count
78
+ count = 0
79
+ start = 0
80
+ while True:
81
+ idx = prompt_lower.find(keyword, start)
82
+ if idx == -1:
83
+ break
84
+ count += 1
85
+ start = idx + len(keyword)
86
+ return count
87
+ else:
88
+ # Single-word: word boundary count
89
+ pattern = r'\b' + re.escape(keyword) + r'\b'
90
+ return len(re.findall(pattern, prompt_lower))
91
+
92
+
93
+ def _compute_confidence(keyword, prompt_lower, occurrence_count, total_intents):
94
+ """Compute confidence score for a detected intent.
95
+
96
+ Rules (applied in priority order):
97
+ - Multiple occurrences of same keyword: 0.98 (capped)
98
+ - Keyword in compound phrase (multiple intents): 0.85
99
+ - Exact keyword match (standalone prompt): 0.95
100
+ - Keyword embedded in context signals: 0.90
101
+ """
102
+ # Multiple occurrences of same keyword → highest confidence (cap)
103
+ if occurrence_count > 1:
104
+ return 0.98
105
+
106
+ # Compound intent (multiple different keywords detected)
107
+ if total_intents > 1:
108
+ return 0.85
109
+
110
+ # Single intent, single occurrence
111
+ stripped = prompt_lower.strip()
112
+
113
+ # Exact/standalone match → high confidence
114
+ if stripped == keyword:
115
+ return 0.95
116
+
117
+ # Keyword embedded in surrounding context → slightly lower
118
+ return 0.90
119
+ # ═══════════════════════════════════════════════════════════
120
+ # FEATURE FLAG CHECK
121
+ # ═══════════════════════════════════════════════════════════
122
+ start_time = time.time()
123
+
124
+ if not get_feature_flag("INTENTGATE", default=False):
125
+ # Feature disabled — return no-op JSON
126
+ json.dump({}, sys.stdout)
127
+ sys.exit(0)
128
+
129
+ # ═══════════════════════════════════════════════════════════
130
+ # INPUT PARSING
131
+ # ═══════════════════════════════════════════════════════════
132
+ data = json_input()
133
+
134
+ prompt = data.get("tool_input", {}).get("user_message", "") or data.get("user_message", "")
135
+ if not prompt:
136
+ json.dump({}, sys.stdout)
137
+ sys.exit(0)
138
+
139
+ prompt_lower = prompt.lower().strip()
140
+
141
+ # ═══════════════════════════════════════════════════════════
142
+ # KEYWORD DETECTION (case-insensitive, multi-keyword, confidence scoring)
143
+ # ═══════════════════════════════════════════════════════════
144
+
145
+ # First pass: detect keywords with occurrence counts
146
+ detected_raw = []
147
+
148
+ for keyword, intent in KEYWORD_INTENT_MAP.items():
149
+ occurrences = _count_keyword_occurrences(keyword, prompt_lower)
150
+ if occurrences > 0:
151
+ detected_raw.append((keyword, intent, occurrences))
152
+
153
+ # Second pass: compute confidence scores
154
+ num_intents = len(detected_raw)
155
+ detected_intents = []
156
+
157
+ for keyword, intent, occurrences in detected_raw:
158
+ confidence = _compute_confidence(keyword, prompt_lower, occurrences, num_intents)
159
+ detected_intents.append({
160
+ "intent": intent,
161
+ "confidence": confidence,
162
+ "keyword": keyword,
163
+ })
164
+ # ═══════════════════════════════════════════════════════════
165
+ # OUTPUT CONSTRUCTION
166
+ # ═══════════════════════════════════════════════════════════
167
+ output = {}
168
+
169
+ if detected_intents:
170
+ # Inject LEADER_HINT with detected intents and confidence scores
171
+ output["LEADER_HINT"] = {
172
+ "detected_intents": detected_intents,
173
+ "keyword_count": len(detected_intents),
174
+ "routing_enabled": True,
175
+ "classification_version": "1.2",
176
+ }
177
+
178
+ # ═══════════════════════════════════════════════════════════
179
+ # PERFORMANCE BUDGET CHECK
180
+ # ═══════════════════════════════════════════════════════════
181
+ elapsed_ms = (time.time() - start_time) * 1000
182
+ check_performance_budget("intentgate-keyword-detector", elapsed_ms, PRE_TOOL_INJECT_MAX_MS)
183
+
184
+ # ═══════════════════════════════════════════════════════════
185
+ # OUTPUT
186
+ # ═══════════════════════════════════════════════════════════
187
+ json.dump(output, sys.stdout)
188
+ sys.exit(0)
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Magic Keyword Router — OMG v1.2
4
+
5
+ PostToolUse hook that reads the LEADER_HINT produced by
6
+ intentgate-keyword-detector, selects the appropriate agent based on intent,
7
+ and writes a routing result to `.omg/state/routing_result.json`.
8
+
9
+ Decision only — no subprocess calls or agent execution.
10
+
11
+ Input sources (checked in priority order):
12
+ 1. LEADER_HINT in stdin JSON (from hook pipeline)
13
+ 2. `.omg/state/leader_hint.json` (persisted by future integrations)
14
+
15
+ Feature flag: OMG_MAGIC_ROUTER_ENABLED (default off)
16
+ """
17
+ import json
18
+ import os
19
+ import sys
20
+ import time
21
+
22
+ HOOKS_DIR = os.path.dirname(__file__)
23
+ if HOOKS_DIR not in sys.path:
24
+ sys.path.insert(0, HOOKS_DIR)
25
+
26
+ try:
27
+ from hooks._common import (
28
+ setup_crash_handler,
29
+ json_input,
30
+ get_feature_flag,
31
+ atomic_json_write,
32
+ get_project_dir,
33
+ check_performance_budget,
34
+ PRE_TOOL_INJECT_MAX_MS,
35
+ )
36
+ except ImportError:
37
+ import importlib
38
+ _common = importlib.import_module("_common")
39
+ setup_crash_handler = _common.setup_crash_handler
40
+ json_input = _common.json_input
41
+ get_feature_flag = _common.get_feature_flag
42
+ atomic_json_write = _common.atomic_json_write
43
+ get_project_dir = _common.get_project_dir
44
+ check_performance_budget = _common.check_performance_budget
45
+ PRE_TOOL_INJECT_MAX_MS = _common.PRE_TOOL_INJECT_MAX_MS
46
+
47
+ try:
48
+ from hooks._agent_registry import INTENT_ROUTING
49
+ except ImportError:
50
+ _registry = importlib.import_module("_agent_registry")
51
+ INTENT_ROUTING = _registry.INTENT_ROUTING
52
+
53
+ setup_crash_handler("magic-keyword-router", fail_closed=False)
54
+
55
+ # ═══════════════════════════════════════════════════════════
56
+ # CONSTANTS
57
+ # ═══════════════════════════════════════════════════════════
58
+ ROUTING_RESULT_PATH = ".omg/state/routing_result.json"
59
+ LEADER_HINT_PATH = ".omg/state/leader_hint.json"
60
+
61
+
62
+ # ═══════════════════════════════════════════════════════════
63
+ # FEATURE FLAG CHECK
64
+ # ═══════════════════════════════════════════════════════════
65
+ start_time = time.time()
66
+
67
+ if not get_feature_flag("MAGIC_ROUTER", default=False):
68
+ # Feature disabled — no-op
69
+ json.dump({}, sys.stdout)
70
+ sys.exit(0)
71
+
72
+ # ═══════════════════════════════════════════════════════════
73
+ # INPUT PARSING
74
+ # ═══════════════════════════════════════════════════════════
75
+ data = json_input()
76
+
77
+
78
+ def _extract_leader_hint(hook_data):
79
+ """Extract LEADER_HINT from hook stdin data.
80
+
81
+ Checks multiple locations where the hint may appear:
82
+ - Top-level "LEADER_HINT" key
83
+ - Nested under "tool_output"
84
+ - Nested under "hookSpecificOutput"
85
+ """
86
+ if not isinstance(hook_data, dict):
87
+ return None
88
+ # Direct top-level
89
+ hint = hook_data.get("LEADER_HINT")
90
+ if hint:
91
+ return hint
92
+ # Nested in tool_output
93
+ tool_output = hook_data.get("tool_output", {})
94
+ if isinstance(tool_output, dict):
95
+ hint = tool_output.get("LEADER_HINT")
96
+ if hint:
97
+ return hint
98
+ # Nested in hookSpecificOutput
99
+ hso = hook_data.get("hookSpecificOutput", {})
100
+ if isinstance(hso, dict):
101
+ hint = hso.get("LEADER_HINT")
102
+ if hint:
103
+ return hint
104
+ return None
105
+
106
+
107
+ def _read_leader_hint_file(project_dir):
108
+ """Read LEADER_HINT from persisted file (secondary source)."""
109
+ path = os.path.join(project_dir, LEADER_HINT_PATH)
110
+ if not os.path.exists(path):
111
+ return None
112
+ try:
113
+ with open(path, "r", encoding="utf-8") as f:
114
+ data = json.load(f)
115
+ # File may contain {"LEADER_HINT": {...}} or the hint directly
116
+ if isinstance(data, dict):
117
+ return data.get("LEADER_HINT", data) if "detected_intents" not in data else data
118
+ return None
119
+ except (json.JSONDecodeError, OSError):
120
+ return None
121
+
122
+
123
+ def _resolve_routing(leader_hint):
124
+ """Given a LEADER_HINT dict, resolve the target agent.
125
+
126
+ Returns (target_agent, intent, confidence) tuple.
127
+ target_agent may be None (e.g., INTENT_STOP → halt).
128
+ """
129
+ detected = leader_hint.get("detected_intents", [])
130
+ if not detected:
131
+ return None, None, 0.0
132
+
133
+ # Pick first non-None routable intent (highest priority = first in list)
134
+ for intent_entry in detected:
135
+ intent_name = intent_entry.get("intent", "")
136
+ confidence = intent_entry.get("confidence", 0.0)
137
+ target = INTENT_ROUTING.get(intent_name)
138
+
139
+ # INTENT_STOP maps to None explicitly — that IS a valid routing result
140
+ if intent_name in INTENT_ROUTING:
141
+ return target, intent_name, confidence
142
+
143
+ # No known intent found
144
+ return None, None, 0.0
145
+
146
+
147
+ # ═══════════════════════════════════════════════════════════
148
+ # LEADER_HINT RESOLUTION (stdin preferred, file fallback)
149
+ # ═══════════════════════════════════════════════════════════
150
+ project_dir = get_project_dir()
151
+ leader_hint = _extract_leader_hint(data)
152
+
153
+ if leader_hint is None:
154
+ leader_hint = _read_leader_hint_file(project_dir)
155
+
156
+ # ═══════════════════════════════════════════════════════════
157
+ # ROUTING DECISION
158
+ # ═══════════════════════════════════════════════════════════
159
+ from datetime import datetime, timezone
160
+
161
+ routing_result_path = os.path.join(project_dir, ROUTING_RESULT_PATH)
162
+ ts = datetime.now(timezone.utc).isoformat()
163
+
164
+ if leader_hint and leader_hint.get("detected_intents"):
165
+ target_agent, intent, confidence = _resolve_routing(leader_hint)
166
+ routing_result = {
167
+ "target_agent": target_agent,
168
+ "intent": intent,
169
+ "confidence": confidence,
170
+ "fallback": False,
171
+ "timestamp": ts,
172
+ }
173
+ else:
174
+ # No LEADER_HINT → fallback
175
+ routing_result = {
176
+ "target_agent": None,
177
+ "intent": None,
178
+ "confidence": 0.0,
179
+ "fallback": True,
180
+ "timestamp": ts,
181
+ }
182
+
183
+ atomic_json_write(routing_result_path, routing_result)
184
+
185
+ # ═══════════════════════════════════════════════════════════
186
+ # PERFORMANCE BUDGET CHECK
187
+ # ═══════════════════════════════════════════════════════════
188
+ elapsed_ms = (time.time() - start_time) * 1000
189
+ check_performance_budget("magic-keyword-router", elapsed_ms, PRE_TOOL_INJECT_MAX_MS)
190
+
191
+ # ═══════════════════════════════════════════════════════════
192
+ # OUTPUT (no-op for PostToolUse — just exit clean)
193
+ # ═══════════════════════════════════════════════════════════
194
+ json.dump({}, sys.stdout)
195
+ sys.exit(0)
@@ -0,0 +1,310 @@
1
+ #!/usr/bin/env python3
2
+ """OMG v1 Policy Engine
3
+
4
+ Centralized policy decision layer for tool access, file access, and supply-chain
5
+ artifact verification.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, asdict
10
+ import os
11
+ import re
12
+ from typing import Any
13
+
14
+
15
+ Action = str
16
+ RiskLevel = str
17
+
18
+
19
+ @dataclass
20
+ class PolicyDecision:
21
+ action: Action # allow | ask | deny
22
+ risk_level: RiskLevel # low | med | high | critical
23
+ reason: str = ""
24
+ controls: list[str] | None = None
25
+
26
+ def to_dict(self) -> dict[str, Any]:
27
+ data = asdict(self)
28
+ if data.get("controls") is None:
29
+ data["controls"] = []
30
+ return data
31
+
32
+
33
+ def allow(reason: str = "", controls: list[str] | None = None) -> PolicyDecision:
34
+ return PolicyDecision("allow", "low", reason, controls or [])
35
+
36
+
37
+ def ask(reason: str, risk_level: RiskLevel = "med", controls: list[str] | None = None) -> PolicyDecision:
38
+ return PolicyDecision("ask", risk_level, reason, controls or [])
39
+
40
+
41
+ def deny(reason: str, risk_level: RiskLevel = "high", controls: list[str] | None = None) -> PolicyDecision:
42
+ return PolicyDecision("deny", risk_level, reason, controls or [])
43
+
44
+
45
+ # === BASH POLICY ============================================================
46
+
47
+ DESTRUCT_PATTERNS = [
48
+ (r"rm\s+-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*\s+/(\s|$|\*)", "rm -rf /"),
49
+ (r"rm\s+-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*\s+~/?(\s|$|\*)", "rm -rf ~"),
50
+ (r"rm\s+-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*\s+\$HOME", "rm -rf $HOME"),
51
+ (r"rm\s+-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*\s+\$\{?HOME\}?", "rm -rf ${HOME}"),
52
+ (r"rm\s+-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*\s+\.\.\s", "rm -rf .."),
53
+ (r":\(\)\s*\{\s*:\|:&\s*\}\s*;:", "fork bomb"),
54
+ (r"function\s+\w+\(\)\s*\{\s*\w+\s*\|\s*\w+\s*&", "potential fork bomb"),
55
+ (r">\s*/dev/sd[a-z]", "overwrite disk"),
56
+ (r"dd\s+.*of=/dev/sd[a-z]", "dd to disk device"),
57
+ (r"sudo\s+(dd|mkfs|fdisk|parted|wipefs)\b", "destructive disk op"),
58
+ (r"sudo\s+rm\b", "sudo rm"),
59
+ (r"echo\s+.*>\s*/proc/", "write to /proc"),
60
+ (r"echo\s+.*>\s*/sys/", "write to /sys"),
61
+ ]
62
+
63
+ PIPE_SHELL_PATTERNS = [
64
+ r"(curl|wget)\s+.*\|\s*(sudo\s+)?(ba)?sh",
65
+ r"(curl|wget)\s+.*\|\s*python[23]?",
66
+ r"(curl|wget)\s+.*\|\s*perl",
67
+ r"(curl|wget)\s+.*\|\s*ruby",
68
+ r"base64\s+.*\|\s*(ba)?sh",
69
+ r"echo\s+.*\|\s*base64\s+-d\s*\|\s*(ba)?sh",
70
+ ]
71
+
72
+ EVAL_PATTERNS = [
73
+ r"\beval\s+\"\$",
74
+ r"\beval\s+\$\(",
75
+ r"\beval\s+`",
76
+ ]
77
+
78
+ SAFE_ENV_REFERENCE = re.compile(r"\.env\.(example|sample|template)\b", re.IGNORECASE)
79
+
80
+ SECRET_FILE_PATTERNS = [
81
+ r"\.(env|pem|key|p12|pfx|jks|keystore|netrc|npmrc|pypirc)\b",
82
+ r"/\.aws/(credentials|config)\b",
83
+ r"/\.kube/config\b",
84
+ r"/id_(rsa|ed25519|ecdsa)\b",
85
+ r"/\.ssh/",
86
+ r"\bsecrets?/",
87
+ r"\bcredentials?\.",
88
+ r"\bpasswords?\.",
89
+ r"\btokens?\.",
90
+ ]
91
+
92
+ READ_COMMANDS = [
93
+ "cat", "less", "more", "head", "tail", "strings", "xxd", "od",
94
+ "hexdump", "base64", "vim", "vi", "nano", "emacs", "view",
95
+ "bat", "pygmentize", "highlight", "source", "\\.",
96
+ "awk", "gawk", "mawk", "perl", "ruby", "python", "python3", "node",
97
+ ]
98
+ READ_PATTERN = r"(?:^|\s|;|&&|\|\|)(?:" + "|".join(re.escape(c) for c in READ_COMMANDS) + r")\s+"
99
+
100
+ EXFIL_COMMANDS = [
101
+ r"\b(cp|mv|ln\s+-s)\s+",
102
+ r"\btar\s+.*-?c",
103
+ r"\bzip\s+",
104
+ ]
105
+
106
+ ASK_PATTERNS = [
107
+ (r"(^|\s)(curl|wget)(\s|$)", "Network egress"),
108
+ (r"(^|\s)(ssh|scp|rsync)(\s|$)", "Remote connection"),
109
+ (r"git\s+push\s+.*(-f|--force)", "Force push"),
110
+ (r"git\s+push\s+.*(main|master|production|release)", "Push to protected branch"),
111
+ (r"chmod\s+(777|666|a\+[rwx])", "Overly permissive chmod"),
112
+ (r"docker\s+run\s+.*--privileged", "Privileged container"),
113
+ (r"python[23]?\s+-c\s+", "Inline Python execution"),
114
+ (r"node\s+-e\s+", "Inline Node execution"),
115
+ ]
116
+
117
+
118
+ def evaluate_bash_command(cmd: str) -> PolicyDecision:
119
+ if not cmd:
120
+ return allow("empty command")
121
+
122
+ for pat, label in DESTRUCT_PATTERNS:
123
+ if re.search(pat, cmd):
124
+ return deny(f"Blocked: {label}", "critical", ["destructive-op"])
125
+
126
+ for pat in PIPE_SHELL_PATTERNS:
127
+ if re.search(pat, cmd):
128
+ return deny("Blocked: pipe-to-shell", "critical", ["remote-code-exec"])
129
+
130
+ for pat in EVAL_PATTERNS:
131
+ if re.search(pat, cmd):
132
+ return deny("Blocked: dynamic eval", "high", ["dynamic-eval"])
133
+
134
+ for secret_pat in SECRET_FILE_PATTERNS:
135
+ if not re.search(secret_pat, cmd, re.IGNORECASE):
136
+ continue
137
+
138
+ if SAFE_ENV_REFERENCE.search(cmd):
139
+ cleaned = SAFE_ENV_REFERENCE.sub("__SAFE_REF__", cmd)
140
+ if not re.search(secret_pat, cleaned, re.IGNORECASE):
141
+ continue
142
+
143
+ if re.search(READ_PATTERN, cmd, re.IGNORECASE):
144
+ return deny("Blocked: reading secret file", "critical", ["secret-access"])
145
+
146
+ if re.search(r"<\s*\S*(" + secret_pat + r")", cmd, re.IGNORECASE):
147
+ return deny("Blocked: reading secret file via redirect", "critical", ["secret-access"])
148
+
149
+ for exfil in EXFIL_COMMANDS:
150
+ if re.search(exfil, cmd):
151
+ return deny("Blocked: copying secret file", "critical", ["secret-exfiltration"])
152
+
153
+ if re.search(r"\bgrep\b", cmd):
154
+ return ask("Searching inside potential secret file — confirm this is safe", "high", ["secret-search"])
155
+
156
+ for pat, label in ASK_PATTERNS:
157
+ if re.search(pat, cmd):
158
+ return ask(f"{label}: {cmd[:120]}", "med", ["human-approval"])
159
+
160
+ return allow("command allowed")
161
+
162
+
163
+ # === FILE POLICY ============================================================
164
+
165
+ BLOCKED_FILES = {
166
+ ".env", ".env.local", ".env.development", ".env.production",
167
+ ".env.staging", ".env.test", ".npmrc", ".pypirc", ".netrc",
168
+ "id_rsa", "id_ed25519", "id_ecdsa", "id_rsa.pub", "id_ed25519.pub", "id_ecdsa.pub",
169
+ }
170
+
171
+ EXAMPLE_FILES = {".env.example", ".env.sample", ".env.template"}
172
+
173
+ BLOCKED_PATH_PATTERNS = [
174
+ r"/\.aws/(credentials|config)$",
175
+ r"/\.kube/config$",
176
+ r"/\.ssh/",
177
+ r"/\.gnupg/",
178
+ r"/secrets?/",
179
+ r"\.(pem|key|p12|pfx|jks|keystore)$",
180
+ r"(^|/)secret[s]?\.",
181
+ r"(^|/)credential[s]?\.",
182
+ r"(^|/)password[s]?\.",
183
+ r"(^|/)token[s]?\.",
184
+ r"(^|/)\.docker/config\.json$",
185
+ r"(^|/)\.git-credentials$",
186
+ ]
187
+
188
+
189
+ # OMG internal credential store paths (exempted from secret-file blocking)
190
+ # Only these exact filenames inside .omg/state/ are allowed.
191
+ _OMG_CREDENTIAL_STORE_ALLOWLIST = frozenset({
192
+ "credentials.enc",
193
+ "credentials.meta",
194
+ })
195
+
196
+
197
+ def _is_omg_credential_path(normalized_path: str) -> bool:
198
+ """Return True if the path is an OMG credential store file.
199
+
200
+ Only exempts files that are:
201
+ 1. Inside .omg/state/ directory
202
+ 2. Named exactly 'credentials.enc' or 'credentials.meta'
203
+ 3. Feature flag MULTI_CREDENTIAL is enabled
204
+
205
+ This is deliberately narrow to prevent path traversal attacks.
206
+ """
207
+ # Import here to avoid circular dependency at module level
208
+ from _common import get_feature_flag
209
+
210
+ # Only exempt if feature is enabled
211
+ if not get_feature_flag("MULTI_CREDENTIAL", default=False):
212
+ return False
213
+
214
+ basename = os.path.basename(normalized_path).lower()
215
+ if basename not in _OMG_CREDENTIAL_STORE_ALLOWLIST:
216
+ return False
217
+
218
+ # Verify it's actually inside .omg/state/
219
+ parent = os.path.dirname(normalized_path)
220
+ return parent.endswith(os.sep + ".omg" + os.sep + "state") or \
221
+ parent.endswith("/.omg/state")
222
+
223
+
224
+ def evaluate_file_access(tool: str, file_path: str) -> PolicyDecision:
225
+ if not file_path:
226
+ return allow("no file")
227
+
228
+ normalized = os.path.normpath(file_path)
229
+ # Resolve symlinks to prevent bypass via symlink to secret file
230
+ try:
231
+ normalized = os.path.realpath(normalized)
232
+ except (OSError, ValueError):
233
+ pass
234
+ basename = os.path.basename(normalized).lower()
235
+ lowpath = normalized.lower()
236
+
237
+ if basename in EXAMPLE_FILES and tool in ("Write", "Edit", "MultiEdit"):
238
+ return deny(
239
+ f"Modifying example env file blocked (Read is allowed): {file_path}",
240
+ "high",
241
+ ["immutable-env-template"],
242
+ )
243
+
244
+ if basename in BLOCKED_FILES:
245
+ return deny(f"Secret file blocked: {file_path}", "critical", ["secret-access"])
246
+
247
+ if re.match(r"^\.env(\..+)?$", basename) and basename not in EXAMPLE_FILES:
248
+ return deny(f"Environment file blocked: {file_path}", "critical", ["secret-access"])
249
+
250
+ # EXEMPTION: OMG credential store files within .omg/state/
251
+ # These are managed by hooks/credential_store.py and must be accessible
252
+ if _is_omg_credential_path(normalized):
253
+ return allow("OMG credential store (managed path)")
254
+
255
+ for pat in BLOCKED_PATH_PATTERNS:
256
+ if re.search(pat, lowpath):
257
+ return deny(f"Sensitive path blocked: {file_path}", "critical", ["secret-access"])
258
+
259
+ return allow("file allowed")
260
+
261
+
262
+ # === SUPPLY CHAIN POLICY ====================================================
263
+
264
+
265
+ def evaluate_supply_artifact(artifact: dict[str, Any], mode: str = "warn_and_run") -> PolicyDecision:
266
+ """Verify artifact trust with Warn-And-Run semantics.
267
+
268
+ mode=warn_and_run: missing trust metadata returns ASK
269
+ critical findings always DENY
270
+ """
271
+ findings = artifact.get("static_scan") or []
272
+ permissions = artifact.get("permissions") or []
273
+ signer = artifact.get("signer")
274
+ checksum = artifact.get("checksum")
275
+
276
+ for finding in findings:
277
+ sev = str((finding or {}).get("severity", "")).lower()
278
+ if sev == "critical":
279
+ return deny("Critical static-scan finding detected", "critical", ["supply-critical-block"])
280
+
281
+ joined_perms = " ".join(str(p) for p in permissions)
282
+ if any(token in joined_perms for token in ["sudo", "rm -rf", "--privileged", "curl |", "wget |"]):
283
+ return deny("Critical permission profile detected in artifact", "critical", ["dangerous-permissions"])
284
+
285
+ if not signer or not checksum:
286
+ if mode == "warn_and_run":
287
+ return ask(
288
+ "Artifact missing signer/checksum metadata (untrusted). Continue with isolation.",
289
+ "high",
290
+ ["isolate-network", "read-only-fs", "manual-approval"],
291
+ )
292
+ return deny("Artifact missing signer/checksum metadata", "high", ["unsigned-artifact"])
293
+
294
+ has_high = any(str((finding or {}).get("severity", "")).lower() == "high" for finding in findings)
295
+ if has_high:
296
+ return ask("High-risk findings present. Explicit approval required.", "high", ["manual-approval"])
297
+
298
+ return allow("artifact trusted")
299
+
300
+
301
+ def to_pretool_hook_output(decision: PolicyDecision) -> dict[str, Any] | None:
302
+ if decision.action == "allow":
303
+ return None
304
+ return {
305
+ "hookSpecificOutput": {
306
+ "hookEventName": "PreToolUse",
307
+ "permissionDecision": decision.action,
308
+ "permissionDecisionReason": decision.reason,
309
+ }
310
+ }