@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,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
|
+
}
|