monoco-toolkit 0.3.12__py3-none-any.whl → 0.4.0__py3-none-any.whl
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.
- monoco/core/automation/__init__.py +0 -11
- monoco/core/automation/handlers.py +108 -26
- monoco/core/config.py +28 -10
- monoco/core/daemon/__init__.py +5 -0
- monoco/core/daemon/pid.py +290 -0
- monoco/core/injection.py +86 -8
- monoco/core/integrations.py +0 -24
- monoco/core/router/__init__.py +1 -39
- monoco/core/router/action.py +3 -142
- monoco/core/scheduler/events.py +28 -2
- monoco/core/setup.py +9 -0
- monoco/core/sync.py +199 -4
- monoco/core/watcher/__init__.py +6 -0
- monoco/core/watcher/base.py +18 -1
- monoco/core/watcher/im.py +460 -0
- monoco/core/watcher/memo.py +40 -48
- monoco/daemon/app.py +3 -60
- monoco/daemon/commands.py +459 -25
- monoco/daemon/scheduler.py +1 -16
- monoco/daemon/services.py +15 -0
- monoco/features/agent/resources/en/AGENTS.md +14 -14
- monoco/features/agent/resources/en/skills/monoco_role_engineer/SKILL.md +101 -0
- monoco/features/agent/resources/en/skills/monoco_role_manager/SKILL.md +95 -0
- monoco/features/agent/resources/en/skills/monoco_role_planner/SKILL.md +177 -0
- monoco/features/agent/resources/en/skills/monoco_role_reviewer/SKILL.md +139 -0
- monoco/features/agent/resources/zh/skills/monoco_role_engineer/SKILL.md +101 -0
- monoco/features/agent/resources/zh/skills/monoco_role_manager/SKILL.md +95 -0
- monoco/features/agent/resources/zh/skills/monoco_role_planner/SKILL.md +177 -0
- monoco/features/agent/resources/zh/skills/monoco_role_reviewer/SKILL.md +139 -0
- monoco/features/hooks/__init__.py +61 -6
- monoco/features/hooks/commands.py +281 -271
- monoco/features/hooks/dispatchers/__init__.py +23 -0
- monoco/features/hooks/dispatchers/agent_dispatcher.py +486 -0
- monoco/features/hooks/dispatchers/git_dispatcher.py +478 -0
- monoco/features/hooks/manager.py +357 -0
- monoco/features/hooks/models.py +262 -0
- monoco/features/hooks/parser.py +322 -0
- monoco/features/hooks/universal_interceptor.py +503 -0
- monoco/features/im/__init__.py +67 -0
- monoco/features/im/core.py +782 -0
- monoco/features/im/models.py +311 -0
- monoco/features/issue/commands.py +65 -50
- monoco/features/issue/core.py +199 -99
- monoco/features/issue/domain_commands.py +0 -19
- monoco/features/issue/resources/en/AGENTS.md +17 -122
- monoco/features/issue/resources/hooks/agent/before-tool.sh +102 -0
- monoco/features/issue/resources/hooks/agent/session-start.sh +88 -0
- monoco/features/issue/resources/hooks/{post-checkout.sh → git/git-post-checkout.sh} +10 -9
- monoco/features/issue/resources/hooks/git/git-pre-commit.sh +31 -0
- monoco/features/issue/resources/hooks/{pre-push.sh → git/git-pre-push.sh} +7 -13
- monoco/features/issue/resources/zh/AGENTS.md +18 -123
- monoco/features/memo/cli.py +15 -64
- monoco/features/memo/core.py +6 -34
- monoco/features/memo/models.py +24 -15
- monoco/features/memo/resources/en/AGENTS.md +31 -0
- monoco/features/memo/resources/zh/AGENTS.md +28 -5
- monoco/main.py +5 -3
- {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/METADATA +1 -1
- monoco_toolkit-0.4.0.dist-info/RECORD +170 -0
- monoco/core/automation/config.py +0 -338
- monoco/core/execution.py +0 -67
- monoco/core/executor/__init__.py +0 -38
- monoco/core/executor/agent_action.py +0 -254
- monoco/core/executor/git_action.py +0 -303
- monoco/core/executor/im_action.py +0 -309
- monoco/core/executor/pytest_action.py +0 -218
- monoco/core/router/router.py +0 -392
- monoco/features/agent/resources/atoms/atom-code-dev.yaml +0 -61
- monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +0 -73
- monoco/features/agent/resources/atoms/atom-knowledge.yaml +0 -55
- monoco/features/agent/resources/atoms/atom-review.yaml +0 -60
- monoco/features/agent/resources/en/skills/monoco_atom_core/SKILL.md +0 -99
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_manager/SKILL.md +0 -93
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_planner/SKILL.md +0 -85
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -114
- monoco/features/agent/resources/workflows/workflow-dev.yaml +0 -83
- monoco/features/agent/resources/workflows/workflow-issue-create.yaml +0 -72
- monoco/features/agent/resources/workflows/workflow-review.yaml +0 -94
- monoco/features/agent/resources/zh/roles/monoco_role_engineer.yaml +0 -49
- monoco/features/agent/resources/zh/roles/monoco_role_manager.yaml +0 -46
- monoco/features/agent/resources/zh/roles/monoco_role_planner.yaml +0 -46
- monoco/features/agent/resources/zh/roles/monoco_role_reviewer.yaml +0 -47
- monoco/features/agent/resources/zh/skills/monoco_atom_core/SKILL.md +0 -99
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_manager/SKILL.md +0 -88
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_planner/SKILL.md +0 -259
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -137
- monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +0 -278
- monoco/features/glossary/resources/en/skills/monoco_atom_glossary/SKILL.md +0 -35
- monoco/features/glossary/resources/zh/skills/monoco_atom_glossary/SKILL.md +0 -35
- monoco/features/hooks/adapter.py +0 -67
- monoco/features/hooks/core.py +0 -441
- monoco/features/i18n/resources/en/skills/monoco_atom_i18n/SKILL.md +0 -96
- monoco/features/i18n/resources/en/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
- monoco/features/i18n/resources/zh/skills/monoco_atom_i18n/SKILL.md +0 -96
- monoco/features/i18n/resources/zh/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
- monoco/features/issue/resources/en/skills/monoco_atom_issue/SKILL.md +0 -165
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_development/SKILL.md +0 -224
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_management/SKILL.md +0 -159
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
- monoco/features/issue/resources/hooks/pre-commit.sh +0 -41
- monoco/features/issue/resources/zh/skills/monoco_atom_issue_lifecycle/SKILL.md +0 -190
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_development/SKILL.md +0 -224
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_management/SKILL.md +0 -159
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
- monoco/features/memo/resources/en/skills/monoco_atom_memo/SKILL.md +0 -77
- monoco/features/memo/resources/en/skills/monoco_workflow_note_processing/SKILL.md +0 -140
- monoco/features/memo/resources/zh/skills/monoco_atom_memo/SKILL.md +0 -77
- monoco/features/memo/resources/zh/skills/monoco_workflow_note_processing/SKILL.md +0 -140
- monoco/features/spike/resources/en/skills/monoco_atom_spike/SKILL.md +0 -76
- monoco/features/spike/resources/en/skills/monoco_workflow_research/SKILL.md +0 -121
- monoco/features/spike/resources/zh/skills/monoco_atom_spike/SKILL.md +0 -76
- monoco/features/spike/resources/zh/skills/monoco_workflow_research/SKILL.md +0 -121
- monoco_toolkit-0.3.12.dist-info/RECORD +0 -202
- {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Universal Interceptor for Agent Hooks (ACL - Anti-Corruption Layer).
|
|
3
|
+
|
|
4
|
+
This module provides runtime protocol translation between different Agent platforms
|
|
5
|
+
(Claude Code, Gemini CLI) and the Monoco unified hook protocol.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python -m monoco.features.hooks.universal_interceptor <hook_script_path>
|
|
9
|
+
|
|
10
|
+
The interceptor:
|
|
11
|
+
1. Auto-detects the Agent platform from input format
|
|
12
|
+
2. Translates agent-specific input to Monoco unified format
|
|
13
|
+
3. Executes the actual hook script
|
|
14
|
+
4. Translates the output back to agent-specific format
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
from abc import ABC, abstractmethod
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from enum import Enum
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any, Optional
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AgentProvider(Enum):
|
|
29
|
+
"""Supported Agent providers."""
|
|
30
|
+
CLAUDE_CODE = "claude-code"
|
|
31
|
+
GEMINI_CLI = "gemini-cli"
|
|
32
|
+
UNKNOWN = "unknown"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class UnifiedDecision:
|
|
37
|
+
"""
|
|
38
|
+
Unified decision model for hook responses.
|
|
39
|
+
|
|
40
|
+
This is the internal format used by Monoco, independent of any specific agent.
|
|
41
|
+
"""
|
|
42
|
+
decision: str # "allow", "deny", "ask"
|
|
43
|
+
reason: str = ""
|
|
44
|
+
message: str = ""
|
|
45
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
46
|
+
|
|
47
|
+
def to_dict(self) -> dict[str, Any]:
|
|
48
|
+
"""Convert to dictionary."""
|
|
49
|
+
return {
|
|
50
|
+
"decision": self.decision,
|
|
51
|
+
"reason": self.reason,
|
|
52
|
+
"message": self.message,
|
|
53
|
+
"metadata": self.metadata,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def from_dict(cls, data: dict[str, Any]) -> "UnifiedDecision":
|
|
58
|
+
"""Create from dictionary."""
|
|
59
|
+
return cls(
|
|
60
|
+
decision=data.get("decision", "ask"),
|
|
61
|
+
reason=data.get("reason", ""),
|
|
62
|
+
message=data.get("message", ""),
|
|
63
|
+
metadata=data.get("metadata", {}),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class UnifiedHookInput:
|
|
69
|
+
"""
|
|
70
|
+
Unified hook input model.
|
|
71
|
+
|
|
72
|
+
This normalizes input from different agent platforms into a common format.
|
|
73
|
+
"""
|
|
74
|
+
event: str # Monoco event name (e.g., "before-tool", "before-agent")
|
|
75
|
+
tool: Optional[str] = None # Tool name (for tool-related events)
|
|
76
|
+
input_data: dict[str, Any] = field(default_factory=dict) # Tool/agent input
|
|
77
|
+
env: dict[str, str] = field(default_factory=dict) # Environment variables
|
|
78
|
+
metadata: dict[str, Any] = field(default_factory=dict) # Additional metadata
|
|
79
|
+
|
|
80
|
+
def to_dict(self) -> dict[str, Any]:
|
|
81
|
+
"""Convert to dictionary."""
|
|
82
|
+
return {
|
|
83
|
+
"event": self.event,
|
|
84
|
+
"tool": self.tool,
|
|
85
|
+
"input": self.input_data,
|
|
86
|
+
"env": self.env,
|
|
87
|
+
"metadata": self.metadata,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class AgentAdapter(ABC):
|
|
92
|
+
"""
|
|
93
|
+
Abstract base class for Agent-specific adapters.
|
|
94
|
+
|
|
95
|
+
Adapters handle the translation between agent-specific protocols
|
|
96
|
+
and the Monoco unified protocol.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(self, provider: AgentProvider):
|
|
100
|
+
self.provider = provider
|
|
101
|
+
|
|
102
|
+
@abstractmethod
|
|
103
|
+
def detect(self, raw_input: str) -> bool:
|
|
104
|
+
"""
|
|
105
|
+
Detect if this adapter should be used based on input format.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
raw_input: Raw JSON string from stdin
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
True if this adapter should handle the input format
|
|
112
|
+
"""
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
@abstractmethod
|
|
116
|
+
def translate_input(self, raw_input: str) -> UnifiedHookInput:
|
|
117
|
+
"""
|
|
118
|
+
Translate agent-specific input to unified format.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
raw_input: Raw JSON string from stdin
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
UnifiedHookInput
|
|
125
|
+
"""
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
@abstractmethod
|
|
129
|
+
def translate_output(self, decision: UnifiedDecision) -> str:
|
|
130
|
+
"""
|
|
131
|
+
Translate unified decision to agent-specific output.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
decision: Unified decision
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
JSON string for agent-specific output
|
|
138
|
+
"""
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class ClaudeAdapter(AgentAdapter):
|
|
143
|
+
"""
|
|
144
|
+
Adapter for Claude Code agent.
|
|
145
|
+
|
|
146
|
+
Event Mapping (Claude -> Monoco):
|
|
147
|
+
- PreToolUse -> before-tool
|
|
148
|
+
- PostToolUse -> after-tool
|
|
149
|
+
- UserPromptSubmit -> before-agent
|
|
150
|
+
- Stop -> after-agent
|
|
151
|
+
- SessionStart -> session-start
|
|
152
|
+
- SessionEnd -> session-end
|
|
153
|
+
|
|
154
|
+
Decision Mapping:
|
|
155
|
+
- Claude "permissionDecision" -> Monoco "decision"
|
|
156
|
+
- Values: "allow", "deny", "ask"
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
# Event name mapping: Claude -> Monoco
|
|
160
|
+
EVENT_MAP = {
|
|
161
|
+
"PreToolUse": "before-tool",
|
|
162
|
+
"PostToolUse": "after-tool",
|
|
163
|
+
"UserPromptSubmit": "before-agent",
|
|
164
|
+
"Stop": "after-agent",
|
|
165
|
+
"SessionStart": "session-start",
|
|
166
|
+
"SessionEnd": "session-end",
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# Reverse mapping: Monoco -> Claude
|
|
170
|
+
REVERSE_EVENT_MAP = {v: k for k, v in EVENT_MAP.items()}
|
|
171
|
+
|
|
172
|
+
def __init__(self):
|
|
173
|
+
super().__init__(AgentProvider.CLAUDE_CODE)
|
|
174
|
+
|
|
175
|
+
def detect(self, raw_input: str) -> bool:
|
|
176
|
+
"""Detect if input is in Claude Code format."""
|
|
177
|
+
try:
|
|
178
|
+
data = json.loads(raw_input)
|
|
179
|
+
# Try both old and new field names
|
|
180
|
+
event = data.get("hook_event_name") or data.get("event", "")
|
|
181
|
+
# Claude Code uses specific event names
|
|
182
|
+
return event in self.EVENT_MAP
|
|
183
|
+
except json.JSONDecodeError:
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
def translate_input(self, raw_input: str) -> UnifiedHookInput:
|
|
187
|
+
"""
|
|
188
|
+
Translate Claude Code input to unified format.
|
|
189
|
+
|
|
190
|
+
Claude Code input format:
|
|
191
|
+
{
|
|
192
|
+
"hook_event_name": "PreToolUse",
|
|
193
|
+
"tool_name": "Bash",
|
|
194
|
+
"tool_input": {"command": "ls -la"},
|
|
195
|
+
...
|
|
196
|
+
}
|
|
197
|
+
"""
|
|
198
|
+
try:
|
|
199
|
+
data = json.loads(raw_input)
|
|
200
|
+
except json.JSONDecodeError:
|
|
201
|
+
data = {}
|
|
202
|
+
|
|
203
|
+
# Use real Claude Code field names (hook_event_name, tool_name, tool_input)
|
|
204
|
+
claude_event = data.get("hook_event_name") or data.get("event", "")
|
|
205
|
+
monoco_event = self.EVENT_MAP.get(claude_event, claude_event.lower())
|
|
206
|
+
|
|
207
|
+
return UnifiedHookInput(
|
|
208
|
+
event=monoco_event,
|
|
209
|
+
tool=data.get("tool_name") or data.get("tool"),
|
|
210
|
+
input_data=data.get("tool_input") or data.get("input", {}),
|
|
211
|
+
env=dict(os.environ),
|
|
212
|
+
metadata={k: v for k, v in data.items() if k not in ("hook_event_name", "event", "tool_name", "tool", "tool_input", "input")},
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def translate_output(self, decision: UnifiedDecision) -> str:
|
|
216
|
+
"""
|
|
217
|
+
Translate unified decision to Claude Code output format.
|
|
218
|
+
|
|
219
|
+
Claude Code output format:
|
|
220
|
+
{
|
|
221
|
+
"permissionDecision": "allow" | "deny" | "ask",
|
|
222
|
+
"reason": "...",
|
|
223
|
+
"message": "..."
|
|
224
|
+
}
|
|
225
|
+
"""
|
|
226
|
+
output = {
|
|
227
|
+
"permissionDecision": decision.decision,
|
|
228
|
+
"reason": decision.reason,
|
|
229
|
+
"message": decision.message,
|
|
230
|
+
}
|
|
231
|
+
return json.dumps(output)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class GeminiAdapter(AgentAdapter):
|
|
235
|
+
"""
|
|
236
|
+
Adapter for Gemini CLI agent.
|
|
237
|
+
|
|
238
|
+
Event Mapping (Gemini -> Monoco):
|
|
239
|
+
- BeforeTool -> before-tool
|
|
240
|
+
- AfterTool -> after-tool
|
|
241
|
+
- BeforeAgent -> before-agent
|
|
242
|
+
- AfterAgent -> after-agent
|
|
243
|
+
- SessionStart -> session-start
|
|
244
|
+
- SessionEnd -> session-end
|
|
245
|
+
|
|
246
|
+
Decision Mapping:
|
|
247
|
+
- Gemini "decision" -> Monoco "decision"
|
|
248
|
+
- Values: "allow", "deny", "ask"
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
# Event name mapping: Gemini -> Monoco
|
|
252
|
+
EVENT_MAP = {
|
|
253
|
+
"BeforeTool": "before-tool",
|
|
254
|
+
"AfterTool": "after-tool",
|
|
255
|
+
"BeforeAgent": "before-agent",
|
|
256
|
+
"AfterAgent": "after-agent",
|
|
257
|
+
"SessionStart": "session-start",
|
|
258
|
+
"SessionEnd": "session-end",
|
|
259
|
+
"BeforeModel": "before-model",
|
|
260
|
+
"AfterModel": "after-model",
|
|
261
|
+
"BeforeToolSelection": "before-tool-selection",
|
|
262
|
+
"Notification": "notification",
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
# Reverse mapping: Monoco -> Gemini
|
|
266
|
+
REVERSE_EVENT_MAP = {v: k for k, v in EVENT_MAP.items()}
|
|
267
|
+
|
|
268
|
+
def __init__(self):
|
|
269
|
+
super().__init__(AgentProvider.GEMINI_CLI)
|
|
270
|
+
|
|
271
|
+
def detect(self, raw_input: str) -> bool:
|
|
272
|
+
"""Detect if input is in Gemini CLI format."""
|
|
273
|
+
try:
|
|
274
|
+
data = json.loads(raw_input)
|
|
275
|
+
# Gemini CLI uses hook_event_name or event
|
|
276
|
+
event = data.get("hook_event_name") or data.get("event", "")
|
|
277
|
+
# Gemini CLI uses specific event names
|
|
278
|
+
return event in self.EVENT_MAP
|
|
279
|
+
except json.JSONDecodeError:
|
|
280
|
+
return False
|
|
281
|
+
|
|
282
|
+
def translate_input(self, raw_input: str) -> UnifiedHookInput:
|
|
283
|
+
"""
|
|
284
|
+
Translate Gemini CLI input to unified format.
|
|
285
|
+
|
|
286
|
+
Gemini CLI input format:
|
|
287
|
+
{
|
|
288
|
+
"hook_event_name": "BeforeTool",
|
|
289
|
+
"tool_name": "Bash",
|
|
290
|
+
"tool_input": {"command": "ls -la"},
|
|
291
|
+
...
|
|
292
|
+
}
|
|
293
|
+
"""
|
|
294
|
+
try:
|
|
295
|
+
data = json.loads(raw_input)
|
|
296
|
+
except json.JSONDecodeError:
|
|
297
|
+
data = {}
|
|
298
|
+
|
|
299
|
+
gemini_event = data.get("hook_event_name") or data.get("event", "")
|
|
300
|
+
monoco_event = self.EVENT_MAP.get(gemini_event, gemini_event.lower())
|
|
301
|
+
|
|
302
|
+
return UnifiedHookInput(
|
|
303
|
+
event=monoco_event,
|
|
304
|
+
tool=data.get("tool_name") or data.get("tool"),
|
|
305
|
+
input_data=data.get("tool_input") or data.get("input", {}),
|
|
306
|
+
env=dict(os.environ),
|
|
307
|
+
metadata={k: v for k, v in data.items() if k not in ("hook_event_name", "event", "tool_name", "tool", "tool_input", "input")},
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
def translate_output(self, decision: UnifiedDecision) -> str:
|
|
311
|
+
"""
|
|
312
|
+
Translate unified decision to Gemini CLI output format.
|
|
313
|
+
|
|
314
|
+
Gemini CLI output format:
|
|
315
|
+
{
|
|
316
|
+
"decision": "allow" | "deny" | "ask",
|
|
317
|
+
"reason": "...",
|
|
318
|
+
"message": "..."
|
|
319
|
+
}
|
|
320
|
+
"""
|
|
321
|
+
output = {
|
|
322
|
+
"decision": decision.decision,
|
|
323
|
+
"reason": decision.reason,
|
|
324
|
+
"message": decision.message,
|
|
325
|
+
}
|
|
326
|
+
return json.dumps(output)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class UniversalInterceptor:
|
|
330
|
+
"""
|
|
331
|
+
Universal interceptor for agent hooks.
|
|
332
|
+
|
|
333
|
+
This is the main entry point for the ACL runtime. It:
|
|
334
|
+
1. Auto-detects the agent platform
|
|
335
|
+
2. Translates input from agent-specific to unified format
|
|
336
|
+
3. Executes the hook script with unified input
|
|
337
|
+
4. Translates output back to agent-specific format
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
def __init__(self):
|
|
341
|
+
self.adapters: list[AgentAdapter] = [
|
|
342
|
+
ClaudeAdapter(),
|
|
343
|
+
GeminiAdapter(),
|
|
344
|
+
]
|
|
345
|
+
self.adapter: Optional[AgentAdapter] = None
|
|
346
|
+
|
|
347
|
+
def detect_adapter(self, raw_input: str) -> AgentAdapter:
|
|
348
|
+
"""
|
|
349
|
+
Auto-detect the appropriate adapter based on input format.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
raw_input: Raw JSON string from stdin
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
The detected adapter
|
|
356
|
+
|
|
357
|
+
Raises:
|
|
358
|
+
RuntimeError: If no adapter can be detected
|
|
359
|
+
"""
|
|
360
|
+
for adapter in self.adapters:
|
|
361
|
+
if adapter.detect(raw_input):
|
|
362
|
+
return adapter
|
|
363
|
+
|
|
364
|
+
raise RuntimeError(
|
|
365
|
+
"Could not detect agent input format. "
|
|
366
|
+
"Expected Claude Code or Gemini CLI event format."
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
def run(self, hook_script_path: str, is_debug: bool = False, stdin_data: Optional[str] = None) -> int:
|
|
370
|
+
"""
|
|
371
|
+
Run the interceptor.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
hook_script_path: Path to the actual hook script to execute
|
|
375
|
+
is_debug: Whether debug mode is enabled for this hook
|
|
376
|
+
stdin_data: Optional stdin data (if already read by caller)
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Exit code (0 for success, non-zero for failure)
|
|
380
|
+
"""
|
|
381
|
+
# 1. Read raw input from stdin (or use provided data)
|
|
382
|
+
raw_input = stdin_data if stdin_data is not None else sys.stdin.read()
|
|
383
|
+
|
|
384
|
+
# 2. Detect adapter based on input format
|
|
385
|
+
try:
|
|
386
|
+
adapter = self.detect_adapter(raw_input)
|
|
387
|
+
except RuntimeError as e:
|
|
388
|
+
print(json.dumps({"error": str(e)}), file=sys.stderr)
|
|
389
|
+
return 1
|
|
390
|
+
|
|
391
|
+
# 3. Translate input to unified format
|
|
392
|
+
unified_input = adapter.translate_input(raw_input)
|
|
393
|
+
|
|
394
|
+
# 4. Execute the hook script
|
|
395
|
+
try:
|
|
396
|
+
unified_output = self._execute_hook(
|
|
397
|
+
hook_script_path, unified_input, adapter.provider.value, is_debug
|
|
398
|
+
)
|
|
399
|
+
except subprocess.CalledProcessError as e:
|
|
400
|
+
# Script failed - return deny decision
|
|
401
|
+
unified_output = UnifiedDecision(
|
|
402
|
+
decision="deny",
|
|
403
|
+
reason=f"Hook script failed: {e}",
|
|
404
|
+
message="The hook script encountered an error.",
|
|
405
|
+
)
|
|
406
|
+
except Exception as e:
|
|
407
|
+
print(json.dumps({"error": str(e)}), file=sys.stderr)
|
|
408
|
+
return 1
|
|
409
|
+
|
|
410
|
+
# 5. Translate output to agent-specific format
|
|
411
|
+
agent_output = adapter.translate_output(unified_output)
|
|
412
|
+
|
|
413
|
+
# 6. Print output for the agent
|
|
414
|
+
print(agent_output)
|
|
415
|
+
return 0
|
|
416
|
+
|
|
417
|
+
def _execute_hook(
|
|
418
|
+
self, script_path: str, unified_input: UnifiedHookInput, provider: str, is_debug: bool = False
|
|
419
|
+
) -> UnifiedDecision:
|
|
420
|
+
"""
|
|
421
|
+
Execute the hook script with unified input.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
script_path: Path to the hook script
|
|
425
|
+
unified_input: Unified input data
|
|
426
|
+
provider: The detected provider name
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
Unified decision from the script
|
|
430
|
+
"""
|
|
431
|
+
script_path = Path(script_path)
|
|
432
|
+
if not script_path.exists():
|
|
433
|
+
# If script doesn't exist, allow by default
|
|
434
|
+
return UnifiedDecision(decision="allow")
|
|
435
|
+
|
|
436
|
+
# Prepare environment
|
|
437
|
+
env = os.environ.copy()
|
|
438
|
+
env["MONOCO_HOOK_EVENT"] = unified_input.event
|
|
439
|
+
env["MONOCO_HOOK_PROVIDER"] = provider
|
|
440
|
+
env["MONOCO_HOOK_TYPE"] = "agent"
|
|
441
|
+
|
|
442
|
+
# Add tool info if available
|
|
443
|
+
if unified_input.tool:
|
|
444
|
+
env["MONOCO_HOOK_TOOL"] = unified_input.tool
|
|
445
|
+
|
|
446
|
+
# Propagate debug flag if set
|
|
447
|
+
if os.environ.get("MONOCO_HOOK_DEBUG") or is_debug:
|
|
448
|
+
env["MONOCO_HOOK_DEBUG"] = "1"
|
|
449
|
+
env["MONOCO_HOOK_IS_DEBUG"] = "true"
|
|
450
|
+
|
|
451
|
+
# Execute the script
|
|
452
|
+
input_json = json.dumps(unified_input.to_dict())
|
|
453
|
+
|
|
454
|
+
result = subprocess.run(
|
|
455
|
+
[str(script_path)],
|
|
456
|
+
input=input_json,
|
|
457
|
+
capture_output=True,
|
|
458
|
+
text=True,
|
|
459
|
+
env=env,
|
|
460
|
+
timeout=300, # 5 minute timeout
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
# Parse output
|
|
464
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
465
|
+
try:
|
|
466
|
+
output_data = json.loads(result.stdout.strip())
|
|
467
|
+
return UnifiedDecision.from_dict(output_data)
|
|
468
|
+
except json.JSONDecodeError:
|
|
469
|
+
# Non-JSON output - treat as allow with message
|
|
470
|
+
return UnifiedDecision(
|
|
471
|
+
decision="allow",
|
|
472
|
+
message=result.stdout.strip(),
|
|
473
|
+
)
|
|
474
|
+
elif result.returncode != 0:
|
|
475
|
+
# Script failed - deny
|
|
476
|
+
return UnifiedDecision(
|
|
477
|
+
decision="deny",
|
|
478
|
+
reason=f"Hook script exited with code {result.returncode}",
|
|
479
|
+
message=result.stderr.strip() or "Hook script failed",
|
|
480
|
+
)
|
|
481
|
+
else:
|
|
482
|
+
# No output - allow by default
|
|
483
|
+
return UnifiedDecision(decision="allow")
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def main() -> int:
|
|
487
|
+
"""
|
|
488
|
+
Main entry point for the universal interceptor.
|
|
489
|
+
|
|
490
|
+
Usage:
|
|
491
|
+
python -m monoco.features.hooks.universal_interceptor <hook_script_path>
|
|
492
|
+
"""
|
|
493
|
+
if len(sys.argv) < 2:
|
|
494
|
+
print("Usage: universal_interceptor <hook_script_path>", file=sys.stderr)
|
|
495
|
+
return 1
|
|
496
|
+
|
|
497
|
+
hook_script_path = sys.argv[1]
|
|
498
|
+
interceptor = UniversalInterceptor()
|
|
499
|
+
return interceptor.run(hook_script_path)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
if __name__ == "__main__":
|
|
503
|
+
sys.exit(main())
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IM (Instant Messaging) Infrastructure (FEAT-0167).
|
|
3
|
+
|
|
4
|
+
Provides core data models, storage, and routing for IM system integration.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from monoco.features.im import IMManager
|
|
8
|
+
>>> im = IMManager(project_root)
|
|
9
|
+
>>> channel = im.channels.create_channel("group123", PlatformType.FEISHU, name="Test Group")
|
|
10
|
+
>>> message = IMMessage(message_id="msg1", channel_id="group123", ...)
|
|
11
|
+
>>> im.messages.save_message(message)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .models import (
|
|
15
|
+
PlatformType,
|
|
16
|
+
ChannelType,
|
|
17
|
+
MessageStatus,
|
|
18
|
+
ParticipantType,
|
|
19
|
+
ContentType,
|
|
20
|
+
IMParticipant,
|
|
21
|
+
Attachment,
|
|
22
|
+
MessageContent,
|
|
23
|
+
ProcessingStep,
|
|
24
|
+
IMMessage,
|
|
25
|
+
IMChannel,
|
|
26
|
+
IMAgentSession,
|
|
27
|
+
IMWebhookConfig,
|
|
28
|
+
IMStats,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
from .core import (
|
|
32
|
+
IMManager,
|
|
33
|
+
IMChannelManager,
|
|
34
|
+
MessageStore,
|
|
35
|
+
IMRouter,
|
|
36
|
+
IMAgentSessionManager,
|
|
37
|
+
IMStorageError,
|
|
38
|
+
ChannelNotFoundError,
|
|
39
|
+
MessageNotFoundError,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
# Models
|
|
44
|
+
"PlatformType",
|
|
45
|
+
"ChannelType",
|
|
46
|
+
"MessageStatus",
|
|
47
|
+
"ParticipantType",
|
|
48
|
+
"ContentType",
|
|
49
|
+
"IMParticipant",
|
|
50
|
+
"Attachment",
|
|
51
|
+
"MessageContent",
|
|
52
|
+
"ProcessingStep",
|
|
53
|
+
"IMMessage",
|
|
54
|
+
"IMChannel",
|
|
55
|
+
"IMAgentSession",
|
|
56
|
+
"IMWebhookConfig",
|
|
57
|
+
"IMStats",
|
|
58
|
+
# Core
|
|
59
|
+
"IMManager",
|
|
60
|
+
"IMChannelManager",
|
|
61
|
+
"MessageStore",
|
|
62
|
+
"IMRouter",
|
|
63
|
+
"IMAgentSessionManager",
|
|
64
|
+
"IMStorageError",
|
|
65
|
+
"ChannelNotFoundError",
|
|
66
|
+
"MessageNotFoundError",
|
|
67
|
+
]
|