@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.
- package/.claude-plugin/marketplace.json +36 -0
- package/.claude-plugin/plugin.json +23 -0
- package/.claude-plugin/scripts/install.sh +49 -0
- package/.claude-plugin/scripts/uninstall.sh +80 -0
- package/.claude-plugin/scripts/update.sh +84 -0
- package/.mcp.json +20 -0
- package/LICENSE +21 -0
- package/OMG-setup.sh +1093 -0
- package/README.md +335 -0
- package/THIRD_PARTY_NOTICES.md +24 -0
- package/UPSTREAM_DIFF.md +20 -0
- package/agents/__init__.py +1 -0
- package/agents/_model_roles.yaml +26 -0
- package/agents/designer.md +67 -0
- package/agents/explore.md +60 -0
- package/agents/model_roles.py +196 -0
- package/agents/omg-api-builder.md +23 -0
- package/agents/omg-architect-mode.md +43 -0
- package/agents/omg-architect.md +13 -0
- package/agents/omg-backend-engineer.md +43 -0
- package/agents/omg-critic.md +16 -0
- package/agents/omg-database-engineer.md +43 -0
- package/agents/omg-escalation-router.md +17 -0
- package/agents/omg-executor.md +12 -0
- package/agents/omg-frontend-designer.md +42 -0
- package/agents/omg-implement-mode.md +50 -0
- package/agents/omg-infra-engineer.md +43 -0
- package/agents/omg-qa-tester.md +16 -0
- package/agents/omg-research-mode.md +43 -0
- package/agents/omg-security-auditor.md +43 -0
- package/agents/omg-testing-engineer.md +43 -0
- package/agents/plan.md +80 -0
- package/agents/quick_task.md +64 -0
- package/agents/reviewer.md +83 -0
- package/agents/task.md +71 -0
- package/commands/OMG:ccg.md +22 -0
- package/commands/OMG:compat.md +57 -0
- package/commands/OMG:crazy.md +125 -0
- package/commands/OMG:domain-init.md +11 -0
- package/commands/OMG:escalate.md +52 -0
- package/commands/OMG:health-check.md +45 -0
- package/commands/OMG:init.md +134 -0
- package/commands/OMG:mode.md +44 -0
- package/commands/OMG:project-init.md +11 -0
- package/commands/OMG:ralph-start.md +43 -0
- package/commands/OMG:ralph-stop.md +23 -0
- package/commands/OMG:teams.md +39 -0
- package/commands/ai-commit.md +113 -0
- package/commands/ccg.md +9 -0
- package/commands/create-agent.md +183 -0
- package/commands/omc-teams.md +9 -0
- package/commands/session-branch.md +85 -0
- package/commands/session-fork.md +53 -0
- package/commands/session-merge.md +134 -0
- package/commands/theme.md +44 -0
- package/config/lsp_languages.yaml +324 -0
- package/config/themes/catppuccin-frappe.yaml +14 -0
- package/config/themes/catppuccin-latte.yaml +14 -0
- package/config/themes/catppuccin-macchiato.yaml +14 -0
- package/config/themes/catppuccin-mocha.yaml +14 -0
- package/config/themes/dracula.yaml +14 -0
- package/config/themes/gruvbox-dark.yaml +14 -0
- package/config/themes/nord.yaml +14 -0
- package/config/themes/one-dark.yaml +14 -0
- package/config/themes/solarized-dark.yaml +14 -0
- package/config/themes/tokyo-night.yaml +14 -0
- package/control_plane/__init__.py +2 -0
- package/control_plane/openapi.yaml +109 -0
- package/control_plane/server.py +107 -0
- package/control_plane/service.py +148 -0
- package/crates/omg-natives/Cargo.toml +17 -0
- package/crates/omg-natives/src/clipboard.rs +5 -0
- package/crates/omg-natives/src/glob.rs +15 -0
- package/crates/omg-natives/src/grep.rs +15 -0
- package/crates/omg-natives/src/highlight.rs +15 -0
- package/crates/omg-natives/src/html.rs +14 -0
- package/crates/omg-natives/src/image.rs +5 -0
- package/crates/omg-natives/src/keys.rs +5 -0
- package/crates/omg-natives/src/lib.rs +36 -0
- package/crates/omg-natives/src/prof.rs +5 -0
- package/crates/omg-natives/src/ps.rs +5 -0
- package/crates/omg-natives/src/shell.rs +5 -0
- package/crates/omg-natives/src/task.rs +5 -0
- package/crates/omg-natives/src/text.rs +14 -0
- package/hooks/_agent_registry.py +421 -0
- package/hooks/_budget.py +31 -0
- package/hooks/_common.py +476 -0
- package/hooks/_learnings.py +126 -0
- package/hooks/_memory.py +103 -0
- package/hooks/circuit-breaker.py +270 -0
- package/hooks/config-guard.py +163 -0
- package/hooks/context_pressure.py +53 -0
- package/hooks/credential_store.py +801 -0
- package/hooks/fetch-rate-limits.py +212 -0
- package/hooks/firewall.py +48 -0
- package/hooks/hashline-formatter-bridge.py +224 -0
- package/hooks/hashline-injector.py +273 -0
- package/hooks/hashline-validator.py +216 -0
- package/hooks/idle-detector.py +95 -0
- package/hooks/intentgate-keyword-detector.py +188 -0
- package/hooks/magic-keyword-router.py +195 -0
- package/hooks/policy_engine.py +310 -0
- package/hooks/post-tool-failure.py +19 -0
- package/hooks/post-write.py +199 -0
- package/hooks/pre-compact.py +204 -0
- package/hooks/pre-tool-inject.py +98 -0
- package/hooks/prompt-enhancer.py +672 -0
- package/hooks/quality-runner.py +191 -0
- package/hooks/secret-guard.py +47 -0
- package/hooks/session-end-capture.py +137 -0
- package/hooks/session-start.py +275 -0
- package/hooks/shadow_manager.py +297 -0
- package/hooks/state_migration.py +209 -0
- package/hooks/stop-gate.py +7 -0
- package/hooks/stop_dispatcher.py +929 -0
- package/hooks/test-validator.py +138 -0
- package/hooks/todo-state-tracker.py +114 -0
- package/hooks/tool-ledger.py +126 -0
- package/hooks/trust_review.py +524 -0
- package/install.sh +9 -0
- package/omg_natives/__init__.py +186 -0
- package/omg_natives/_bindings.py +165 -0
- package/omg_natives/clipboard.py +36 -0
- package/omg_natives/glob.py +42 -0
- package/omg_natives/grep.py +61 -0
- package/omg_natives/highlight.py +54 -0
- package/omg_natives/html.py +157 -0
- package/omg_natives/image.py +51 -0
- package/omg_natives/keys.py +46 -0
- package/omg_natives/prof.py +39 -0
- package/omg_natives/ps.py +93 -0
- package/omg_natives/shell.py +58 -0
- package/omg_natives/task.py +41 -0
- package/omg_natives/text.py +50 -0
- package/package.json +26 -0
- package/plugins/README.md +82 -0
- package/plugins/advanced/commands/OMG:code-review.md +114 -0
- package/plugins/advanced/commands/OMG:deep-plan.md +221 -0
- package/plugins/advanced/commands/OMG:handoff.md +115 -0
- package/plugins/advanced/commands/OMG:learn.md +110 -0
- package/plugins/advanced/commands/OMG:maintainer.md +31 -0
- package/plugins/advanced/commands/OMG:ralph-start.md +43 -0
- package/plugins/advanced/commands/OMG:ralph-stop.md +23 -0
- package/plugins/advanced/commands/OMG:security-review.md +119 -0
- package/plugins/advanced/commands/OMG:sequential-thinking.md +20 -0
- package/plugins/advanced/commands/OMG:ship.md +46 -0
- package/plugins/advanced/plugin.json +96 -0
- package/plugins/core/plugin.json +82 -0
- package/pytest.ini +5 -0
- package/registry/__init__.py +1 -0
- package/registry/verify_artifact.py +90 -0
- package/rules/contextual/architect-mode.md +9 -0
- package/rules/contextual/big-picture.md +20 -0
- package/rules/contextual/code-hygiene.md +26 -0
- package/rules/contextual/context-management.md +19 -0
- package/rules/contextual/context-minimization.md +32 -0
- package/rules/contextual/ddd-sdd.md +28 -0
- package/rules/contextual/dependency-safety.md +16 -0
- package/rules/contextual/doc-check.md +13 -0
- package/rules/contextual/implement-mode.md +9 -0
- package/rules/contextual/infra-safety.md +14 -0
- package/rules/contextual/outside-in.md +13 -0
- package/rules/contextual/persistent-mode.md +24 -0
- package/rules/contextual/research-mode.md +9 -0
- package/rules/contextual/security-domains.md +25 -0
- package/rules/contextual/vision-detection.md +27 -0
- package/rules/contextual/web-search.md +25 -0
- package/rules/contextual/write-verify.md +23 -0
- package/rules/core/00-truth.md +20 -0
- package/rules/core/01-surgical.md +19 -0
- package/rules/core/02-circuit-breaker.md +22 -0
- package/rules/core/03-ensemble.md +28 -0
- package/rules/core/04-testing.md +30 -0
- package/runtime/__init__.py +32 -0
- package/runtime/adapters/__init__.py +13 -0
- package/runtime/adapters/claude.py +60 -0
- package/runtime/adapters/gpt.py +53 -0
- package/runtime/adapters/local.py +53 -0
- package/runtime/business_workflow.py +220 -0
- package/runtime/compat.py +1299 -0
- package/runtime/custom_agent_loader.py +366 -0
- package/runtime/dispatcher.py +47 -0
- package/runtime/ecosystem.py +371 -0
- package/runtime/legacy_compat.py +7 -0
- package/runtime/omc_compat.py +7 -0
- package/runtime/omc_contract_snapshot.json +916 -0
- package/runtime/omg_compat_contract_snapshot.json +916 -0
- package/runtime/subagent_dispatcher.py +362 -0
- package/runtime/team_router.py +838 -0
- package/scripts/check-omc-contract-snapshot.py +12 -0
- package/scripts/check-omg-compat-contract-snapshot.py +137 -0
- package/scripts/check-omg-standalone-clean.py +102 -0
- package/scripts/legacy_to_omg_migrate.py +29 -0
- package/scripts/migrate-omc.py +464 -0
- package/scripts/omc_to_omg_migrate.py +12 -0
- package/scripts/omg.py +493 -0
- package/scripts/settings-merge.py +224 -0
- package/scripts/verify-no-omc.sh +5 -0
- package/scripts/verify-standalone.sh +21 -0
- package/templates/idea.yml +30 -0
- package/templates/policy.yaml +15 -0
- package/templates/profile.yaml +25 -0
- package/templates/runtime.yaml +12 -0
- package/templates/working-memory.md +17 -0
- package/tools/__init__.py +2 -0
- package/tools/browser_consent.py +289 -0
- package/tools/browser_stealth.py +481 -0
- package/tools/browser_tool.py +448 -0
- package/tools/changelog_generator.py +268 -0
- package/tools/commit_splitter.py +361 -0
- package/tools/config_discovery.py +151 -0
- package/tools/config_merger.py +449 -0
- package/tools/git_inspector.py +298 -0
- package/tools/lsp_client.py +275 -0
- package/tools/lsp_discovery.py +231 -0
- package/tools/lsp_operations.py +392 -0
- package/tools/python_repl.py +656 -0
- package/tools/python_sandbox.py +609 -0
- package/tools/search_providers/__init__.py +77 -0
- package/tools/search_providers/brave.py +115 -0
- package/tools/search_providers/exa.py +116 -0
- package/tools/search_providers/jina.py +104 -0
- package/tools/search_providers/perplexity.py +139 -0
- package/tools/search_providers/synthetic.py +74 -0
- package/tools/session_snapshot.py +736 -0
- package/tools/ssh_manager.py +912 -0
- package/tools/theme_engine.py +294 -0
- package/tools/theme_selector.py +137 -0
- 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