@mamdouh-aboammar/agentic-workflow 1.2.0
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/plugin.json +10 -0
- package/.codex-plugin/plugin.json +13 -0
- package/.skills.json +19 -0
- package/AGENTS.md +1344 -0
- package/CLAUDE.md +178 -0
- package/GEMINI.md +102 -0
- package/LICENSE +21 -0
- package/README.md +350 -0
- package/SKILL.md +132 -0
- package/bin/agentic-hooks.sh +79 -0
- package/bin/cli.js +1060 -0
- package/core/__init__.py +52 -0
- package/core/ai_evaluator.py +117 -0
- package/core/autopilot_engine.py +368 -0
- package/core/clean_code_guard.py +188 -0
- package/core/engine_py/__init__.py +29 -0
- package/core/engine_py/agent_worker.py +136 -0
- package/core/engine_py/decider.py +150 -0
- package/core/engine_py/energy.py +45 -0
- package/core/engine_py/event_bus.py +63 -0
- package/core/engine_py/executor.py +186 -0
- package/core/engine_py/models.py +193 -0
- package/core/engine_py/queue.py +314 -0
- package/core/engine_py/runner.py +116 -0
- package/core/engine_py/system_workers.py +70 -0
- package/core/engine_py/toon_adapter.py +586 -0
- package/core/engine_py/verification_controller.py +208 -0
- package/core/engine_py/worker.py +167 -0
- package/core/engine_spec/event_schema.json +65 -0
- package/core/engine_spec/example_workflow.yaml +73 -0
- package/core/engine_spec/workflow_schema.json +127 -0
- package/core/hooks/__init__.py +29 -0
- package/core/hooks/adapters/__init__.py +25 -0
- package/core/hooks/adapters/claude_adapter.py +83 -0
- package/core/hooks/adapters/cli_agent_adapter.py +82 -0
- package/core/hooks/adapters/codex_adapter.py +78 -0
- package/core/hooks/adapters/cursor_adapter.py +73 -0
- package/core/hooks/adapters/gemini_adapter.py +93 -0
- package/core/hooks/adapters/homebrew_adapter.py +69 -0
- package/core/hooks/adapters/mcp_proxy.py +133 -0
- package/core/hooks/adapters/shell_adapter.py +65 -0
- package/core/hooks/dispatcher.py +118 -0
- package/core/hooks/policy_engine.py +375 -0
- package/core/hooks/session_end.py +141 -0
- package/core/hooks/types.py +147 -0
- package/core/integrations/__init__.py +28 -0
- package/core/integrations/installer.py +225 -0
- package/core/integrations/lifecycle_director.py +175 -0
- package/core/integrations/registry.py +105 -0
- package/core/multi_agent_system.py +164 -0
- package/core/skills_indexer.py +742 -0
- package/core/system/__init__.py +25 -0
- package/core/system/announcements.py +72 -0
- package/core/system/dependencies.py +69 -0
- package/core/system/doctor.py +171 -0
- package/core/system/health.py +144 -0
- package/core/system/installer.py +137 -0
- package/core/system/notifications.py +97 -0
- package/core/system/refresher.py +110 -0
- package/core/system/updater.py +167 -0
- package/core/system/version_tracker.py +65 -0
- package/docs/architecture_plan.md +7 -0
- package/docs/guides/failure-recovery.md +714 -0
- package/docs/implementation_summary.md +10 -0
- package/docs/protocols/autopilot-execution.md +148 -0
- package/docs/protocols/code-change-protocol.md +49 -0
- package/docs/protocols/context-preservation-detail.md +114 -0
- package/docs/protocols/quality-gates.md +110 -0
- package/docs/protocols/ulw-mode.md +60 -0
- package/docs/research_findings.md +10 -0
- package/docs/solutions/autonomous-autopilot-engine-architecture.md +38 -0
- package/install.sh +111 -0
- package/marketplace.json +37 -0
- package/package.json +81 -0
- package/skills/agentic-workflow/SKILL.md +132 -0
- package/skills/agentic-workflow/skill-spec.json +100 -0
- package/soul.md +445 -0
- package/src/engine_ts/decider.ts +186 -0
- package/src/engine_ts/event-bus.ts +57 -0
- package/src/engine_ts/executor.ts +262 -0
- package/src/engine_ts/index.ts +12 -0
- package/src/engine_ts/queue.ts +93 -0
- package/src/engine_ts/runner.ts +108 -0
- package/src/engine_ts/skills-indexer.ts +264 -0
- package/src/engine_ts/toon-adapter.ts +91 -0
- package/src/engine_ts/types.ts +134 -0
- package/src/engine_ts/verification-controller.ts +204 -0
- package/src/engine_ts/worker.ts +280 -0
- package/src/hooks/adapters/claude-adapter.ts +54 -0
- package/src/hooks/adapters/cli-agent-adapter.ts +46 -0
- package/src/hooks/adapters/codex-adapter.ts +69 -0
- package/src/hooks/adapters/cursor-adapter.ts +60 -0
- package/src/hooks/adapters/gemini-adapter.ts +71 -0
- package/src/hooks/adapters/homebrew-adapter.ts +36 -0
- package/src/hooks/adapters/mcp-proxy.ts +66 -0
- package/src/hooks/adapters/shell-adapter.ts +42 -0
- package/src/hooks/dispatcher.ts +113 -0
- package/src/hooks/index.ts +16 -0
- package/src/hooks/policy-engine.ts +376 -0
- package/src/hooks/session-end.ts +125 -0
- package/src/hooks/types.ts +61 -0
- package/src/index.d.ts +34 -0
- package/src/index.ts +23 -0
- package/src/integrations/index.ts +7 -0
- package/src/integrations/installer.ts +208 -0
- package/src/integrations/lifecycle-director.ts +139 -0
- package/src/integrations/registry.ts +82 -0
- package/src/system/announcements.ts +143 -0
- package/src/system/dependencies.ts +176 -0
- package/src/system/doctor.ts +374 -0
- package/src/system/health.ts +270 -0
- package/src/system/index.ts +14 -0
- package/src/system/installer.ts +262 -0
- package/src/system/notifications.ts +180 -0
- package/src/system/refresher.ts +207 -0
- package/src/system/types.ts +268 -0
- package/src/system/updater.ts +219 -0
- package/src/system/version-tracker.ts +137 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Universal Agentic Hooks Framework (UAHF) — Adapters
|
|
3
|
+
===================================================
|
|
4
|
+
Adapters for normalizing hooks across Claude, Cursor, Antigravity, Gemini CLI, Codex, Shell, Homebrew, MCP, and CLI.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .claude_adapter import ClaudeHookAdapter
|
|
8
|
+
from .gemini_adapter import GeminiHookAdapter
|
|
9
|
+
from .cursor_adapter import CursorHookAdapter
|
|
10
|
+
from .codex_adapter import CodexHookAdapter
|
|
11
|
+
from .shell_adapter import ShellHookAdapter
|
|
12
|
+
from .homebrew_adapter import HomebrewHookAdapter
|
|
13
|
+
from .mcp_proxy import McpHookProxy
|
|
14
|
+
from .cli_agent_adapter import CliAgentAdapter
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"ClaudeHookAdapter",
|
|
18
|
+
"GeminiHookAdapter",
|
|
19
|
+
"CursorHookAdapter",
|
|
20
|
+
"CodexHookAdapter",
|
|
21
|
+
"ShellHookAdapter",
|
|
22
|
+
"HomebrewHookAdapter",
|
|
23
|
+
"McpHookProxy",
|
|
24
|
+
"CliAgentAdapter",
|
|
25
|
+
]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Claude Code Hook Adapter
|
|
3
|
+
========================
|
|
4
|
+
Consumes native Claude Code PreToolUse, PostToolUse, and Session lifecycle hooks.
|
|
5
|
+
Interprets stdin JSON, normalizes to HookEvent, and controls Claude via exit codes (0 vs 2).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
from ..types import (
|
|
13
|
+
HookEvent,
|
|
14
|
+
HookResult,
|
|
15
|
+
HookSource,
|
|
16
|
+
HookType,
|
|
17
|
+
HookVerdict,
|
|
18
|
+
)
|
|
19
|
+
from ..policy_engine import UniversalPolicyEngine
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ClaudeHookAdapter:
|
|
23
|
+
"""Consumes and mediates Claude Code lifecycle hooks."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, policy_engine: Optional[UniversalPolicyEngine] = None):
|
|
26
|
+
self.engine = policy_engine or UniversalPolicyEngine()
|
|
27
|
+
|
|
28
|
+
def parse_payload(self, raw_input: str, hook_type: HookType = HookType.PRE_TOOL) -> Optional[HookEvent]:
|
|
29
|
+
try:
|
|
30
|
+
data = json.loads(raw_input) if isinstance(raw_input, str) else raw_input
|
|
31
|
+
except Exception:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
if not isinstance(data, dict):
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
tool_name = data.get("tool_name") or data.get("name")
|
|
38
|
+
tool_input = data.get("tool_input") or data.get("input") or {}
|
|
39
|
+
tool_response = data.get("tool_response") or data.get("output")
|
|
40
|
+
|
|
41
|
+
command = None
|
|
42
|
+
file_path = None
|
|
43
|
+
|
|
44
|
+
if isinstance(tool_input, dict):
|
|
45
|
+
command = tool_input.get("command")
|
|
46
|
+
file_path = tool_input.get("file_path") or tool_input.get("path") or tool_input.get("target")
|
|
47
|
+
|
|
48
|
+
return HookEvent(
|
|
49
|
+
source=HookSource.CLAUDE,
|
|
50
|
+
hook_type=hook_type,
|
|
51
|
+
tool_name=tool_name,
|
|
52
|
+
command=command,
|
|
53
|
+
file_path=file_path,
|
|
54
|
+
args=tool_input if isinstance(tool_input, dict) else {"raw": tool_input},
|
|
55
|
+
output=tool_response,
|
|
56
|
+
metadata={"raw_claude_payload": data},
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def handle(self, raw_input: str, hook_type: HookType = HookType.PRE_TOOL) -> HookResult:
|
|
60
|
+
event = self.parse_payload(raw_input, hook_type)
|
|
61
|
+
if not event:
|
|
62
|
+
return HookResult(event_id="claude_empty", verdict=HookVerdict.ALLOW, exit_code=0)
|
|
63
|
+
|
|
64
|
+
result = self.engine.evaluate(event)
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
def run_cli(self, hook_type_str: str = "pre_tool"):
|
|
68
|
+
"""CLI runner for direct pipe execution in .claude/settings.json."""
|
|
69
|
+
raw = sys.stdin.read()
|
|
70
|
+
if not raw.strip():
|
|
71
|
+
sys.exit(0)
|
|
72
|
+
|
|
73
|
+
htype = HookType.POST_TOOL if "post" in hook_type_str.lower() else HookType.PRE_TOOL
|
|
74
|
+
result = self.handle(raw, htype)
|
|
75
|
+
|
|
76
|
+
if result.verdict == HookVerdict.BLOCK:
|
|
77
|
+
print(f"🛑 [UAHF Claude Hook] {result.message}", file=sys.stderr)
|
|
78
|
+
sys.exit(result.exit_code or 2)
|
|
79
|
+
elif result.verdict == HookVerdict.WARN:
|
|
80
|
+
print(f"⚠️ [UAHF Claude Hook] {result.message}", file=sys.stderr)
|
|
81
|
+
sys.exit(0)
|
|
82
|
+
|
|
83
|
+
sys.exit(0)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI Agent Wrapper & Interceptor Adapter
|
|
3
|
+
=======================================
|
|
4
|
+
Supervises external CLI agents (OpenAI Codex, Moonshot Kimi, Aider, etc.).
|
|
5
|
+
Intercepts agent execution, enforces pre-flight governance, and injects protective hooks.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from typing import List, Optional
|
|
13
|
+
|
|
14
|
+
from ..types import (
|
|
15
|
+
HookEvent,
|
|
16
|
+
HookResult,
|
|
17
|
+
HookSource,
|
|
18
|
+
HookType,
|
|
19
|
+
HookVerdict,
|
|
20
|
+
)
|
|
21
|
+
from ..policy_engine import UniversalPolicyEngine
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CliAgentAdapter:
|
|
25
|
+
"""Wraps and mediates execution of autonomous CLI agents."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, policy_engine: Optional[UniversalPolicyEngine] = None):
|
|
28
|
+
self.engine = policy_engine or UniversalPolicyEngine()
|
|
29
|
+
|
|
30
|
+
def identify_agent_source(self, binary_name: str) -> HookSource:
|
|
31
|
+
name = os.path.basename(binary_name).lower()
|
|
32
|
+
if "codex" in name:
|
|
33
|
+
return HookSource.CODEX
|
|
34
|
+
elif "kimi" in name:
|
|
35
|
+
return HookSource.KIMI
|
|
36
|
+
elif "cursor" in name:
|
|
37
|
+
return HookSource.CURSOR
|
|
38
|
+
elif "antigravity" in name:
|
|
39
|
+
return HookSource.ANTIGRAVITY
|
|
40
|
+
elif "claude" in name:
|
|
41
|
+
return HookSource.CLAUDE
|
|
42
|
+
return HookSource.CLI
|
|
43
|
+
|
|
44
|
+
def run_supervised(self, command_args: List[str]) -> int:
|
|
45
|
+
if not command_args:
|
|
46
|
+
print("❌ No command provided to CLI Agent supervisor.", file=sys.stderr)
|
|
47
|
+
return 1
|
|
48
|
+
|
|
49
|
+
binary = command_args[0]
|
|
50
|
+
source = self.identify_agent_source(binary)
|
|
51
|
+
|
|
52
|
+
# Pre-execution policy check on the agent invocation itself
|
|
53
|
+
full_command = " ".join(command_args)
|
|
54
|
+
event = HookEvent(
|
|
55
|
+
source=source,
|
|
56
|
+
hook_type=HookType.SESSION_START,
|
|
57
|
+
command=full_command,
|
|
58
|
+
tool_name=binary,
|
|
59
|
+
args={"raw_args": command_args[1:]},
|
|
60
|
+
agent_id=f"agent_{source.value}",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
result = self.engine.evaluate(event)
|
|
64
|
+
if result.verdict == HookVerdict.BLOCK:
|
|
65
|
+
print(f"\033[1;31m🛑 [CLI Agent Guard] INVOCATION BLOCKED:\033[0m\n {result.message}", file=sys.stderr)
|
|
66
|
+
return 2
|
|
67
|
+
|
|
68
|
+
# Inject environment hook overrides so subshells spawned by the agent hit our hooks
|
|
69
|
+
env = os.environ.copy()
|
|
70
|
+
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))
|
|
71
|
+
hook_bin = os.path.join(repo_root, "bin")
|
|
72
|
+
|
|
73
|
+
current_path = env.get("PATH", "")
|
|
74
|
+
if hook_bin not in current_path:
|
|
75
|
+
env["PATH"] = f"{hook_bin}:{current_path}"
|
|
76
|
+
|
|
77
|
+
env["AGENTIC_HOOKS_ACTIVE"] = "1"
|
|
78
|
+
env["AGENTIC_AGENT_NAME"] = source.value
|
|
79
|
+
|
|
80
|
+
# Execute target agent
|
|
81
|
+
proc = subprocess.run(command_args, env=env)
|
|
82
|
+
return proc.returncode
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAI Codex & ChatGPT Plugin Hook Driver Adapter
|
|
3
|
+
=================================================
|
|
4
|
+
Consumes execution events from OpenAI Codex and ChatGPT plugin environments.
|
|
5
|
+
Normalizes events to HookEvent(source=HookSource.CODEX) and applies sandbox security
|
|
6
|
+
and governance policies.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
from ..types import (
|
|
13
|
+
HookEvent,
|
|
14
|
+
HookResult,
|
|
15
|
+
HookSource,
|
|
16
|
+
HookType,
|
|
17
|
+
HookVerdict,
|
|
18
|
+
)
|
|
19
|
+
from ..policy_engine import UniversalPolicyEngine
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CodexHookAdapter:
|
|
23
|
+
"""Consumes and mediates OpenAI Codex & ChatGPT plugin lifecycle events."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, policy_engine: Optional[UniversalPolicyEngine] = None):
|
|
26
|
+
self.engine = policy_engine or UniversalPolicyEngine()
|
|
27
|
+
|
|
28
|
+
def parse_payload(self, raw_input: Any, hook_type: HookType = HookType.PRE_TOOL) -> Optional[HookEvent]:
|
|
29
|
+
"""Parses a Codex / ChatGPT tool call payload into a normalized HookEvent."""
|
|
30
|
+
try:
|
|
31
|
+
data = json.loads(raw_input) if isinstance(raw_input, str) else raw_input
|
|
32
|
+
except Exception:
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
if not isinstance(data, dict):
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
tool_name = data.get("name") or data.get("tool_name") or data.get("function")
|
|
39
|
+
tool_args = data.get("arguments") or data.get("args") or data.get("input") or {}
|
|
40
|
+
output = data.get("output") or data.get("response")
|
|
41
|
+
|
|
42
|
+
if isinstance(tool_args, str):
|
|
43
|
+
try:
|
|
44
|
+
tool_args = json.loads(tool_args)
|
|
45
|
+
except Exception:
|
|
46
|
+
tool_args = {"raw": tool_args}
|
|
47
|
+
|
|
48
|
+
command = None
|
|
49
|
+
file_path = None
|
|
50
|
+
|
|
51
|
+
if isinstance(tool_args, dict):
|
|
52
|
+
command = tool_args.get("command") or tool_args.get("cmd") or tool_args.get("code")
|
|
53
|
+
file_path = tool_args.get("path") or tool_args.get("file_path") or tool_args.get("filename")
|
|
54
|
+
|
|
55
|
+
return HookEvent(
|
|
56
|
+
source=HookSource.CODEX,
|
|
57
|
+
hook_type=hook_type,
|
|
58
|
+
tool_name=tool_name or ("exec" if command else "plugin_call"),
|
|
59
|
+
command=command,
|
|
60
|
+
file_path=file_path,
|
|
61
|
+
args=tool_args if isinstance(tool_args, dict) else {"raw": tool_args},
|
|
62
|
+
output=output,
|
|
63
|
+
metadata={"raw_codex_payload": data},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def handle(self, raw_input: Any, hook_type: HookType = HookType.PRE_TOOL) -> HookResult:
|
|
67
|
+
"""Evaluates Codex event against universal policies."""
|
|
68
|
+
event = self.parse_payload(raw_input, hook_type)
|
|
69
|
+
if not event:
|
|
70
|
+
return HookResult(event_id="codex_empty", verdict=HookVerdict.ALLOW, exit_code=0)
|
|
71
|
+
|
|
72
|
+
return self.engine.evaluate(event)
|
|
73
|
+
|
|
74
|
+
def evaluate_tool(self, name: str, arguments: Dict[str, Any], is_pre: bool = True, output: Optional[Any] = None) -> HookResult:
|
|
75
|
+
"""Convenience method for programmatic Codex tool call evaluation."""
|
|
76
|
+
hook_type = HookType.PRE_TOOL if is_pre else HookType.POST_TOOL
|
|
77
|
+
payload = {"name": name, "arguments": arguments, "output": output}
|
|
78
|
+
return self.handle(payload, hook_type)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cursor IDE & Windsurf Hook Driver Adapter
|
|
3
|
+
=========================================
|
|
4
|
+
Consumes tool execution and terminal command events from Cursor IDE rules (.cursor/rules)
|
|
5
|
+
and Windsurf cascades. Normalizes events to HookEvent(source=HookSource.CURSOR) and applies
|
|
6
|
+
governance policies.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
from ..types import (
|
|
13
|
+
HookEvent,
|
|
14
|
+
HookResult,
|
|
15
|
+
HookSource,
|
|
16
|
+
HookType,
|
|
17
|
+
HookVerdict,
|
|
18
|
+
)
|
|
19
|
+
from ..policy_engine import UniversalPolicyEngine
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CursorHookAdapter:
|
|
23
|
+
"""Consumes and mediates Cursor IDE & Windsurf lifecycle events."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, policy_engine: Optional[UniversalPolicyEngine] = None):
|
|
26
|
+
self.engine = policy_engine or UniversalPolicyEngine()
|
|
27
|
+
|
|
28
|
+
def parse_payload(self, raw_input: Any, hook_type: HookType = HookType.PRE_TOOL) -> Optional[HookEvent]:
|
|
29
|
+
"""Parses Cursor / Windsurf command or tool invocation payload."""
|
|
30
|
+
try:
|
|
31
|
+
data = json.loads(raw_input) if isinstance(raw_input, str) else raw_input
|
|
32
|
+
except Exception:
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
if not isinstance(data, dict):
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
tool_name = data.get("tool") or data.get("tool_name") or data.get("action")
|
|
39
|
+
command = data.get("command") or data.get("cmd")
|
|
40
|
+
file_path = data.get("file_path") or data.get("path") or data.get("target_file")
|
|
41
|
+
tool_args = data.get("args") or data.get("parameters") or {}
|
|
42
|
+
output = data.get("output") or data.get("result")
|
|
43
|
+
|
|
44
|
+
if isinstance(tool_args, dict):
|
|
45
|
+
if not command:
|
|
46
|
+
command = tool_args.get("command") or tool_args.get("cmd")
|
|
47
|
+
if not file_path:
|
|
48
|
+
file_path = tool_args.get("file_path") or tool_args.get("path") or tool_args.get("target_file")
|
|
49
|
+
|
|
50
|
+
return HookEvent(
|
|
51
|
+
source=HookSource.CURSOR,
|
|
52
|
+
hook_type=hook_type,
|
|
53
|
+
tool_name=tool_name or ("terminal" if command else "file_op"),
|
|
54
|
+
command=command,
|
|
55
|
+
file_path=file_path,
|
|
56
|
+
args=tool_args if isinstance(tool_args, dict) else {"raw": tool_args},
|
|
57
|
+
output=output,
|
|
58
|
+
metadata={"raw_cursor_payload": data},
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def handle(self, raw_input: Any, hook_type: HookType = HookType.PRE_TOOL) -> HookResult:
|
|
62
|
+
"""Evaluates Cursor event against universal policies."""
|
|
63
|
+
event = self.parse_payload(raw_input, hook_type)
|
|
64
|
+
if not event:
|
|
65
|
+
return HookResult(event_id="cursor_empty", verdict=HookVerdict.ALLOW, exit_code=0)
|
|
66
|
+
|
|
67
|
+
return self.engine.evaluate(event)
|
|
68
|
+
|
|
69
|
+
def evaluate_command(self, command_line: str, is_pre: bool = True, output: Optional[str] = None) -> HookResult:
|
|
70
|
+
"""Convenience method for evaluating Cursor terminal commands."""
|
|
71
|
+
hook_type = HookType.PRE_COMMAND if is_pre else HookType.POST_COMMAND
|
|
72
|
+
payload = {"command": command_line, "output": output}
|
|
73
|
+
return self.handle(payload, hook_type)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Google Antigravity & Gemini CLI Hook Driver Adapter
|
|
3
|
+
===================================================
|
|
4
|
+
Consumes tool execution events from Google Antigravity and Gemini CLI environments.
|
|
5
|
+
Normalizes tool calls (run_command, write_to_file, replace_file_content, view_file, invoke_subagent)
|
|
6
|
+
into canonical HookEvent instances and applies UAHF policy engine gates.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Any, Dict, Optional
|
|
14
|
+
|
|
15
|
+
from ..types import (
|
|
16
|
+
HookEvent,
|
|
17
|
+
HookResult,
|
|
18
|
+
HookSource,
|
|
19
|
+
HookType,
|
|
20
|
+
HookVerdict,
|
|
21
|
+
)
|
|
22
|
+
from ..policy_engine import UniversalPolicyEngine
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GeminiHookAdapter:
|
|
26
|
+
"""Consumes and mediates Gemini CLI & Google Antigravity lifecycle tool events."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, policy_engine: Optional[UniversalPolicyEngine] = None):
|
|
29
|
+
self.engine = policy_engine or UniversalPolicyEngine()
|
|
30
|
+
|
|
31
|
+
def parse_payload(self, raw_input: Any, hook_type: HookType = HookType.PRE_TOOL) -> Optional[HookEvent]:
|
|
32
|
+
"""Parses a Gemini CLI / Antigravity tool call payload into a normalized HookEvent."""
|
|
33
|
+
try:
|
|
34
|
+
data = json.loads(raw_input) if isinstance(raw_input, str) else raw_input
|
|
35
|
+
except Exception:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
if not isinstance(data, dict):
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
tool_name = data.get("tool_name") or data.get("name") or data.get("tool")
|
|
42
|
+
tool_args = data.get("args") or data.get("arguments") or data.get("parameters") or {}
|
|
43
|
+
tool_response = data.get("response") or data.get("output") or data.get("result")
|
|
44
|
+
|
|
45
|
+
command = None
|
|
46
|
+
file_path = None
|
|
47
|
+
|
|
48
|
+
if isinstance(tool_args, dict):
|
|
49
|
+
# Antigravity run_command convention
|
|
50
|
+
command = tool_args.get("CommandLine") or tool_args.get("command") or tool_args.get("cmd")
|
|
51
|
+
# Antigravity file tools (view_file, write_to_file, replace_file_content)
|
|
52
|
+
file_path = (
|
|
53
|
+
tool_args.get("AbsolutePath")
|
|
54
|
+
or tool_args.get("TargetFile")
|
|
55
|
+
or tool_args.get("file_path")
|
|
56
|
+
or tool_args.get("path")
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return HookEvent(
|
|
60
|
+
source=HookSource.ANTIGRAVITY,
|
|
61
|
+
hook_type=hook_type,
|
|
62
|
+
tool_name=tool_name,
|
|
63
|
+
command=command,
|
|
64
|
+
file_path=file_path,
|
|
65
|
+
args=tool_args if isinstance(tool_args, dict) else {"raw": tool_args},
|
|
66
|
+
output=tool_response,
|
|
67
|
+
metadata={"raw_gemini_payload": data},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def handle(self, raw_input: Any, hook_type: HookType = HookType.PRE_TOOL) -> HookResult:
|
|
71
|
+
"""Evaluates payload against universal policies."""
|
|
72
|
+
event = self.parse_payload(raw_input, hook_type)
|
|
73
|
+
if not event:
|
|
74
|
+
return HookResult(event_id="gemini_empty", verdict=HookVerdict.ALLOW, exit_code=0)
|
|
75
|
+
|
|
76
|
+
result = self.engine.evaluate(event)
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
def evaluate_tool_call(
|
|
80
|
+
self,
|
|
81
|
+
tool_name: str,
|
|
82
|
+
arguments: Dict[str, Any],
|
|
83
|
+
is_pre: bool = True,
|
|
84
|
+
output: Optional[Any] = None
|
|
85
|
+
) -> HookResult:
|
|
86
|
+
"""Programmatic evaluation entry point for Python-driven Gemini / Antigravity agents."""
|
|
87
|
+
hook_type = HookType.PRE_TOOL if is_pre else HookType.POST_TOOL
|
|
88
|
+
payload = {
|
|
89
|
+
"tool_name": tool_name,
|
|
90
|
+
"args": arguments,
|
|
91
|
+
"output": output
|
|
92
|
+
}
|
|
93
|
+
return self.handle(payload, hook_type)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Homebrew Package Manager Hook Adapter
|
|
3
|
+
=====================================
|
|
4
|
+
Dedicated gatekeeper for Homebrew command interception and package hygiene.
|
|
5
|
+
Enforces prohibitions against heavy or banned packages (e.g. Colima) and unsafe flags.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from typing import List, Optional
|
|
12
|
+
|
|
13
|
+
from ..types import (
|
|
14
|
+
HookEvent,
|
|
15
|
+
HookResult,
|
|
16
|
+
HookSource,
|
|
17
|
+
HookType,
|
|
18
|
+
HookVerdict,
|
|
19
|
+
)
|
|
20
|
+
from ..policy_engine import UniversalPolicyEngine
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HomebrewHookAdapter:
|
|
24
|
+
"""Intercepts and verifies brew operations before execution."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, policy_engine: Optional[UniversalPolicyEngine] = None):
|
|
27
|
+
self.engine = policy_engine or UniversalPolicyEngine()
|
|
28
|
+
|
|
29
|
+
def evaluate_brew_args(self, args: List[str]) -> HookResult:
|
|
30
|
+
full_command = f"brew {' '.join(args)}".strip()
|
|
31
|
+
package_target = None
|
|
32
|
+
for i, arg in enumerate(args):
|
|
33
|
+
if arg in ["install", "reinstall", "cask"] and i + 1 < len(args):
|
|
34
|
+
package_target = args[i + 1]
|
|
35
|
+
break
|
|
36
|
+
|
|
37
|
+
event = HookEvent(
|
|
38
|
+
source=HookSource.HOMEBREW,
|
|
39
|
+
hook_type=HookType.PRE_COMMAND,
|
|
40
|
+
command=full_command,
|
|
41
|
+
tool_name="homebrew",
|
|
42
|
+
args={"raw_args": args, "package": package_target},
|
|
43
|
+
)
|
|
44
|
+
return self.engine.evaluate(event)
|
|
45
|
+
|
|
46
|
+
def run_shim(self, brew_args: List[str]) -> int:
|
|
47
|
+
result = self.evaluate_brew_args(brew_args)
|
|
48
|
+
if result.verdict == HookVerdict.BLOCK:
|
|
49
|
+
print(f"\033[1;31m🛑 [Homebrew Hook Guard] OPERATION BLOCKED:\033[0m\n {result.message}", file=sys.stderr)
|
|
50
|
+
return 2
|
|
51
|
+
elif result.verdict == HookVerdict.WARN:
|
|
52
|
+
print(f"\033[1;33m⚠️ [Homebrew Hook Guard] ADVISORY:\033[0m {result.message}", file=sys.stderr)
|
|
53
|
+
|
|
54
|
+
# Locate real brew binary
|
|
55
|
+
real_brew = None
|
|
56
|
+
for candidate in ["/opt/homebrew/bin/brew", "/usr/local/bin/brew"]:
|
|
57
|
+
if shutil.which(candidate):
|
|
58
|
+
real_brew = candidate
|
|
59
|
+
break
|
|
60
|
+
if not real_brew:
|
|
61
|
+
real_brew = shutil.which("brew")
|
|
62
|
+
|
|
63
|
+
if not real_brew:
|
|
64
|
+
print("❌ [Homebrew Hook Guard] Homebrew executable not found on system PATH.", file=sys.stderr)
|
|
65
|
+
return 127
|
|
66
|
+
|
|
67
|
+
# Pass through to real brew
|
|
68
|
+
res = subprocess.run([real_brew] + brew_args)
|
|
69
|
+
return res.returncode
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Universal MCP (Model Context Protocol) Hook Proxy
|
|
3
|
+
=================================================
|
|
4
|
+
Transparent JSON-RPC proxy sitting between any MCP client (Cursor, Antigravity, Claude,
|
|
5
|
+
Codex, Kimi) and downstream MCP servers.
|
|
6
|
+
Intercepts 'tools/call' requests for policy gating and inspects tool responses for leaks.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
from ..types import (
|
|
18
|
+
HookEvent,
|
|
19
|
+
HookResult,
|
|
20
|
+
HookSource,
|
|
21
|
+
HookType,
|
|
22
|
+
HookVerdict,
|
|
23
|
+
)
|
|
24
|
+
from ..policy_engine import UniversalPolicyEngine
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class McpHookProxy:
|
|
28
|
+
"""Proxies and governs Model Context Protocol tool invocations."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, policy_engine: Optional[UniversalPolicyEngine] = None):
|
|
31
|
+
self.engine = policy_engine or UniversalPolicyEngine()
|
|
32
|
+
|
|
33
|
+
def inspect_request(self, message: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
34
|
+
"""
|
|
35
|
+
Inspect an MCP client JSON-RPC request.
|
|
36
|
+
If it's a tools/call and policy triggers a BLOCK, return a synthetic JSON-RPC error.
|
|
37
|
+
Otherwise return None (allow pass-through).
|
|
38
|
+
"""
|
|
39
|
+
if message.get("method") != "tools/call":
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
params = message.get("params") or {}
|
|
43
|
+
tool_name = params.get("name")
|
|
44
|
+
tool_args = params.get("arguments") or {}
|
|
45
|
+
|
|
46
|
+
event = HookEvent(
|
|
47
|
+
source=HookSource.MCP,
|
|
48
|
+
hook_type=HookType.PRE_TOOL,
|
|
49
|
+
tool_name=tool_name,
|
|
50
|
+
command=tool_args.get("command") or tool_args.get("cmd"),
|
|
51
|
+
file_path=tool_args.get("path") or tool_args.get("file_path"),
|
|
52
|
+
args=tool_args,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
result = self.engine.evaluate(event)
|
|
56
|
+
if result.verdict == HookVerdict.BLOCK:
|
|
57
|
+
msg_id = message.get("id")
|
|
58
|
+
return {
|
|
59
|
+
"jsonrpc": "2.0",
|
|
60
|
+
"id": msg_id,
|
|
61
|
+
"error": {
|
|
62
|
+
"code": -32000,
|
|
63
|
+
"message": f"MCP Tool Execution Blocked by Policy: {result.message}",
|
|
64
|
+
"data": {"rule_id": result.rule_id, "verdict": "blocked"},
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
def inspect_response(self, response: Dict[str, Any]) -> Dict[str, Any]:
|
|
70
|
+
"""
|
|
71
|
+
Inspect MCP server JSON-RPC response before delivering back to the client.
|
|
72
|
+
Checks for secret leakage in tool output.
|
|
73
|
+
"""
|
|
74
|
+
result_payload = response.get("result")
|
|
75
|
+
if not result_payload:
|
|
76
|
+
return response
|
|
77
|
+
|
|
78
|
+
event = HookEvent(
|
|
79
|
+
source=HookSource.MCP,
|
|
80
|
+
hook_type=HookType.POST_TOOL,
|
|
81
|
+
output=json.dumps(result_payload),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
res = self.engine.evaluate(event)
|
|
85
|
+
if res.verdict == HookVerdict.WARN and "SECRET LEAK DETECTED" in res.message:
|
|
86
|
+
# Mask or annotate leak advisory
|
|
87
|
+
if isinstance(result_payload, dict) and "content" in result_payload:
|
|
88
|
+
result_payload["_security_advisory"] = res.message
|
|
89
|
+
return response
|
|
90
|
+
|
|
91
|
+
def run_proxy(self, server_command: List[str]):
|
|
92
|
+
"""Run downstream MCP server as subprocess, proxying stdin/stdout."""
|
|
93
|
+
proc = subprocess.Popen(
|
|
94
|
+
server_command,
|
|
95
|
+
stdin=subprocess.PIPE,
|
|
96
|
+
stdout=subprocess.PIPE,
|
|
97
|
+
stderr=sys.stderr,
|
|
98
|
+
text=True,
|
|
99
|
+
bufsize=1,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
for line in sys.stdin:
|
|
104
|
+
line = line.strip()
|
|
105
|
+
if not line:
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
msg = json.loads(line)
|
|
110
|
+
blocked_resp = self.inspect_request(msg)
|
|
111
|
+
if blocked_resp:
|
|
112
|
+
print(json.dumps(blocked_resp), flush=True)
|
|
113
|
+
continue
|
|
114
|
+
except Exception as parse_err:
|
|
115
|
+
logger.debug("MCP JSON parse error: %s", parse_err)
|
|
116
|
+
|
|
117
|
+
# Forward to server
|
|
118
|
+
if proc.stdin:
|
|
119
|
+
proc.stdin.write(line + "\n")
|
|
120
|
+
proc.stdin.flush()
|
|
121
|
+
|
|
122
|
+
# Read response from server
|
|
123
|
+
if proc.stdout:
|
|
124
|
+
resp_line = proc.stdout.readline()
|
|
125
|
+
if resp_line:
|
|
126
|
+
try:
|
|
127
|
+
resp_json = json.loads(resp_line.strip())
|
|
128
|
+
sanitized = self.inspect_response(resp_json)
|
|
129
|
+
print(json.dumps(sanitized), flush=True)
|
|
130
|
+
except Exception:
|
|
131
|
+
print(resp_line.strip(), flush=True)
|
|
132
|
+
finally:
|
|
133
|
+
proc.terminate()
|