mcpower-proxy 0.0.58__py3-none-any.whl → 0.0.73__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.
- ide_tools/__init__.py +12 -0
- ide_tools/common/__init__.py +6 -0
- ide_tools/common/hooks/__init__.py +6 -0
- ide_tools/common/hooks/init.py +125 -0
- ide_tools/common/hooks/output.py +64 -0
- ide_tools/common/hooks/prompt_submit.py +186 -0
- ide_tools/common/hooks/read_file.py +170 -0
- ide_tools/common/hooks/shell_execution.py +196 -0
- ide_tools/common/hooks/types.py +35 -0
- ide_tools/common/hooks/utils.py +276 -0
- ide_tools/cursor/__init__.py +11 -0
- ide_tools/cursor/constants.py +58 -0
- ide_tools/cursor/format.py +35 -0
- ide_tools/cursor/router.py +100 -0
- ide_tools/router.py +48 -0
- main.py +11 -4
- {mcpower_proxy-0.0.58.dist-info → mcpower_proxy-0.0.73.dist-info}/METADATA +15 -3
- mcpower_proxy-0.0.73.dist-info/RECORD +59 -0
- {mcpower_proxy-0.0.58.dist-info → mcpower_proxy-0.0.73.dist-info}/top_level.txt +1 -0
- modules/apis/security_policy.py +11 -6
- modules/decision_handler.py +219 -0
- modules/logs/audit_trail.py +22 -17
- modules/logs/logger.py +14 -18
- modules/redaction/redactor.py +112 -107
- modules/ui/__init__.py +1 -1
- modules/ui/confirmation.py +0 -1
- modules/utils/cli.py +36 -6
- modules/utils/ids.py +55 -10
- modules/utils/json.py +3 -3
- wrapper/__version__.py +1 -1
- wrapper/middleware.py +121 -210
- wrapper/server.py +19 -11
- mcpower_proxy-0.0.58.dist-info/RECORD +0 -43
- {mcpower_proxy-0.0.58.dist-info → mcpower_proxy-0.0.73.dist-info}/WHEEL +0 -0
- {mcpower_proxy-0.0.58.dist-info → mcpower_proxy-0.0.73.dist-info}/entry_points.txt +0 -0
- {mcpower_proxy-0.0.58.dist-info → mcpower_proxy-0.0.73.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common shell execution handler - IDE-agnostic
|
|
3
|
+
|
|
4
|
+
Handles both request (before) and response (after) inspection for shell commands.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Optional, Dict, List
|
|
9
|
+
|
|
10
|
+
from modules.logs.audit_trail import AuditTrailLogger
|
|
11
|
+
from modules.logs.logger import MCPLogger
|
|
12
|
+
from modules.redaction import redact
|
|
13
|
+
from modules.utils.ids import get_session_id, read_app_uid, get_project_mcpower_dir
|
|
14
|
+
from .types import HookConfig
|
|
15
|
+
from .output import output_result, output_error
|
|
16
|
+
from .utils import create_validator, inspect_and_enforce
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def handle_shell_execution(
|
|
20
|
+
logger: MCPLogger,
|
|
21
|
+
audit_logger: AuditTrailLogger,
|
|
22
|
+
stdin_input: str,
|
|
23
|
+
prompt_id: str,
|
|
24
|
+
event_id: str,
|
|
25
|
+
cwd: Optional[str],
|
|
26
|
+
config: HookConfig,
|
|
27
|
+
tool_name: str,
|
|
28
|
+
is_request: bool = True
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
Generic shell execution handler - handles both request and response
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
logger: Logger instance
|
|
35
|
+
audit_logger: Audit logger instance
|
|
36
|
+
stdin_input: Raw input string from stdin
|
|
37
|
+
prompt_id: Prompt identifier
|
|
38
|
+
event_id: Event identifier
|
|
39
|
+
cwd: Current working directory
|
|
40
|
+
config: Hook configuration (IDE-specific)
|
|
41
|
+
tool_name: IDE-specific tool name (e.g., "beforeShellExecution", "PreToolUse(Bash)")
|
|
42
|
+
is_request: True for before (request), False for after (response)
|
|
43
|
+
"""
|
|
44
|
+
await _handle_shell_operation(
|
|
45
|
+
logger=logger,
|
|
46
|
+
audit_logger=audit_logger,
|
|
47
|
+
stdin_input=stdin_input,
|
|
48
|
+
prompt_id=prompt_id,
|
|
49
|
+
event_id=event_id,
|
|
50
|
+
cwd=cwd,
|
|
51
|
+
config=config,
|
|
52
|
+
is_request=is_request,
|
|
53
|
+
required_fields={"command": str, "cwd": str} if is_request else {"command": str, "output": str},
|
|
54
|
+
redact_fields=["command"] if is_request else ["command", "output"],
|
|
55
|
+
tool_name=tool_name,
|
|
56
|
+
operation_name="Command" if is_request else "Command output",
|
|
57
|
+
audit_event_type="agent_request" if is_request else "mcp_response",
|
|
58
|
+
audit_forwarded_event_type="agent_request_forwarded" if is_request else "mcp_response_forwarded"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def _handle_shell_operation(
|
|
63
|
+
logger: MCPLogger,
|
|
64
|
+
audit_logger: AuditTrailLogger,
|
|
65
|
+
stdin_input: str,
|
|
66
|
+
prompt_id: str,
|
|
67
|
+
event_id: str,
|
|
68
|
+
cwd: Optional[str],
|
|
69
|
+
config: HookConfig,
|
|
70
|
+
is_request: bool,
|
|
71
|
+
required_fields: Dict[str, type],
|
|
72
|
+
redact_fields: List[str],
|
|
73
|
+
tool_name: str,
|
|
74
|
+
operation_name: str,
|
|
75
|
+
audit_event_type: str,
|
|
76
|
+
audit_forwarded_event_type: str
|
|
77
|
+
):
|
|
78
|
+
"""
|
|
79
|
+
Internal shell operation handler - shared logic for request and response
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
is_request: True for request inspection, False for response inspection
|
|
83
|
+
required_fields: Fields to validate in input
|
|
84
|
+
redact_fields: Fields to redact for logging and API calls
|
|
85
|
+
tool_name: Hook name (e.g., "beforeShellExecution", "afterShellExecution")
|
|
86
|
+
operation_name: Display name (e.g., "Command", "Command output")
|
|
87
|
+
audit_event_type: Audit event name for incoming operation
|
|
88
|
+
audit_forwarded_event_type: Audit event name for forwarded operation
|
|
89
|
+
"""
|
|
90
|
+
session_id = get_session_id()
|
|
91
|
+
|
|
92
|
+
logger.info(f"{tool_name} handler started (client={config.client_name}, prompt_id={prompt_id}, event_id={event_id}, cwd={cwd})")
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
# Validate input
|
|
96
|
+
try:
|
|
97
|
+
validator = create_validator(required_fields=required_fields)
|
|
98
|
+
input_data = validator(stdin_input)
|
|
99
|
+
except ValueError as e:
|
|
100
|
+
logger.error(f"Input validation error: {e}")
|
|
101
|
+
output_error(logger, config.output_format, "permission", str(e))
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
app_uid = read_app_uid(logger, get_project_mcpower_dir(cwd))
|
|
105
|
+
audit_logger.set_app_uid(app_uid)
|
|
106
|
+
|
|
107
|
+
# Redact sensitive data for logging
|
|
108
|
+
redacted_data = {}
|
|
109
|
+
for k, v in input_data.items():
|
|
110
|
+
if k in required_fields:
|
|
111
|
+
redacted_data[k] = redact(v) if k in redact_fields else v
|
|
112
|
+
|
|
113
|
+
logger.info(f"Analyzing {tool_name}: {redacted_data}")
|
|
114
|
+
|
|
115
|
+
# Use different structure for request vs response events
|
|
116
|
+
# Requests: params nested, Responses: unpacked at root
|
|
117
|
+
if is_request:
|
|
118
|
+
audit_data = {
|
|
119
|
+
"server": config.server_name,
|
|
120
|
+
"tool": tool_name,
|
|
121
|
+
"params": redacted_data
|
|
122
|
+
}
|
|
123
|
+
else:
|
|
124
|
+
audit_data = {
|
|
125
|
+
"server": config.server_name,
|
|
126
|
+
"tool": tool_name,
|
|
127
|
+
**redacted_data
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
audit_logger.log_event(
|
|
131
|
+
audit_event_type,
|
|
132
|
+
audit_data,
|
|
133
|
+
event_id=event_id
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Build content_data with redacted fields
|
|
137
|
+
content_data = redacted_data
|
|
138
|
+
|
|
139
|
+
# Call security API and enforce decision
|
|
140
|
+
try:
|
|
141
|
+
decision = await inspect_and_enforce(
|
|
142
|
+
is_request=is_request,
|
|
143
|
+
session_id=session_id,
|
|
144
|
+
logger=logger,
|
|
145
|
+
audit_logger=audit_logger,
|
|
146
|
+
app_uid=app_uid,
|
|
147
|
+
event_id=event_id,
|
|
148
|
+
server_name=config.server_name,
|
|
149
|
+
tool_name=tool_name,
|
|
150
|
+
content_data=content_data,
|
|
151
|
+
prompt_id=prompt_id,
|
|
152
|
+
cwd=cwd,
|
|
153
|
+
client_name=config.client_name
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Log audit event for forwarding
|
|
157
|
+
# Use different structure for request vs response
|
|
158
|
+
if is_request:
|
|
159
|
+
forwarded_data = {
|
|
160
|
+
"server": config.server_name,
|
|
161
|
+
"tool": tool_name,
|
|
162
|
+
"params": redacted_data
|
|
163
|
+
}
|
|
164
|
+
else:
|
|
165
|
+
forwarded_data = {
|
|
166
|
+
"server": config.server_name,
|
|
167
|
+
"tool": tool_name,
|
|
168
|
+
**redacted_data
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
audit_logger.log_event(
|
|
172
|
+
audit_forwarded_event_type,
|
|
173
|
+
forwarded_data,
|
|
174
|
+
event_id=event_id
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
reasons = decision.get("reasons", [])
|
|
178
|
+
user_message = f"{operation_name} approved"
|
|
179
|
+
if not reasons:
|
|
180
|
+
agent_message = f"{operation_name} approved by security policy"
|
|
181
|
+
else:
|
|
182
|
+
agent_message = f"{operation_name} approved: {'; '.join(reasons)}"
|
|
183
|
+
output_result(logger, config.output_format, "permission", True, user_message, agent_message)
|
|
184
|
+
|
|
185
|
+
except Exception as e:
|
|
186
|
+
# Decision enforcement failed - block
|
|
187
|
+
error_msg = str(e)
|
|
188
|
+
user_message = f"{operation_name} blocked by security policy"
|
|
189
|
+
if "User blocked" in error_msg or "User denied" in error_msg:
|
|
190
|
+
user_message = f"{operation_name} blocked by user"
|
|
191
|
+
|
|
192
|
+
output_result(logger, config.output_format, "permission", False, user_message, error_msg)
|
|
193
|
+
|
|
194
|
+
except Exception as e:
|
|
195
|
+
logger.error(f"Unexpected error in {tool_name} handler: {e}", exc_info=True)
|
|
196
|
+
output_error(logger, config.output_format, "permission", f"Unexpected error: {str(e)}")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common types for IDE hooks - IDE-agnostic
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Callable, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class OutputFormat:
|
|
11
|
+
"""
|
|
12
|
+
Defines how to format hook output for a specific IDE.
|
|
13
|
+
This is a generic interface - IDEs provide their own implementations.
|
|
14
|
+
"""
|
|
15
|
+
# Exit codes
|
|
16
|
+
allow_exit_code: int
|
|
17
|
+
deny_exit_code: int
|
|
18
|
+
error_exit_code: int
|
|
19
|
+
|
|
20
|
+
# Output formatter function
|
|
21
|
+
# Args: (hook_type: str, allowed: bool, user_msg: Optional[str], agent_msg: Optional[str]) -> str
|
|
22
|
+
formatter: Callable[[str, bool, Optional[str], Optional[str]], str]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class HookConfig:
|
|
27
|
+
"""
|
|
28
|
+
Configuration for a specific hook execution.
|
|
29
|
+
IDE-specific modules create instances of this with their own output format.
|
|
30
|
+
"""
|
|
31
|
+
output_format: OutputFormat
|
|
32
|
+
server_name: str # IDE-specific tool server name
|
|
33
|
+
client_name: str # IDE-specific client name (e.g. "cursor", "claude-code")
|
|
34
|
+
max_content_length: int # Maximum content length before skipping API call
|
|
35
|
+
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common utilities for IDE hooks - IDE-agnostic
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from collections import Counter
|
|
8
|
+
from typing import Dict, Any, List, Callable, Optional
|
|
9
|
+
|
|
10
|
+
from mcpower_shared.mcp_types import create_policy_request, create_policy_response, AgentContext, EnvironmentContext
|
|
11
|
+
from modules.apis.security_policy import SecurityPolicyClient
|
|
12
|
+
from modules.decision_handler import DecisionHandler
|
|
13
|
+
from modules.logs.audit_trail import AuditTrailLogger
|
|
14
|
+
from modules.logs.logger import MCPLogger
|
|
15
|
+
from modules.redaction import redact
|
|
16
|
+
from modules.utils.json import safe_json_dumps
|
|
17
|
+
from wrapper.__version__ import __version__
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_validator(
|
|
21
|
+
required_fields: Dict[str, type],
|
|
22
|
+
optional_fields: Optional[Dict[str, type]] = None
|
|
23
|
+
) -> Callable[[str], Dict[str, Any]]:
|
|
24
|
+
"""
|
|
25
|
+
Factory for input validators
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
required_fields: Dict mapping field names to their expected types
|
|
29
|
+
optional_fields: Dict mapping optional field names to their expected types
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Validator function that parses and validates input
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def parse_and_validate_input(stdin_input: str) -> Dict[str, Any]:
|
|
36
|
+
try:
|
|
37
|
+
if not stdin_input.strip():
|
|
38
|
+
raise ValueError("No input provided")
|
|
39
|
+
input_data = json.loads(stdin_input)
|
|
40
|
+
except json.JSONDecodeError as e:
|
|
41
|
+
raise ValueError(f"Failed to parse input: {e}")
|
|
42
|
+
|
|
43
|
+
for field, expected_type in required_fields.items():
|
|
44
|
+
if field not in input_data:
|
|
45
|
+
raise ValueError(f"No {field} provided in input")
|
|
46
|
+
if not isinstance(input_data[field], expected_type):
|
|
47
|
+
raise ValueError(f"{field} must be a {expected_type.__name__}")
|
|
48
|
+
|
|
49
|
+
if optional_fields:
|
|
50
|
+
for field, expected_type in optional_fields.items():
|
|
51
|
+
if field in input_data and not isinstance(input_data[field], expected_type):
|
|
52
|
+
raise ValueError(f"{field} must be a {expected_type.__name__}")
|
|
53
|
+
|
|
54
|
+
return input_data
|
|
55
|
+
|
|
56
|
+
return parse_and_validate_input
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def extract_redaction_patterns(redacted_content: str) -> Dict[str, int]:
|
|
60
|
+
"""
|
|
61
|
+
Extract redaction pattern types and their counts from redacted content
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
redacted_content: Content with [REDACTED-type] placeholders
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Dict mapping redaction types to counts
|
|
68
|
+
"""
|
|
69
|
+
pattern = r'\[REDACTED-([^\]]+)\]'
|
|
70
|
+
matches = re.findall(pattern, redacted_content)
|
|
71
|
+
return dict(Counter(matches))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def build_sensitive_data_types(patterns: Dict[str, int], context: str = "file") -> Dict[str, Dict[str, Any]]:
|
|
75
|
+
"""
|
|
76
|
+
Convert redaction patterns to structured sensitive_data_types dict
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
patterns: Dict mapping pattern text to occurrence counts (from extract_redaction_patterns)
|
|
80
|
+
context: Context string for description (e.g., "file", "prompt text")
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Dict mapping data types to occurrence info with descriptions
|
|
84
|
+
"""
|
|
85
|
+
sensitive_data_types = {}
|
|
86
|
+
for pattern_text, count in patterns.items():
|
|
87
|
+
data_type = pattern_text.replace("[REDACTED-", "").replace("]", "")
|
|
88
|
+
sensitive_data_types[data_type] = {
|
|
89
|
+
"occurrences": count,
|
|
90
|
+
"description": f"Found {count} instance(s) of {data_type} in {context}"
|
|
91
|
+
}
|
|
92
|
+
return sensitive_data_types
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def process_single_file_for_redaction(
|
|
96
|
+
file_path: str,
|
|
97
|
+
content: str,
|
|
98
|
+
logger: MCPLogger
|
|
99
|
+
) -> Optional[Dict[str, Any]]:
|
|
100
|
+
"""
|
|
101
|
+
Process a single file's content for redaction patterns
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
file_path: Path to the file being processed
|
|
105
|
+
content: File content to check for redactions
|
|
106
|
+
logger: MCPLogger instance
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Dict with redaction info if sensitive data found, None otherwise
|
|
110
|
+
"""
|
|
111
|
+
redacted = redact(content)
|
|
112
|
+
patterns = extract_redaction_patterns(redacted)
|
|
113
|
+
if patterns:
|
|
114
|
+
sensitive_data_types = build_sensitive_data_types(patterns, "file")
|
|
115
|
+
logger.info(f"Found {len(patterns)} sensitive data type(s) in: {file_path}")
|
|
116
|
+
return {
|
|
117
|
+
"file_path": file_path,
|
|
118
|
+
"contains_sensitive_data": True,
|
|
119
|
+
"sensitive_data_types": sensitive_data_types,
|
|
120
|
+
"risk_summary": f"File contains {sum(patterns.values())} sensitive data item(s) across {len(patterns)} type(s)"
|
|
121
|
+
}
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def process_attachments_for_redaction(
|
|
126
|
+
attachments: List[Dict[str, Any]],
|
|
127
|
+
logger: MCPLogger
|
|
128
|
+
) -> List[Dict[str, Any]]:
|
|
129
|
+
"""
|
|
130
|
+
Process file attachments and extract redaction patterns
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
attachments: List of attachment dicts with 'type' and 'file_path' or 'filePath'
|
|
134
|
+
logger: MCPLogger instance
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
List of files with redactions found
|
|
138
|
+
"""
|
|
139
|
+
files_with_redactions = []
|
|
140
|
+
|
|
141
|
+
for attachment in attachments:
|
|
142
|
+
att_type = attachment.get("type")
|
|
143
|
+
att_path = attachment.get("file_path") or attachment.get("filePath")
|
|
144
|
+
|
|
145
|
+
if att_type != "file":
|
|
146
|
+
logger.debug(f"Skipping non-file attachment (type={att_type}): {att_path}")
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
if not att_path:
|
|
150
|
+
logger.debug("Skipping attachment with no file_path")
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
with open(att_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
155
|
+
content = f.read()
|
|
156
|
+
|
|
157
|
+
result = process_single_file_for_redaction(att_path, content, logger)
|
|
158
|
+
if result:
|
|
159
|
+
files_with_redactions.append(result)
|
|
160
|
+
|
|
161
|
+
except Exception as e:
|
|
162
|
+
logger.warning(f"Could not read attachment file {att_path}: {e}")
|
|
163
|
+
|
|
164
|
+
return files_with_redactions
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
async def inspect_and_enforce(
|
|
168
|
+
is_request: bool,
|
|
169
|
+
session_id: str,
|
|
170
|
+
logger: MCPLogger,
|
|
171
|
+
audit_logger: AuditTrailLogger,
|
|
172
|
+
app_uid: str,
|
|
173
|
+
event_id: str,
|
|
174
|
+
server_name: str,
|
|
175
|
+
tool_name: str,
|
|
176
|
+
content_data: Dict[str, Any],
|
|
177
|
+
prompt_id: str,
|
|
178
|
+
cwd: Optional[str],
|
|
179
|
+
current_files: Optional[List[str]] = None,
|
|
180
|
+
client_name: str = "ide-tools"
|
|
181
|
+
) -> Dict[str, Any]:
|
|
182
|
+
"""
|
|
183
|
+
Generic handler for API inspection and decision enforcement
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
is_request: True for request inspection, False for response inspection
|
|
187
|
+
session_id: Session identifier
|
|
188
|
+
logger: Logger instance
|
|
189
|
+
audit_logger: Audit logger instance
|
|
190
|
+
app_uid: Application UID
|
|
191
|
+
event_id: Event identifier
|
|
192
|
+
server_name: Server name (IDE-specific, e.g. "cursor_tools_mcp")
|
|
193
|
+
tool_name: Tool/hook name
|
|
194
|
+
content_data: Data to inspect
|
|
195
|
+
prompt_id: Prompt identifier
|
|
196
|
+
cwd: Current working directory
|
|
197
|
+
current_files: Optional list of current files
|
|
198
|
+
client_name: Client name (e.g. "cursor", "claude-code")
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Decision dict from security API
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
Exception: If decision blocks the operation or API call fails
|
|
205
|
+
"""
|
|
206
|
+
agent_context = AgentContext(
|
|
207
|
+
last_user_prompt="",
|
|
208
|
+
user_prompt_id=prompt_id,
|
|
209
|
+
context_summary=""
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
env_context = EnvironmentContext(
|
|
213
|
+
session_id=session_id,
|
|
214
|
+
workspace={
|
|
215
|
+
"roots": [cwd] if cwd else [],
|
|
216
|
+
"current_files": current_files or []
|
|
217
|
+
},
|
|
218
|
+
client=client_name,
|
|
219
|
+
client_version=__version__,
|
|
220
|
+
selection_hash=""
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
async with SecurityPolicyClient(
|
|
224
|
+
session_id=session_id,
|
|
225
|
+
logger=logger,
|
|
226
|
+
audit_logger=audit_logger,
|
|
227
|
+
app_id=app_uid
|
|
228
|
+
) as client:
|
|
229
|
+
if is_request:
|
|
230
|
+
policy_request = create_policy_request(
|
|
231
|
+
event_id=event_id,
|
|
232
|
+
server_name=server_name,
|
|
233
|
+
server_transport="stdio",
|
|
234
|
+
tool_name=tool_name,
|
|
235
|
+
agent_context=agent_context,
|
|
236
|
+
env_context=env_context,
|
|
237
|
+
arguments=content_data
|
|
238
|
+
)
|
|
239
|
+
decision = await client.inspect_policy_request(
|
|
240
|
+
policy_request=policy_request,
|
|
241
|
+
prompt_id=prompt_id
|
|
242
|
+
)
|
|
243
|
+
else:
|
|
244
|
+
policy_response = create_policy_response(
|
|
245
|
+
event_id=event_id,
|
|
246
|
+
server_name=server_name,
|
|
247
|
+
server_transport="stdio",
|
|
248
|
+
tool_name=tool_name,
|
|
249
|
+
response_content=safe_json_dumps(content_data),
|
|
250
|
+
agent_context=agent_context,
|
|
251
|
+
env_context=env_context
|
|
252
|
+
)
|
|
253
|
+
decision = await client.inspect_policy_response(
|
|
254
|
+
policy_response=policy_response,
|
|
255
|
+
prompt_id=prompt_id
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
await DecisionHandler(
|
|
259
|
+
logger=logger,
|
|
260
|
+
audit_logger=audit_logger,
|
|
261
|
+
session_id=session_id,
|
|
262
|
+
app_id=app_uid
|
|
263
|
+
).enforce_decision(
|
|
264
|
+
decision=decision,
|
|
265
|
+
is_request=is_request,
|
|
266
|
+
event_id=event_id,
|
|
267
|
+
tool_name=tool_name,
|
|
268
|
+
content_data=content_data,
|
|
269
|
+
operation_type="hook",
|
|
270
|
+
prompt_id=prompt_id,
|
|
271
|
+
server_name=server_name,
|
|
272
|
+
error_message_prefix=f"Operation blocked by security policy"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
return decision
|
|
276
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cursor Hook Constants
|
|
3
|
+
|
|
4
|
+
Configuration values specific to Cursor hook handlers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
from ide_tools.common.hooks.types import HookConfig, OutputFormat
|
|
10
|
+
from ide_tools.cursor.format import cursor_output_formatter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HookPermission(str, Enum):
|
|
14
|
+
"""Cursor hook response permission values"""
|
|
15
|
+
ALLOW = "allow"
|
|
16
|
+
DENY = "deny"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Cursor-specific configuration
|
|
20
|
+
CURSOR_CONFIG = HookConfig(
|
|
21
|
+
output_format=OutputFormat(
|
|
22
|
+
allow_exit_code=0,
|
|
23
|
+
deny_exit_code=1,
|
|
24
|
+
error_exit_code=1,
|
|
25
|
+
formatter=cursor_output_formatter
|
|
26
|
+
),
|
|
27
|
+
server_name="mcpower_cursor",
|
|
28
|
+
client_name="cursor",
|
|
29
|
+
max_content_length=100000
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Hook descriptions from https://cursor.com/docs/agent/hooks#hook-events
|
|
33
|
+
CURSOR_HOOKS = {
|
|
34
|
+
"beforeShellExecution": {
|
|
35
|
+
"name": "beforeShellExecution",
|
|
36
|
+
"description": "Triggered before a shell command is executed by the agent. "
|
|
37
|
+
"Allows inspection and potential blocking of shell commands.",
|
|
38
|
+
"version": "1.0.0"
|
|
39
|
+
},
|
|
40
|
+
"afterShellExecution": {
|
|
41
|
+
"name": "afterShellExecution",
|
|
42
|
+
"description": "Triggered after a shell command completes execution. "
|
|
43
|
+
"Provides access to command output and exit status.",
|
|
44
|
+
"version": "1.0.0"
|
|
45
|
+
},
|
|
46
|
+
"beforeReadFile": {
|
|
47
|
+
"name": "beforeReadFile",
|
|
48
|
+
"description": "Triggered before the agent reads a file. "
|
|
49
|
+
"Allows inspection and potential blocking of file read operations.",
|
|
50
|
+
"version": "1.0.0"
|
|
51
|
+
},
|
|
52
|
+
"beforeSubmitPrompt": {
|
|
53
|
+
"name": "beforeSubmitPrompt",
|
|
54
|
+
"description": "Triggered before a prompt is submitted to the AI model. "
|
|
55
|
+
"Allows inspection and modification of prompts.",
|
|
56
|
+
"version": "1.0.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cursor-specific output formatting
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def cursor_output_formatter(hook_type: str, allowed: bool, user_msg: Optional[str], agent_msg: Optional[str]) -> str:
|
|
10
|
+
"""
|
|
11
|
+
Format output for Cursor IDE
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
hook_type: "permission" or "continue"
|
|
15
|
+
allowed: True for allow/continue, False for deny/block
|
|
16
|
+
user_msg: Message for user
|
|
17
|
+
agent_msg: Message for agent/logs
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
JSON string in Cursor format
|
|
21
|
+
"""
|
|
22
|
+
if hook_type == "permission":
|
|
23
|
+
result = {"permission": "allow" if allowed else "deny"}
|
|
24
|
+
if user_msg:
|
|
25
|
+
result["user_message"] = user_msg
|
|
26
|
+
if agent_msg:
|
|
27
|
+
result["agent_message"] = agent_msg
|
|
28
|
+
else: # continue
|
|
29
|
+
result = {"continue": allowed}
|
|
30
|
+
if user_msg:
|
|
31
|
+
result["user_message"] = user_msg
|
|
32
|
+
if agent_msg:
|
|
33
|
+
result["agent_message"] = agent_msg
|
|
34
|
+
|
|
35
|
+
return json.dumps(result)
|